diff --git a/.changeset/honest-poets-sniff.md b/.changeset/honest-poets-sniff.md new file mode 100644 index 0000000000..9b0348e2aa --- /dev/null +++ b/.changeset/honest-poets-sniff.md @@ -0,0 +1,6 @@ +--- +'@sap-ux/cf-deploy-config-writer': patch +'@sap-ux/ui5-config': patch +--- + +add new cf deploy writer and dependencies that it requires diff --git a/packages/cf-deploy-config-writer/.eslintignore b/packages/cf-deploy-config-writer/.eslintignore new file mode 100644 index 0000000000..5d34d8f445 --- /dev/null +++ b/packages/cf-deploy-config-writer/.eslintignore @@ -0,0 +1,3 @@ +test/test-output +test/sample +dist \ No newline at end of file diff --git a/packages/cf-deploy-config-writer/.eslintrc.js b/packages/cf-deploy-config-writer/.eslintrc.js new file mode 100644 index 0000000000..b717f83ae9 --- /dev/null +++ b/packages/cf-deploy-config-writer/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['../../.eslintrc'], + parserOptions: { + project: './tsconfig.eslint.json', + tsconfigRootDir: __dirname + } +}; diff --git a/packages/cf-deploy-config-writer/.gitignore b/packages/cf-deploy-config-writer/.gitignore new file mode 100644 index 0000000000..6a99deaf75 --- /dev/null +++ b/packages/cf-deploy-config-writer/.gitignore @@ -0,0 +1 @@ +./test/test-output \ No newline at end of file diff --git a/packages/cf-deploy-config-writer/CHANGELOG.md b/packages/cf-deploy-config-writer/CHANGELOG.md new file mode 100644 index 0000000000..b27490ac18 --- /dev/null +++ b/packages/cf-deploy-config-writer/CHANGELOG.md @@ -0,0 +1 @@ +# @sap-ux/deploy-config-writer \ No newline at end of file diff --git a/packages/cf-deploy-config-writer/LICENSE b/packages/cf-deploy-config-writer/LICENSE new file mode 100644 index 0000000000..f49a4e16e6 --- /dev/null +++ b/packages/cf-deploy-config-writer/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/packages/cf-deploy-config-writer/README.md b/packages/cf-deploy-config-writer/README.md new file mode 100644 index 0000000000..216a1d597c --- /dev/null +++ b/packages/cf-deploy-config-writer/README.md @@ -0,0 +1,84 @@ +# @sap-ux/cf-deploy-config-writer + +Add or amend Cloud Foundry deployment configuration to SAP projects. + +## Prerequisites +* For CAP Projects a CDS binary is required, for more information refer to the [CDS Tool](https://www.npmjs.com/package/@sap/cds) +* For HTML5 Projects an MTA binary is required, for more information refer to the [MTA Tool](https://www.npmjs.com/package/mta), this is required to support the [mta-lib](https://www.npmjs.com/package/@sap/mta-lib) library which handles the flows for interacting with the `mta.yaml` configuration. + +## Installation +Npm +`npm install --save @sap-ux/cf-deploy-config-writer` + +Yarn +`yarn add @sap-ux/cf-deploy-config-writer` + +Pnpm +`pnpm add @sap-ux/cf-deploy-config-writer` + +## Usage +Calling the MtaConfig library to add routing modules, HTML5 apps, destinations, and mta extension configurations to an existing MTA configuration file. Dependent on the [MTA Tool](https://www.npmjs.com/package/mta) for exploring and validating the changes being made. +```Typescript +import { MtaConfig } from '@sap-ux/cf-deploy-config-writer'; +// Create a new instance of MtaConfig +const mtaConfig = await MtaConfig.newInstance('path/to/mta.yaml'); +// Carry out some operations... +// 1. Add routing modules and also add managed approuter configuration +await mtaConfig.addRoutingModules(true); +// 2. Add new HTML5 app +await mtaConfig.addApp('myui5app', './'); +// 3. Append a destination instance to the destination service, required by consumers of CAP services (e.g. approuter, destinations) +await mtaConfig.appendInstanceBasedDestination('mynewdestination'); +// 4. Append mta extension configuration +await mtaConfig.addMtaExtensionConfig('mynewdestination', 'https://my-service-url.base', { + key: 'ApiKey', + value: `${apiHubKey}` +}); +// 5. Save changes +await mtaConfig.save(); +``` + +Calling the `generateAppConfig` function to append Cloud Foundry configuration to a HTML5 application; +```Typescript +import { generateAppConfig, DefaultMTADestination } from '@sap-ux/cf-deploy-config-writer' +import { join } from 'path'; + +const exampleWriter = async () => { + const appPath = join(__dirname, 'testapp'); + // Append managed approuter configuration, toggle `cfAddManagedAppRouter` to false to ommit the managed approuter configuration being appended to the mta.yaml + const fs = await generateAppConfig({appPath, destinationName: 'SAPBTPDestination'}); + // For CAP flows, set the destination to DefaultMTADestination to create a destination service instance between the HTML5 and CAP Project + const fs = await generateAppConfig({appPath, addManagedAppRouter: true, destinationName: DefaultMTADestination}); + return new Promise((resolve) => { + fs.commit(resolve); // When using with Yeoman it handle the fs commit. + }); +} +// Calling the function +await exampleWriter(); +``` + +Calling the `generateBaseConfig` function to generate a `new` Cloud Foundry configuration, supporting managed | standalone configurations; +```Typescript +import { generateBaseConfig, RouterModuleType } from '@sap-ux/cf-deploy-config-writer' +import { join } from 'path'; + +const exampleWriter = async () => { + const mtaPath = join(__dirname, 'testapp'); + // Generate a managed approuter configuration, toggle the routerType to Standard for a standalone configuration + const fs = await generateBaseConfig({ mtaId: 'myapp', routerType: RouterModuleType.Managed, mtaPath }); + return new Promise((resolve) => { + fs.commit(resolve); // When using with Yeoman it handle the fs commit. + }); +} +// Calling the function +await exampleWriter(); +``` + +## Keywords +SAP Fiori elements +SAP UI5 +SAP Deployment +Cloud Foundry +MTA +Multi-Target Application + diff --git a/packages/cf-deploy-config-writer/jest.config.js b/packages/cf-deploy-config-writer/jest.config.js new file mode 100644 index 0000000000..9e9be597ec --- /dev/null +++ b/packages/cf-deploy-config-writer/jest.config.js @@ -0,0 +1,2 @@ +const config = require('../../jest.base'); +module.exports = config; diff --git a/packages/cf-deploy-config-writer/package.json b/packages/cf-deploy-config-writer/package.json new file mode 100644 index 0000000000..df1f122845 --- /dev/null +++ b/packages/cf-deploy-config-writer/package.json @@ -0,0 +1,62 @@ +{ + "name": "@sap-ux/cf-deploy-config-writer", + "description": "Add or amend Cloud Foundry and ABAP deployment configuration for SAP projects", + "version": "0.0.0", + "repository": { + "type": "git", + "url": "https://github.com/SAP/open-ux-tools.git", + "directory": "packages/cf-deploy-config-writer" + }, + "bugs": { + "url": "https://github.com/SAP/open-ux-tools/issues?q=is%3Aopen+is%3Aissue+label%3Abug+label%3Acf-deploy-config-writer" + }, + "license": "Apache-2.0", + "main": "dist/index.js", + "author": "@SAP/ux-tools-team", + "scripts": { + "build": "tsc --build", + "clean": "rimraf dist coverage *.tsbuildinfo", + "format": "prettier --write '**/*.{js,json,ts,yaml,yml}' --ignore-path ../../.prettierignore", + "lint": "eslint . --ext .ts", + "lint:fix": "eslint . --ext .ts --fix", + "test": "jest --ci --forceExit --detectOpenHandles --colors", + "watch": "tsc --watch" + }, + "files": [ + "LICENSE", + "dist", + "templates", + "!dist/*.map", + "!dist/**/*.map" + ], + "dependencies": { + "@sap-ux/project-access": "workspace:*", + "@sap-ux/yaml": "workspace:*", + "@sap-ux/btp-utils": "workspace:*", + "@sap-ux/logger": "workspace:*", + "@sap-ux/ui5-config": "workspace:*", + "@sap/mta-lib": "1.7.4", + "@sap/cf-tools": "3.2.0", + "semver": "7.5.4", + "ejs": "3.1.10", + "i18next": "21.10.0", + "mem-fs": "2.1.0", + "mem-fs-editor": "9.4.0", + "hasbin": "1.2.3" + }, + "devDependencies": { + "@types/ejs": "3.1.2", + "@types/mem-fs": "1.1.2", + "@types/mem-fs-editor": "7.0.1", + "@types/hasbin": "1.2.2", + "@types/fs-extra": "9.0.13", + "@types/js-yaml": "4.0.9", + "@types/semver": "7.5.2", + "memfs": "3.4.13", + "js-yaml": "3.14.0", + "fs-extra": "10.0.0" + }, + "engines": { + "node": ">=18.x" + } +} \ No newline at end of file diff --git a/packages/cf-deploy-config-writer/src/cf-writer/app-config.ts b/packages/cf-deploy-config-writer/src/cf-writer/app-config.ts new file mode 100644 index 0000000000..b1830153be --- /dev/null +++ b/packages/cf-deploy-config-writer/src/cf-writer/app-config.ts @@ -0,0 +1,438 @@ +import { dirname, join, relative } from 'path'; +import { spawnSync } from 'child_process'; +import { create as createStorage } from 'mem-fs'; +import { create, type Editor } from 'mem-fs-editor'; +import { sync } from 'hasbin'; +import { UI5Config as UI5ConfigInstance } from '@sap-ux/ui5-config'; +import { + type Manifest, + getMtaPath, + findCapProjectRoot, + readUi5Yaml, + FileName, + updatePackageScript +} from '@sap-ux/project-access'; +import { Authentication } from '@sap-ux/btp-utils'; +import { + MTAExecutable, + CDSExecutable, + CDSBinNotFound, + MTABinNotFound, + NoAuthType, + CDSAddMtaParams, + DefaultMTADestination, + EmptyDestination, + XSAppFile, + MTABuildScript, + appDeployMTAScript, + undeployMTAScript, + UI5DeployBuildScript, + rootDeployMTAScript, + MTAFileExtension, + XSSecurityFile, + ResourceMTADestination +} from '../constants'; +import { + readManifest, + getTemplatePath, + toPosixPath, + getDestinationProperties, + addGitIgnore, + addRootPackage, + addXSSecurityConfig, + addCommonPackageDependencies +} from '../utils'; +import { + type MtaConfig, + getMtaConfig, + getMtaId, + toMtaModuleName, + createMTA, + addMtaDeployParameters +} from '../mta-config'; +import LoggerHelper from '../logger-helper'; +import { t } from '../i18n'; +import { type Logger } from '@sap-ux/logger'; +import { type UI5Config, type FioriToolsProxyConfig } from '@sap-ux/ui5-config'; +import { type CFConfig, type CFAppConfig, ApiHubType, type MTABaseConfig } from '../types'; + +/** + * Add a managed approuter configuration to an existing HTML5 application. + * + * @param cfAppConfig writer configuration + * @param fs an optional reference to a mem-fs editor + * @param logger optional logger instance + * @returns file system reference + */ +export async function generateAppConfig(cfAppConfig: CFAppConfig, fs?: Editor, logger?: Logger): Promise { + if (!fs) { + fs = create(createStorage()); + } + if (logger) { + LoggerHelper.logger = logger; + } + validateMtaConfig(); + await generateDeployConfig(cfAppConfig, fs); + return fs; +} + +/** + * Validate the conditions to allow deployment configuration to be added. + * + */ +function validateMtaConfig(): void { + // CF writer is dependent on the mta-lib library, which in turn relies on the mta executable being installed and available in the path + if (!sync(MTAExecutable)) { + throw new Error(MTABinNotFound); + } +} + +/** + * Returns the updated configuration for the given HTML5 app, after reading all the required files. + * + * @param cfAppConfig writer configuration + * @param fs reference to a mem-fs editor + * @returns updated writer configuration + */ +async function getUpdatedConfig(cfAppConfig: CFAppConfig, fs: Editor): Promise { + const isLCAP = cfAppConfig.lcapMode ?? false; + const { rootPath, isCap, mtaId, mtaPath, hasRoot, capRoot } = await getProjectProperties(cfAppConfig); + const { serviceHost, destination } = await processUI5Config(cfAppConfig.appPath, fs); + const { servicePath, firstServicePathSegment, appId } = await processManifest(cfAppConfig.appPath, fs); + const { isFullUrlDest, destinationAuthType } = await getDestinationProperties( + cfAppConfig.destinationName ?? destination + ); + + const config = { + appPath: cfAppConfig.appPath.replace(/\/$/, ''), + destinationName: cfAppConfig.destinationName ?? destination, + addManagedAppRouter: cfAppConfig.addManagedAppRouter ?? true, + lcapMode: !isCap ? false : isLCAP, // Restricting local changes is only applicable for CAP flows + isMtaRoot: hasRoot ?? false, + serviceHost: cfAppConfig.serviceHost ?? serviceHost, + rootPath: rootPath.replace(/\/$/, ''), + mtaId, + mtaPath, + destinationAuthType, + isCap, + servicePath, + firstServicePathSegment, + appId, + capRoot, + isFullUrlDest + } as CFConfig; + LoggerHelper.logger?.debug(`CF Config loaded: ${JSON.stringify(config, null, 2)}`); + return config; +} + +/** + * Get project properties. + * + * @param config writer configuration + * @returns project properties + */ +async function getProjectProperties(config: CFAppConfig): Promise<{ + rootPath: string; + isCap: boolean; + hasRoot: boolean; + mtaId: string | undefined; + capRoot: string | undefined; + mtaPath: string | undefined; +}> { + let isCap = false; + let rootPath: string; + let mtaPath: string | undefined; + let mtaId: string | undefined; + const foundMtaPath = await getMtaPath(config.appPath); + if (foundMtaPath) { + mtaPath = dirname(foundMtaPath.mtaPath); + mtaId = await getMtaId(mtaPath); + } + const hasRoot = foundMtaPath?.hasRoot ?? false; + const capRoot = (await findCapProjectRoot(config.appPath)) ?? undefined; + if (capRoot) { + // CDS executable is required for CAP projects as the mta.yaml file is generated by the cds deploy command + if (!sync(CDSExecutable)) { + throw new Error(CDSBinNotFound); + } + isCap = true; + rootPath = capRoot; + } else { + rootPath = mtaPath ?? config.appPath; + } + return { rootPath, isCap, mtaId, mtaPath, hasRoot, capRoot }; +} + +/** + * Reads the ui5.yaml file and returns the service base, firstServicePathSegment and destination if found. + * + * @param appPath path to the application + * @param fs reference to a mem-fs editor + * @returns serviceBase, firstServicePathSegment and destination properties + */ +async function processUI5Config( + appPath: string, + fs: Editor +): Promise<{ + serviceHost: string | undefined; + destination: string | undefined; + firstServicePathSegment: string | undefined; +}> { + let destination; + let serviceHost; + let firstServicePathSegment; + try { + const ui5YamlConfig: UI5Config = await readUi5Yaml(appPath, FileName.Ui5Yaml, fs); + const toolsConfig = ui5YamlConfig.findCustomMiddleware('fiori-tools-proxy'); + if (toolsConfig?.configuration?.backend?.length === 1) { + destination = toolsConfig?.configuration?.backend[0].destination; + serviceHost = toolsConfig?.configuration?.backend[0].url; + firstServicePathSegment = toolsConfig?.configuration?.backend[0].path; + } + } catch (error) { + LoggerHelper.logger?.debug(t('debug.ui5YamlDoesNotExist')); + } + return { destination, serviceHost, firstServicePathSegment }; +} + +/** + * Reads the manifest.json file and returns the service path, firstServicePathSegment and appId if found. + * + * @param appPath path to the application + * @param fs reference to a mem-fs editor + * @returns servicePath, firstServicePathSegment and appId properties + */ +async function processManifest( + appPath: string, + fs: Editor +): Promise<{ + servicePath: string | undefined; + firstServicePathSegment: string | undefined; + appId: string | undefined; +}> { + const manifest = await readManifest(join(appPath, 'webapp/manifest.json'), fs); + const appId = manifest?.['sap.app']?.id ? toMtaModuleName(manifest?.['sap.app']?.id) : undefined; + const servicePath = manifest?.['sap.app']?.dataSources?.mainService?.uri; + const firstServicePathSegment = servicePath?.substring(0, servicePath?.indexOf('/', 1)); + return { servicePath, firstServicePathSegment, appId }; +} + +/** + * + * @param cfAppConfig writer configuration + * @param fs reference to a mem-fs editor + */ +async function generateDeployConfig(cfAppConfig: CFAppConfig, fs: Editor): Promise { + const cfConfig = await getUpdatedConfig(cfAppConfig, fs); + // Generate MTA Config, LCAP will generate the mta.yaml on the fly so we dont care about it! + if (!cfConfig.lcapMode) { + createMTAConfig(cfConfig); + await generateSupportingConfig(cfConfig, fs); + await updateMtaConfig(cfConfig); + } + // Generate HTML5 config + await appendCloudFoundryConfigurations(cfConfig, fs); + await updateManifest(cfConfig, fs); + await updateHTML5AppPackage(cfConfig, fs); + await updateRootPackage(cfConfig, fs); +} + +/** + * Creates the MTA configuration file. + * + * @param cfConfig writer configuration + */ +function createMTAConfig(cfConfig: CFConfig): void { + if (!cfConfig.mtaId) { + if (cfConfig.isCap) { + const result = spawnSync(CDSExecutable, CDSAddMtaParams, { + cwd: cfConfig.rootPath + }); + if (result.error) { + throw new Error(CDSBinNotFound); + } + } else { + createMTA({ mtaId: cfConfig.appId, mtaPath: cfConfig.mtaPath ?? cfConfig.rootPath } as MTABaseConfig); + } + cfConfig.mtaId = cfConfig.appId; + cfConfig.mtaPath = cfConfig.rootPath; + } +} + +/** + * Generate CF specific configurations to support deployment and undeployment. + * + * @param config writer configuration + * @param fs reference to a mem-fs editor + */ +async function generateSupportingConfig(config: CFConfig, fs: Editor): Promise { + const mtaId: string | undefined = config.mtaId ?? (await getMtaId(config.rootPath)); + // Add specific MTA ID configurations + const mtaConfig = { mtaId: mtaId ?? config.appId, mtaPath: config.rootPath } as MTABaseConfig; + if (mtaId && !fs.exists(join(config.rootPath, 'package.json'))) { + addRootPackage(mtaConfig, fs); + } + if (config.addManagedAppRouter && !fs.exists(join(config.rootPath, XSSecurityFile))) { + addXSSecurityConfig(mtaConfig, fs); + } + // Be a good developer and add a .gitignore if missing from the existing project root + if (!fs.exists(join(config.rootPath, '.gitignore'))) { + addGitIgnore(config.rootPath, fs); + } +} + +/** + * Updates the MTA configuration file. + * + * @param cfConfig writer configuration + */ +async function updateMtaConfig(cfConfig: CFConfig): Promise { + const mtaInstance = await getMtaConfig(cfConfig.rootPath); + if (mtaInstance) { + await mtaInstance.addRoutingModules(cfConfig.addManagedAppRouter); + const appModule = cfConfig.appId; + const appRelativePath = toPosixPath(relative(cfConfig.rootPath, cfConfig.appPath)); + await mtaInstance.addApp(appModule, appRelativePath ?? '.'); + await addMtaDeployParameters(mtaInstance); + if ((cfConfig.destinationName && cfConfig.isCap) || cfConfig.destinationName === DefaultMTADestination) { + // If the destination instance identifier is passed, create a destination instance + cfConfig.destinationName = + cfConfig.destinationName === DefaultMTADestination + ? mtaInstance.getFormattedPrefix(ResourceMTADestination) + : cfConfig.destinationName; + await mtaInstance.appendInstanceBasedDestination(cfConfig.destinationName); + // This is required where a managed or standalone router hasnt been added yet to mta.yaml + if (!mtaInstance.hasManagedXsuaaResource()) { + cfConfig.destinationAuthType = Authentication.NO_AUTHENTICATION; + } + } + await saveMta(cfConfig, mtaInstance); + cfConfig.cloudServiceName = mtaInstance.cloudServiceName; + } +} + +/** + * Apply changes to mta.yaml. + * + * @param cfConfig writer configuration + * @param mtaInstance MTA configuration instance + */ +async function saveMta(cfConfig: CFConfig, mtaInstance: MtaConfig): Promise { + if (await mtaInstance.save()) { + // Add mtaext if required for API Hub Enterprise connectivity + if (cfConfig.apiHubConfig?.apiHubType === ApiHubType.apiHubEnterprise) { + try { + await mtaInstance.addMtaExtensionConfig(cfConfig.destinationName, cfConfig.serviceHost, { + key: 'ApiKey', + value: cfConfig.apiHubConfig.apiHubKey + }); + } catch (error) { + LoggerHelper.logger?.error(t('error.mtaExtensionFailed', { error })); + } + } + } +} + +/** + * Appends the Cloud Foundry specific configurations to the project. + * + * @param cfConfig writer configuration + * @param fs reference to a mem-fs editor + */ +async function appendCloudFoundryConfigurations(cfConfig: CFConfig, fs: Editor): Promise { + // When data source is none in app generator, it is not required to provide destination + if (cfConfig.destinationName && cfConfig.destinationName !== EmptyDestination) { + fs.copyTpl(getTemplatePath('app/xs-app-destination.json'), join(cfConfig.appPath, XSAppFile), { + destination: cfConfig.destinationName, + servicePathSegment: `${cfConfig.firstServicePathSegment}${cfConfig.isFullUrlDest ? '/.*' : ''}`, // For service URL's, pull out everything after the last slash + targetPath: `${cfConfig.isFullUrlDest ? '' : cfConfig.firstServicePathSegment}/$1`, // Pull group 1 from the regex + authentication: cfConfig.destinationAuthType === NoAuthType ? 'none' : 'xsuaa' + }); + } else { + fs.copyTpl(getTemplatePath('app/xs-app-no-destination.json'), join(cfConfig.appPath, XSAppFile)); + } + await generateUI5DeployConfig(cfConfig, fs); +} + +/** + * Updates the manifest.json file with the cloud service name. + * + * @param cfConfig writer configuration + * @param fs reference to a mem-fs editor + */ +async function updateManifest(cfConfig: CFConfig, fs: Editor): Promise { + const manifest = await readManifest(join(cfConfig.appPath, 'webapp/manifest.json'), fs); + if (manifest && cfConfig.cloudServiceName) { + const sapCloud = { + ...(manifest['sap.cloud'] || {}), + public: true, + service: cfConfig.cloudServiceName + } as Manifest['sap.cloud']; + fs.extendJSON(join(cfConfig.appPath, 'webapp/manifest.json'), { + 'sap.cloud': sapCloud + }); + } +} + +/** + * Updates the package.json file with the necessary scripts and dependencies. + * + * @param cfConfig writer configuration + * @param fs reference to a mem-fs editor + */ +async function updateHTML5AppPackage(cfConfig: CFConfig, fs: Editor): Promise { + let deployArgs: string[] = []; + if (fs.exists(join(cfConfig.appPath, MTAFileExtension))) { + deployArgs = ['-e', MTAFileExtension]; + } + await updatePackageScript(cfConfig.appPath, 'build:cf', UI5DeployBuildScript, fs); + await updatePackageScript(cfConfig.appPath, 'build:mta', MTABuildScript, fs); + await updatePackageScript(cfConfig.appPath, 'deploy', appDeployMTAScript(deployArgs), fs); + await updatePackageScript(cfConfig.appPath, 'undeploy', undeployMTAScript(cfConfig.mtaId ?? cfConfig.appId), fs); + await addCommonPackageDependencies(cfConfig.appPath, fs); +} + +/** + * Update the root package.json with scripts to deploy the MTA. + * + * @param cfConfig writer configuration + * @param fs reference to a mem-fs editor + */ +async function updateRootPackage(cfConfig: CFConfig, fs: Editor): Promise { + const packageExists = fs.exists(join(cfConfig.rootPath, 'package.json')); + // Append mta scripts only if mta.yaml is a different level to the HTML5 app + if (cfConfig.isMtaRoot && packageExists) { + let deployArgs: string[] = []; + if (fs.exists(join(cfConfig.rootPath, MTAFileExtension))) { + deployArgs = ['-e', MTAFileExtension]; + } + [ + { name: 'undeploy', run: undeployMTAScript(cfConfig.mtaId ?? cfConfig.appId) }, + { name: 'build', run: `${MTABuildScript} --mtar archive` }, + { name: 'deploy', run: rootDeployMTAScript(deployArgs) } + ].forEach(async (script) => { + await updatePackageScript(cfConfig.rootPath, script.name, script.run, fs); + }); + await addCommonPackageDependencies(cfConfig.rootPath, fs); + } +} +/** + * Generate UI5 deploy config. + * + * @param cfConfig - the deploy config + * @param fs reference to a mem-fs editor + * @returns the deploy config + */ +export async function generateUI5DeployConfig(cfConfig: CFConfig, fs: Editor): Promise { + const ui5BaseConfig = await readUi5Yaml(cfConfig.appPath, FileName.Ui5Yaml, fs); + const addTranspileTask = !!ui5BaseConfig.findCustomMiddleware('ui5-tooling-transpile-task'); + const addModulesTask = !!ui5BaseConfig.findCustomMiddleware('ui5-tooling-modules-task'); + const baseUi5Doc = ui5BaseConfig.removeConfig('server'); + const ui5DeployConfig = await UI5ConfigInstance.newInstance(baseUi5Doc.toString()); + ui5DeployConfig.addComment({ + comment: ' yaml-language-server: $schema=https://sap.github.io/ui5-tooling/schema/ui5.yaml.json', + location: 'beginning' + }); + ui5DeployConfig.addCloudFoundryDeployTask(cfConfig.appId, addModulesTask, addTranspileTask); + fs.write(join(cfConfig.appPath, FileName.UI5DeployYaml), ui5DeployConfig.toString()); +} diff --git a/packages/cf-deploy-config-writer/src/cf-writer/base-config.ts b/packages/cf-deploy-config-writer/src/cf-writer/base-config.ts new file mode 100644 index 0000000000..57a7446d99 --- /dev/null +++ b/packages/cf-deploy-config-writer/src/cf-writer/base-config.ts @@ -0,0 +1,162 @@ +import { join } from 'path'; +import { create as createStorage } from 'mem-fs'; +import { create, type Editor } from 'mem-fs-editor'; +import { sync } from 'hasbin'; +import { apiGetInstanceCredentials } from '@sap/cf-tools'; +import { MTAExecutable, MTABinNotFound, RouterModule, XSAppFile } from '../constants'; +import { + getTemplatePath, + toMtaModuleName, + validateVersion, + addGitIgnore, + addRootPackage, + addXSSecurityConfig +} from '../utils'; +import LoggerHelper from '../logger-helper'; +import { t } from '../i18n'; +import { MtaConfig, createMTA, addMtaDeployParameters, addMtaBuildParams } from '../mta-config'; +import { type Logger } from '@sap-ux/logger'; +import { type CFBaseConfig, RouterModuleType, type MTABaseConfig } from '../types'; + +/** + * Add a standalone | managed approuter to an empty target folder. + * + * @param config writer configuration + * @param fs an optional reference to a mem-fs editor + * @param logger optional logger instance + * @returns file system reference + */ +export async function generateBaseConfig(config: CFBaseConfig, fs?: Editor, logger?: Logger): Promise { + if (!fs) { + fs = create(createStorage()); + } + if (logger) { + LoggerHelper.logger = logger; + } + validateMtaConfig(config, fs); + updateBaseConfig(config); + createMTA(config as MTABaseConfig); + await addRoutingConfig(config, fs); + addSupportingConfig(config, fs); + LoggerHelper.logger?.debug(`CF Config ${JSON.stringify(config, null, 2)}`); + return fs; +} + +/** + * Add standalone or managed approuter to the target folder. + * + * @param config writer configuration + * @param fs reference to a mem-fs editor + */ +async function addRoutingConfig(config: CFBaseConfig, fs: Editor): Promise { + const mtaConfigInstance = await MtaConfig.newInstance(config.mtaPath); + if (config.routerType === RouterModuleType.Standard) { + await addStandaloneRouter(config, mtaConfigInstance, fs); + } else { + await mtaConfigInstance.addRoutingModules(true); + } + await addMtaDeployParameters(mtaConfigInstance); + await addMtaBuildParams(mtaConfigInstance); + await mtaConfigInstance.save(); +} + +/** + * Update the writer configuration with defaults. + * + * @param config writer configuration + */ +function updateBaseConfig(config: CFBaseConfig): void { + config.mtaPath = config.mtaPath.replace(/\/$/, ''); + config.addConnectivityService ||= false; + config.mtaId = toMtaModuleName(config.mtaId); +} + +/** + * Add standalone approuter to the target folder. + * + * @param cfConfig writer configuration + * @param mtaInstance MTA configuration instance + * @param fs reference to a mem-fs editor + */ +async function addStandaloneRouter(cfConfig: CFBaseConfig, mtaInstance: MtaConfig, fs: Editor): Promise { + await mtaInstance.addStandaloneRouter(true); + if (cfConfig.addConnectivityService) { + await mtaInstance.addConnectivityResource(); + } + const { abapServiceName, abapService } = cfConfig.abapServiceProvider ?? {}; + if (abapServiceName && abapService) { + await mtaInstance.addAbapService(abapServiceName, abapService); + } + + fs.copyTpl(getTemplatePath(`router/package.json`), join(cfConfig.mtaPath, `${RouterModule}/package.json`)); + + if (abapServiceName) { + let serviceKey; + try { + const instanceCredentials = await apiGetInstanceCredentials(abapServiceName); + serviceKey = instanceCredentials?.credentials; + } catch { + LoggerHelper.logger?.error(t('error.serviceKeyFailed')); + } + const endpoints = serviceKey?.endpoints ? Object.keys(serviceKey.endpoints) : ['']; + const service = serviceKey ? serviceKey['sap.cloud.service'] : ''; + fs.copyTpl( + getTemplatePath('router/xs-app-abapservice.json'), + join(cfConfig.mtaPath, `${RouterModule}/${XSAppFile}`), + { servicekeyService: service, servicekeyEndpoint: endpoints[0] } + ); + } else { + fs.copyTpl( + getTemplatePath('router/xs-app-server.json'), + join(cfConfig.mtaPath, `${RouterModule}/${XSAppFile}`) + ); + } +} + +/** + * Add supporting configuration to the target folder. + * + * @param config writer configuration + * @param fs reference to a mem-fs editor + */ +function addSupportingConfig(config: CFBaseConfig, fs: Editor): void { + addRootPackage(config, fs); + addGitIgnore(config.mtaPath, fs); + addXSSecurityConfig(config, fs); +} + +/** + * Validate the writer configuration to ensure all required parameters are present. + * + * @param config writer configuration + * @param fs reference to a mem-fs editor + */ +function validateMtaConfig(config: CFBaseConfig, fs: Editor): void { + // We use mta-lib, which in turn relies on the mta executable being installed and available in the path + if (!sync(MTAExecutable)) { + throw new Error(MTABinNotFound); + } + + if (!config.routerType || !config.mtaId || !config.mtaPath) { + throw new Error(t('error.missingMtaParameters')); + } + if (config.mtaId.length > 128 || !/^[a-zA-Z_]/.test(config.mtaId)) { + throw new Error(t('error.invalidMtaId')); + } + if (!/^[\w\-.]*$/.test(config.mtaId)) { + throw new Error(t('error.invalidMtaIdWithChars')); + } + + validateVersion(config.mtaVersion); + + if ( + config.abapServiceProvider && + (!config.abapServiceProvider.abapService || !config.abapServiceProvider.abapServiceName) + ) { + throw new Error(t('error.missingABAPServiceBindingDetails')); + } + + if (fs.exists(join(config.mtaPath, config.mtaId))) { + throw new Error(t('error.mtaAlreadyExists')); + } +} diff --git a/packages/cf-deploy-config-writer/src/cf-writer/index.ts b/packages/cf-deploy-config-writer/src/cf-writer/index.ts new file mode 100644 index 0000000000..82ffb7df67 --- /dev/null +++ b/packages/cf-deploy-config-writer/src/cf-writer/index.ts @@ -0,0 +1,2 @@ +export { generateAppConfig } from './app-config'; +export { generateBaseConfig } from './base-config'; diff --git a/packages/cf-deploy-config-writer/src/constants.ts b/packages/cf-deploy-config-writer/src/constants.ts new file mode 100644 index 0000000000..b5cb5f242a --- /dev/null +++ b/packages/cf-deploy-config-writer/src/constants.ts @@ -0,0 +1,115 @@ +import { UI5_DEFAULT } from '@sap-ux/ui5-config'; +import { t } from './i18n'; + +export const WelcomeFile = 'welcomeFile'; +export const XSAppFile = 'xs-app.json'; +export const XSSecurityFile = 'xs-security.json'; +export const NoAuthType = 'NoAuthentication'; +export const MTABuildResult = 'build-result'; +export const MTABuildParams = 'build-parameters'; +export const MTAFileExtension = 'mta-ext.mtaext'; +export const DefaultServiceURL = '${default-url}'; +export const ManagedXSUAA = 'managed:xsuaa'; +export const SRV_API = 'srv-api'; +export const DefaultMTADestination = 'fiori-default-srv-api'; +export const EmptyDestination = 'NONE'; +export const ResourceMTADestination = '%s-srv-api'; +export const MTAYamlFile = 'mta.yaml'; +export const MTADescription = 'Generated by Fiori Tools'; +export const RouterModule = 'router'; +export const CloudFoundry = 'cf'; +export const deployMode = 'deploy_mode'; +export const enableParallelDeployments = 'enable-parallel-deployments'; +export const CDSExecutable = 'cds'; +export const CDSPackage = '@sap/cds-dk'; +export const MTAExecutable = 'mta'; +export const MTAPackage = 'mta'; +export const MTAPackageVersion = '^1.2.27'; +export const MTAVersion = '0.0.1'; +export const RimrafVersion = '^5.0.5'; +export const Rimraf = 'rimraf'; +export const MbtPackage = 'mbt'; +export const MbtPackageVersion = '^1.2.29'; +export const UI5BuilderWebIdePackage = '@sap/ui5-builder-webide-extension'; +export const UI5BuilderWebIdePackageVersion = '^1.1.9'; +export const UI5TaskZipperPackage = 'ui5-task-zipper'; +export const UI5TaskZipperPackageVersion = '^3.1.3'; +export const CDSAddMtaParams = ['add', 'mta']; +export const MTAAPIDestination = { + Name: ResourceMTADestination, + Type: 'HTTP', + URL: `~{srv-api/srv-url}`, + ProxyType: 'Internet', + Authentication: 'NoAuthentication', + 'HTML5.DynamicDestination': true, + 'HTML5.ForwardAuthToken': true +}; +export const UI5Destination = { + Name: 'ui5', + Type: 'HTTP', + URL: UI5_DEFAULT.SAPUI5_CDN, + ProxyType: 'Internet', + Authentication: 'NoAuthentication' +}; +export const UI5ResourceDestination = { + 'init_data': { + instance: { + 'existing_destinations_policy': 'update', + destinations: [ + { + Name: 'ui5', + Type: 'HTTP', + URL: UI5_DEFAULT.SAPUI5_CDN, + ProxyType: 'Internet', + Authentication: 'NoAuthentication' + } + ] + } + } +}; + +export const UI5StandaloneModuleDestination = { + group: 'destinations', + properties: { + forwardAuthToken: false, + name: 'ui5', + url: UI5_DEFAULT.SAPUI5_CDN + } +}; + +export const DestinationServiceConfig = { + config: { + 'HTML5Runtime_enabled': true, + version: '1.0.0', + ...UI5ResourceDestination + } +}; + +export const ServiceAPIRequires = { + name: SRV_API, + properties: { + 'srv-url': DefaultServiceURL + } +}; +export const HTMLAppBuildParams = { + builder: 'custom', + 'build-result': 'dist', + commands: ['npm install', 'npm run build:cf'], + 'supported-platforms': [] +}; +export const UI5DeployBuildScript = + 'ui5 build preload --clean-dest --config ui5-deploy.yaml --include-task=generateCachebusterInfo'; +export const MTABuildScript = 'rimraf resources mta_archives && mbt build --mtar archive'; +export const appDeployMTAScript = (args: string[]): string => { + const mtaArgs = args.length > 0 ? ` ${args.join(' ')}` : ''; + return `fiori cfDeploy${mtaArgs}`; +}; +export const rootDeployMTAScript = (args: string[]): string => { + const mtaArgs = args.length > 0 ? `${args.join(' ')} ` : ''; + return `cf deploy mta_archives/archive.mtar ${mtaArgs}--retries 1`; +}; +export const undeployMTAScript = (mtaId: string): string => + `cf undeploy ${mtaId} --delete-services --delete-service-keys --delete-service-brokers`; +const cannotFindBinary = (bin: string, pkg: string): string => t('error.cannotFindBinary', { bin, pkg }); +export const CDSBinNotFound = cannotFindBinary(CDSExecutable, CDSPackage); +export const MTABinNotFound = cannotFindBinary(MTAExecutable, MTAPackage); diff --git a/packages/cf-deploy-config-writer/src/i18n.ts b/packages/cf-deploy-config-writer/src/i18n.ts new file mode 100644 index 0000000000..a280b240cc --- /dev/null +++ b/packages/cf-deploy-config-writer/src/i18n.ts @@ -0,0 +1,37 @@ +import type { TOptions } from 'i18next'; +import i18next from 'i18next'; +import translations from './translations/cf-deploy-config-writer.i18n.json'; + +const NS = 'cf-deploy-config-writer'; + +/** + * Initialize i18next with the translations for this module. + */ +export async function initI18n(): Promise { + await i18next.init({ + resources: { + en: { + [NS]: translations + } + }, + lng: 'en', + fallbackLng: 'en', + defaultNS: NS, + ns: [NS] + }); +} + +/** + * Helper function facading the call to i18next. + * + * @param key i18n key + * @param options additional options + * @returns {string} localized string stored for the given key + */ +export function t(key: string, options?: TOptions): string { + return i18next.t(key, options); +} + +initI18n().catch(() => { + // Ignore any errors since the write will still work +}); diff --git a/packages/cf-deploy-config-writer/src/index.ts b/packages/cf-deploy-config-writer/src/index.ts new file mode 100644 index 0000000000..d52fea8e20 --- /dev/null +++ b/packages/cf-deploy-config-writer/src/index.ts @@ -0,0 +1,4 @@ +export * from './mta-config'; +export * from './cf-writer'; +export { DefaultMTADestination } from './constants'; +export { CFBaseConfig, CFAppConfig, RouterModuleType, ApiHubConfig, ApiHubType } from './types'; diff --git a/packages/cf-deploy-config-writer/src/logger-helper.ts b/packages/cf-deploy-config-writer/src/logger-helper.ts new file mode 100644 index 0000000000..46401366dc --- /dev/null +++ b/packages/cf-deploy-config-writer/src/logger-helper.ts @@ -0,0 +1,26 @@ +import { ToolsLogger, type Logger } from '@sap-ux/logger'; + +/** + * Static logger prevents passing of logger references through all functions, as this is a cross-cutting concern. + */ +export default class LoggerHelper { + private static _logger: Logger = new ToolsLogger({ logPrefix: '@sap-ux/cf-deploy-config-writer' }); + + /** + * Get the logger. + * + * @returns the logger + */ + public static get logger(): Logger { + return LoggerHelper._logger; + } + + /** + * Set the logger. + * + * @param value the logger to set + */ + public static set logger(value: Logger) { + LoggerHelper._logger = value; + } +} diff --git a/packages/cf-deploy-config-writer/src/mta-config/index.ts b/packages/cf-deploy-config-writer/src/mta-config/index.ts new file mode 100644 index 0000000000..f3dd171f80 --- /dev/null +++ b/packages/cf-deploy-config-writer/src/mta-config/index.ts @@ -0,0 +1,84 @@ +import { readFileSync, writeFileSync } from 'fs'; +import { join } from 'path'; +import { render } from 'ejs'; +import { MtaConfig } from './mta'; +import { getTemplatePath } from '../utils'; +import { MTAYamlFile, MTAVersion, MTADescription, deployMode, enableParallelDeployments } from '../constants'; +import type { mta } from '@sap/mta-lib'; +import type { MTABaseConfig } from '../types'; +import LoggerHelper from '../logger-helper'; + +/** + * Get the MTA ID, read from the root path specified. + * + * @param rootPath Path to the root folder + * @returns MTA ID if found + */ +export async function getMtaId(rootPath: string): Promise { + return (await getMtaConfig(rootPath))?.prefix; +} + +/** + * Get the MTA configuration from the target folder. + * + * @param rootPath Path to the root folder + * @returns MtaConfig instance if found + */ +export async function getMtaConfig(rootPath: string): Promise { + return await MtaConfig.newInstance(rootPath, LoggerHelper.logger); +} + +/** + * Generate an MTA ID that is suitable for CF deployment. + * + * @param appId Name of the app, like `sap.ux.app` and restrict to 128 characters + * @returns Name that's acceptable in an mta.yaml + */ +export function toMtaModuleName(appId: string): string { + return appId.replace(/[`~!@#$%^&*()_|+\-=?;:'",.<>]/gi, '').slice(0, 128); +} + +/** + * Create an MTA file in the target folder, needs to be written to disk as subsequent calls are dependent on it being on the file system i.e mta-lib. + * + * @param config writer configuration + */ +export function createMTA(config: MTABaseConfig): void { + const mtaTemplate = readFileSync(getTemplatePath(`app/${MTAYamlFile}`), 'utf-8'); + const mtaContents = render(mtaTemplate, { + id: config.mtaId, + mtaDescription: config.mtaDescription ?? MTADescription, + mtaVersion: config.mtaVersion ?? MTAVersion + }); + // Written to disk immediately! Subsequent calls are dependent on it being on the file system i.e mta-lib. + writeFileSync(join(config.mtaPath, MTAYamlFile), mtaContents); +} + +/** + * Add the build parameters to the MTA configuration. + * + * @param mtaInstance MTA instance + */ +export async function addMtaBuildParams(mtaInstance: MtaConfig): Promise { + let params = await mtaInstance.getBuildParameters(); + params = { ...(params || {}), ...{} } as mta.ProjectBuildParameters; + params['before-all'] ||= []; + const buildParams: mta.BuildParameters = { builder: 'custom', commands: ['npm install'] }; + params['before-all'].push(buildParams); + await mtaInstance.updateBuildParams(params); +} + +/** + * Add the deploy parameters to the MTA configuration. + * + * @param mtaInstance MTA instance + */ +export async function addMtaDeployParameters(mtaInstance: MtaConfig): Promise { + let params = await mtaInstance.getParameters(); + params = { ...(params || {}), ...{} } as mta.Parameters; + params[deployMode] = 'html5-repo'; + params[enableParallelDeployments] = true; + await mtaInstance.updateParameters(params); +} + +export * from './mta'; diff --git a/packages/cf-deploy-config-writer/src/mta-config/mta.ts b/packages/cf-deploy-config-writer/src/mta-config/mta.ts new file mode 100644 index 0000000000..31e11bf4b1 --- /dev/null +++ b/packages/cf-deploy-config-writer/src/mta-config/mta.ts @@ -0,0 +1,978 @@ +import { format } from 'util'; +import { dirname, join } from 'path'; +import { existsSync, readFileSync, writeFileSync } from 'fs'; +import { render } from 'ejs'; +import { Mta, type mta } from '@sap/mta-lib'; +import { type Destination, isGenericODataDestination, isAbapEnvironmentOnBtp } from '@sap-ux/btp-utils'; +import { YamlDocument } from '@sap-ux/yaml'; +import { getMtaPath } from '@sap-ux/project-access'; +import { + CloudFoundry, + RouterModule, + MTAYamlFile, + ResourceMTADestination, + DefaultMTADestination, + SRV_API, + ManagedXSUAA, + MTAFileExtension, + MTABuildParams, + MTABuildResult, + DestinationServiceConfig, + UI5ResourceDestination, + UI5Destination, + MTAAPIDestination, + UI5StandaloneModuleDestination, + ServiceAPIRequires, + HTMLAppBuildParams, + MTAVersion +} from '../constants'; +import { t } from '../i18n'; +import type { Logger } from '@sap-ux/logger'; +import type { YAMLMap, YAMLSeq } from '@sap-ux/yaml'; +import { + CloudFoundryServiceType, + type HTML5App, + type ModuleType, + type ResourceType, + type MTADestinationType +} from '../types'; + +/** + * A class representing interactions with the MTA binary, found at https://sap.github.io/cloud-mta-build-tool/. + */ +export class MtaConfig { + private readonly mta: Mta; + private readonly apps: Map = new Map(); + private readonly modules: Map = new Map(); + private readonly resources: Map = new Map(); + private readonly log: Logger | undefined; + private dirty = false; + private mtaId: string; + + /** + * Returns a new instance of MtaConfig. + * + * @static + * @param {string} mtaDir - the path to the mta.yaml file + * @param {Logger} logger - the logger instance + * @returns {MtaConfig} the MtaConfig instance + */ + public static async newInstance(mtaDir: string, logger?: Logger): Promise { + return new MtaConfig(mtaDir, logger).init(); + } + + /** + * Creates an instance of Mta. + * + * @param {string} mtaDir - the path to the mta.yaml file + * @param {Logger} logger - the logger instance + * @memberof Mta + */ + private constructor(mtaDir: string, logger: Logger | undefined) { + this.mta = new Mta(mtaDir, false); + this.log = logger; + } + + /** + * Load the modules and resources, read from the mta.yaml file. + * + * @returns {Promise} an MtaConfig instance + */ + private async init(): Promise { + try { + await this.loadMTAResources(); + await this.loadMTAModules(); + this.mtaId = await this.mta.getMtaID(); + } catch (e) { + this.log?.error(t('error.unableToLoadMTA')); + } + return this; + } + + /** + * Determines if the MTA configuration contains a known resource or module. + * + * @param requires resource to validate + * @param resourceType managed or existing service + * @returns true if the resource exists, false otherwise + * @private + */ + private targetExists(requires: mta.Requires[], resourceType: string): boolean { + return ( + requires && + requires.findIndex( + (requireEle) => + requireEle.parameters?.['content-target'] === true && + this.resources.get(resourceType)?.name === requireEle.name + ) !== -1 + ); + } + + private async loadMTAResources(): Promise { + // Handle resources first, modules need to verify if resources exist + const resources = (await this.mta.getResources()) || []; + resources.forEach((resource) => { + if (resource.parameters?.service) { + if (resource.parameters?.service === 'html5-apps-repo') { + this.resources.set( + resource.parameters['service-plan'] === 'app-host' + ? 'html5-apps-repo:app-host' + : 'html5-apps-repo:app-runtime', + resource + ); + } else if (resource.parameters?.service === 'xsuaa') { + this.resources.set(ManagedXSUAA, resource); + } else if (resource.type === CloudFoundryServiceType.Existing) { + this.resources.set(resource.name, resource); + } else { + this.resources.set(resource.parameters.service, resource); + } + } + }); + this.log?.debug(t('debug.mtaLoaded', { type: 'resources' })); + } + + private async loadMTAModules(): Promise { + const modules = (await this.mta.getModules()) || []; + modules.forEach((module: mta.Module) => { + if (module.type) { + if (module.type === 'html5') { + this.apps.set(module.name, module); + } else if (this.targetExists(module.requires ?? [], 'destination')) { + this.modules.set('com.sap.application.content:destination', module); + } else if (this.targetExists(module.requires ?? [], 'html5-apps-repo:app-host')) { + this.modules.set('com.sap.application.content:resource', module); + } else { + this.modules.set(module.type as ModuleType, module); // i.e. 'approuter.nodejs' + } + } + }); + this.log?.debug(t('debug.mtaLoaded', { type: 'modules' })); + } + + private async addAppContent(): Promise { + if (!this.resources.has('html5-apps-repo:app-host')) { + await this.addHtml5Host(); + } + // Setup the basic module template, artifacts will be added in another step + const appHostName = this.resources.get('html5-apps-repo:app-host')?.name; + if (appHostName) { + const deployer: mta.Module = { + name: `${this.prefix.slice(0, 38)}-app-content`, + type: 'com.sap.application.content', + path: '.', + requires: [ + { + name: appHostName, + parameters: { + 'content-target': true + } + } + ], + 'build-parameters': { + 'build-result': 'resources', + requires: [] + } + }; + await this.mta.addModule(deployer); + this.modules.set('com.sap.application.content:resource', deployer); + this.dirty = true; + } + } + + private async addUaa(): Promise { + const resource: mta.Resource = { + name: `${this.prefix.slice(0, 46)}-uaa`, + type: 'org.cloudfoundry.managed-service', + parameters: { + 'service-plan': 'application', + service: 'xsuaa', + config: { xsappname: this.prefix + '-${space-guid}', 'tenant-mode': 'dedicated' } + } + }; + await this.mta.addResource(resource); + this.resources.set('xsuaa', resource); + this.dirty = true; + } + + private async addHtml5Runtime(): Promise { + const resource: mta.Resource = { + name: `${this.prefix.slice(0, 29)}-html5-repo-runtime`, + type: 'org.cloudfoundry.managed-service', + parameters: { 'service-plan': 'app-runtime', service: 'html5-apps-repo' } + }; + await this.mta.addResource(resource); + this.resources.set('html5-apps-repo:app-runtime', resource); + this.dirty = true; + } + + private async addHtml5Host(): Promise { + const html5host = `${this.prefix.slice(0, 40)}-repo-host`; // Need to cater for -key being added too! + const resource: mta.Resource = { + name: html5host, + type: 'org.cloudfoundry.managed-service', + parameters: { + 'service-name': `${this.prefix.slice(0, 36)}-html5-service`, + 'service-plan': 'app-host', + service: 'html5-apps-repo' + } + }; + await this.mta.addResource(resource); + this.resources.set('html5-apps-repo:app-host', resource); + this.dirty = true; + } + + /** + * Add a destination service to the MTA. + * + * @param isManagedApp - If the destination service is for a managed app + */ + private async addDestinationResource(isManagedApp = false): Promise { + const destinationName = `${this.prefix.slice(0, 30)}-destination-service`; + const resource: mta.Resource = { + name: destinationName, + type: 'org.cloudfoundry.managed-service', + parameters: { + service: 'destination', + 'service-name': destinationName, + 'service-plan': 'lite', + config: { + ...DestinationServiceConfig.config, + ['HTML5Runtime_enabled']: isManagedApp + } + } + }; + await this.mta.addResource(resource); + this.resources.set('destination', resource); + this.dirty = true; + } + + /** + * Update the destination service in the MTA if not already present. + * + * @param isManagedApp - If the destination service is for a managed app, false by default + */ + private async updateDestinationResource(isManagedApp = false): Promise { + const resource = this.resources.get('destination'); + if (resource) { + // Append HTML5Runtime_enabled flag, needs to reflect the router type i.e. managed | standalone + resource.parameters = { + ...(resource.parameters ?? {}), + config: { + ...(resource.parameters?.config ?? {}), + ['HTML5Runtime_enabled']: isManagedApp + } + }; + // Ensure the instance destinations exist + if (!resource.parameters?.config?.init_data?.instance?.destinations) { + resource.parameters.config = { + ...resource.parameters.config, + ...UI5ResourceDestination + }; + } + // Append the UI5 destination if missing + if ( + !resource.parameters?.config?.init_data?.instance?.destinations?.some( + (destination: MTADestinationType) => destination.Name === UI5Destination.Name + ) + ) { + resource.parameters.config.init_data.instance.destinations.push(UI5Destination); + } + await this.mta.updateResource(resource); + this.resources.set('destination', resource); + this.dirty = true; + } + } + + /** + * Update the server module to include the required dependencies to ensure endpoints are secured. + * + * @param moduleType known module type + */ + private async updateServerModule(moduleType: ModuleType): Promise { + // Update the CAP API to only allow xsuaa calls, this requires the security.json to be present + const uaaResource = this.resources.get(ManagedXSUAA); + const serverModule = this.modules.get(moduleType); + if (serverModule) { + // Ensure the server module is providing srv-api + if (!serverModule.provides?.some((ele) => ele.name === SRV_API)) { + // Add it back for all this to work! + serverModule.provides = [...(serverModule.provides ?? []), ...[ServiceAPIRequires]]; + } + // Ensure there is an xsuaa instance to allow the xs-app.json to use xsuaa to lockdown the endpoints + if (uaaResource && !serverModule.requires?.some((ele) => ele.name === uaaResource.name)) { + serverModule.requires = [...(serverModule.requires ?? []), ...[{ name: uaaResource.name }]]; + } + await this.mta.updateModule(serverModule); + this.modules.set(moduleType, serverModule); + this.dirty = true; + } + } + + private async addManagedUaa(): Promise { + const resource: mta.Resource = { + name: `${this.prefix.slice(0, 46)}-uaa`, + type: 'org.cloudfoundry.managed-service', + parameters: { + path: './xs-security.json', + service: 'xsuaa', + 'service-name': `${this.prefix}-xsuaa-srv`, + 'service-plan': 'application' + } + }; + await this.mta.addResource(resource); + this.resources.set(ManagedXSUAA, resource); + this.dirty = true; + } + + /** + * Verify if the destination is valid and if WebIDEUsage is set to ODATA_GENERIC or ODATA_ABAP. + * + * @param {MTADestinationType} destination - destination object + * @returns {boolean} - true if the destination is valid, false otherwise + */ + private isODataDestination(destination: Destination): boolean { + return isGenericODataDestination(destination) || isAbapEnvironmentOnBtp(destination); + } + + /** + * + * @private + * @returns {Promise} A promise that resolves when the change request has been processed. + */ + private async cleanupMissingResources(): Promise { + if (!this.modules.has('com.sap.application.content:resource')) { + await this.addAppContent(); + } + + // For Approuter Configuration generators, the destination resource is missing for both Standalone | Managed + if (this.resources.get('destination')) { + // Ensure the resource is added + await this.updateDestinationResource(this.modules.has('com.sap.application.content:destination')); + } else { + // No destination resource found, add it, more common for standalone + await this.addDestinationResource(this.modules.has('com.sap.application.content:destination')); + } + } + + /** + * Returns the MTA prefix, read from the MTA ID. + * + * @returns {string} the MTA ID + */ + public get prefix(): string { + return this.mtaId; + } + + /** + * Returns the path to the standalone approuter module. + * + * @returns {string | undefined} the MTA ID + */ + public get standaloneRouterPath(): string | undefined { + return this.modules.get('approuter.nodejs')?.path; + } + + /** + * Returns the cloud service name, read from the content module which contains destinations. + * + * @returns {string | undefined} the cloud service name + */ + public get cloudServiceName(): string | undefined { + let cloudServiceName; + this.modules.forEach((contentModule) => { + const moduleDestinations: MTADestinationType[] = + contentModule.parameters?.content?.instance?.destinations || []; + if (contentModule.type === 'com.sap.application.content' && moduleDestinations.length) { + // In theory, if there is more than one, it should be same! + moduleDestinations.some((destination: MTADestinationType) => { + cloudServiceName = destination['sap.cloud.service'] || undefined; + if (cloudServiceName) { + return true; // breakout + } + }); + } + }); + return cloudServiceName; + } + + /** + * Returns the mta parameters. + * + * @returns {Promise} the MTA parameters + */ + public async getParameters(): Promise { + return this.mta.getParameters(); + } + + /** + * Returns the mta build parameters. + * + * @returns {Promise} the MTA build parameters + */ + public async getBuildParameters(): Promise { + return this.mta.getBuildParameters(); + } + + /** + * Update the MTA parameters. + * + * @param parameters the MTA parameters being applied + * @returns {Promise} A promise that resolves when the change request has been processed. + */ + public async updateParameters(parameters: mta.Parameters): Promise { + await this.mta.updateParameters(parameters); + } + + /** + * Update the MTA build parameters i.e. build-parameters -> before-all. + * + * @param parameters the MTA build parameters being applied + * @returns {Promise} A promise that resolves when the change request has been processed. + */ + public async updateBuildParams(parameters: mta.ProjectBuildParameters): Promise { + await this.mta.updateBuildParameters(parameters); + } + + /** + * Append the UI5 app to the MTA. + * + * @param {string} appModule the name of the app module i.e. myui5app + * @param {string} appPath path to the UI5 app i.e. ./apps/myui5app + * @returns {Promise} A promise that resolves when the change request has been processed. + */ + public async addApp(appModule: string, appPath: string): Promise { + // If an existing content module exists whether standalone/managed, append the new artifact + const contentModule = this.modules.get('com.sap.application.content:resource'); + if (contentModule) { + contentModule[MTABuildParams] = contentModule[MTABuildParams] ?? {}; + contentModule[MTABuildParams][MTABuildResult] = + contentModule[MTABuildParams]?.[MTABuildResult] ?? `resources`; // Default + contentModule[MTABuildParams].requires = contentModule[MTABuildParams].requires ?? []; + if ( + contentModule[MTABuildParams].requires?.findIndex( + (app: mta.Requires) => app.name === appModule.slice(0, 128) + ) === -1 + ) { + contentModule[MTABuildParams].requires.push({ + name: appModule.slice(0, 128), + artifacts: [`${appModule.slice(0, 128)}.zip`], + 'target-path': `${contentModule[MTABuildParams][MTABuildResult]}/` + }); + } + await this.mta.updateModule(contentModule); + } + + // Add application module + const html5Module = this.apps.get(appModule); + if (!html5Module) { + const app: HTML5App = { + name: appModule.slice(0, 50), + type: 'html5', + path: appPath, + 'build-parameters': HTMLAppBuildParams as HTML5App['build-parameters'] + }; + await this.mta.addModule(app); + this.apps.set(appModule, app); + } + this.dirty = true; + } + + /** + * Append the connectivity service to the list of resources. + * + * @returns {Promise} A promise that resolves when the change request has been processed. + */ + public async addConnectivityResource(): Promise { + const serviceType: ResourceType = 'connectivity'; + const resourceType = CloudFoundryServiceType.Managed; + const resourceName = `${this.prefix.slice(0, 37)}-connectivity`; + + const router = this.modules.get('approuter.nodejs'); + if (router) { + if (router.requires?.findIndex((resource) => resource.name === resourceName) === -1) { + router.requires.push({ name: resourceName }); + await this.mta.updateModule(router); + } + } + + const connectivityResource: mta.Resource = { + name: resourceName, + type: resourceType, + parameters: { + service: serviceType, + 'service-plan': 'lite' + } + }; + + if (!this.resources.has(serviceType)) { + await this.mta.addResource(connectivityResource); + this.resources.set(serviceType, connectivityResource); + } + this.dirty = true; + } + + /** + * Append and/or cleanup the destination resource if missing in mta.yaml. + * + * @param {boolean} isManagedApp - if true, append managed approuter configuration + * @returns {Promise} - A promise that resolves when the change request has been processed. + */ + public async addRoutingModules(isManagedApp = false): Promise { + if (isManagedApp && !this.modules.has('com.sap.application.content:destination')) { + await this.addManagedAppRouter(); + } + + await this.cleanupMissingResources(); + + // Handle standalone | managed + for (const module of [ + this.modules.get('com.sap.application.content:destination'), + this.modules.get('approuter.nodejs') + ].filter((elem) => elem !== undefined)) { + const destinationName = + this.resources.get('destination')?.name ?? `${this.prefix.slice(0, 30)}-destination-service`; + if (module?.requires?.findIndex((app) => app.name === destinationName) === -1) { + if (module.type === 'approuter.nodejs') { + module.requires.push({ + name: destinationName, + ...UI5StandaloneModuleDestination + }); + } + if (module.type === 'com.sap.application.content') { + module.requires.push({ + name: destinationName, + parameters: { + 'content-target': true + } + }); + } + await this.mta.updateModule(module); + this.dirty = true; + } + } + } + + /** + * Append ABAP service to the modules and resources. + * + * @param {string} serviceName The name of the service i.e. myabapservice-abap-service + * @param {string} btpService The SAP BTP service i.e. xsuaa | html5-apps-repo | app-host | destination + * @returns {Promise} A promise that resolves when the change request has been processed. + */ + public async addAbapService(serviceName: string, btpService: string): Promise { + const newResourceName = `${this.prefix.slice(0, 24)}-abap-${serviceName.slice(0, 20)}`; + const router = this.modules.get('approuter.nodejs'); + if (router) { + if (router.requires?.findIndex((resource) => resource.name === newResourceName) === -1) { + router.requires.push({ name: newResourceName }); + await this.mta.updateModule(router); + } + } + const abapServiceResource: mta.Resource = { + name: newResourceName, + type: CloudFoundryServiceType.Existing, + parameters: { + 'service-name': serviceName, + protocol: ['ODataV2'], + service: btpService, + 'service-plan': '16_abap_64_db' + } + }; + + if (!this.resources.has(newResourceName)) { + await this.mta.addResource(abapServiceResource); + this.resources.set(newResourceName, abapServiceResource); + } + this.dirty = true; + } + + /** + * Validate if mta contains an ABAP resource. + * + * @returns {boolean} true | false if mta.yaml contains an abap resource + */ + public get isABAPServiceFound(): boolean { + let isAbapDirectServiceBinding = false; + const resourceNames = Array.from(this.resources.keys()); + for (const resourceName of resourceNames) { + if (resourceName.includes(`${this.prefix}-abap-`)) { + isAbapDirectServiceBinding = true; + break; + } + } + return isAbapDirectServiceBinding; + } + + /** + * Append the standalone app router module. + * + * @param {boolean} fromServerGenerator If true, the request is from the server generator, so the path changes. + * @returns {Promise} A promise that resolves when the change request has been processed. + */ + public async addStandaloneRouter(fromServerGenerator = false): Promise { + if (!this.resources.has('xsuaa')) { + await this.addUaa(); + } + if (!this.resources.has('html5-apps-repo:app-runtime')) { + await this.addHtml5Runtime(); + } + if (!this.resources.has('destination')) { + await this.addDestinationResource(); + } + + const appRuntimeName = this.resources.get('html5-apps-repo:app-runtime')?.name; + const xsuaaName = this.resources.get('xsuaa')?.name; + const destinationName = this.resources.get('destination')?.name; + if (destinationName && xsuaaName && appRuntimeName) { + const router: mta.Module = { + name: `${this.prefix.slice(0, 43)}-router`, + type: 'approuter.nodejs', + path: fromServerGenerator ? `${RouterModule}` : `${CloudFoundry}/${RouterModule}`, + parameters: { + 'disk-quota': '256M', + memory: '256M' + }, + requires: [ + { name: appRuntimeName }, + { name: xsuaaName }, + { + name: destinationName, + group: 'destinations', + properties: { name: 'ui5', url: 'https://ui5.sap.com', forwardAuthToken: false } + } + ] + }; + await this.mta.addModule(router); + this.modules.set('approuter.nodejs', router); + this.dirty = true; + } + } + + /** + * Validate if mta contains an XSUAA resource. + * + * @returns {boolean} true if the mta contains an XSUAA resource + */ + public hasManagedXsuaaResource(): boolean { + return this.resources.has(ManagedXSUAA); + } + + /** + * Add an mta extension config, either creating a new mtaext file or extending an existing one. + * + * @param {string} instanceDestName The name of the instance destination that will be created + * @param {string} destUrl The URL of the instance destination that will be created, usually the url base, the service path is provided by the manifest.json + * @param headerConfig The additional header config of the instance destination + * @param {string} headerConfig.key The key of the header config + * @param {string} headerConfig.value The value of the header config + * @returns {Promise} A promise that resolves when the change request has been processed. + * @see https://help.sap.com/docs/SAP_HANA_PLATFORM/4505d0bdaf4948449b7f7379d24d0f0d/51ac525c78244282919029d8f5e2e35d.html?locale=en-US&version=2.0.00 + */ + public async addMtaExtensionConfig( + instanceDestName: string | undefined, + destUrl: string, + headerConfig: { key: string; value: string } + ): Promise { + /** + * This does not use mta.lib to create the mtaext file as it does not support configurable mta config file names e.g. *.mtaext + * And it will merge resources found in existing mtaext files rather than allow them to be written back to the existing file + * To add additional destination instances a destination service must exist in mta.yaml resources. + * + * 1) Find an existing destination service + * 2) Use the existing destination service from the mta.yaml (type dest see https://github.com/SAP-samples/fiori-tools-samples/blob/main/cap/cap-fiori-mta/mta.yaml#L87) + * 3) Create or update the mtaext config + */ + + let destinationServiceName = this.resources.get('destination')?.name; + if (!destinationServiceName) { + this.log?.info(t('info.existingDestinationNotFound')); + destinationServiceName = `${this.prefix}-destination-service`; + } + + const appMtaId = this.mtaId; + const mtaExtFilePath = join(this.mta.mtaDirPath, MTAFileExtension); + let mtaExtensionYamlFile; + + try { + const mtaExtContents = readFileSync(mtaExtFilePath, 'utf-8'); + mtaExtensionYamlFile = await YamlDocument.newInstance(mtaExtContents); + } catch (err) { + // File does not exist or cannot be parsed, either way we create a new one + this.log?.info(t('info.existingMTAExtensionNotFound')); + } + + // Create a new mta extension file + if (!mtaExtensionYamlFile) { + const mtaExt = { + appMtaId, + mtaExtensionId: `${appMtaId}-ext`, + destinationName: instanceDestName, + destinationUrl: destUrl, + headerKey: headerConfig.key, + headerValue: headerConfig.value, + destinationServiceName: destinationServiceName, + mtaVersion: MTAVersion + }; + const mtaExtTemplate = readFileSync(join(__dirname, `../../templates/app/${MTAFileExtension}`), 'utf-8'); + writeFileSync(mtaExtFilePath, render(mtaExtTemplate, mtaExt)); + this.log?.info(t('info.mtaExtensionCreated', { appMtaId, mtaExtFile: MTAFileExtension })); + } else { + // Create an entry in an existing mta extension file + const resources: YAMLSeq = mtaExtensionYamlFile.getSequence({ path: 'resources' }); + const resIdx = resources.items.findIndex((item) => { + return (item as YAMLMap).get('name') === destinationServiceName; + }); + if (resIdx > -1) { + const nodeToInsert = { + Authentication: 'NoAuthentication', + Name: instanceDestName, + ProxyType: `Internet`, + Type: `HTTP`, + URL: destUrl, + [`URL.headers.${headerConfig.key}`]: headerConfig.value + }; + mtaExtensionYamlFile.appendTo({ + path: `resources.${resIdx}.parameters.config.init_data.instance.destinations`, + value: nodeToInsert + }); + writeFileSync(mtaExtFilePath, mtaExtensionYamlFile.toString()); + this.log?.info(t('info.mtaExtensionUpdated', { mtaExtFile: MTAFileExtension })); + } else { + this.log?.error(t('error.updatingMTAExtensionFailed', { mtaExtFilePath })); + } + } + } + + /** + * Append a destination instance to the mta.yaml file, required by consumers of CAP services (e.g. approuter, destinations). + * + * @param {string} cfDestination The new destination instance name + * @returns {Promise} A promise that resolves when the change request has been processed + */ + public async appendInstanceBasedDestination(cfDestination: string | undefined): Promise { + // Part 1. Update the destination service with the new instance based destination + const destinationResource = this.resources.get('destination'); + if (destinationResource) { + if (!destinationResource.requires?.some((ele) => ele.name === SRV_API)) { + destinationResource.requires = [ + ...(destinationResource.requires ?? []), + ...[ + { + name: SRV_API + } + ] + ]; + } + // If the destination provided is `fiori-default-srv-api` then use the default destination name + const capDestName = + cfDestination === DefaultMTADestination + ? this.getFormattedPrefix(ResourceMTADestination) + : cfDestination; + // Ensure the destination does not exist already! + if ( + !destinationResource.parameters?.config?.init_data?.instance?.destinations?.some( + (destination: MTADestinationType) => destination.Name === capDestName + ) + ) { + destinationResource.parameters?.config?.init_data?.instance?.destinations?.push({ + ...MTAAPIDestination, + Name: capDestName + }); + } + await this.mta.updateResource(destinationResource); + this.resources.set('destination', destinationResource); + // Only make additional modifications if the MTA destination is added + await this.updateServerModule( + this.modules.has('nodejs') ? ('nodejs' as ModuleType) : ('java' as ModuleType) + ); + this.dirty = true; + } + } + + /** + * Save changes to the mta.yaml file. + * + * @returns {Promise} return current state read state. + */ + public async save(): Promise { + if (this.dirty) { + await this.mta.save(); + } + return this.dirty; + } + + /** + * Add a managed app router to the MTA. + * + * @returns {Promise} A promise that resolves when the change request has been processed. + */ + public async addManagedAppRouter(): Promise { + if (!this.resources.has('destination')) { + await this.addDestinationResource(true); + } + if (!this.resources.has(ManagedXSUAA)) { + await this.addManagedUaa(); + } + if (!this.resources.has('html5-apps-repo:app-host')) { + await this.addHtml5Host(); + } + + const destinationName = this.resources.get('destination')?.name; + const appHostName = this.resources.get('html5-apps-repo:app-host')?.name; + const appHostServiceName = this.resources.get('html5-apps-repo:app-host')?.parameters?.['service-name']; + const managedXSUAAName = this.resources.get(ManagedXSUAA)?.name; + const managedXSUAAServiceName = this.resources.get(ManagedXSUAA)?.parameters?.['service-name']; + if (destinationName && appHostName && managedXSUAAName && managedXSUAAServiceName) { + const router: mta.Module = { + name: `${this.prefix.slice(0, 30)}-destination-content`, + type: 'com.sap.application.content', + requires: [ + { + name: destinationName, + parameters: { + 'content-target': true + } + }, + { + name: appHostName, + parameters: { + 'service-key': { + name: `${appHostName}-key` + } + } + }, + { + name: managedXSUAAName, + parameters: { + 'service-key': { + name: `${managedXSUAAName}-key` + } + } + } + ], + parameters: { + content: { + instance: { + destinations: [ + { + Name: `${this.prefix.slice(0, 35)}_html_repo_host`, + ServiceInstanceName: appHostServiceName, + ServiceKeyName: `${appHostName}-key`, + 'sap.cloud.service': `${this.prefix}` + }, + { + Authentication: 'OAuth2UserTokenExchange', + Name: `${this.prefix.slice(0, 46)}_uaa`, + ServiceInstanceName: managedXSUAAServiceName, + ServiceKeyName: `${managedXSUAAName}-key`, + 'sap.cloud.service': `${this.prefix}` + } + ], + 'existing_destinations_policy': 'ignore' + } + } + }, + 'build-parameters': { + 'no-source': true + } + }; + await this.mta.addModule(router); + this.modules.set('com.sap.application.content:destination', router); + this.dirty = true; + } + } + + /** + * Get the exposed destinations, read from the mta.yaml. + * + * @param {boolean} checkWebIDEUsage - check if the destination contains WebIDEUsage property odata_gen or odata_abap + * @returns {string[]} Return a list of destination names read from the mta.yaml + */ + public getExposedDestinations(checkWebIDEUsage = false): string[] { + const exposedDestinations: string[] = []; + // Pull destinations from two places: + // 1. Resources + const destinationResources = this.resources.get('destination'); + if (destinationResources) { + // instance + destinationResources.parameters?.config?.init_data?.instance?.destinations?.forEach( + (dest: Destination) => + (checkWebIDEUsage ? this.isODataDestination(dest) : true) && exposedDestinations.push(dest.Name) + ); + // subaccount + destinationResources.parameters?.config?.init_data?.subaccount?.destinations?.forEach( + (dest: Destination) => + (checkWebIDEUsage ? this.isODataDestination(dest) : true) && exposedDestinations.push(dest.Name) + ); + } + + // 2. Modules + const destinationModules = this.modules.get('com.sap.application.content:destination'); + if (destinationModules) { + destinationModules.parameters?.content?.instance?.destinations?.map( + (dest: Destination) => + (checkWebIDEUsage ? this.isODataDestination(dest) : true) && exposedDestinations.push(dest.Name) + ); + } + return exposedDestinations; + } + + /** + * Format the string with the mta prefix, read from the mta.yaml file. + * + * @param {string} formatString format string i.e. `%s-srv-api` becomes `mymtaid-srv-api` + * @returns {string} return a formatted prefix value. + */ + public getFormattedPrefix(formatString: string): string { + return format(formatString, this.prefix).replace(/[^\w-]/g, '_'); + } +} + +/** + * Returns true if there's an MTA configuration file in the supplied directory. + * + * @param {string} dir directory to check for MTA configuration file + * @returns {boolean} true | false if MTA configuration file is found + */ +export function isMTAFound(dir: string): boolean { + return existsSync(join(dir, MTAYamlFile)); +} + +/** + * Returns true if there's an MTA configuration file in the supplied directory and contains an ABAP service binding. + * + * @param {string} appPath UI5 Fiori project folder path + * @param {boolean} findMtaPath If findMtaPath=true, need to validate if the Fiori app is inside MTA project. + * @param {string} mtaPath If findMtaPath=false, the generator already knows if the Fiori app + * @param {Logger} logger - option logger instance + * @returns {boolean} true if mta.yaml is found and ABAP service binding is found + */ +export async function useAbapDirectServiceBinding( + appPath: string, + findMtaPath: boolean, + mtaPath = '', + logger?: Logger +): Promise { + try { + let rootPath; + if (findMtaPath) { + const foundMtaPath = await getMtaPath(appPath); + if (foundMtaPath) { + rootPath = dirname(foundMtaPath.mtaPath); + } + } else if (mtaPath) { + rootPath = dirname(mtaPath); + } + + if (rootPath) { + const mtaConfig = await MtaConfig.newInstance(rootPath, logger); + return mtaConfig.isABAPServiceFound; + } else { + return false; + } + } catch (error) { + logger?.debug(t('debug.logError', { error, method: 'useAbapDirectServiceBinding' })); + return false; + } +} diff --git a/packages/cf-deploy-config-writer/src/translations/cf-deploy-config-writer.i18n.json b/packages/cf-deploy-config-writer/src/translations/cf-deploy-config-writer.i18n.json new file mode 100644 index 0000000000..c086be3905 --- /dev/null +++ b/packages/cf-deploy-config-writer/src/translations/cf-deploy-config-writer.i18n.json @@ -0,0 +1,25 @@ +{ + "debug": { + "logError": "{{method}} error found {{ error }}", + "mtaLoaded": "MTA {{ type }} loaded", + "ui5YamlDoesNotExist": "File ui5.yaml does not exist in the project" + }, + "error": { + "unableToLoadMTA": "Unable to load mta.yaml configuration", + "updatingMTAExtensionFailed": "Unable to add mta extension configuration to file: {{mtaExtFilePath}}", + "cannotFindBinary": "Cannot find the \"{{bin}}\" executable. Please add it to the path or use \"npm i -g {{pkg}}\" to install it.", + "mtaExtensionFailed": "Unable to create or update the mta extension file for Api Hub Enterprise destination configuration: {{error}}", + "serviceKeyFailed": "Failed to fetch service key", + "missingMtaParameters": "Missing required parameters, MTA path, MTA ID or router type is missing", + "invalidMtaIdWithChars": "The MTA ID can only contain letters, numbers, dashes, periods, underscores", + "invalidMtaId": "The MTA ID must start with a letter or underscore and be less than 128 characters long", + "missingABAPServiceBindingDetails": "Missing ABAP service details for direct service binding", + "mtaAlreadyExists": "A folder with same name already exists in the target directory" + }, + "info":{ + "existingMTAExtensionNotFound": "Cannot find a valid existing mta extension file, a new one will be created", + "existingDestinationNotFound": "A destination service resource cannot be found in the mta.yaml. An mta extension destination instance cannot be added", + "mtaExtensionCreated": "Created file: {{mtaExtFile}} to extend mta module {{appMtaId}} with destination configuration", + "mtaExtensionUpdated": "Updated file: {{mtaExtFile}} with module destination configuration" + } +} diff --git a/packages/cf-deploy-config-writer/src/types/index.ts b/packages/cf-deploy-config-writer/src/types/index.ts new file mode 100644 index 0000000000..de8e875b5b --- /dev/null +++ b/packages/cf-deploy-config-writer/src/types/index.ts @@ -0,0 +1,105 @@ +import type { Destination, Authentication } from '@sap-ux/btp-utils'; + +export type ResourceType = + | 'xsuaa' + | 'destination' + | 'portal' + | 'connectivity' + | 'managed:xsuaa' + | 'html5-apps-repo:app-host' + | 'html5-apps-repo:app-runtime'; +export type ModuleType = + | 'hdb' + | 'nodejs' + | 'java' + | 'approuter.nodejs' + | 'com.sap.application.content' + | 'com.sap.application.content:destination' + | 'com.sap.application.content:resource' + | 'html5' + | 'com.sap.portal.content'; +export enum CloudFoundryServiceType { + Existing = 'org.cloudfoundry.existing-service', + Managed = 'org.cloudfoundry.managed-service' +} +export type MTADestinationType = Destination & { + ServiceInstanceName: string; + ServiceKeyName: string; + 'sap.cloud.service': string; +}; +export enum RouterModuleType { + Standard = 'standard', + Managed = 'managed' +} +export interface MTABaseConfig { + mtaId: string; + mtaPath: string; + mtaDescription?: string; + mtaVersion?: string; +} +export interface CFBaseConfig extends MTABaseConfig { + routerType: RouterModuleType; + addConnectivityService?: boolean; + addDestinationService?: boolean; + abapServiceProvider?: { + abapServiceName?: string; + abapService?: string; + }; +} +export interface CFAppConfig { + appPath: string; + addManagedAppRouter?: boolean; // Enabled by default + destinationName?: string; + apiHubConfig?: ApiHubConfig; + serviceHost?: string; // Data service host + lcapMode?: boolean; +} +export interface CFConfig extends CFAppConfig, CFBaseConfig { + appId: string; + rootPath: string; + serviceHost: string; + capRoot?: string; + isCap?: boolean; + servicePath?: string; + firstServicePathSegment?: string; + isFullUrlDest?: boolean; + destinationAuthType?: Authentication; + cloudServiceName?: string; + isMtaRoot?: boolean; +} +export const enum ApiHubType { + apiHub = 'API_HUB', + apiHubEnterprise = 'API_HUB_ENTERPRISE' +} +export interface ApiHubConfig { + apiHubKey: string; + apiHubType: ApiHubType; +} +export interface XSAppRoute { + source?: string; + target?: string; + destination?: string; + csrfProtection?: boolean; + scope?: string; + service?: string; + endpoint?: string; + authenticationType?: string; + dependency?: string; +} +export type XSAppRouteProperties = keyof XSAppRoute; +export interface XSAppDocument { + authenticationMethod?: string; + routes?: XSAppRoute[]; + welcomeFile?: string; +} +export interface HTML5App { + path: string; + 'build-parameters': { + builder: string; + 'build-result': string; + commands: string[]; + 'supported-platforms': string[]; + }; + name: string; + type: string; +} diff --git a/packages/cf-deploy-config-writer/src/utils.ts b/packages/cf-deploy-config-writer/src/utils.ts new file mode 100644 index 0000000000..8c57a5e948 --- /dev/null +++ b/packages/cf-deploy-config-writer/src/utils.ts @@ -0,0 +1,163 @@ +import { join, normalize, posix } from 'path'; +import { coerce, satisfies } from 'semver'; +import { isAppStudio, listDestinations, isFullUrlDestination, Authentication } from '@sap-ux/btp-utils'; +import { addPackageDevDependency, type Manifest } from '@sap-ux/project-access'; +import { + MbtPackage, + MbtPackageVersion, + MTAVersion, + Rimraf, + RimrafVersion, + UI5BuilderWebIdePackage, + UI5BuilderWebIdePackageVersion, + UI5TaskZipperPackage, + UI5TaskZipperPackageVersion, + XSSecurityFile +} from './constants'; +import type { Editor } from 'mem-fs-editor'; +import type { Destinations } from '@sap-ux/btp-utils'; +import type { MTABaseConfig } from './types'; + +let cachedDestinationsList: Destinations = {}; + +/** + * Read manifest file for processing. + * + * @param manifestPath Path to the manifest file + * @param fs reference to a mem-fs editor + * @returns Manifest object + */ +export async function readManifest(manifestPath: string, fs: Editor): Promise { + return fs.readJSON(manifestPath) as unknown as Manifest; +} + +/** + * Locates template files relative to the dist folder. + * This helps to locate templates when this module is bundled and the dir structure is flattened, maintaining the relative paths. + * + * @param relativeTemplatePath - optional, the path of the required template relative to the ./templates folder. If not specified the root templates folder is returned. + * @returns the path of the template specified or templates root folder + */ +export function getTemplatePath(relativeTemplatePath: string = ''): string { + return join(__dirname, '../templates', relativeTemplatePath); +} + +/** + * Convert an app name to an MTA ID that is suitable for CF deployment. + * + * @param id Name of the app, like `sap.ux.app` + * @returns Name that's acceptable in an mta.yaml + */ +export function toMtaModuleName(id: string): string { + return id.replace(/[`~!@#$%^&*()_|+\-=?;:'",.<>]/gi, ''); +} + +/** + * Return a consistent file path across different platforms. + * + * @param dirPath Path to the directory + * @returns Path to the directory with consistent separators + */ +export function toPosixPath(dirPath: string): string { + return normalize(dirPath).split(/[\\/]/g).join(posix.sep); +} + +/** + * Get the destination properties, based on the destination value. + * + * @param destination destination name + * @returns Destination properties, default properties returned if not found + */ +export async function getDestinationProperties( + destination: string | undefined +): Promise<{ isFullUrlDest: boolean; destinationAuthType: Authentication }> { + const destinationProperties = { + isFullUrlDest: false, + destinationAuthType: Authentication.NO_AUTHENTICATION + }; + if (isAppStudio() && destination) { + const destinations = await getBTPDestinations(); + if (destinations[destination]) { + destinationProperties.isFullUrlDest = isFullUrlDestination(destinations[destination]); + destinationProperties.destinationAuthType = destinations[destination].Authentication as Authentication; + } + } + return destinationProperties; +} + +/** + * Retrieve the list of destinations from SAP BTP. + * + * @returns Destinations list + */ +export async function getBTPDestinations(): Promise { + if (Object.keys(cachedDestinationsList).length === 0) { + cachedDestinationsList = await listDestinations(); + } + return cachedDestinationsList; +} + +/** + * Validates the MTA version passed in the config. + * + * @param mtaVersion MTA version + * @returns true if the version is valid + */ +export function validateVersion(mtaVersion?: string): boolean { + const version = coerce(mtaVersion); + if ((mtaVersion && !version) || (version && !satisfies(version, `>=${MTAVersion}`))) { + throw new Error('Invalid MTA version specified. Please use version 0.0.1 or higher.'); + } + return true; +} + +/** + * Append xs-security.json to project folder. + * + * @param root0 MTA base configuration + * @param root0.mtaPath Path to the MTA project + * @param root0.mtaId MTA ID + * @param fs reference to a mem-fs editor + */ +export function addXSSecurityConfig({ mtaPath, mtaId }: MTABaseConfig, fs: Editor): void { + fs.copyTpl(getTemplatePath(`common/${XSSecurityFile}`), join(mtaPath, XSSecurityFile), { + id: mtaId.slice(0, 100) + }); +} + +/** + * Append .gitignore to project folder. + * + * @param targetPath Path to the project folder + * @param fs reference to a mem-fs editor + */ +export function addGitIgnore(targetPath: string, fs: Editor): void { + fs.copyTpl(getTemplatePath('gitignore.tmpl'), join(targetPath, '.gitignore'), {}); +} + +/** + * Append server package.json to project folder. + * + * @param root0 MTA base configuration + * @param root0.mtaPath Path to the MTA project + * @param root0.mtaId MTA ID + * @param fs reference to a mem-fs editor + */ +export function addRootPackage({ mtaPath, mtaId }: MTABaseConfig, fs: Editor): void { + fs.copyTpl(getTemplatePath('package.json'), join(mtaPath, 'package.json'), { + mtaId: mtaId + }); +} + +/** + * Add common dependencies to the HTML5 app package.json. + * + * @param targetPath Path to the package.json file + * @param fs reference to a mem-fs editor + */ +export async function addCommonPackageDependencies(targetPath: string, fs: Editor): Promise { + await addPackageDevDependency(targetPath, Rimraf, RimrafVersion, fs); + await addPackageDevDependency(targetPath, MbtPackage, MbtPackageVersion, fs); + await addPackageDevDependency(targetPath, UI5BuilderWebIdePackage, UI5BuilderWebIdePackageVersion, fs); + await addPackageDevDependency(targetPath, UI5TaskZipperPackage, UI5TaskZipperPackageVersion, fs); +} diff --git a/packages/cf-deploy-config-writer/templates/app/mta-ext.mtaext b/packages/cf-deploy-config-writer/templates/app/mta-ext.mtaext new file mode 100644 index 0000000000..fec340f9be --- /dev/null +++ b/packages/cf-deploy-config-writer/templates/app/mta-ext.mtaext @@ -0,0 +1,25 @@ +## SAP UX Tools generated mtaext file +_schema-version: "3.2" +ID: <%- mtaExtensionId %> +extends: <%- appMtaId %> +version: <%- mtaVersion %> + +resources: +- name: <%- destinationServiceName %> + parameters: + config: + init_data: + instance: + destinations: + - Authentication: NoAuthentication + Name: <%- destinationName %> + ProxyType: Internet + Type: HTTP + URL: <%- destinationUrl %> + URL.headers.<%- headerKey %>: <%- headerValue %> + - Authentication: NoAuthentication + Name: ui5 + Type: HTTP + URL: https://ui5.sap.com + ProxyType: Internet + existing_destinations_policy: update diff --git a/packages/cf-deploy-config-writer/templates/app/mta.yaml b/packages/cf-deploy-config-writer/templates/app/mta.yaml new file mode 100644 index 0000000000..d0804b2bf0 --- /dev/null +++ b/packages/cf-deploy-config-writer/templates/app/mta.yaml @@ -0,0 +1,12 @@ +_schema-version: "3.2" +ID: <%- id %> +description: <%- mtaDescription %> +version: <%- mtaVersion %> + +parameters: + enable-parallel-deployments: true + deploy_mode: html5-repo + +modules: [] + +resources: [] diff --git a/packages/cf-deploy-config-writer/templates/app/xs-app-destination.json b/packages/cf-deploy-config-writer/templates/app/xs-app-destination.json new file mode 100644 index 0000000000..f752def861 --- /dev/null +++ b/packages/cf-deploy-config-writer/templates/app/xs-app-destination.json @@ -0,0 +1,31 @@ +{ + "welcomeFile": "/index.html", + "authenticationMethod": "route", + "routes": [ + { + "source": "^<%- servicePathSegment %>/(.*)$", + "target": "<%- targetPath %>", + "destination": "<%- destination %>", + "authenticationType": "<%- authentication %>", + "csrfProtection": false + }, + { + "source": "^/resources/(.*)$", + "target": "/resources/$1", + "authenticationType": "none", + "destination": "ui5" + }, + { + "source": "^/test-resources/(.*)$", + "target": "/test-resources/$1", + "authenticationType": "none", + "destination": "ui5" + }, + { + "source": "^(.*)$", + "target": "$1", + "service": "html5-apps-repo-rt", + "authenticationType": "xsuaa" + } + ] +} diff --git a/packages/cf-deploy-config-writer/templates/app/xs-app-no-destination.json b/packages/cf-deploy-config-writer/templates/app/xs-app-no-destination.json new file mode 100644 index 0000000000..f2094a35c8 --- /dev/null +++ b/packages/cf-deploy-config-writer/templates/app/xs-app-no-destination.json @@ -0,0 +1,24 @@ +{ + "welcomeFile": "/index.html", + "authenticationMethod": "route", + "routes": [ + { + "source": "^/resources/(.*)$", + "target": "/resources/$1", + "authenticationType": "none", + "destination": "ui5" + }, + { + "source": "^/test-resources/(.*)$", + "target": "/test-resources/$1", + "authenticationType": "none", + "destination": "ui5" + }, + { + "source": "^(.*)$", + "target": "$1", + "service": "html5-apps-repo-rt", + "authenticationType": "xsuaa" + } + ] +} diff --git a/packages/cf-deploy-config-writer/templates/common/xs-security.json b/packages/cf-deploy-config-writer/templates/common/xs-security.json new file mode 100644 index 0000000000..e60009700a --- /dev/null +++ b/packages/cf-deploy-config-writer/templates/common/xs-security.json @@ -0,0 +1,7 @@ +{ + "xsappname": "<%- id %>", + "tenant-mode": "dedicated", + "description": "Security profile of called application", + "scopes": [], + "role-templates": [] +} diff --git a/packages/cf-deploy-config-writer/templates/gitignore.tmpl b/packages/cf-deploy-config-writer/templates/gitignore.tmpl new file mode 100644 index 0000000000..647906e4ca --- /dev/null +++ b/packages/cf-deploy-config-writer/templates/gitignore.tmpl @@ -0,0 +1,10 @@ +node_modules/ +dist/ +.scp/ +.env +Makefile*.mta +mta_archives +mta-* +resources +archive.zip +.*_mta_build_tmp \ No newline at end of file diff --git a/packages/cf-deploy-config-writer/templates/package.json b/packages/cf-deploy-config-writer/templates/package.json new file mode 100644 index 0000000000..0724e87104 --- /dev/null +++ b/packages/cf-deploy-config-writer/templates/package.json @@ -0,0 +1,15 @@ +{ + "name": "mta-project", + "version": "0.0.1", + "description": "Build and deployment scripts", + "scripts": { + "clean": "rimraf resources mta_archives mta-op*", + "build": "rimraf resources mta_archives && mbt build --mtar archive", + "deploy": "cf deploy mta_archives/archive.mtar --retries 1", + "undeploy": "cf undeploy <%- mtaId %> --delete-services --delete-service-keys --delete-service-brokers" + }, + "devDependencies": { + "mbt": "^1.2.27", + "rimraf": "^5.0.5" + } +} diff --git a/packages/cf-deploy-config-writer/templates/router/package.json b/packages/cf-deploy-config-writer/templates/router/package.json new file mode 100644 index 0000000000..50f3e8353a --- /dev/null +++ b/packages/cf-deploy-config-writer/templates/router/package.json @@ -0,0 +1,18 @@ +{ + "name": "app-router", + "private": true, + "description": "App router", + "engines": { + "node": ">= 16.0.0" + }, + "scripts": { + "start": "node node_modules/@sap/approuter/approuter.js", + "start-local": "node node_modules/@sap/html5-repo-mock/index.js" + }, + "dependencies": { + "@sap/approuter": "^14" + }, + "devDependencies": { + "@sap/html5-repo-mock": "^2.1.0" + } +} diff --git a/packages/cf-deploy-config-writer/templates/router/xs-app-abapservice.json b/packages/cf-deploy-config-writer/templates/router/xs-app-abapservice.json new file mode 100644 index 0000000000..652c5e14ce --- /dev/null +++ b/packages/cf-deploy-config-writer/templates/router/xs-app-abapservice.json @@ -0,0 +1,25 @@ +{ + "authenticationMethod": "route", + "routes": [ + { + "source": "^/sap/(.*)$", + "target": "/sap/$1", + "service": "<%- servicekeyService %>", + "endpoint": "<%- servicekeyEndpoint %>", + "authenticationType": "xsuaa", + "csrfProtection": false + }, + { + "source": "^/resources/(.*)$", + "target": "/resources/$1", + "authenticationType": "none", + "destination": "ui5" + }, + { + "source": "^/test-resources/(.*)$", + "target": "/test-resources/$1", + "authenticationType": "none", + "destination": "ui5" + } + ] +} diff --git a/packages/cf-deploy-config-writer/templates/router/xs-app-server.json b/packages/cf-deploy-config-writer/templates/router/xs-app-server.json new file mode 100644 index 0000000000..4c7a8957dc --- /dev/null +++ b/packages/cf-deploy-config-writer/templates/router/xs-app-server.json @@ -0,0 +1,17 @@ +{ + "authenticationMethod": "route", + "routes": [ + { + "source": "^(?:/app|/app/.*)?/resources/(.*)$", + "target": "/resources/$1", + "authenticationType": "none", + "destination": "ui5" + }, + { + "source": "^(?:/app|/app/.*)?/test-resources/(.*)$", + "target": "/test-resources/$1", + "authenticationType": "none", + "destination": "ui5" + } + ] +} diff --git a/packages/cf-deploy-config-writer/templates/router/xs-app.json b/packages/cf-deploy-config-writer/templates/router/xs-app.json new file mode 100644 index 0000000000..806098f351 --- /dev/null +++ b/packages/cf-deploy-config-writer/templates/router/xs-app.json @@ -0,0 +1,18 @@ +{ + "welcomeFile": "/<%- appName %>/", + "authenticationMethod": "route", + "routes": [ + { + "source": "^/resources/(.*)$", + "target": "/resources/$1", + "authenticationType": "none", + "destination": "ui5" + }, + { + "source": "^/test-resources/(.*)$", + "target": "/test-resources/$1", + "authenticationType": "none", + "destination": "ui5" + } + ] +} diff --git a/packages/cf-deploy-config-writer/test/sample/basicapp/package.json b/packages/cf-deploy-config-writer/test/sample/basicapp/package.json new file mode 100644 index 0000000000..34e86b420e --- /dev/null +++ b/packages/cf-deploy-config-writer/test/sample/basicapp/package.json @@ -0,0 +1,29 @@ +{ + "name": "basicapp", + "version": "0.0.1", + "private": true, + "description": "An SAP Fiori application.", + "keywords": [ + "ui5", + "openui5", + "sapui5" + ], + "main": "webapp/index.html", + "dependencies": {}, + "devDependencies": { + "@ui5/cli": "^3.0.0", + "@sap/ux-ui5-tooling": "1" + }, + "scripts": { + "start": "fiori run --open \"test/flpSandbox.html?sap-ui-xx-viewCache=false#basicapp-display\"", + "start-local": "fiori run --config ./ui5-local.yaml --open \"test/flpSandbox.html?sap-ui-xx-viewCache=false#basicapp-display\"", + "build": "ui5 build --config=ui5.yaml --clean-dest --dest dist", + "deploy": "fiori verify", + "deploy-config": "fiori add deploy-config", + "start-noflp": "fiori run --open \"index.html?sap-ui-xx-viewCache=false\"", + "start-variants-management": "fiori run --open \"preview.html?sap-ui-xx-viewCache=false&fiori-tools-rta-mode=true&sap-ui-rta-skip-flex-validation=true#preview-app\"", + "unit-tests": "fiori run --open 'test/unit/unitTests.qunit.html'", + "int-tests": "fiori run --open 'test/integration/opaTests.qunit.html'" + }, + "sapuxLayer": "VENDOR" +} diff --git a/packages/cf-deploy-config-writer/test/sample/basicapp/ui5.yaml b/packages/cf-deploy-config-writer/test/sample/basicapp/ui5.yaml new file mode 100644 index 0000000000..93b117ad20 --- /dev/null +++ b/packages/cf-deploy-config-writer/test/sample/basicapp/ui5.yaml @@ -0,0 +1,26 @@ +specVersion: '2.4' +metadata: + name: 'basicapp' +type: application +server: + customMiddleware: + - name: fiori-tools-proxy + afterMiddleware: compression + configuration: + ignoreCertError: false # If set to true, certificate errors will be ignored. E.g. self-signed certificates will be accepted + backend: + - path: /sap + url: https://abap.hanavlab.ondemand.com + scp: true + destination: TestDestination + ui5: + path: + - /resources + - /test-resources + url: https://ui5.sap.com + version: # The UI5 version, for instance, 1.78.1. Empty means latest version + - name: fiori-tools-appreload + afterMiddleware: compression + configuration: + port: 35729 + path: webapp diff --git a/packages/cf-deploy-config-writer/test/sample/basicapp/webapp/i18n/i18n.properties b/packages/cf-deploy-config-writer/test/sample/basicapp/webapp/i18n/i18n.properties new file mode 100644 index 0000000000..9e5857c34d --- /dev/null +++ b/packages/cf-deploy-config-writer/test/sample/basicapp/webapp/i18n/i18n.properties @@ -0,0 +1,18 @@ +# This is the resource bundle for project13356 + +#Texts for manifest.json + +#XTIT: Application name +appTitle=App Gen App Title + +#YDES: Application description +appDescription=App Gen Desc + +flpTitle=FLP Title +flpSubtitle=FLP Subtitle + +# User app specific + +click=Click +reload=Reload +error=Please check your network connection \ No newline at end of file diff --git a/packages/cf-deploy-config-writer/test/sample/basicapp/webapp/manifest.json b/packages/cf-deploy-config-writer/test/sample/basicapp/webapp/manifest.json new file mode 100755 index 0000000000..5dc9965d21 --- /dev/null +++ b/packages/cf-deploy-config-writer/test/sample/basicapp/webapp/manifest.json @@ -0,0 +1,105 @@ +{ + "_version": "1.59.0", + "sap.app": { + "id": "basicapp", + "type": "application", + "i18n": "i18n/i18n.properties", + "applicationVersion": { + "version": "0.0.1" + }, + "title": "{{appTitle}}", + "description": "{{appDescription}}", + "resources": "resources.json", + "sourceTemplate": { + "id": "@sap/generator-fiori:basic", + "version": "1.14.2-pre-20240708140014-4973a7ece.0", + "toolsId": "c1c8a120-2e9a-4d6e-8363-7e2622948044" + } + }, + "sap.ui": { + "technology": "UI5", + "icons": { + "icon": "", + "favIcon": "", + "phone": "", + "phone@2": "", + "tablet": "", + "tablet@2": "" + }, + "deviceTypes": { + "desktop": true, + "tablet": true, + "phone": true + } + }, + "sap.ui5": { + "flexEnabled": true, + "dependencies": { + "minUI5Version": "1.125.1", + "libs": { + "sap.m": {}, + "sap.ui.core": {}, + "sap.f": {}, + "sap.suite.ui.generic.template": {}, + "sap.ui.comp": {}, + "sap.ui.generic.app": {}, + "sap.ui.table": {}, + "sap.ushell": {} + } + }, + "contentDensities": { + "compact": true, + "cozy": true + }, + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "settings": { + "bundleName": "basicapp.i18n.i18n" + } + } + }, + "resources": { + "css": [ + { + "uri": "css/style.css" + } + ] + }, + "routing": { + "config": { + "routerClass": "sap.m.routing.Router", + "viewType": "XML", + "async": true, + "viewPath": "basicapp.view", + "controlAggregation": "pages", + "controlId": "app", + "clearControlAggregation": false + }, + "routes": [ + { + "name": "RouteView1", + "pattern": ":?query:", + "target": [ + "TargetView1" + ] + } + ], + "targets": { + "TargetView1": { + "viewType": "XML", + "transition": "slide", + "clearControlAggregation": false, + "viewId": "View1", + "viewName": "View1" + } + } + }, + "rootView": { + "viewName": "basicapp.view.App", + "type": "XML", + "async": true, + "id": "App" + } + } +} diff --git a/packages/cf-deploy-config-writer/test/sample/basiccap/app/lrop/package.json b/packages/cf-deploy-config-writer/test/sample/basiccap/app/lrop/package.json new file mode 100644 index 0000000000..d3a60b3f16 --- /dev/null +++ b/packages/cf-deploy-config-writer/test/sample/basiccap/app/lrop/package.json @@ -0,0 +1,29 @@ +{ + "name": "lrop", + "version": "0.0.1", + "private": true, + "description": "An SAP Fiori application.", + "keywords": [ + "ui5", + "openui5", + "sapui5" + ], + "main": "webapp/index.html", + "dependencies": {}, + "devDependencies": { + "@ui5/cli": "^3.0.0", + "@sap/ux-ui5-tooling": "1" + }, + "scripts": { + "start": "fiori run --open \"test/flpSandbox.html?sap-ui-xx-viewCache=false#lrop-display\"", + "start-local": "fiori run --config ./ui5-local.yaml --open \"test/flpSandbox.html?sap-ui-xx-viewCache=false#lrop-display\"", + "build": "ui5 build --config=ui5.yaml --clean-dest --dest dist", + "deploy": "fiori verify", + "deploy-config": "fiori add deploy-config", + "start-noflp": "fiori run --open \"index.html?sap-ui-xx-viewCache=false\"", + "start-variants-management": "fiori run --open \"preview.html?sap-ui-xx-viewCache=false&fiori-tools-rta-mode=true&sap-ui-rta-skip-flex-validation=true#preview-app\"", + "unit-tests": "fiori run --open 'test/unit/unitTests.qunit.html'", + "int-tests": "fiori run --open 'test/integration/opaTests.qunit.html'" + }, + "sapuxLayer": "VENDOR" +} diff --git a/packages/cf-deploy-config-writer/test/sample/basiccap/app/lrop/ui5.yaml b/packages/cf-deploy-config-writer/test/sample/basiccap/app/lrop/ui5.yaml new file mode 100644 index 0000000000..2ed57afe24 --- /dev/null +++ b/packages/cf-deploy-config-writer/test/sample/basiccap/app/lrop/ui5.yaml @@ -0,0 +1,26 @@ +specVersion: '2.4' +metadata: + name: 'lrop' +type: application +server: + customMiddleware: + - name: fiori-tools-proxy + afterMiddleware: compression + configuration: + ignoreCertError: false # If set to true, certificate errors will be ignored. E.g. self-signed certificates will be accepted + backend: + - path: /sap + url: https://abap.hanavlab.ondemand.com + scp: true + destination: TestDestination + ui5: + path: + - /resources + - /test-resources + url: https://ui5.sap.com + version: # The UI5 version, for instance, 1.78.1. Empty means latest version + - name: fiori-tools-appreload + afterMiddleware: compression + configuration: + port: 35729 + path: webapp diff --git a/packages/cf-deploy-config-writer/test/sample/basiccap/app/lrop/webapp/i18n/i18n.properties b/packages/cf-deploy-config-writer/test/sample/basiccap/app/lrop/webapp/i18n/i18n.properties new file mode 100644 index 0000000000..9e5857c34d --- /dev/null +++ b/packages/cf-deploy-config-writer/test/sample/basiccap/app/lrop/webapp/i18n/i18n.properties @@ -0,0 +1,18 @@ +# This is the resource bundle for project13356 + +#Texts for manifest.json + +#XTIT: Application name +appTitle=App Gen App Title + +#YDES: Application description +appDescription=App Gen Desc + +flpTitle=FLP Title +flpSubtitle=FLP Subtitle + +# User app specific + +click=Click +reload=Reload +error=Please check your network connection \ No newline at end of file diff --git a/packages/cf-deploy-config-writer/test/sample/basiccap/app/lrop/webapp/manifest.json b/packages/cf-deploy-config-writer/test/sample/basiccap/app/lrop/webapp/manifest.json new file mode 100755 index 0000000000..4da4467d09 --- /dev/null +++ b/packages/cf-deploy-config-writer/test/sample/basiccap/app/lrop/webapp/manifest.json @@ -0,0 +1,164 @@ +{ + "_version": "1.8.0", + "sap.app": { + "id": "com.fiori.tools.lrop", + "type": "application", + "i18n": "i18n/i18n.properties", + "applicationVersion": { + "version": "1.0.0" + }, + "title": "{{appTitle}}", + "description": "{{appDescription}}", + "tags": { + "keywords": [] + }, + "ach": "", + "resources": "resources.json", + "dataSources": { + "mainService": { + "uri": "/sap/opu/odata/sap/ZUI_RAP_TRAVEL_M_U025/", + "type": "OData", + "settings": { + "annotations": [ + "ZUI_RAP_TRAVEL_M_U025_VAN", + "annotation" + ], + "localUri": "localService/metadata.xml" + } + }, + "annotation": { + "type": "ODataAnnotation", + "uri": "annotations/annotation.xml", + "settings": { + "localUri": "annotations/annotation.xml" + } + } + }, + "offline": false, + "sourceTemplate": { + "id": "ui5template.smartTemplate", + "version": "1.40.12" + } + }, + "sap.ui": { + "technology": "UI5", + "icons": { + "icon": "", + "favIcon": "", + "phone": "", + "phone@2": "", + "tablet": "", + "tablet@2": "" + }, + "deviceTypes": { + "desktop": true, + "tablet": true, + "phone": true + }, + "supportedThemes": [ + "sap_hcb", + "sap_belize" + ] + }, + "sap.ui5": { + "resources": { + "js": [], + "css": [] + }, + "dependencies": { + "minUI5Version": "1.65.0", + "libs": {}, + "components": {} + }, + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "uri": "i18n/i18n.properties" + }, + "@i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "uri": "i18n/i18n.properties" + }, + "i18n|sap.suite.ui.generic.template.ListReport|Travel": { + "type": "sap.ui.model.resource.ResourceModel", + "uri": "i18n/ListReport/Travel/i18n.properties" + }, + "i18n|sap.suite.ui.generic.template.ObjectPage|Travel": { + "type": "sap.ui.model.resource.ResourceModel", + "uri": "i18n/ObjectPage/Travel/i18n.properties" + }, + "i18n|sap.suite.ui.generic.template.ObjectPage|Booking": { + "type": "sap.ui.model.resource.ResourceModel", + "uri": "i18n/ObjectPage/Booking/i18n.properties" + }, + "": { + "dataSource": "mainService", + "preload": true, + "settings": { + "defaultBindingMode": "TwoWay", + "defaultCountMode": "Inline", + "refreshAfterChange": false, + "metadataUrlParams": { + "sap-value-list": "none" + } + } + } + }, + "extends": { + "extensions": {} + }, + "contentDensities": { + "compact": true, + "cozy": true + } + }, + "sap.ui.generic.app": { + "_version": "1.3.0", + "settings": { + "forceGlobalRefresh": false, + "objectPageHeaderType": "Dynamic", + "showDraftToggle": false + }, + "pages": { + "ListReport|Travel": { + "entitySet": "Travel", + "component": { + "name": "sap.suite.ui.generic.template.ListReport", + "list": true, + "settings": { + "condensedTableLayout": true, + "smartVariantManagement": true, + "enableTableFilterInPageVariant": true + } + }, + "pages": { + "ObjectPage|Travel": { + "entitySet": "Travel", + "component": { + "name": "sap.suite.ui.generic.template.ObjectPage" + }, + "pages": { + "ObjectPage|to_Booking": { + "navigationProperty": "to_Booking", + "entitySet": "Booking", + "component": { + "name": "sap.suite.ui.generic.template.ObjectPage" + } + } + } + } + } + } + } + }, + "sap.platform.abap": { + "uri": "" + }, + "sap.fiori": { + "registrationIds": [], + "archeType": "transactional" + }, + "sap.platform.hcp": { + "uri": "" + } +} diff --git a/packages/cf-deploy-config-writer/test/sample/basiccap/app/services.cds b/packages/cf-deploy-config-writer/test/sample/basiccap/app/services.cds new file mode 100644 index 0000000000..df81e2f0c6 --- /dev/null +++ b/packages/cf-deploy-config-writer/test/sample/basiccap/app/services.cds @@ -0,0 +1,2 @@ + +using from './lrop/annotations'; \ No newline at end of file diff --git a/packages/cf-deploy-config-writer/test/sample/basiccap/package.json b/packages/cf-deploy-config-writer/test/sample/basiccap/package.json new file mode 100644 index 0000000000..51afff3731 --- /dev/null +++ b/packages/cf-deploy-config-writer/test/sample/basiccap/package.json @@ -0,0 +1,3 @@ +{ + "cds": {} +} diff --git a/packages/cf-deploy-config-writer/test/sample/basiccap/srv/keep b/packages/cf-deploy-config-writer/test/sample/basiccap/srv/keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/cf-deploy-config-writer/test/sample/cap/app/lrop/package.json b/packages/cf-deploy-config-writer/test/sample/cap/app/lrop/package.json new file mode 100644 index 0000000000..d3a60b3f16 --- /dev/null +++ b/packages/cf-deploy-config-writer/test/sample/cap/app/lrop/package.json @@ -0,0 +1,29 @@ +{ + "name": "lrop", + "version": "0.0.1", + "private": true, + "description": "An SAP Fiori application.", + "keywords": [ + "ui5", + "openui5", + "sapui5" + ], + "main": "webapp/index.html", + "dependencies": {}, + "devDependencies": { + "@ui5/cli": "^3.0.0", + "@sap/ux-ui5-tooling": "1" + }, + "scripts": { + "start": "fiori run --open \"test/flpSandbox.html?sap-ui-xx-viewCache=false#lrop-display\"", + "start-local": "fiori run --config ./ui5-local.yaml --open \"test/flpSandbox.html?sap-ui-xx-viewCache=false#lrop-display\"", + "build": "ui5 build --config=ui5.yaml --clean-dest --dest dist", + "deploy": "fiori verify", + "deploy-config": "fiori add deploy-config", + "start-noflp": "fiori run --open \"index.html?sap-ui-xx-viewCache=false\"", + "start-variants-management": "fiori run --open \"preview.html?sap-ui-xx-viewCache=false&fiori-tools-rta-mode=true&sap-ui-rta-skip-flex-validation=true#preview-app\"", + "unit-tests": "fiori run --open 'test/unit/unitTests.qunit.html'", + "int-tests": "fiori run --open 'test/integration/opaTests.qunit.html'" + }, + "sapuxLayer": "VENDOR" +} diff --git a/packages/cf-deploy-config-writer/test/sample/cap/app/lrop/ui5.yaml b/packages/cf-deploy-config-writer/test/sample/cap/app/lrop/ui5.yaml new file mode 100644 index 0000000000..2ed57afe24 --- /dev/null +++ b/packages/cf-deploy-config-writer/test/sample/cap/app/lrop/ui5.yaml @@ -0,0 +1,26 @@ +specVersion: '2.4' +metadata: + name: 'lrop' +type: application +server: + customMiddleware: + - name: fiori-tools-proxy + afterMiddleware: compression + configuration: + ignoreCertError: false # If set to true, certificate errors will be ignored. E.g. self-signed certificates will be accepted + backend: + - path: /sap + url: https://abap.hanavlab.ondemand.com + scp: true + destination: TestDestination + ui5: + path: + - /resources + - /test-resources + url: https://ui5.sap.com + version: # The UI5 version, for instance, 1.78.1. Empty means latest version + - name: fiori-tools-appreload + afterMiddleware: compression + configuration: + port: 35729 + path: webapp diff --git a/packages/cf-deploy-config-writer/test/sample/cap/app/lrop/webapp/i18n/i18n.properties b/packages/cf-deploy-config-writer/test/sample/cap/app/lrop/webapp/i18n/i18n.properties new file mode 100644 index 0000000000..9e5857c34d --- /dev/null +++ b/packages/cf-deploy-config-writer/test/sample/cap/app/lrop/webapp/i18n/i18n.properties @@ -0,0 +1,18 @@ +# This is the resource bundle for project13356 + +#Texts for manifest.json + +#XTIT: Application name +appTitle=App Gen App Title + +#YDES: Application description +appDescription=App Gen Desc + +flpTitle=FLP Title +flpSubtitle=FLP Subtitle + +# User app specific + +click=Click +reload=Reload +error=Please check your network connection \ No newline at end of file diff --git a/packages/cf-deploy-config-writer/test/sample/cap/app/lrop/webapp/manifest.json b/packages/cf-deploy-config-writer/test/sample/cap/app/lrop/webapp/manifest.json new file mode 100755 index 0000000000..4da4467d09 --- /dev/null +++ b/packages/cf-deploy-config-writer/test/sample/cap/app/lrop/webapp/manifest.json @@ -0,0 +1,164 @@ +{ + "_version": "1.8.0", + "sap.app": { + "id": "com.fiori.tools.lrop", + "type": "application", + "i18n": "i18n/i18n.properties", + "applicationVersion": { + "version": "1.0.0" + }, + "title": "{{appTitle}}", + "description": "{{appDescription}}", + "tags": { + "keywords": [] + }, + "ach": "", + "resources": "resources.json", + "dataSources": { + "mainService": { + "uri": "/sap/opu/odata/sap/ZUI_RAP_TRAVEL_M_U025/", + "type": "OData", + "settings": { + "annotations": [ + "ZUI_RAP_TRAVEL_M_U025_VAN", + "annotation" + ], + "localUri": "localService/metadata.xml" + } + }, + "annotation": { + "type": "ODataAnnotation", + "uri": "annotations/annotation.xml", + "settings": { + "localUri": "annotations/annotation.xml" + } + } + }, + "offline": false, + "sourceTemplate": { + "id": "ui5template.smartTemplate", + "version": "1.40.12" + } + }, + "sap.ui": { + "technology": "UI5", + "icons": { + "icon": "", + "favIcon": "", + "phone": "", + "phone@2": "", + "tablet": "", + "tablet@2": "" + }, + "deviceTypes": { + "desktop": true, + "tablet": true, + "phone": true + }, + "supportedThemes": [ + "sap_hcb", + "sap_belize" + ] + }, + "sap.ui5": { + "resources": { + "js": [], + "css": [] + }, + "dependencies": { + "minUI5Version": "1.65.0", + "libs": {}, + "components": {} + }, + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "uri": "i18n/i18n.properties" + }, + "@i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "uri": "i18n/i18n.properties" + }, + "i18n|sap.suite.ui.generic.template.ListReport|Travel": { + "type": "sap.ui.model.resource.ResourceModel", + "uri": "i18n/ListReport/Travel/i18n.properties" + }, + "i18n|sap.suite.ui.generic.template.ObjectPage|Travel": { + "type": "sap.ui.model.resource.ResourceModel", + "uri": "i18n/ObjectPage/Travel/i18n.properties" + }, + "i18n|sap.suite.ui.generic.template.ObjectPage|Booking": { + "type": "sap.ui.model.resource.ResourceModel", + "uri": "i18n/ObjectPage/Booking/i18n.properties" + }, + "": { + "dataSource": "mainService", + "preload": true, + "settings": { + "defaultBindingMode": "TwoWay", + "defaultCountMode": "Inline", + "refreshAfterChange": false, + "metadataUrlParams": { + "sap-value-list": "none" + } + } + } + }, + "extends": { + "extensions": {} + }, + "contentDensities": { + "compact": true, + "cozy": true + } + }, + "sap.ui.generic.app": { + "_version": "1.3.0", + "settings": { + "forceGlobalRefresh": false, + "objectPageHeaderType": "Dynamic", + "showDraftToggle": false + }, + "pages": { + "ListReport|Travel": { + "entitySet": "Travel", + "component": { + "name": "sap.suite.ui.generic.template.ListReport", + "list": true, + "settings": { + "condensedTableLayout": true, + "smartVariantManagement": true, + "enableTableFilterInPageVariant": true + } + }, + "pages": { + "ObjectPage|Travel": { + "entitySet": "Travel", + "component": { + "name": "sap.suite.ui.generic.template.ObjectPage" + }, + "pages": { + "ObjectPage|to_Booking": { + "navigationProperty": "to_Booking", + "entitySet": "Booking", + "component": { + "name": "sap.suite.ui.generic.template.ObjectPage" + } + } + } + } + } + } + } + }, + "sap.platform.abap": { + "uri": "" + }, + "sap.fiori": { + "registrationIds": [], + "archeType": "transactional" + }, + "sap.platform.hcp": { + "uri": "" + } +} diff --git a/packages/cf-deploy-config-writer/test/sample/cap/app/services.cds b/packages/cf-deploy-config-writer/test/sample/cap/app/services.cds new file mode 100644 index 0000000000..df81e2f0c6 --- /dev/null +++ b/packages/cf-deploy-config-writer/test/sample/cap/app/services.cds @@ -0,0 +1,2 @@ + +using from './lrop/annotations'; \ No newline at end of file diff --git a/packages/cf-deploy-config-writer/test/sample/cap/mta.yaml b/packages/cf-deploy-config-writer/test/sample/cap/mta.yaml new file mode 100644 index 0000000000..a120430ff8 --- /dev/null +++ b/packages/cf-deploy-config-writer/test/sample/cap/mta.yaml @@ -0,0 +1,51 @@ +_schema-version: '3.1' +ID: cappapp +version: 1.0.0 +description: "A simple CAP project." +parameters: + enable-parallel-deployments: true +build-parameters: + before-all: + - builder: custom + commands: + - npm ci + - npx -p @sap/cds-dk cds build --production +modules: + # --------------------- SERVER MODULE ------------------------ + - name: cappapp-srv + # ------------------------------------------------------------ + type: nodejs + path: gen/srv + parameters: + buildpack: nodejs_buildpack + build-parameters: + builder: npm-ci + provides: + - name: srv-api # required by consumers of CAP services (e.g. approuter) + properties: + srv-url: ${default-url} + + # -------------------- SIDECAR MODULE ------------------------ + - name: cappapp-db-deployer + # ------------------------------------------------------------ + type: hdb + path: gen/db + parameters: + buildpack: nodejs_buildpack + requires: + # 'hana' and 'xsuaa' resources extracted from CAP configuration + - name: cappapp-db +resources: + # services extracted from CAP configuration + # 'service-plan' can be configured via 'cds.requires..vcap.plan' + # ------------------------------------------------------------ + - name: cappapp-db + # ------------------------------------------------------------ + type: com.sap.xs.hdi-container + parameters: + service: hana # or 'hanatrial' on trial landscapes + service-plan: hdi-shared + properties: + hdi-service-name: ${service-name} + + diff --git a/packages/cf-deploy-config-writer/test/sample/cap/package.json b/packages/cf-deploy-config-writer/test/sample/cap/package.json new file mode 100644 index 0000000000..51afff3731 --- /dev/null +++ b/packages/cf-deploy-config-writer/test/sample/cap/package.json @@ -0,0 +1,3 @@ +{ + "cds": {} +} diff --git a/packages/cf-deploy-config-writer/test/sample/cap/srv/keep b/packages/cf-deploy-config-writer/test/sample/cap/srv/keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/cf-deploy-config-writer/test/sample/lrop/package.json b/packages/cf-deploy-config-writer/test/sample/lrop/package.json new file mode 100644 index 0000000000..d3a60b3f16 --- /dev/null +++ b/packages/cf-deploy-config-writer/test/sample/lrop/package.json @@ -0,0 +1,29 @@ +{ + "name": "lrop", + "version": "0.0.1", + "private": true, + "description": "An SAP Fiori application.", + "keywords": [ + "ui5", + "openui5", + "sapui5" + ], + "main": "webapp/index.html", + "dependencies": {}, + "devDependencies": { + "@ui5/cli": "^3.0.0", + "@sap/ux-ui5-tooling": "1" + }, + "scripts": { + "start": "fiori run --open \"test/flpSandbox.html?sap-ui-xx-viewCache=false#lrop-display\"", + "start-local": "fiori run --config ./ui5-local.yaml --open \"test/flpSandbox.html?sap-ui-xx-viewCache=false#lrop-display\"", + "build": "ui5 build --config=ui5.yaml --clean-dest --dest dist", + "deploy": "fiori verify", + "deploy-config": "fiori add deploy-config", + "start-noflp": "fiori run --open \"index.html?sap-ui-xx-viewCache=false\"", + "start-variants-management": "fiori run --open \"preview.html?sap-ui-xx-viewCache=false&fiori-tools-rta-mode=true&sap-ui-rta-skip-flex-validation=true#preview-app\"", + "unit-tests": "fiori run --open 'test/unit/unitTests.qunit.html'", + "int-tests": "fiori run --open 'test/integration/opaTests.qunit.html'" + }, + "sapuxLayer": "VENDOR" +} diff --git a/packages/cf-deploy-config-writer/test/sample/lrop/ui5.yaml b/packages/cf-deploy-config-writer/test/sample/lrop/ui5.yaml new file mode 100644 index 0000000000..d220835347 --- /dev/null +++ b/packages/cf-deploy-config-writer/test/sample/lrop/ui5.yaml @@ -0,0 +1,24 @@ +specVersion: '2.4' +metadata: + name: 'lrop' +type: application +server: + customMiddleware: + - name: fiori-tools-proxy + afterMiddleware: compression + configuration: + ignoreCertError: false # If set to true, certificate errors will be ignored. E.g. self-signed certificates will be accepted + backend: + - path: /sap + url: https://abap.hanavlab.ondemand.com + ui5: + path: + - /resources + - /test-resources + url: https://ui5.sap.com + version: # The UI5 version, for instance, 1.78.1. Empty means latest version + - name: fiori-tools-appreload + afterMiddleware: compression + configuration: + port: 35729 + path: webapp diff --git a/packages/cf-deploy-config-writer/test/sample/lrop/webapp/i18n/i18n.properties b/packages/cf-deploy-config-writer/test/sample/lrop/webapp/i18n/i18n.properties new file mode 100644 index 0000000000..9e5857c34d --- /dev/null +++ b/packages/cf-deploy-config-writer/test/sample/lrop/webapp/i18n/i18n.properties @@ -0,0 +1,18 @@ +# This is the resource bundle for project13356 + +#Texts for manifest.json + +#XTIT: Application name +appTitle=App Gen App Title + +#YDES: Application description +appDescription=App Gen Desc + +flpTitle=FLP Title +flpSubtitle=FLP Subtitle + +# User app specific + +click=Click +reload=Reload +error=Please check your network connection \ No newline at end of file diff --git a/packages/cf-deploy-config-writer/test/sample/lrop/webapp/manifest.json b/packages/cf-deploy-config-writer/test/sample/lrop/webapp/manifest.json new file mode 100755 index 0000000000..4da4467d09 --- /dev/null +++ b/packages/cf-deploy-config-writer/test/sample/lrop/webapp/manifest.json @@ -0,0 +1,164 @@ +{ + "_version": "1.8.0", + "sap.app": { + "id": "com.fiori.tools.lrop", + "type": "application", + "i18n": "i18n/i18n.properties", + "applicationVersion": { + "version": "1.0.0" + }, + "title": "{{appTitle}}", + "description": "{{appDescription}}", + "tags": { + "keywords": [] + }, + "ach": "", + "resources": "resources.json", + "dataSources": { + "mainService": { + "uri": "/sap/opu/odata/sap/ZUI_RAP_TRAVEL_M_U025/", + "type": "OData", + "settings": { + "annotations": [ + "ZUI_RAP_TRAVEL_M_U025_VAN", + "annotation" + ], + "localUri": "localService/metadata.xml" + } + }, + "annotation": { + "type": "ODataAnnotation", + "uri": "annotations/annotation.xml", + "settings": { + "localUri": "annotations/annotation.xml" + } + } + }, + "offline": false, + "sourceTemplate": { + "id": "ui5template.smartTemplate", + "version": "1.40.12" + } + }, + "sap.ui": { + "technology": "UI5", + "icons": { + "icon": "", + "favIcon": "", + "phone": "", + "phone@2": "", + "tablet": "", + "tablet@2": "" + }, + "deviceTypes": { + "desktop": true, + "tablet": true, + "phone": true + }, + "supportedThemes": [ + "sap_hcb", + "sap_belize" + ] + }, + "sap.ui5": { + "resources": { + "js": [], + "css": [] + }, + "dependencies": { + "minUI5Version": "1.65.0", + "libs": {}, + "components": {} + }, + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "uri": "i18n/i18n.properties" + }, + "@i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "uri": "i18n/i18n.properties" + }, + "i18n|sap.suite.ui.generic.template.ListReport|Travel": { + "type": "sap.ui.model.resource.ResourceModel", + "uri": "i18n/ListReport/Travel/i18n.properties" + }, + "i18n|sap.suite.ui.generic.template.ObjectPage|Travel": { + "type": "sap.ui.model.resource.ResourceModel", + "uri": "i18n/ObjectPage/Travel/i18n.properties" + }, + "i18n|sap.suite.ui.generic.template.ObjectPage|Booking": { + "type": "sap.ui.model.resource.ResourceModel", + "uri": "i18n/ObjectPage/Booking/i18n.properties" + }, + "": { + "dataSource": "mainService", + "preload": true, + "settings": { + "defaultBindingMode": "TwoWay", + "defaultCountMode": "Inline", + "refreshAfterChange": false, + "metadataUrlParams": { + "sap-value-list": "none" + } + } + } + }, + "extends": { + "extensions": {} + }, + "contentDensities": { + "compact": true, + "cozy": true + } + }, + "sap.ui.generic.app": { + "_version": "1.3.0", + "settings": { + "forceGlobalRefresh": false, + "objectPageHeaderType": "Dynamic", + "showDraftToggle": false + }, + "pages": { + "ListReport|Travel": { + "entitySet": "Travel", + "component": { + "name": "sap.suite.ui.generic.template.ListReport", + "list": true, + "settings": { + "condensedTableLayout": true, + "smartVariantManagement": true, + "enableTableFilterInPageVariant": true + } + }, + "pages": { + "ObjectPage|Travel": { + "entitySet": "Travel", + "component": { + "name": "sap.suite.ui.generic.template.ObjectPage" + }, + "pages": { + "ObjectPage|to_Booking": { + "navigationProperty": "to_Booking", + "entitySet": "Booking", + "component": { + "name": "sap.suite.ui.generic.template.ObjectPage" + } + } + } + } + } + } + } + }, + "sap.platform.abap": { + "uri": "" + }, + "sap.fiori": { + "registrationIds": [], + "archeType": "transactional" + }, + "sap.platform.hcp": { + "uri": "" + } +} diff --git a/packages/cf-deploy-config-writer/test/sample/multi/mta-ext.mtaext b/packages/cf-deploy-config-writer/test/sample/multi/mta-ext.mtaext new file mode 100644 index 0000000000..e15fcd5f92 --- /dev/null +++ b/packages/cf-deploy-config-writer/test/sample/multi/mta-ext.mtaext @@ -0,0 +1,20 @@ +## SAP UX Tools generated mtaext file +_schema-version: "3.2" +ID: test-mta-ext +extends: test-mta +version: 1.0.0 + +resources: +- name: qa-destination-service + parameters: + config: + init_data: + instance: + destinations: + - Authentication: NoAuthentication + Name: ABHE_NorthwindProduct_theme + ProxyType: Internet + Type: HTTP + URL: https://api.hana.on.demand + URL.headers.ApiKey: 1234567890abcdefg + existing_destinations_policy: update diff --git a/packages/cf-deploy-config-writer/test/sample/multi/mta.yaml b/packages/cf-deploy-config-writer/test/sample/multi/mta.yaml new file mode 100644 index 0000000000..6d1afaa2e9 --- /dev/null +++ b/packages/cf-deploy-config-writer/test/sample/multi/mta.yaml @@ -0,0 +1,6 @@ +_schema-version: "3.1" +ID: multiproject +description: Fiori elements app +version: 0.0.1 +modules: [] +resources: [] \ No newline at end of file diff --git a/packages/cf-deploy-config-writer/test/sample/multi/package.json b/packages/cf-deploy-config-writer/test/sample/multi/package.json new file mode 100644 index 0000000000..0ee365eb42 --- /dev/null +++ b/packages/cf-deploy-config-writer/test/sample/multi/package.json @@ -0,0 +1,29 @@ +{ + "name": "multi", + "version": "0.0.1", + "private": true, + "description": "An SAP Fiori application.", + "keywords": [ + "ui5", + "openui5", + "sapui5" + ], + "main": "webapp/index.html", + "dependencies": {}, + "devDependencies": { + "@ui5/cli": "^3.0.0", + "@sap/ux-ui5-tooling": "1" + }, + "scripts": { + "start": "fiori run --open \"test/flpSandbox.html?sap-ui-xx-viewCache=false#lrop-display\"", + "start-local": "fiori run --config ./ui5-local.yaml --open \"test/flpSandbox.html?sap-ui-xx-viewCache=false#lrop-display\"", + "build": "ui5 build --config=ui5.yaml --clean-dest --dest dist", + "deploy": "fiori verify", + "deploy-config": "fiori add deploy-config", + "start-noflp": "fiori run --open \"index.html?sap-ui-xx-viewCache=false\"", + "start-variants-management": "fiori run --open \"preview.html?sap-ui-xx-viewCache=false&fiori-tools-rta-mode=true&sap-ui-rta-skip-flex-validation=true#preview-app\"", + "unit-tests": "fiori run --open 'test/unit/unitTests.qunit.html'", + "int-tests": "fiori run --open 'test/integration/opaTests.qunit.html'" + }, + "sapuxLayer": "VENDOR" +} diff --git a/packages/cf-deploy-config-writer/test/sample/multi/ui5.yaml b/packages/cf-deploy-config-writer/test/sample/multi/ui5.yaml new file mode 100644 index 0000000000..f820e9a9b9 --- /dev/null +++ b/packages/cf-deploy-config-writer/test/sample/multi/ui5.yaml @@ -0,0 +1,24 @@ +specVersion: '2.4' +metadata: + name: 'multi' +type: application +server: + customMiddleware: + - name: fiori-tools-proxy + afterMiddleware: compression + configuration: + ignoreCertError: false # If set to true, certificate errors will be ignored. E.g. self-signed certificates will be accepted + backend: + - path: /sap + url: https://abap.hanavlab.ondemand.com + ui5: + path: + - /resources + - /test-resources + url: https://ui5.sap.com + version: # The UI5 version, for instance, 1.78.1. Empty means latest version + - name: fiori-tools-appreload + afterMiddleware: compression + configuration: + port: 35729 + path: webapp diff --git a/packages/cf-deploy-config-writer/test/sample/multi/webapp/i18n/i18n.properties b/packages/cf-deploy-config-writer/test/sample/multi/webapp/i18n/i18n.properties new file mode 100644 index 0000000000..9e5857c34d --- /dev/null +++ b/packages/cf-deploy-config-writer/test/sample/multi/webapp/i18n/i18n.properties @@ -0,0 +1,18 @@ +# This is the resource bundle for project13356 + +#Texts for manifest.json + +#XTIT: Application name +appTitle=App Gen App Title + +#YDES: Application description +appDescription=App Gen Desc + +flpTitle=FLP Title +flpSubtitle=FLP Subtitle + +# User app specific + +click=Click +reload=Reload +error=Please check your network connection \ No newline at end of file diff --git a/packages/cf-deploy-config-writer/test/sample/multi/webapp/manifest.json b/packages/cf-deploy-config-writer/test/sample/multi/webapp/manifest.json new file mode 100755 index 0000000000..4da4467d09 --- /dev/null +++ b/packages/cf-deploy-config-writer/test/sample/multi/webapp/manifest.json @@ -0,0 +1,164 @@ +{ + "_version": "1.8.0", + "sap.app": { + "id": "com.fiori.tools.lrop", + "type": "application", + "i18n": "i18n/i18n.properties", + "applicationVersion": { + "version": "1.0.0" + }, + "title": "{{appTitle}}", + "description": "{{appDescription}}", + "tags": { + "keywords": [] + }, + "ach": "", + "resources": "resources.json", + "dataSources": { + "mainService": { + "uri": "/sap/opu/odata/sap/ZUI_RAP_TRAVEL_M_U025/", + "type": "OData", + "settings": { + "annotations": [ + "ZUI_RAP_TRAVEL_M_U025_VAN", + "annotation" + ], + "localUri": "localService/metadata.xml" + } + }, + "annotation": { + "type": "ODataAnnotation", + "uri": "annotations/annotation.xml", + "settings": { + "localUri": "annotations/annotation.xml" + } + } + }, + "offline": false, + "sourceTemplate": { + "id": "ui5template.smartTemplate", + "version": "1.40.12" + } + }, + "sap.ui": { + "technology": "UI5", + "icons": { + "icon": "", + "favIcon": "", + "phone": "", + "phone@2": "", + "tablet": "", + "tablet@2": "" + }, + "deviceTypes": { + "desktop": true, + "tablet": true, + "phone": true + }, + "supportedThemes": [ + "sap_hcb", + "sap_belize" + ] + }, + "sap.ui5": { + "resources": { + "js": [], + "css": [] + }, + "dependencies": { + "minUI5Version": "1.65.0", + "libs": {}, + "components": {} + }, + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "uri": "i18n/i18n.properties" + }, + "@i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "uri": "i18n/i18n.properties" + }, + "i18n|sap.suite.ui.generic.template.ListReport|Travel": { + "type": "sap.ui.model.resource.ResourceModel", + "uri": "i18n/ListReport/Travel/i18n.properties" + }, + "i18n|sap.suite.ui.generic.template.ObjectPage|Travel": { + "type": "sap.ui.model.resource.ResourceModel", + "uri": "i18n/ObjectPage/Travel/i18n.properties" + }, + "i18n|sap.suite.ui.generic.template.ObjectPage|Booking": { + "type": "sap.ui.model.resource.ResourceModel", + "uri": "i18n/ObjectPage/Booking/i18n.properties" + }, + "": { + "dataSource": "mainService", + "preload": true, + "settings": { + "defaultBindingMode": "TwoWay", + "defaultCountMode": "Inline", + "refreshAfterChange": false, + "metadataUrlParams": { + "sap-value-list": "none" + } + } + } + }, + "extends": { + "extensions": {} + }, + "contentDensities": { + "compact": true, + "cozy": true + } + }, + "sap.ui.generic.app": { + "_version": "1.3.0", + "settings": { + "forceGlobalRefresh": false, + "objectPageHeaderType": "Dynamic", + "showDraftToggle": false + }, + "pages": { + "ListReport|Travel": { + "entitySet": "Travel", + "component": { + "name": "sap.suite.ui.generic.template.ListReport", + "list": true, + "settings": { + "condensedTableLayout": true, + "smartVariantManagement": true, + "enableTableFilterInPageVariant": true + } + }, + "pages": { + "ObjectPage|Travel": { + "entitySet": "Travel", + "component": { + "name": "sap.suite.ui.generic.template.ObjectPage" + }, + "pages": { + "ObjectPage|to_Booking": { + "navigationProperty": "to_Booking", + "entitySet": "Booking", + "component": { + "name": "sap.suite.ui.generic.template.ObjectPage" + } + } + } + } + } + } + } + }, + "sap.platform.abap": { + "uri": "" + }, + "sap.fiori": { + "registrationIds": [], + "archeType": "transactional" + }, + "sap.platform.hcp": { + "uri": "" + } +} diff --git a/packages/cf-deploy-config-writer/test/unit/__snapshots__/cap.test.ts.snap b/packages/cf-deploy-config-writer/test/unit/__snapshots__/cap.test.ts.snap new file mode 100644 index 0000000000..6e71aad507 --- /dev/null +++ b/packages/cf-deploy-config-writer/test/unit/__snapshots__/cap.test.ts.snap @@ -0,0 +1,457 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CF Writer Generate deployment config for CAP project Add destination instance to a HTML5 app inside a CAP project 1`] = ` +Object { + ".gitignore": Object { + "contents": "node_modules/ +dist/ +.scp/ +.env +Makefile*.mta +mta_archives +mta-* +resources +archive.zip +.*_mta_build_tmp", + "state": "modified", + }, + "app/lrop/package.json": Object { + "contents": "{ + \\"name\\": \\"lrop\\", + \\"version\\": \\"0.0.1\\", + \\"private\\": true, + \\"description\\": \\"An SAP Fiori application.\\", + \\"keywords\\": [ + \\"ui5\\", + \\"openui5\\", + \\"sapui5\\" + ], + \\"main\\": \\"webapp/index.html\\", + \\"dependencies\\": {}, + \\"devDependencies\\": { + \\"@ui5/cli\\": \\"^3.0.0\\", + \\"@sap/ux-ui5-tooling\\": \\"1\\", + \\"rimraf\\": \\"^5.0.5\\", + \\"mbt\\": \\"^1.2.29\\", + \\"@sap/ui5-builder-webide-extension\\": \\"^1.1.9\\", + \\"ui5-task-zipper\\": \\"^3.1.3\\" + }, + \\"scripts\\": { + \\"start\\": \\"fiori run --open \\\\\\"test/flpSandbox.html?sap-ui-xx-viewCache=false#lrop-display\\\\\\"\\", + \\"start-local\\": \\"fiori run --config ./ui5-local.yaml --open \\\\\\"test/flpSandbox.html?sap-ui-xx-viewCache=false#lrop-display\\\\\\"\\", + \\"build\\": \\"ui5 build --config=ui5.yaml --clean-dest --dest dist\\", + \\"deploy\\": \\"fiori cfDeploy\\", + \\"deploy-config\\": \\"fiori add deploy-config\\", + \\"start-noflp\\": \\"fiori run --open \\\\\\"index.html?sap-ui-xx-viewCache=false\\\\\\"\\", + \\"start-variants-management\\": \\"fiori run --open \\\\\\"preview.html?sap-ui-xx-viewCache=false&fiori-tools-rta-mode=true&sap-ui-rta-skip-flex-validation=true#preview-app\\\\\\"\\", + \\"unit-tests\\": \\"fiori run --open 'test/unit/unitTests.qunit.html'\\", + \\"int-tests\\": \\"fiori run --open 'test/integration/opaTests.qunit.html'\\", + \\"build:cf\\": \\"ui5 build preload --clean-dest --config ui5-deploy.yaml --include-task=generateCachebusterInfo\\", + \\"build:mta\\": \\"rimraf resources mta_archives && mbt build --mtar archive\\", + \\"undeploy\\": \\"cf undeploy cappapp --delete-services --delete-service-keys --delete-service-brokers\\" + }, + \\"sapuxLayer\\": \\"VENDOR\\" +} +", + "state": "modified", + }, + "app/lrop/ui5-deploy.yaml": Object { + "contents": "# yaml-language-server: $schema=https://sap.github.io/ui5-tooling/schema/ui5.yaml.json + +specVersion: '2.4' +metadata: + name: 'lrop' +type: application +builder: + resources: + excludes: + - /test/** + - /localService/** + customTasks: + - name: webide-extension-task-updateManifestJson + afterTask: replaceVersion + configuration: + appFolder: webapp + destDir: dist + - name: ui5-task-zipper + afterTask: generateCachebusterInfo + configuration: + archiveName: comfioritoolslrop + additionalFiles: + - xs-app.json +", + "state": "modified", + }, + "app/lrop/webapp/manifest.json": Object { + "contents": "{ + \\"_version\\": \\"1.8.0\\", + \\"sap.app\\": { + \\"id\\": \\"com.fiori.tools.lrop\\", + \\"type\\": \\"application\\", + \\"i18n\\": \\"i18n/i18n.properties\\", + \\"applicationVersion\\": { + \\"version\\": \\"1.0.0\\" + }, + \\"title\\": \\"{{appTitle}}\\", + \\"description\\": \\"{{appDescription}}\\", + \\"tags\\": { + \\"keywords\\": [] + }, + \\"ach\\": \\"\\", + \\"resources\\": \\"resources.json\\", + \\"dataSources\\": { + \\"mainService\\": { + \\"uri\\": \\"/sap/opu/odata/sap/ZUI_RAP_TRAVEL_M_U025/\\", + \\"type\\": \\"OData\\", + \\"settings\\": { + \\"annotations\\": [ + \\"ZUI_RAP_TRAVEL_M_U025_VAN\\", + \\"annotation\\" + ], + \\"localUri\\": \\"localService/metadata.xml\\" + } + }, + \\"annotation\\": { + \\"type\\": \\"ODataAnnotation\\", + \\"uri\\": \\"annotations/annotation.xml\\", + \\"settings\\": { + \\"localUri\\": \\"annotations/annotation.xml\\" + } + } + }, + \\"offline\\": false, + \\"sourceTemplate\\": { + \\"id\\": \\"ui5template.smartTemplate\\", + \\"version\\": \\"1.40.12\\" + } + }, + \\"sap.ui\\": { + \\"technology\\": \\"UI5\\", + \\"icons\\": { + \\"icon\\": \\"\\", + \\"favIcon\\": \\"\\", + \\"phone\\": \\"\\", + \\"phone@2\\": \\"\\", + \\"tablet\\": \\"\\", + \\"tablet@2\\": \\"\\" + }, + \\"deviceTypes\\": { + \\"desktop\\": true, + \\"tablet\\": true, + \\"phone\\": true + }, + \\"supportedThemes\\": [ + \\"sap_hcb\\", + \\"sap_belize\\" + ] + }, + \\"sap.ui5\\": { + \\"resources\\": { + \\"js\\": [], + \\"css\\": [] + }, + \\"dependencies\\": { + \\"minUI5Version\\": \\"1.65.0\\", + \\"libs\\": {}, + \\"components\\": {} + }, + \\"models\\": { + \\"i18n\\": { + \\"type\\": \\"sap.ui.model.resource.ResourceModel\\", + \\"uri\\": \\"i18n/i18n.properties\\" + }, + \\"@i18n\\": { + \\"type\\": \\"sap.ui.model.resource.ResourceModel\\", + \\"uri\\": \\"i18n/i18n.properties\\" + }, + \\"i18n|sap.suite.ui.generic.template.ListReport|Travel\\": { + \\"type\\": \\"sap.ui.model.resource.ResourceModel\\", + \\"uri\\": \\"i18n/ListReport/Travel/i18n.properties\\" + }, + \\"i18n|sap.suite.ui.generic.template.ObjectPage|Travel\\": { + \\"type\\": \\"sap.ui.model.resource.ResourceModel\\", + \\"uri\\": \\"i18n/ObjectPage/Travel/i18n.properties\\" + }, + \\"i18n|sap.suite.ui.generic.template.ObjectPage|Booking\\": { + \\"type\\": \\"sap.ui.model.resource.ResourceModel\\", + \\"uri\\": \\"i18n/ObjectPage/Booking/i18n.properties\\" + }, + \\"\\": { + \\"dataSource\\": \\"mainService\\", + \\"preload\\": true, + \\"settings\\": { + \\"defaultBindingMode\\": \\"TwoWay\\", + \\"defaultCountMode\\": \\"Inline\\", + \\"refreshAfterChange\\": false, + \\"metadataUrlParams\\": { + \\"sap-value-list\\": \\"none\\" + } + } + } + }, + \\"extends\\": { + \\"extensions\\": {} + }, + \\"contentDensities\\": { + \\"compact\\": true, + \\"cozy\\": true + } + }, + \\"sap.ui.generic.app\\": { + \\"_version\\": \\"1.3.0\\", + \\"settings\\": { + \\"forceGlobalRefresh\\": false, + \\"objectPageHeaderType\\": \\"Dynamic\\", + \\"showDraftToggle\\": false + }, + \\"pages\\": { + \\"ListReport|Travel\\": { + \\"entitySet\\": \\"Travel\\", + \\"component\\": { + \\"name\\": \\"sap.suite.ui.generic.template.ListReport\\", + \\"list\\": true, + \\"settings\\": { + \\"condensedTableLayout\\": true, + \\"smartVariantManagement\\": true, + \\"enableTableFilterInPageVariant\\": true + } + }, + \\"pages\\": { + \\"ObjectPage|Travel\\": { + \\"entitySet\\": \\"Travel\\", + \\"component\\": { + \\"name\\": \\"sap.suite.ui.generic.template.ObjectPage\\" + }, + \\"pages\\": { + \\"ObjectPage|to_Booking\\": { + \\"navigationProperty\\": \\"to_Booking\\", + \\"entitySet\\": \\"Booking\\", + \\"component\\": { + \\"name\\": \\"sap.suite.ui.generic.template.ObjectPage\\" + } + } + } + } + } + } + } + }, + \\"sap.platform.abap\\": { + \\"uri\\": \\"\\" + }, + \\"sap.fiori\\": { + \\"registrationIds\\": [], + \\"archeType\\": \\"transactional\\" + }, + \\"sap.platform.hcp\\": { + \\"uri\\": \\"\\" + }, + \\"sap.cloud\\": { + \\"public\\": true, + \\"service\\": \\"cappapp\\" + } +} +", + "state": "modified", + }, + "app/lrop/xs-app.json": Object { + "contents": "{ + \\"welcomeFile\\": \\"/index.html\\", + \\"authenticationMethod\\": \\"route\\", + \\"routes\\": [ + { + \\"source\\": \\"^/sap/(.*)$\\", + \\"target\\": \\"/sap/$1\\", + \\"destination\\": \\"cappapp-srv-api\\", + \\"authenticationType\\": \\"none\\", + \\"csrfProtection\\": false + }, + { + \\"source\\": \\"^/resources/(.*)$\\", + \\"target\\": \\"/resources/$1\\", + \\"authenticationType\\": \\"none\\", + \\"destination\\": \\"ui5\\" + }, + { + \\"source\\": \\"^/test-resources/(.*)$\\", + \\"target\\": \\"/test-resources/$1\\", + \\"authenticationType\\": \\"none\\", + \\"destination\\": \\"ui5\\" + }, + { + \\"source\\": \\"^(.*)$\\", + \\"target\\": \\"$1\\", + \\"service\\": \\"html5-apps-repo-rt\\", + \\"authenticationType\\": \\"xsuaa\\" + } + ] +} +", + "state": "modified", + }, + "package.json": Object { + "contents": "{ + \\"cds\\": {}, + \\"devDependencies\\": { + \\"rimraf\\": \\"^5.0.5\\", + \\"mbt\\": \\"^1.2.29\\", + \\"@sap/ui5-builder-webide-extension\\": \\"^1.1.9\\", + \\"ui5-task-zipper\\": \\"^3.1.3\\" + } +} +", + "state": "modified", + }, + "xs-security.json": Object { + "contents": "{ + \\"xsappname\\": \\"cappapp\\", + \\"tenant-mode\\": \\"dedicated\\", + \\"description\\": \\"Security profile of called application\\", + \\"scopes\\": [], + \\"role-templates\\": [] +} +", + "state": "modified", + }, +} +`; + +exports[`CF Writer Generate deployment config for CAP project Add destination instance to a HTML5 app inside a CAP project 2`] = ` +"_schema-version: '3.1' +ID: cappapp +version: 1.0.0 +description: A simple CAP project. +parameters: + enable-parallel-deployments: true + deploy_mode: html5-repo +build-parameters: + before-all: + - builder: custom + commands: + - npm ci + - npx -p @sap/cds-dk cds build --production +modules: + - name: cappapp-srv + type: nodejs + path: gen/srv + parameters: + buildpack: nodejs_buildpack + build-parameters: + builder: npm-ci + provides: + - name: srv-api + properties: + srv-url: '\${default-url}' + requires: + - name: cappapp-uaa + - name: cappapp-db-deployer + type: hdb + path: gen/db + parameters: + buildpack: nodejs_buildpack + requires: + - name: cappapp-db + - name: cappapp-destination-content + type: com.sap.application.content + requires: + - name: cappapp-destination-service + parameters: + content-target: true + - name: cappapp-repo-host + parameters: + service-key: + name: cappapp-repo-host-key + - name: cappapp-uaa + parameters: + service-key: + name: cappapp-uaa-key + parameters: + content: + instance: + destinations: + - Name: cappapp_html_repo_host + ServiceInstanceName: cappapp-html5-service + ServiceKeyName: cappapp-repo-host-key + sap.cloud.service: cappapp + - Authentication: OAuth2UserTokenExchange + Name: cappapp_uaa + ServiceInstanceName: cappapp-xsuaa-srv + ServiceKeyName: cappapp-uaa-key + sap.cloud.service: cappapp + existing_destinations_policy: ignore + build-parameters: + no-source: true + - name: cappapp-app-content + type: com.sap.application.content + path: . + requires: + - name: cappapp-repo-host + parameters: + content-target: true + build-parameters: + build-result: resources + requires: + - name: comfioritoolslrop + artifacts: + - comfioritoolslrop.zip + target-path: resources/ + - name: comfioritoolslrop + type: html5 + path: app/lrop + build-parameters: + builder: custom + build-result: dist + commands: + - npm install + - 'npm run build:cf' + supported-platforms: [] +resources: + - name: cappapp-db + type: com.sap.xs.hdi-container + parameters: + service: hana + service-plan: hdi-shared + properties: + hdi-service-name: '\${service-name}' + - name: cappapp-destination-service + type: org.cloudfoundry.managed-service + parameters: + service: destination + service-name: cappapp-destination-service + service-plan: lite + config: + HTML5Runtime_enabled: true + version: 1.0.0 + init_data: + instance: + existing_destinations_policy: update + destinations: + - Name: ui5 + Type: HTTP + URL: 'https://ui5.sap.com' + ProxyType: Internet + Authentication: NoAuthentication + - Name: cappapp-srv-api + Type: HTTP + URL: '~{srv-api/srv-url}' + ProxyType: Internet + Authentication: NoAuthentication + HTML5.DynamicDestination: true + HTML5.ForwardAuthToken: true + requires: + - name: srv-api + - name: cappapp-uaa + type: org.cloudfoundry.managed-service + parameters: + path: ./xs-security.json + service: xsuaa + service-name: cappapp-xsuaa-srv + service-plan: application + - name: cappapp-repo-host + type: org.cloudfoundry.managed-service + parameters: + service-name: cappapp-html5-service + service-plan: app-host + service: html5-apps-repo +" +`; diff --git a/packages/cf-deploy-config-writer/test/unit/__snapshots__/index-app.test.ts.snap b/packages/cf-deploy-config-writer/test/unit/__snapshots__/index-app.test.ts.snap new file mode 100644 index 0000000000..5d6fee548e --- /dev/null +++ b/packages/cf-deploy-config-writer/test/unit/__snapshots__/index-app.test.ts.snap @@ -0,0 +1,1131 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CF Writer App Generate HTML5 App Config Generate deployment configs - HTML5 App and destination read from ui5.yaml 1`] = ` +Object { + ".gitignore": Object { + "contents": "node_modules/ +dist/ +.scp/ +.env +Makefile*.mta +mta_archives +mta-* +resources +archive.zip +.*_mta_build_tmp", + "state": "modified", + }, + "package.json": Object { + "contents": "{ + \\"name\\": \\"basicapp\\", + \\"version\\": \\"0.0.1\\", + \\"private\\": true, + \\"description\\": \\"An SAP Fiori application.\\", + \\"keywords\\": [ + \\"ui5\\", + \\"openui5\\", + \\"sapui5\\" + ], + \\"main\\": \\"webapp/index.html\\", + \\"dependencies\\": {}, + \\"devDependencies\\": { + \\"@ui5/cli\\": \\"^3.0.0\\", + \\"@sap/ux-ui5-tooling\\": \\"1\\", + \\"rimraf\\": \\"^5.0.5\\", + \\"mbt\\": \\"^1.2.29\\", + \\"@sap/ui5-builder-webide-extension\\": \\"^1.1.9\\", + \\"ui5-task-zipper\\": \\"^3.1.3\\" + }, + \\"scripts\\": { + \\"start\\": \\"fiori run --open \\\\\\"test/flpSandbox.html?sap-ui-xx-viewCache=false#basicapp-display\\\\\\"\\", + \\"start-local\\": \\"fiori run --config ./ui5-local.yaml --open \\\\\\"test/flpSandbox.html?sap-ui-xx-viewCache=false#basicapp-display\\\\\\"\\", + \\"build\\": \\"ui5 build --config=ui5.yaml --clean-dest --dest dist\\", + \\"deploy\\": \\"fiori cfDeploy\\", + \\"deploy-config\\": \\"fiori add deploy-config\\", + \\"start-noflp\\": \\"fiori run --open \\\\\\"index.html?sap-ui-xx-viewCache=false\\\\\\"\\", + \\"start-variants-management\\": \\"fiori run --open \\\\\\"preview.html?sap-ui-xx-viewCache=false&fiori-tools-rta-mode=true&sap-ui-rta-skip-flex-validation=true#preview-app\\\\\\"\\", + \\"unit-tests\\": \\"fiori run --open 'test/unit/unitTests.qunit.html'\\", + \\"int-tests\\": \\"fiori run --open 'test/integration/opaTests.qunit.html'\\", + \\"build:cf\\": \\"ui5 build preload --clean-dest --config ui5-deploy.yaml --include-task=generateCachebusterInfo\\", + \\"build:mta\\": \\"rimraf resources mta_archives && mbt build --mtar archive\\", + \\"undeploy\\": \\"cf undeploy basicapp --delete-services --delete-service-keys --delete-service-brokers\\" + }, + \\"sapuxLayer\\": \\"VENDOR\\" +} +", + "state": "modified", + }, + "ui5-deploy.yaml": Object { + "contents": "# yaml-language-server: $schema=https://sap.github.io/ui5-tooling/schema/ui5.yaml.json + +specVersion: '2.4' +metadata: + name: 'basicapp' +type: application +builder: + resources: + excludes: + - /test/** + - /localService/** + customTasks: + - name: webide-extension-task-updateManifestJson + afterTask: replaceVersion + configuration: + appFolder: webapp + destDir: dist + - name: ui5-task-zipper + afterTask: generateCachebusterInfo + configuration: + archiveName: basicapp + additionalFiles: + - xs-app.json +", + "state": "modified", + }, + "webapp/manifest.json": Object { + "contents": "{ + \\"_version\\": \\"1.59.0\\", + \\"sap.app\\": { + \\"id\\": \\"basicapp\\", + \\"type\\": \\"application\\", + \\"i18n\\": \\"i18n/i18n.properties\\", + \\"applicationVersion\\": { + \\"version\\": \\"0.0.1\\" + }, + \\"title\\": \\"{{appTitle}}\\", + \\"description\\": \\"{{appDescription}}\\", + \\"resources\\": \\"resources.json\\", + \\"sourceTemplate\\": { + \\"id\\": \\"@sap/generator-fiori:basic\\", + \\"version\\": \\"1.14.2-pre-20240708140014-4973a7ece.0\\", + \\"toolsId\\": \\"c1c8a120-2e9a-4d6e-8363-7e2622948044\\" + } + }, + \\"sap.ui\\": { + \\"technology\\": \\"UI5\\", + \\"icons\\": { + \\"icon\\": \\"\\", + \\"favIcon\\": \\"\\", + \\"phone\\": \\"\\", + \\"phone@2\\": \\"\\", + \\"tablet\\": \\"\\", + \\"tablet@2\\": \\"\\" + }, + \\"deviceTypes\\": { + \\"desktop\\": true, + \\"tablet\\": true, + \\"phone\\": true + } + }, + \\"sap.ui5\\": { + \\"flexEnabled\\": true, + \\"dependencies\\": { + \\"minUI5Version\\": \\"1.125.1\\", + \\"libs\\": { + \\"sap.m\\": {}, + \\"sap.ui.core\\": {}, + \\"sap.f\\": {}, + \\"sap.suite.ui.generic.template\\": {}, + \\"sap.ui.comp\\": {}, + \\"sap.ui.generic.app\\": {}, + \\"sap.ui.table\\": {}, + \\"sap.ushell\\": {} + } + }, + \\"contentDensities\\": { + \\"compact\\": true, + \\"cozy\\": true + }, + \\"models\\": { + \\"i18n\\": { + \\"type\\": \\"sap.ui.model.resource.ResourceModel\\", + \\"settings\\": { + \\"bundleName\\": \\"basicapp.i18n.i18n\\" + } + } + }, + \\"resources\\": { + \\"css\\": [ + { + \\"uri\\": \\"css/style.css\\" + } + ] + }, + \\"routing\\": { + \\"config\\": { + \\"routerClass\\": \\"sap.m.routing.Router\\", + \\"viewType\\": \\"XML\\", + \\"async\\": true, + \\"viewPath\\": \\"basicapp.view\\", + \\"controlAggregation\\": \\"pages\\", + \\"controlId\\": \\"app\\", + \\"clearControlAggregation\\": false + }, + \\"routes\\": [ + { + \\"name\\": \\"RouteView1\\", + \\"pattern\\": \\":?query:\\", + \\"target\\": [ + \\"TargetView1\\" + ] + } + ], + \\"targets\\": { + \\"TargetView1\\": { + \\"viewType\\": \\"XML\\", + \\"transition\\": \\"slide\\", + \\"clearControlAggregation\\": false, + \\"viewId\\": \\"View1\\", + \\"viewName\\": \\"View1\\" + } + } + }, + \\"rootView\\": { + \\"viewName\\": \\"basicapp.view.App\\", + \\"type\\": \\"XML\\", + \\"async\\": true, + \\"id\\": \\"App\\" + } + }, + \\"sap.cloud\\": { + \\"public\\": true, + \\"service\\": \\"basicapp\\" + } +} +", + "state": "modified", + }, + "xs-app.json": Object { + "contents": "{ + \\"welcomeFile\\": \\"/index.html\\", + \\"authenticationMethod\\": \\"route\\", + \\"routes\\": [ + { + \\"source\\": \\"^undefined/.*/(.*)$\\", + \\"target\\": \\"/$1\\", + \\"destination\\": \\"TestDestination\\", + \\"authenticationType\\": \\"none\\", + \\"csrfProtection\\": false + }, + { + \\"source\\": \\"^/resources/(.*)$\\", + \\"target\\": \\"/resources/$1\\", + \\"authenticationType\\": \\"none\\", + \\"destination\\": \\"ui5\\" + }, + { + \\"source\\": \\"^/test-resources/(.*)$\\", + \\"target\\": \\"/test-resources/$1\\", + \\"authenticationType\\": \\"none\\", + \\"destination\\": \\"ui5\\" + }, + { + \\"source\\": \\"^(.*)$\\", + \\"target\\": \\"$1\\", + \\"service\\": \\"html5-apps-repo-rt\\", + \\"authenticationType\\": \\"xsuaa\\" + } + ] +} +", + "state": "modified", + }, + "xs-security.json": Object { + "contents": "{ + \\"xsappname\\": \\"basicapp\\", + \\"tenant-mode\\": \\"dedicated\\", + \\"description\\": \\"Security profile of called application\\", + \\"scopes\\": [], + \\"role-templates\\": [] +} +", + "state": "modified", + }, +} +`; + +exports[`CF Writer App Generate HTML5 App Config Generate deployment configs - HTML5 App and destination read from ui5.yaml 2`] = ` +"_schema-version: \\"3.2\\" +ID: basicapp +description: Generated by Fiori Tools +version: 0.0.1 +modules: +- name: basicapp-destination-content + type: com.sap.application.content + requires: + - name: basicapp-destination-service + parameters: + content-target: true + - name: basicapp-repo-host + parameters: + service-key: + name: basicapp-repo-host-key + - name: basicapp-uaa + parameters: + service-key: + name: basicapp-uaa-key + parameters: + content: + instance: + destinations: + - Name: basicapp_html_repo_host + ServiceInstanceName: basicapp-html5-service + ServiceKeyName: basicapp-repo-host-key + sap.cloud.service: basicapp + - Authentication: OAuth2UserTokenExchange + Name: basicapp_uaa + ServiceInstanceName: basicapp-xsuaa-srv + ServiceKeyName: basicapp-uaa-key + sap.cloud.service: basicapp + existing_destinations_policy: ignore + build-parameters: + no-source: true +- name: basicapp-app-content + type: com.sap.application.content + path: . + requires: + - name: basicapp-repo-host + parameters: + content-target: true + build-parameters: + build-result: resources + requires: + - artifacts: + - basicapp.zip + name: basicapp + target-path: resources/ +- name: basicapp + type: html5 + path: . + build-parameters: + build-result: dist + builder: custom + commands: + - npm install + - npm run build:cf + supported-platforms: [] +resources: +- name: basicapp-destination-service + type: org.cloudfoundry.managed-service + parameters: + config: + HTML5Runtime_enabled: true + init_data: + instance: + destinations: + - Authentication: NoAuthentication + Name: ui5 + ProxyType: Internet + Type: HTTP + URL: https://ui5.sap.com + existing_destinations_policy: update + version: 1.0.0 + service: destination + service-name: basicapp-destination-service + service-plan: lite +- name: basicapp-uaa + type: org.cloudfoundry.managed-service + parameters: + path: ./xs-security.json + service: xsuaa + service-name: basicapp-xsuaa-srv + service-plan: application +- name: basicapp-repo-host + type: org.cloudfoundry.managed-service + parameters: + service: html5-apps-repo + service-name: basicapp-html5-service + service-plan: app-host +parameters: + deploy_mode: html5-repo + enable-parallel-deployments: true +" +`; + +exports[`CF Writer App Generate HTML5 App Config Generate deployment configs - HTML5 App with managed approuter attached to a multi target application 1`] = ` +Object { + ".gitignore": Object { + "contents": "node_modules/ +dist/ +.scp/ +.env +Makefile*.mta +mta_archives +mta-* +resources +archive.zip +.*_mta_build_tmp", + "state": "modified", + }, + "package.json": Object { + "contents": "{ + \\"name\\": \\"multi\\", + \\"version\\": \\"0.0.1\\", + \\"private\\": true, + \\"description\\": \\"An SAP Fiori application.\\", + \\"keywords\\": [ + \\"ui5\\", + \\"openui5\\", + \\"sapui5\\" + ], + \\"main\\": \\"webapp/index.html\\", + \\"dependencies\\": {}, + \\"devDependencies\\": { + \\"@ui5/cli\\": \\"^3.0.0\\", + \\"@sap/ux-ui5-tooling\\": \\"1\\", + \\"rimraf\\": \\"^5.0.5\\", + \\"mbt\\": \\"^1.2.29\\", + \\"@sap/ui5-builder-webide-extension\\": \\"^1.1.9\\", + \\"ui5-task-zipper\\": \\"^3.1.3\\" + }, + \\"scripts\\": { + \\"start\\": \\"fiori run --open \\\\\\"test/flpSandbox.html?sap-ui-xx-viewCache=false#lrop-display\\\\\\"\\", + \\"start-local\\": \\"fiori run --config ./ui5-local.yaml --open \\\\\\"test/flpSandbox.html?sap-ui-xx-viewCache=false#lrop-display\\\\\\"\\", + \\"build\\": \\"ui5 build --config=ui5.yaml --clean-dest --dest dist\\", + \\"deploy\\": \\"fiori cfDeploy -e mta-ext.mtaext\\", + \\"deploy-config\\": \\"fiori add deploy-config\\", + \\"start-noflp\\": \\"fiori run --open \\\\\\"index.html?sap-ui-xx-viewCache=false\\\\\\"\\", + \\"start-variants-management\\": \\"fiori run --open \\\\\\"preview.html?sap-ui-xx-viewCache=false&fiori-tools-rta-mode=true&sap-ui-rta-skip-flex-validation=true#preview-app\\\\\\"\\", + \\"unit-tests\\": \\"fiori run --open 'test/unit/unitTests.qunit.html'\\", + \\"int-tests\\": \\"fiori run --open 'test/integration/opaTests.qunit.html'\\", + \\"build:cf\\": \\"ui5 build preload --clean-dest --config ui5-deploy.yaml --include-task=generateCachebusterInfo\\", + \\"build:mta\\": \\"rimraf resources mta_archives && mbt build --mtar archive\\", + \\"undeploy\\": \\"cf undeploy multiproject --delete-services --delete-service-keys --delete-service-brokers\\" + }, + \\"sapuxLayer\\": \\"VENDOR\\" +} +", + "state": "modified", + }, + "ui5-deploy.yaml": Object { + "contents": "# yaml-language-server: $schema=https://sap.github.io/ui5-tooling/schema/ui5.yaml.json + +specVersion: '2.4' +metadata: + name: 'multi' +type: application +builder: + resources: + excludes: + - /test/** + - /localService/** + customTasks: + - name: webide-extension-task-updateManifestJson + afterTask: replaceVersion + configuration: + appFolder: webapp + destDir: dist + - name: ui5-task-zipper + afterTask: generateCachebusterInfo + configuration: + archiveName: comfioritoolslrop + additionalFiles: + - xs-app.json +", + "state": "modified", + }, + "webapp/manifest.json": Object { + "contents": "{ + \\"_version\\": \\"1.8.0\\", + \\"sap.app\\": { + \\"id\\": \\"com.fiori.tools.lrop\\", + \\"type\\": \\"application\\", + \\"i18n\\": \\"i18n/i18n.properties\\", + \\"applicationVersion\\": { + \\"version\\": \\"1.0.0\\" + }, + \\"title\\": \\"{{appTitle}}\\", + \\"description\\": \\"{{appDescription}}\\", + \\"tags\\": { + \\"keywords\\": [] + }, + \\"ach\\": \\"\\", + \\"resources\\": \\"resources.json\\", + \\"dataSources\\": { + \\"mainService\\": { + \\"uri\\": \\"/sap/opu/odata/sap/ZUI_RAP_TRAVEL_M_U025/\\", + \\"type\\": \\"OData\\", + \\"settings\\": { + \\"annotations\\": [ + \\"ZUI_RAP_TRAVEL_M_U025_VAN\\", + \\"annotation\\" + ], + \\"localUri\\": \\"localService/metadata.xml\\" + } + }, + \\"annotation\\": { + \\"type\\": \\"ODataAnnotation\\", + \\"uri\\": \\"annotations/annotation.xml\\", + \\"settings\\": { + \\"localUri\\": \\"annotations/annotation.xml\\" + } + } + }, + \\"offline\\": false, + \\"sourceTemplate\\": { + \\"id\\": \\"ui5template.smartTemplate\\", + \\"version\\": \\"1.40.12\\" + } + }, + \\"sap.ui\\": { + \\"technology\\": \\"UI5\\", + \\"icons\\": { + \\"icon\\": \\"\\", + \\"favIcon\\": \\"\\", + \\"phone\\": \\"\\", + \\"phone@2\\": \\"\\", + \\"tablet\\": \\"\\", + \\"tablet@2\\": \\"\\" + }, + \\"deviceTypes\\": { + \\"desktop\\": true, + \\"tablet\\": true, + \\"phone\\": true + }, + \\"supportedThemes\\": [ + \\"sap_hcb\\", + \\"sap_belize\\" + ] + }, + \\"sap.ui5\\": { + \\"resources\\": { + \\"js\\": [], + \\"css\\": [] + }, + \\"dependencies\\": { + \\"minUI5Version\\": \\"1.65.0\\", + \\"libs\\": {}, + \\"components\\": {} + }, + \\"models\\": { + \\"i18n\\": { + \\"type\\": \\"sap.ui.model.resource.ResourceModel\\", + \\"uri\\": \\"i18n/i18n.properties\\" + }, + \\"@i18n\\": { + \\"type\\": \\"sap.ui.model.resource.ResourceModel\\", + \\"uri\\": \\"i18n/i18n.properties\\" + }, + \\"i18n|sap.suite.ui.generic.template.ListReport|Travel\\": { + \\"type\\": \\"sap.ui.model.resource.ResourceModel\\", + \\"uri\\": \\"i18n/ListReport/Travel/i18n.properties\\" + }, + \\"i18n|sap.suite.ui.generic.template.ObjectPage|Travel\\": { + \\"type\\": \\"sap.ui.model.resource.ResourceModel\\", + \\"uri\\": \\"i18n/ObjectPage/Travel/i18n.properties\\" + }, + \\"i18n|sap.suite.ui.generic.template.ObjectPage|Booking\\": { + \\"type\\": \\"sap.ui.model.resource.ResourceModel\\", + \\"uri\\": \\"i18n/ObjectPage/Booking/i18n.properties\\" + }, + \\"\\": { + \\"dataSource\\": \\"mainService\\", + \\"preload\\": true, + \\"settings\\": { + \\"defaultBindingMode\\": \\"TwoWay\\", + \\"defaultCountMode\\": \\"Inline\\", + \\"refreshAfterChange\\": false, + \\"metadataUrlParams\\": { + \\"sap-value-list\\": \\"none\\" + } + } + } + }, + \\"extends\\": { + \\"extensions\\": {} + }, + \\"contentDensities\\": { + \\"compact\\": true, + \\"cozy\\": true + } + }, + \\"sap.ui.generic.app\\": { + \\"_version\\": \\"1.3.0\\", + \\"settings\\": { + \\"forceGlobalRefresh\\": false, + \\"objectPageHeaderType\\": \\"Dynamic\\", + \\"showDraftToggle\\": false + }, + \\"pages\\": { + \\"ListReport|Travel\\": { + \\"entitySet\\": \\"Travel\\", + \\"component\\": { + \\"name\\": \\"sap.suite.ui.generic.template.ListReport\\", + \\"list\\": true, + \\"settings\\": { + \\"condensedTableLayout\\": true, + \\"smartVariantManagement\\": true, + \\"enableTableFilterInPageVariant\\": true + } + }, + \\"pages\\": { + \\"ObjectPage|Travel\\": { + \\"entitySet\\": \\"Travel\\", + \\"component\\": { + \\"name\\": \\"sap.suite.ui.generic.template.ObjectPage\\" + }, + \\"pages\\": { + \\"ObjectPage|to_Booking\\": { + \\"navigationProperty\\": \\"to_Booking\\", + \\"entitySet\\": \\"Booking\\", + \\"component\\": { + \\"name\\": \\"sap.suite.ui.generic.template.ObjectPage\\" + } + } + } + } + } + } + } + }, + \\"sap.platform.abap\\": { + \\"uri\\": \\"\\" + }, + \\"sap.fiori\\": { + \\"registrationIds\\": [], + \\"archeType\\": \\"transactional\\" + }, + \\"sap.platform.hcp\\": { + \\"uri\\": \\"\\" + }, + \\"sap.cloud\\": { + \\"public\\": true, + \\"service\\": \\"multiproject\\" + } +} +", + "state": "modified", + }, + "xs-app.json": Object { + "contents": "{ + \\"welcomeFile\\": \\"/index.html\\", + \\"authenticationMethod\\": \\"route\\", + \\"routes\\": [ + { + \\"source\\": \\"^/resources/(.*)$\\", + \\"target\\": \\"/resources/$1\\", + \\"authenticationType\\": \\"none\\", + \\"destination\\": \\"ui5\\" + }, + { + \\"source\\": \\"^/test-resources/(.*)$\\", + \\"target\\": \\"/test-resources/$1\\", + \\"authenticationType\\": \\"none\\", + \\"destination\\": \\"ui5\\" + }, + { + \\"source\\": \\"^(.*)$\\", + \\"target\\": \\"$1\\", + \\"service\\": \\"html5-apps-repo-rt\\", + \\"authenticationType\\": \\"xsuaa\\" + } + ] +} +", + "state": "modified", + }, + "xs-security.json": Object { + "contents": "{ + \\"xsappname\\": \\"multiproject\\", + \\"tenant-mode\\": \\"dedicated\\", + \\"description\\": \\"Security profile of called application\\", + \\"scopes\\": [], + \\"role-templates\\": [] +} +", + "state": "modified", + }, +} +`; + +exports[`CF Writer App Generate HTML5 App Config Generate deployment configs - HTML5 App with managed approuter attached to a multi target application 2`] = ` +"_schema-version: \\"3.1\\" +ID: multiproject +description: Fiori elements app +version: 0.0.1 +modules: +- name: multiproject-destination-content + type: com.sap.application.content + requires: + - name: multiproject-destination-service + parameters: + content-target: true + - name: multiproject-repo-host + parameters: + service-key: + name: multiproject-repo-host-key + - name: multiproject-uaa + parameters: + service-key: + name: multiproject-uaa-key + parameters: + content: + instance: + destinations: + - Name: multiproject_html_repo_host + ServiceInstanceName: multiproject-html5-service + ServiceKeyName: multiproject-repo-host-key + sap.cloud.service: multiproject + - Authentication: OAuth2UserTokenExchange + Name: multiproject_uaa + ServiceInstanceName: multiproject-xsuaa-srv + ServiceKeyName: multiproject-uaa-key + sap.cloud.service: multiproject + existing_destinations_policy: ignore + build-parameters: + no-source: true +- name: multiproject-app-content + type: com.sap.application.content + path: . + requires: + - name: multiproject-repo-host + parameters: + content-target: true + build-parameters: + build-result: resources + requires: + - artifacts: + - comfioritoolslrop.zip + name: comfioritoolslrop + target-path: resources/ +- name: comfioritoolslrop + type: html5 + path: . + build-parameters: + build-result: dist + builder: custom + commands: + - npm install + - npm run build:cf + supported-platforms: [] +resources: +- name: multiproject-destination-service + type: org.cloudfoundry.managed-service + parameters: + config: + HTML5Runtime_enabled: true + init_data: + instance: + destinations: + - Authentication: NoAuthentication + Name: ui5 + ProxyType: Internet + Type: HTTP + URL: https://ui5.sap.com + existing_destinations_policy: update + version: 1.0.0 + service: destination + service-name: multiproject-destination-service + service-plan: lite +- name: multiproject-uaa + type: org.cloudfoundry.managed-service + parameters: + path: ./xs-security.json + service: xsuaa + service-name: multiproject-xsuaa-srv + service-plan: application +- name: multiproject-repo-host + type: org.cloudfoundry.managed-service + parameters: + service: html5-apps-repo + service-name: multiproject-html5-service + service-plan: app-host +parameters: + deploy_mode: html5-repo + enable-parallel-deployments: true +" +`; + +exports[`CF Writer App Generate HTML5 App Config Generate deployment configs - HTML5 App with managed approuter attached with no destination available 1`] = ` +Object { + ".gitignore": Object { + "contents": "node_modules/ +dist/ +.scp/ +.env +Makefile*.mta +mta_archives +mta-* +resources +archive.zip +.*_mta_build_tmp", + "state": "modified", + }, + "package.json": Object { + "contents": "{ + \\"name\\": \\"lrop\\", + \\"version\\": \\"0.0.1\\", + \\"private\\": true, + \\"description\\": \\"An SAP Fiori application.\\", + \\"keywords\\": [ + \\"ui5\\", + \\"openui5\\", + \\"sapui5\\" + ], + \\"main\\": \\"webapp/index.html\\", + \\"dependencies\\": {}, + \\"devDependencies\\": { + \\"@ui5/cli\\": \\"^3.0.0\\", + \\"@sap/ux-ui5-tooling\\": \\"1\\", + \\"rimraf\\": \\"^5.0.5\\", + \\"mbt\\": \\"^1.2.29\\", + \\"@sap/ui5-builder-webide-extension\\": \\"^1.1.9\\", + \\"ui5-task-zipper\\": \\"^3.1.3\\" + }, + \\"scripts\\": { + \\"start\\": \\"fiori run --open \\\\\\"test/flpSandbox.html?sap-ui-xx-viewCache=false#lrop-display\\\\\\"\\", + \\"start-local\\": \\"fiori run --config ./ui5-local.yaml --open \\\\\\"test/flpSandbox.html?sap-ui-xx-viewCache=false#lrop-display\\\\\\"\\", + \\"build\\": \\"ui5 build --config=ui5.yaml --clean-dest --dest dist\\", + \\"deploy\\": \\"fiori cfDeploy\\", + \\"deploy-config\\": \\"fiori add deploy-config\\", + \\"start-noflp\\": \\"fiori run --open \\\\\\"index.html?sap-ui-xx-viewCache=false\\\\\\"\\", + \\"start-variants-management\\": \\"fiori run --open \\\\\\"preview.html?sap-ui-xx-viewCache=false&fiori-tools-rta-mode=true&sap-ui-rta-skip-flex-validation=true#preview-app\\\\\\"\\", + \\"unit-tests\\": \\"fiori run --open 'test/unit/unitTests.qunit.html'\\", + \\"int-tests\\": \\"fiori run --open 'test/integration/opaTests.qunit.html'\\", + \\"build:cf\\": \\"ui5 build preload --clean-dest --config ui5-deploy.yaml --include-task=generateCachebusterInfo\\", + \\"build:mta\\": \\"rimraf resources mta_archives && mbt build --mtar archive\\", + \\"undeploy\\": \\"cf undeploy comfioritoolslrop --delete-services --delete-service-keys --delete-service-brokers\\" + }, + \\"sapuxLayer\\": \\"VENDOR\\" +} +", + "state": "modified", + }, + "ui5-deploy.yaml": Object { + "contents": "# yaml-language-server: $schema=https://sap.github.io/ui5-tooling/schema/ui5.yaml.json + +specVersion: '2.4' +metadata: + name: 'lrop' +type: application +builder: + resources: + excludes: + - /test/** + - /localService/** + customTasks: + - name: webide-extension-task-updateManifestJson + afterTask: replaceVersion + configuration: + appFolder: webapp + destDir: dist + - name: ui5-task-zipper + afterTask: generateCachebusterInfo + configuration: + archiveName: comfioritoolslrop + additionalFiles: + - xs-app.json +", + "state": "modified", + }, + "webapp/manifest.json": Object { + "contents": "{ + \\"_version\\": \\"1.8.0\\", + \\"sap.app\\": { + \\"id\\": \\"com.fiori.tools.lrop\\", + \\"type\\": \\"application\\", + \\"i18n\\": \\"i18n/i18n.properties\\", + \\"applicationVersion\\": { + \\"version\\": \\"1.0.0\\" + }, + \\"title\\": \\"{{appTitle}}\\", + \\"description\\": \\"{{appDescription}}\\", + \\"tags\\": { + \\"keywords\\": [] + }, + \\"ach\\": \\"\\", + \\"resources\\": \\"resources.json\\", + \\"dataSources\\": { + \\"mainService\\": { + \\"uri\\": \\"/sap/opu/odata/sap/ZUI_RAP_TRAVEL_M_U025/\\", + \\"type\\": \\"OData\\", + \\"settings\\": { + \\"annotations\\": [ + \\"ZUI_RAP_TRAVEL_M_U025_VAN\\", + \\"annotation\\" + ], + \\"localUri\\": \\"localService/metadata.xml\\" + } + }, + \\"annotation\\": { + \\"type\\": \\"ODataAnnotation\\", + \\"uri\\": \\"annotations/annotation.xml\\", + \\"settings\\": { + \\"localUri\\": \\"annotations/annotation.xml\\" + } + } + }, + \\"offline\\": false, + \\"sourceTemplate\\": { + \\"id\\": \\"ui5template.smartTemplate\\", + \\"version\\": \\"1.40.12\\" + } + }, + \\"sap.ui\\": { + \\"technology\\": \\"UI5\\", + \\"icons\\": { + \\"icon\\": \\"\\", + \\"favIcon\\": \\"\\", + \\"phone\\": \\"\\", + \\"phone@2\\": \\"\\", + \\"tablet\\": \\"\\", + \\"tablet@2\\": \\"\\" + }, + \\"deviceTypes\\": { + \\"desktop\\": true, + \\"tablet\\": true, + \\"phone\\": true + }, + \\"supportedThemes\\": [ + \\"sap_hcb\\", + \\"sap_belize\\" + ] + }, + \\"sap.ui5\\": { + \\"resources\\": { + \\"js\\": [], + \\"css\\": [] + }, + \\"dependencies\\": { + \\"minUI5Version\\": \\"1.65.0\\", + \\"libs\\": {}, + \\"components\\": {} + }, + \\"models\\": { + \\"i18n\\": { + \\"type\\": \\"sap.ui.model.resource.ResourceModel\\", + \\"uri\\": \\"i18n/i18n.properties\\" + }, + \\"@i18n\\": { + \\"type\\": \\"sap.ui.model.resource.ResourceModel\\", + \\"uri\\": \\"i18n/i18n.properties\\" + }, + \\"i18n|sap.suite.ui.generic.template.ListReport|Travel\\": { + \\"type\\": \\"sap.ui.model.resource.ResourceModel\\", + \\"uri\\": \\"i18n/ListReport/Travel/i18n.properties\\" + }, + \\"i18n|sap.suite.ui.generic.template.ObjectPage|Travel\\": { + \\"type\\": \\"sap.ui.model.resource.ResourceModel\\", + \\"uri\\": \\"i18n/ObjectPage/Travel/i18n.properties\\" + }, + \\"i18n|sap.suite.ui.generic.template.ObjectPage|Booking\\": { + \\"type\\": \\"sap.ui.model.resource.ResourceModel\\", + \\"uri\\": \\"i18n/ObjectPage/Booking/i18n.properties\\" + }, + \\"\\": { + \\"dataSource\\": \\"mainService\\", + \\"preload\\": true, + \\"settings\\": { + \\"defaultBindingMode\\": \\"TwoWay\\", + \\"defaultCountMode\\": \\"Inline\\", + \\"refreshAfterChange\\": false, + \\"metadataUrlParams\\": { + \\"sap-value-list\\": \\"none\\" + } + } + } + }, + \\"extends\\": { + \\"extensions\\": {} + }, + \\"contentDensities\\": { + \\"compact\\": true, + \\"cozy\\": true + } + }, + \\"sap.ui.generic.app\\": { + \\"_version\\": \\"1.3.0\\", + \\"settings\\": { + \\"forceGlobalRefresh\\": false, + \\"objectPageHeaderType\\": \\"Dynamic\\", + \\"showDraftToggle\\": false + }, + \\"pages\\": { + \\"ListReport|Travel\\": { + \\"entitySet\\": \\"Travel\\", + \\"component\\": { + \\"name\\": \\"sap.suite.ui.generic.template.ListReport\\", + \\"list\\": true, + \\"settings\\": { + \\"condensedTableLayout\\": true, + \\"smartVariantManagement\\": true, + \\"enableTableFilterInPageVariant\\": true + } + }, + \\"pages\\": { + \\"ObjectPage|Travel\\": { + \\"entitySet\\": \\"Travel\\", + \\"component\\": { + \\"name\\": \\"sap.suite.ui.generic.template.ObjectPage\\" + }, + \\"pages\\": { + \\"ObjectPage|to_Booking\\": { + \\"navigationProperty\\": \\"to_Booking\\", + \\"entitySet\\": \\"Booking\\", + \\"component\\": { + \\"name\\": \\"sap.suite.ui.generic.template.ObjectPage\\" + } + } + } + } + } + } + } + }, + \\"sap.platform.abap\\": { + \\"uri\\": \\"\\" + }, + \\"sap.fiori\\": { + \\"registrationIds\\": [], + \\"archeType\\": \\"transactional\\" + }, + \\"sap.platform.hcp\\": { + \\"uri\\": \\"\\" + }, + \\"sap.cloud\\": { + \\"public\\": true, + \\"service\\": \\"comfioritoolslrop\\" + } +} +", + "state": "modified", + }, + "xs-app.json": Object { + "contents": "{ + \\"welcomeFile\\": \\"/index.html\\", + \\"authenticationMethod\\": \\"route\\", + \\"routes\\": [ + { + \\"source\\": \\"^/resources/(.*)$\\", + \\"target\\": \\"/resources/$1\\", + \\"authenticationType\\": \\"none\\", + \\"destination\\": \\"ui5\\" + }, + { + \\"source\\": \\"^/test-resources/(.*)$\\", + \\"target\\": \\"/test-resources/$1\\", + \\"authenticationType\\": \\"none\\", + \\"destination\\": \\"ui5\\" + }, + { + \\"source\\": \\"^(.*)$\\", + \\"target\\": \\"$1\\", + \\"service\\": \\"html5-apps-repo-rt\\", + \\"authenticationType\\": \\"xsuaa\\" + } + ] +} +", + "state": "modified", + }, + "xs-security.json": Object { + "contents": "{ + \\"xsappname\\": \\"comfioritoolslrop\\", + \\"tenant-mode\\": \\"dedicated\\", + \\"description\\": \\"Security profile of called application\\", + \\"scopes\\": [], + \\"role-templates\\": [] +} +", + "state": "modified", + }, +} +`; + +exports[`CF Writer App Generate HTML5 App Config Generate deployment configs - HTML5 App with managed approuter attached with no destination available 2`] = ` +"_schema-version: \\"3.2\\" +ID: comfioritoolslrop +description: Generated by Fiori Tools +version: 0.0.1 +modules: +- name: comfioritoolslrop-destination-content + type: com.sap.application.content + requires: + - name: comfioritoolslrop-destination-service + parameters: + content-target: true + - name: comfioritoolslrop-repo-host + parameters: + service-key: + name: comfioritoolslrop-repo-host-key + - name: comfioritoolslrop-uaa + parameters: + service-key: + name: comfioritoolslrop-uaa-key + parameters: + content: + instance: + destinations: + - Name: comfioritoolslrop_html_repo_host + ServiceInstanceName: comfioritoolslrop-html5-service + ServiceKeyName: comfioritoolslrop-repo-host-key + sap.cloud.service: comfioritoolslrop + - Authentication: OAuth2UserTokenExchange + Name: comfioritoolslrop_uaa + ServiceInstanceName: comfioritoolslrop-xsuaa-srv + ServiceKeyName: comfioritoolslrop-uaa-key + sap.cloud.service: comfioritoolslrop + existing_destinations_policy: ignore + build-parameters: + no-source: true +- name: comfioritoolslrop-app-content + type: com.sap.application.content + path: . + requires: + - name: comfioritoolslrop-repo-host + parameters: + content-target: true + build-parameters: + build-result: resources + requires: + - artifacts: + - comfioritoolslrop.zip + name: comfioritoolslrop + target-path: resources/ +- name: comfioritoolslrop + type: html5 + path: . + build-parameters: + build-result: dist + builder: custom + commands: + - npm install + - npm run build:cf + supported-platforms: [] +resources: +- name: comfioritoolslrop-destination-service + type: org.cloudfoundry.managed-service + parameters: + config: + HTML5Runtime_enabled: true + init_data: + instance: + destinations: + - Authentication: NoAuthentication + Name: ui5 + ProxyType: Internet + Type: HTTP + URL: https://ui5.sap.com + existing_destinations_policy: update + version: 1.0.0 + service: destination + service-name: comfioritoolslrop-destination-service + service-plan: lite +- name: comfioritoolslrop-uaa + type: org.cloudfoundry.managed-service + parameters: + path: ./xs-security.json + service: xsuaa + service-name: comfioritoolslrop-xsuaa-srv + service-plan: application +- name: comfioritoolslrop-repo-host + type: org.cloudfoundry.managed-service + parameters: + service: html5-apps-repo + service-name: comfioritoolslrop-html5-service + service-plan: app-host +parameters: + deploy_mode: html5-repo + enable-parallel-deployments: true +" +`; diff --git a/packages/cf-deploy-config-writer/test/unit/__snapshots__/index-base.test.ts.snap b/packages/cf-deploy-config-writer/test/unit/__snapshots__/index-base.test.ts.snap new file mode 100644 index 0000000000..322b140fb6 --- /dev/null +++ b/packages/cf-deploy-config-writer/test/unit/__snapshots__/index-base.test.ts.snap @@ -0,0 +1,471 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CF Writer Base Generate Base Config - Managed Generate deployment configs - managed 1`] = ` +Object { + ".gitignore": Object { + "contents": "node_modules/ +dist/ +.scp/ +.env +Makefile*.mta +mta_archives +mta-* +resources +archive.zip +.*_mta_build_tmp", + "state": "modified", + }, + "package.json": Object { + "contents": "{ + \\"name\\": \\"mta-project\\", + \\"version\\": \\"0.0.1\\", + \\"description\\": \\"Build and deployment scripts\\", + \\"scripts\\": { + \\"clean\\": \\"rimraf resources mta_archives mta-op*\\", + \\"build\\": \\"rimraf resources mta_archives && mbt build --mtar archive\\", + \\"deploy\\": \\"cf deploy mta_archives/archive.mtar --retries 1\\", + \\"undeploy\\": \\"cf undeploy managed --delete-services --delete-service-keys --delete-service-brokers\\" + }, + \\"devDependencies\\": { + \\"mbt\\": \\"^1.2.27\\", + \\"rimraf\\": \\"^5.0.5\\" + } +} +", + "state": "modified", + }, + "xs-security.json": Object { + "contents": "{ + \\"xsappname\\": \\"managed\\", + \\"tenant-mode\\": \\"dedicated\\", + \\"description\\": \\"Security profile of called application\\", + \\"scopes\\": [], + \\"role-templates\\": [] +} +", + "state": "modified", + }, +} +`; + +exports[`CF Writer Base Generate Base Config - Managed Generate deployment configs - managed 2`] = ` +"_schema-version: \\"3.2\\" +ID: managed +description: MyManagedDescription +version: 0.0.1 +modules: +- name: managed-destination-content + type: com.sap.application.content + requires: + - name: managed-destination-service + parameters: + content-target: true + - name: managed-repo-host + parameters: + service-key: + name: managed-repo-host-key + - name: managed-uaa + parameters: + service-key: + name: managed-uaa-key + parameters: + content: + instance: + destinations: + - Name: managed_html_repo_host + ServiceInstanceName: managed-html5-service + ServiceKeyName: managed-repo-host-key + sap.cloud.service: managed + - Authentication: OAuth2UserTokenExchange + Name: managed_uaa + ServiceInstanceName: managed-xsuaa-srv + ServiceKeyName: managed-uaa-key + sap.cloud.service: managed + existing_destinations_policy: ignore + build-parameters: + no-source: true +- name: managed-app-content + type: com.sap.application.content + path: . + requires: + - name: managed-repo-host + parameters: + content-target: true + build-parameters: + build-result: resources + requires: [] +resources: +- name: managed-destination-service + type: org.cloudfoundry.managed-service + parameters: + config: + HTML5Runtime_enabled: true + init_data: + instance: + destinations: + - Authentication: NoAuthentication + Name: ui5 + ProxyType: Internet + Type: HTTP + URL: https://ui5.sap.com + existing_destinations_policy: update + version: 1.0.0 + service: destination + service-name: managed-destination-service + service-plan: lite +- name: managed-uaa + type: org.cloudfoundry.managed-service + parameters: + path: ./xs-security.json + service: xsuaa + service-name: managed-xsuaa-srv + service-plan: application +- name: managed-repo-host + type: org.cloudfoundry.managed-service + parameters: + service: html5-apps-repo + service-name: managed-html5-service + service-plan: app-host +parameters: + deploy_mode: html5-repo + enable-parallel-deployments: true +build-parameters: + before-all: + - builder: custom + commands: + - npm install +" +`; + +exports[`CF Writer Base Generate Base Config - Standalone Generate deployment configs - standalone with ABAP service provider 1`] = ` +Object { + ".gitignore": Object { + "contents": "node_modules/ +dist/ +.scp/ +.env +Makefile*.mta +mta_archives +mta-* +resources +archive.zip +.*_mta_build_tmp", + "state": "modified", + }, + "package.json": Object { + "contents": "{ + \\"name\\": \\"mta-project\\", + \\"version\\": \\"0.0.1\\", + \\"description\\": \\"Build and deployment scripts\\", + \\"scripts\\": { + \\"clean\\": \\"rimraf resources mta_archives mta-op*\\", + \\"build\\": \\"rimraf resources mta_archives && mbt build --mtar archive\\", + \\"deploy\\": \\"cf deploy mta_archives/archive.mtar --retries 1\\", + \\"undeploy\\": \\"cf undeploy standalonewithabapserviceprovider --delete-services --delete-service-keys --delete-service-brokers\\" + }, + \\"devDependencies\\": { + \\"mbt\\": \\"^1.2.27\\", + \\"rimraf\\": \\"^5.0.5\\" + } +} +", + "state": "modified", + }, + "router/package.json": Object { + "contents": "{ + \\"name\\": \\"app-router\\", + \\"private\\": true, + \\"description\\": \\"App router\\", + \\"engines\\": { + \\"node\\": \\">= 16.0.0\\" + }, + \\"scripts\\": { + \\"start\\": \\"node node_modules/@sap/approuter/approuter.js\\", + \\"start-local\\": \\"node node_modules/@sap/html5-repo-mock/index.js\\" + }, + \\"dependencies\\": { + \\"@sap/approuter\\": \\"^14\\" + }, + \\"devDependencies\\": { + \\"@sap/html5-repo-mock\\": \\"^2.1.0\\" + } +} +", + "state": "modified", + }, + "router/xs-app.json": Object { + "contents": "{ + \\"authenticationMethod\\": \\"route\\", + \\"routes\\": [ + { + \\"source\\": \\"^/sap/(.*)$\\", + \\"target\\": \\"/sap/$1\\", + \\"service\\": \\"TestService\\", + \\"endpoint\\": \\"TestEndPoint\\", + \\"authenticationType\\": \\"xsuaa\\", + \\"csrfProtection\\": false + }, + { + \\"source\\": \\"^/resources/(.*)$\\", + \\"target\\": \\"/resources/$1\\", + \\"authenticationType\\": \\"none\\", + \\"destination\\": \\"ui5\\" + }, + { + \\"source\\": \\"^/test-resources/(.*)$\\", + \\"target\\": \\"/test-resources/$1\\", + \\"authenticationType\\": \\"none\\", + \\"destination\\": \\"ui5\\" + } + ] +} +", + "state": "modified", + }, + "xs-security.json": Object { + "contents": "{ + \\"xsappname\\": \\"standalonewithabapserviceprovider\\", + \\"tenant-mode\\": \\"dedicated\\", + \\"description\\": \\"Security profile of called application\\", + \\"scopes\\": [], + \\"role-templates\\": [] +} +", + "state": "modified", + }, +} +`; + +exports[`CF Writer Base Generate Base Config - Standalone Generate deployment configs - standalone with ABAP service provider 2`] = ` +"_schema-version: \\"3.2\\" +ID: standalonewithabapserviceprovider +description: Generated by Fiori Tools +version: 0.0.1 +modules: +- name: standalonewithabapserviceprovider-router + type: approuter.nodejs + path: router + requires: + - name: standalonewithabapserviceprov-html5-repo-runtime + - name: standalonewithabapserviceprovider-uaa + - name: standalonewithabapserviceprovi-destination-service + group: destinations + properties: + forwardAuthToken: false + name: ui5 + url: https://ui5.sap.com + - name: standalonewithabapservic-abap-Y11_00.0035 + parameters: + disk-quota: 256M + memory: 256M +resources: +- name: standalonewithabapserviceprovider-uaa + type: org.cloudfoundry.managed-service + parameters: + config: + tenant-mode: dedicated + xsappname: standalonewithabapserviceprovider-\${space-guid} + service: xsuaa + service-plan: application +- name: standalonewithabapserviceprov-html5-repo-runtime + type: org.cloudfoundry.managed-service + parameters: + service: html5-apps-repo + service-plan: app-runtime +- name: standalonewithabapserviceprovi-destination-service + type: org.cloudfoundry.managed-service + parameters: + config: + HTML5Runtime_enabled: false + init_data: + instance: + destinations: + - Authentication: NoAuthentication + Name: ui5 + ProxyType: Internet + Type: HTTP + URL: https://ui5.sap.com + existing_destinations_policy: update + version: 1.0.0 + service: destination + service-name: standalonewithabapserviceprovi-destination-service + service-plan: lite +- name: standalonewithabapservic-abap-Y11_00.0035 + type: org.cloudfoundry.existing-service + parameters: + protocol: + - ODataV2 + service: abap-haas + service-name: Y11_00.0035 + service-plan: 16_abap_64_db +parameters: + deploy_mode: html5-repo + enable-parallel-deployments: true +build-parameters: + before-all: + - builder: custom + commands: + - npm install +" +`; + +exports[`CF Writer Base Generate Base Config - Standalone Generate deployment configs - standalone with connectivity service 1`] = ` +Object { + ".gitignore": Object { + "contents": "node_modules/ +dist/ +.scp/ +.env +Makefile*.mta +mta_archives +mta-* +resources +archive.zip +.*_mta_build_tmp", + "state": "modified", + }, + "package.json": Object { + "contents": "{ + \\"name\\": \\"mta-project\\", + \\"version\\": \\"0.0.1\\", + \\"description\\": \\"Build and deployment scripts\\", + \\"scripts\\": { + \\"clean\\": \\"rimraf resources mta_archives mta-op*\\", + \\"build\\": \\"rimraf resources mta_archives && mbt build --mtar archive\\", + \\"deploy\\": \\"cf deploy mta_archives/archive.mtar --retries 1\\", + \\"undeploy\\": \\"cf undeploy standalonewithconnectivityservice --delete-services --delete-service-keys --delete-service-brokers\\" + }, + \\"devDependencies\\": { + \\"mbt\\": \\"^1.2.27\\", + \\"rimraf\\": \\"^5.0.5\\" + } +} +", + "state": "modified", + }, + "router/package.json": Object { + "contents": "{ + \\"name\\": \\"app-router\\", + \\"private\\": true, + \\"description\\": \\"App router\\", + \\"engines\\": { + \\"node\\": \\">= 16.0.0\\" + }, + \\"scripts\\": { + \\"start\\": \\"node node_modules/@sap/approuter/approuter.js\\", + \\"start-local\\": \\"node node_modules/@sap/html5-repo-mock/index.js\\" + }, + \\"dependencies\\": { + \\"@sap/approuter\\": \\"^14\\" + }, + \\"devDependencies\\": { + \\"@sap/html5-repo-mock\\": \\"^2.1.0\\" + } +} +", + "state": "modified", + }, + "router/xs-app.json": Object { + "contents": "{ + \\"authenticationMethod\\": \\"route\\", + \\"routes\\": [ + { + \\"source\\": \\"^(?:/app|/app/.*)?/resources/(.*)$\\", + \\"target\\": \\"/resources/$1\\", + \\"authenticationType\\": \\"none\\", + \\"destination\\": \\"ui5\\" + }, + { + \\"source\\": \\"^(?:/app|/app/.*)?/test-resources/(.*)$\\", + \\"target\\": \\"/test-resources/$1\\", + \\"authenticationType\\": \\"none\\", + \\"destination\\": \\"ui5\\" + } + ] +} +", + "state": "modified", + }, + "xs-security.json": Object { + "contents": "{ + \\"xsappname\\": \\"standalonewithconnectivityservice\\", + \\"tenant-mode\\": \\"dedicated\\", + \\"description\\": \\"Security profile of called application\\", + \\"scopes\\": [], + \\"role-templates\\": [] +} +", + "state": "modified", + }, +} +`; + +exports[`CF Writer Base Generate Base Config - Standalone Generate deployment configs - standalone with connectivity service 2`] = ` +"_schema-version: \\"3.2\\" +ID: standalonewithconnectivityservice +description: Generated by Fiori Tools +version: 0.0.1 +modules: +- name: standalonewithconnectivityservice-router + type: approuter.nodejs + path: router + requires: + - name: standalonewithconnectivityser-html5-repo-runtime + - name: standalonewithconnectivityservice-uaa + - name: standalonewithconnectivityserv-destination-service + group: destinations + properties: + forwardAuthToken: false + name: ui5 + url: https://ui5.sap.com + - name: standalonewithconnectivityservice-connectivity + parameters: + disk-quota: 256M + memory: 256M +resources: +- name: standalonewithconnectivityservice-uaa + type: org.cloudfoundry.managed-service + parameters: + config: + tenant-mode: dedicated + xsappname: standalonewithconnectivityservice-\${space-guid} + service: xsuaa + service-plan: application +- name: standalonewithconnectivityser-html5-repo-runtime + type: org.cloudfoundry.managed-service + parameters: + service: html5-apps-repo + service-plan: app-runtime +- name: standalonewithconnectivityserv-destination-service + type: org.cloudfoundry.managed-service + parameters: + config: + HTML5Runtime_enabled: false + init_data: + instance: + destinations: + - Authentication: NoAuthentication + Name: ui5 + ProxyType: Internet + Type: HTTP + URL: https://ui5.sap.com + existing_destinations_policy: update + version: 1.0.0 + service: destination + service-name: standalonewithconnectivityserv-destination-service + service-plan: lite +- name: standalonewithconnectivityservice-connectivity + type: org.cloudfoundry.managed-service + parameters: + service: connectivity + service-plan: lite +parameters: + deploy_mode: html5-repo + enable-parallel-deployments: true +build-parameters: + before-all: + - builder: custom + commands: + - npm install +" +`; diff --git a/packages/cf-deploy-config-writer/test/unit/__snapshots__/mta.test.ts.snap b/packages/cf-deploy-config-writer/test/unit/__snapshots__/mta.test.ts.snap new file mode 100644 index 0000000000..c93b3a8996 --- /dev/null +++ b/packages/cf-deploy-config-writer/test/unit/__snapshots__/mta.test.ts.snap @@ -0,0 +1,511 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Validate common flows Validate adding managed approuter 2`] = ` +"_schema-version: '3.2' +ID: basicApp +version: 0.0.1 +parameters: + enable-parallel-deployments: true + deploy_mode: html5-repo +modules: + - name: basicApp-destination-content + type: com.sap.application.content + requires: + - name: basicApp-destination-service + parameters: + content-target: true + - name: basicApp-repo-host + parameters: + service-key: + name: basicApp-repo-host-key + - name: basicApp-uaa + parameters: + service-key: + name: basicApp-uaa-key + parameters: + content: + instance: + destinations: + - Name: basicApp_html_repo_host + ServiceInstanceName: basicApp-html5-service + ServiceKeyName: basicApp-repo-host-key + sap.cloud.service: basicApp + - Authentication: OAuth2UserTokenExchange + Name: basicApp_uaa + ServiceInstanceName: basicApp-xsuaa-srv + ServiceKeyName: basicApp-uaa-key + sap.cloud.service: basicApp + existing_destinations_policy: ignore + build-parameters: + no-source: true + - name: basicApp-app-content + type: com.sap.application.content + path: . + requires: + - name: basicApp-repo-host + parameters: + content-target: true + build-parameters: + build-result: resources + requires: + - name: myhtml5app + artifacts: + - myhtml5app.zip + target-path: resources/ + - name: myhtml5app + type: html5 + path: ./ + build-parameters: + builder: custom + build-result: dist + commands: + - npm install + - 'npm run build:cf' + supported-platforms: [] +resources: + - name: basicApp-destination-service + type: org.cloudfoundry.managed-service + parameters: + service: destination + service-name: basicApp-destination-service + service-plan: lite + config: + HTML5Runtime_enabled: true + version: 1.0.0 + init_data: + instance: + existing_destinations_policy: update + destinations: + - Name: ui5 + Type: HTTP + URL: 'https://ui5.sap.com' + ProxyType: Internet + Authentication: NoAuthentication + - name: basicApp-uaa + type: org.cloudfoundry.managed-service + parameters: + path: ./xs-security.json + service: xsuaa + service-name: basicApp-xsuaa-srv + service-plan: application + - name: basicApp-repo-host + type: org.cloudfoundry.managed-service + parameters: + service-name: basicApp-html5-service + service-plan: app-host + service: html5-apps-repo + - name: basicApp-connectivity + type: org.cloudfoundry.managed-service + parameters: + service: connectivity + service-plan: lite +" +`; + +exports[`Validate common flows Validate adding managed approuter and destinations to cds generated mta.yaml 2`] = ` +"_schema-version: '3.1' +ID: managedAppCAPProject +version: 1.0.0 +description: A simple CAP project. +parameters: + enable-parallel-deployments: true + deploy_mode: html5-repo +build-parameters: + before-all: + - builder: custom + commands: + - npm ci + - npx cds build --production +modules: + - name: managedAppCAPProject-srv + type: nodejs + path: gen/srv + parameters: + buildpack: nodejs_buildpack + readiness-health-check-type: http + readiness-health-check-http-endpoint: /health + build-parameters: + builder: npm + provides: + - name: srv-api + properties: + srv-url: '\${default-url}' + requires: + - name: managedAppCAPProject-db + - name: managedAppCAPProject-uaa + - name: managedAppCAPProject-db-deployer + type: hdb + path: gen/db + parameters: + buildpack: nodejs_buildpack + requires: + - name: managedAppCAPProject-db + - name: managedAppCAPProject-destination-content + type: com.sap.application.content + requires: + - name: managedAppCAPProject-destination-service + parameters: + content-target: true + - name: managedAppCAPProject-repo-host + parameters: + service-key: + name: managedAppCAPProject-repo-host-key + - name: managedAppCAPProject-uaa + parameters: + service-key: + name: managedAppCAPProject-uaa-key + parameters: + content: + instance: + destinations: + - Name: managedAppCAPProject_html_repo_host + ServiceInstanceName: managedAppCAPProject-html5-service + ServiceKeyName: managedAppCAPProject-repo-host-key + sap.cloud.service: managedAppCAPProject + - Authentication: OAuth2UserTokenExchange + Name: managedAppCAPProject_uaa + ServiceInstanceName: managedAppCAPProject-xsuaa-srv + ServiceKeyName: managedAppCAPProject-uaa-key + sap.cloud.service: managedAppCAPProject + existing_destinations_policy: ignore + build-parameters: + no-source: true + - name: managedAppCAPProject-app-content + type: com.sap.application.content + path: . + requires: + - name: managedAppCAPProject-repo-host + parameters: + content-target: true + build-parameters: + build-result: resources + requires: + - name: myhtml5app + artifacts: + - myhtml5app.zip + target-path: resources/ + - name: myhtml5app + type: html5 + path: ./ + build-parameters: + builder: custom + build-result: dist + commands: + - npm install + - 'npm run build:cf' + supported-platforms: [] +resources: + - name: managedAppCAPProject-db + type: com.sap.xs.hdi-container + parameters: + service: hana + service-plan: hdi-shared + - name: managedAppCAPProject-destination-service + type: org.cloudfoundry.managed-service + parameters: + service: destination + service-name: managedAppCAPProject-destination-service + service-plan: lite + config: + HTML5Runtime_enabled: true + version: 1.0.0 + init_data: + instance: + existing_destinations_policy: update + destinations: + - Name: ui5 + Type: HTTP + URL: 'https://ui5.sap.com' + ProxyType: Internet + Authentication: NoAuthentication + - Name: managedAppCAPProject-srv-api + Type: HTTP + URL: '~{srv-api/srv-url}' + ProxyType: Internet + Authentication: NoAuthentication + HTML5.DynamicDestination: true + HTML5.ForwardAuthToken: true + requires: + - name: srv-api + - name: managedAppCAPProject-uaa + type: org.cloudfoundry.managed-service + parameters: + path: ./xs-security.json + service: xsuaa + service-name: managedAppCAPProject-xsuaa-srv + service-plan: application + - name: managedAppCAPProject-repo-host + type: org.cloudfoundry.managed-service + parameters: + service-name: managedAppCAPProject-html5-service + service-plan: app-host + service: html5-apps-repo +" +`; + +exports[`Validate common flows Validate adding standalone approuter 2`] = ` +"_schema-version: '3.2' +ID: standaloneBasic +description: Fiori elements app +version: 0.0.1 +build-parameters: + before-all: + - builder: custom + commands: + - npm install +parameters: + enable-parallel-deployments: true + deploy_mode: html5-repo +modules: + - name: standaloneBasic-router + type: approuter.nodejs + path: router + parameters: + disk-quota: 256M + memory: 256M + requires: + - name: standaloneBasic-html5-repo-runtime + - name: standaloneBasic-uaa + - name: standaloneBasic-destination-service + group: destinations + properties: + name: ui5 + url: 'https://ui5.sap.com' + forwardAuthToken: false + - name: standaloneBasic-abap-abapservice + - name: standaloneBasic-connectivity + - name: standaloneBasic-app-content + type: com.sap.application.content + path: . + requires: + - name: standaloneBasic-repo-host + parameters: + content-target: true + build-parameters: + build-result: resources + requires: + - name: myhtml5app + artifacts: + - myhtml5app.zip + target-path: resources/ + - name: myhtml5app + type: html5 + path: ./ + build-parameters: + builder: custom + build-result: dist + commands: + - npm install + - 'npm run build:cf' + supported-platforms: [] +resources: + - name: standaloneBasic-uaa + type: org.cloudfoundry.managed-service + parameters: + service-plan: application + service: xsuaa + config: + xsappname: 'standaloneBasic-\${space-guid}' + tenant-mode: dedicated + - name: standaloneBasic-html5-repo-runtime + type: org.cloudfoundry.managed-service + parameters: + service-plan: app-runtime + service: html5-apps-repo + - name: standaloneBasic-destination-service + type: org.cloudfoundry.managed-service + parameters: + service: destination + service-name: standaloneBasic-destination-service + service-plan: lite + config: + HTML5Runtime_enabled: false + version: 1.0.0 + init_data: + instance: + existing_destinations_policy: update + destinations: + - Name: ui5 + Type: HTTP + URL: 'https://ui5.sap.com' + ProxyType: Internet + Authentication: NoAuthentication + - name: standaloneBasic-repo-host + type: org.cloudfoundry.managed-service + parameters: + service-name: standaloneBasic-html5-service + service-plan: app-host + service: html5-apps-repo + - name: standaloneBasic-abap-abapservice + type: org.cloudfoundry.existing-service + parameters: + service-name: abapservice + protocol: + - ODataV2 + service: abapservice + service-plan: 16_abap_64_db + - name: standaloneBasic-connectivity + type: org.cloudfoundry.managed-service + parameters: + service: connectivity + service-plan: lite +" +`; + +exports[`Validate common flows Validate adding standalone approuter with missing module destination 1`] = ` +"_schema-version: '3.2' +ID: standaloneApp +description: Fiori elements app +version: 0.0.1 +modules: + - name: standaloneApp-router + type: approuter.nodejs + path: router + requires: + - name: standaloneApp-html5-repo-runtime + - name: standaloneApp-uaa + - name: standaloneApp-destination + group: destinations + properties: + forwardAuthToken: false + name: ui5 + url: 'https://ui5.sap.com' + parameters: + disk-quota: 256M + memory: 256M + - name: standaloneApp-app-content + type: com.sap.application.content + path: . + requires: + - name: standaloneApp-repo-host + parameters: + content-target: true + build-parameters: + build-result: resources + requires: [] +resources: + - name: standaloneApp-uaa + type: org.cloudfoundry.managed-service + parameters: + config: + tenant-mode: dedicated + xsappname: 'standaloneApp-\${org}' + service: xsuaa + service-plan: application + - name: standaloneApp-html5-repo-runtime + type: org.cloudfoundry.managed-service + parameters: + service: html5-apps-repo + service-plan: app-runtime + - name: standaloneApp-destination + type: org.cloudfoundry.managed-service + parameters: + service: destination + service-plan: lite + config: + HTML5Runtime_enabled: false + init_data: + instance: + existing_destinations_policy: update + destinations: + - Name: ui5 + Type: HTTP + URL: 'https://ui5.sap.com' + ProxyType: Internet + Authentication: NoAuthentication + - name: standaloneApp-repo-host + type: org.cloudfoundry.managed-service + parameters: + service-name: standaloneApp-html5-service + service-plan: app-host + service: html5-apps-repo +parameters: + deploy_mode: html5-repo + enable-parallel-deployments: true +build-parameters: + before-all: + - builder: custom + commands: + - npm install +" +`; + +exports[`Validate common flows Validate destination service is correctly updated if missing instances 1`] = ` +"_schema-version: '3.2' +ID: managedApp +version: 0.0.1 +modules: + - name: managedApp-destination-content + type: com.sap.application.content + requires: + - name: managedApp-destination-service + parameters: + content-target: true + - name: managedApp_html_repo_host + parameters: + service-key: + name: managedApp_html_repo_host-key + - name: uaa_managedApp + parameters: + service-key: + name: uaa_managedApp-key + parameters: + content: + instance: + destinations: + - Name: uniqueid_managedApp_html_repo_host + ServiceInstanceName: managedApp-html5-app-host-service + ServiceKeyName: managedApp_html_repo_host-key + sap.cloud.service: test2804 + - Authentication: OAuth2UserTokenExchange + Name: test2804_uaa_managedApp + ServiceInstanceName: managedApp-xsuaa-service + ServiceKeyName: uaa_managedApp-key + sap.cloud.service: test2804 + existing_destinations_policy: ignore + build-parameters: + no-source: true + - name: managedApp-app-content + type: com.sap.application.content + path: . + requires: + - name: managedApp_html_repo_host + parameters: + content-target: true + build-parameters: + build-result: resources + requires: [] +resources: + - name: managedApp-destination-service + type: org.cloudfoundry.managed-service + parameters: + config: + HTML5Runtime_enabled: true + version: 1.0.0 + init_data: + instance: + existing_destinations_policy: update + destinations: + - Name: ui5 + Type: HTTP + URL: 'https://ui5.sap.com' + ProxyType: Internet + Authentication: NoAuthentication + service: destination + service-name: managedApp-destination-service + service-plan: lite + - name: managedApp_html_repo_host + type: org.cloudfoundry.managed-service + parameters: + service: html5-apps-repo + service-name: managedApp-html5-app-host-service + service-plan: app-host + - name: uaa_managedApp + type: org.cloudfoundry.managed-service + parameters: + path: ./xs-security.json + service: xsuaa + service-name: managedApp-xsuaa-service + service-plan: application +" +`; diff --git a/packages/cf-deploy-config-writer/test/unit/cap.test.ts b/packages/cf-deploy-config-writer/test/unit/cap.test.ts new file mode 100644 index 0000000000..14612f7b3c --- /dev/null +++ b/packages/cf-deploy-config-writer/test/unit/cap.test.ts @@ -0,0 +1,108 @@ +import * as childProcess from 'child_process'; +import * as projectAccess from '@sap-ux/project-access'; +import { join } from 'path'; +import fsExtra from 'fs-extra'; +import hasbin from 'hasbin'; +import { create as createStorage } from 'mem-fs'; +import { create } from 'mem-fs-editor'; +import { generateAppConfig } from '../../src'; +import type { Editor } from 'mem-fs-editor'; +import { DefaultMTADestination, MTABinNotFound, CDSBinNotFound } from '../../src/constants'; +import { isAppStudio } from '@sap-ux/btp-utils'; + +jest.mock('@sap/mta-lib', () => { + return { + // eslint-disable-next-line @typescript-eslint/no-var-requires + Mta: require('./mockMta').MockMta + }; +}); + +jest.mock('@sap-ux/btp-utils', () => ({ + ...jest.requireActual('@sap-ux/btp-utils'), + isAppStudio: jest.fn() +})); +const isAppStudioMock = isAppStudio as jest.Mock; + +jest.mock('child_process'); + +let hasSyncMock: jest.SpyInstance; +let spawnMock: jest.SpyInstance; +let unitTestFs: Editor; + +describe('CF Writer', () => { + const outputDir = join(__dirname, '../test-output', 'cap'); + + beforeEach(() => { + jest.resetAllMocks(); + jest.restoreAllMocks(); + unitTestFs = create(createStorage()); + spawnMock = jest.spyOn(childProcess, 'spawnSync').mockImplementation(() => ({ status: 0 } as any)); + isAppStudioMock.mockReturnValue(false); + hasSyncMock = jest.spyOn(hasbin, 'sync').mockImplementation(() => true); + }); + + beforeAll(async () => { + jest.clearAllMocks(); + jest.spyOn(hasbin, 'sync').mockReturnValue(true); + fsExtra.removeSync(outputDir); + jest.mock('hasbin', () => { + return { + ...(jest.requireActual('hasbin') as {}), + sync: hasSyncMock + }; + }); + }); + + afterAll(async () => { + jest.resetAllMocks(); + }); + + describe('Generate deployment config for CAP project', () => { + test('Add destination instance to a HTML5 app inside a CAP project', async () => { + const capPath = join(outputDir, 'cap'); + const getMtaPathMock = jest.spyOn(projectAccess, 'getMtaPath'); + const findCapProjectRootMock = jest.spyOn(projectAccess, 'findCapProjectRoot'); + fsExtra.mkdirSync(outputDir, { recursive: true }); + fsExtra.mkdirSync(capPath); + fsExtra.copySync(join(__dirname, '../sample/cap'), capPath); + await generateAppConfig( + { + appPath: join(capPath, 'app/lrop'), + destinationName: DefaultMTADestination, + addManagedAppRouter: true + }, + unitTestFs + ); + expect(getMtaPathMock).toBeCalledWith(expect.stringContaining(capPath)); + expect(findCapProjectRootMock).toBeCalledWith(expect.stringContaining(capPath)); + expect(spawnMock).not.toHaveBeenCalled(); + expect(unitTestFs.dump(capPath)).toMatchSnapshot(); + expect(unitTestFs.read(join(capPath, 'mta.yaml'))).toMatchSnapshot(); + }); + + test('Validate dependency on MTA binary', async () => { + hasSyncMock.mockReturnValue(false); + const capPath = join(outputDir, 'cap'); + await expect(generateAppConfig({ appPath: capPath }, unitTestFs)).rejects.toThrowError(MTABinNotFound); + }); + + test('Validate dependency on CDS', async () => { + spawnMock = jest.spyOn(childProcess, 'spawnSync').mockImplementation(() => ({ error: 1 } as any)); + const capPath = join(outputDir, 'capcds'); + fsExtra.mkdirSync(outputDir, { recursive: true }); + fsExtra.mkdirSync(capPath); + fsExtra.copySync(join(__dirname, '../sample/basiccap'), capPath); + await expect( + generateAppConfig( + { + appPath: join(capPath, 'app/lrop'), + destinationName: DefaultMTADestination, + addManagedAppRouter: true + }, + unitTestFs + ) + ).rejects.toThrowError(CDSBinNotFound); + expect(spawnMock).not.toHaveBeenCalledWith(''); + }); + }); +}); diff --git a/packages/cf-deploy-config-writer/test/unit/fixtures/mta-ext/app/mta.yaml b/packages/cf-deploy-config-writer/test/unit/fixtures/mta-ext/app/mta.yaml new file mode 100644 index 0000000000..e2704aea76 --- /dev/null +++ b/packages/cf-deploy-config-writer/test/unit/fixtures/mta-ext/app/mta.yaml @@ -0,0 +1,76 @@ +_schema-version: "3.2" +ID: test-mta +description: An SAP Fiori application. +version: 0.0.1 +modules: +- name: test-mta-destination-content + type: com.sap.application.content + requires: + - name: test-mta-destination-service + parameters: + content-target: true + - name: test-mta-repo-host + parameters: + service-key: + name: test-mta-repo-host-key + - name: test-mta-uaa + parameters: + service-key: + name: test-mta-uaa-key + parameters: + content: + instance: + destinations: + - Name: test-mta_html_repo_host + ServiceInstanceName: test-mta-html5-srv + ServiceKeyName: test-mta-repo-host-key + sap.cloud.service: test-mta + - Authentication: OAuth2UserTokenExchange + Name: test-mta_uaa + ServiceInstanceName: test-mta-xsuaa-srv + ServiceKeyName: test-mta-uaa-key + sap.cloud.service: test-mta + existing_destinations_policy: ignore + build-parameters: + no-source: true +- name: test-mta-app-content + type: com.sap.application.content + path: . + requires: + - name: test-mta-repo-host + parameters: + content-target: true + build-parameters: + build-result: resources + requires: + - artifacts: + - test-mta.zip + name: test-mta + target-path: resources/ +- name: test-mta + type: html5 + path: . + build-parameters: + build-result: dist + builder: custom + commands: + - npm install + - npm run build:cf + supported-platforms: [] +resources: +- name: test-mta-uaa + type: org.cloudfoundry.managed-service + parameters: + path: ./xs-security.json + service: xsuaa + service-name: test-mta-xsuaa-srv + service-plan: application +- name: test-mta-repo-host + type: org.cloudfoundry.managed-service + parameters: + service: html5-apps-repo + service-name: test-mta-html5-srv + service-plan: app-host +parameters: + deploy_mode: html5-repo + enable-parallel-deployments: true diff --git a/packages/cf-deploy-config-writer/test/unit/fixtures/mta-ext/app/ui5.yaml b/packages/cf-deploy-config-writer/test/unit/fixtures/mta-ext/app/ui5.yaml new file mode 100644 index 0000000000..2b71d6b5d4 --- /dev/null +++ b/packages/cf-deploy-config-writer/test/unit/fixtures/mta-ext/app/ui5.yaml @@ -0,0 +1,32 @@ +# yaml-language-server: $schema=https://sap.github.io/ui5-tooling/schema/ui5.yaml.json + +specVersion: "2.5" +metadata: + name: abhexso +type: application +server: + customMiddleware: + - name: fiori-tools-proxy + afterMiddleware: compression + configuration: + ignoreCertError: false # If set to true, certificate errors will be ignored. E.g. self-signed certificates will be accepted + ui5: + path: + - /resources + - /test-resources + url: https://ui5.sap.com + backend: + - apiHub: true + path: /northwindV2 + url: https://kran3.apim.hana.ondemand.com + - name: fiori-tools-appreload + afterMiddleware: compression + configuration: + port: 35729 + path: webapp + delay: 300 + - name: fiori-tools-preview + afterMiddleware: fiori-tools-appreload + configuration: + component: abhexso + ui5Theme: sap_horizon diff --git a/packages/cf-deploy-config-writer/test/unit/fixtures/mta-ext/app/webapp/manifest.json b/packages/cf-deploy-config-writer/test/unit/fixtures/mta-ext/app/webapp/manifest.json new file mode 100644 index 0000000000..651553f9c4 --- /dev/null +++ b/packages/cf-deploy-config-writer/test/unit/fixtures/mta-ext/app/webapp/manifest.json @@ -0,0 +1,151 @@ +{ + "_version": "1.40.0", + "sap.app": { + "id": "abhez6", + "type": "application", + "i18n": "i18n/i18n.properties", + "applicationVersion": { + "version": "0.0.1" + }, + "title": "{{appTitle}}", + "description": "{{appDescription}}", + "resources": "resources.json", + "sourceTemplate": { + "id": "@sap/generator-fiori:lrop", + "version": "1.7.1", + "toolsId": "099189bf-899c-4abc-8a06-1d685d948f04" + }, + "dataSources": { + "mainService": { + "uri": "/northwindV2/test/path/", + "type": "OData", + "settings": { + "annotations": [ + "annotation" + ], + "localUri": "localService/metadata.xml", + "odataVersion": "2.0" + } + }, + "annotation": { + "type": "ODataAnnotation", + "uri": "annotations/annotation.xml", + "settings": { + "localUri": "annotations/annotation.xml" + } + } + } + }, + "sap.ui": { + "technology": "UI5", + "icons": { + "icon": "", + "favIcon": "", + "phone": "", + "phone@2": "", + "tablet": "", + "tablet@2": "" + }, + "deviceTypes": { + "desktop": true, + "tablet": true, + "phone": true + } + }, + "sap.ui5": { + "flexEnabled": true, + "dependencies": { + "minUI5Version": "1.102.1", + "libs": { + "sap.m": {}, + "sap.ui.core": {}, + "sap.ushell": {}, + "sap.f": {}, + "sap.ui.comp": {}, + "sap.ui.generic.app": {}, + "sap.suite.ui.generic.template": {} + } + }, + "contentDensities": { + "compact": true, + "cozy": true + }, + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "settings": { + "bundleName": "abhez6.i18n.i18n" + } + }, + "": { + "dataSource": "mainService", + "preload": true, + "settings": { + "defaultBindingMode": "TwoWay", + "defaultCountMode": "Inline", + "refreshAfterChange": false, + "metadataUrlParams": { + "sap-value-list": "none" + } + } + }, + "@i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "uri": "i18n/i18n.properties" + } + }, + "resources": { + "css": [] + }, + "routing": { + "config": {}, + "routes": [], + "targets": {} + } + }, + "sap.ui.generic.app": { + "_version": "1.3.0", + "settings": { + "forceGlobalRefresh": false, + "objectPageHeaderType": "Dynamic", + "considerAnalyticalParameters": true, + "showDraftToggle": false + }, + "pages": { + "ListReport|Categories": { + "entitySet": "Categories", + "component": { + "name": "sap.suite.ui.generic.template.ListReport", + "list": true, + "settings": { + "condensedTableLayout": true, + "smartVariantManagement": true, + "enableTableFilterInPageVariant": true, + "filterSettings": { + "dateSettings": { + "useDateRange": true + } + } + } + }, + "pages": { + "ObjectPage|Categories": { + "entitySet": "Categories", + "defaultLayoutTypeIfExternalNavigation": "MidColumnFullScreen", + "component": { + "name": "sap.suite.ui.generic.template.ObjectPage" + } + } + } + } + } + }, + "sap.fiori": { + "registrationIds": [], + "archeType": "transactional" + }, + "sap.cloud": { + "public": true, + "service": "abhez6" + } +} \ No newline at end of file diff --git a/packages/cf-deploy-config-writer/test/unit/fixtures/mta-ext/mixed/mta.yaml b/packages/cf-deploy-config-writer/test/unit/fixtures/mta-ext/mixed/mta.yaml new file mode 100644 index 0000000000..e2704aea76 --- /dev/null +++ b/packages/cf-deploy-config-writer/test/unit/fixtures/mta-ext/mixed/mta.yaml @@ -0,0 +1,76 @@ +_schema-version: "3.2" +ID: test-mta +description: An SAP Fiori application. +version: 0.0.1 +modules: +- name: test-mta-destination-content + type: com.sap.application.content + requires: + - name: test-mta-destination-service + parameters: + content-target: true + - name: test-mta-repo-host + parameters: + service-key: + name: test-mta-repo-host-key + - name: test-mta-uaa + parameters: + service-key: + name: test-mta-uaa-key + parameters: + content: + instance: + destinations: + - Name: test-mta_html_repo_host + ServiceInstanceName: test-mta-html5-srv + ServiceKeyName: test-mta-repo-host-key + sap.cloud.service: test-mta + - Authentication: OAuth2UserTokenExchange + Name: test-mta_uaa + ServiceInstanceName: test-mta-xsuaa-srv + ServiceKeyName: test-mta-uaa-key + sap.cloud.service: test-mta + existing_destinations_policy: ignore + build-parameters: + no-source: true +- name: test-mta-app-content + type: com.sap.application.content + path: . + requires: + - name: test-mta-repo-host + parameters: + content-target: true + build-parameters: + build-result: resources + requires: + - artifacts: + - test-mta.zip + name: test-mta + target-path: resources/ +- name: test-mta + type: html5 + path: . + build-parameters: + build-result: dist + builder: custom + commands: + - npm install + - npm run build:cf + supported-platforms: [] +resources: +- name: test-mta-uaa + type: org.cloudfoundry.managed-service + parameters: + path: ./xs-security.json + service: xsuaa + service-name: test-mta-xsuaa-srv + service-plan: application +- name: test-mta-repo-host + type: org.cloudfoundry.managed-service + parameters: + service: html5-apps-repo + service-name: test-mta-html5-srv + service-plan: app-host +parameters: + deploy_mode: html5-repo + enable-parallel-deployments: true diff --git a/packages/cf-deploy-config-writer/test/unit/fixtures/mta-ext/multi/mta-ext.mtaext b/packages/cf-deploy-config-writer/test/unit/fixtures/mta-ext/multi/mta-ext.mtaext new file mode 100644 index 0000000000..e15fcd5f92 --- /dev/null +++ b/packages/cf-deploy-config-writer/test/unit/fixtures/mta-ext/multi/mta-ext.mtaext @@ -0,0 +1,20 @@ +## SAP UX Tools generated mtaext file +_schema-version: "3.2" +ID: test-mta-ext +extends: test-mta +version: 1.0.0 + +resources: +- name: qa-destination-service + parameters: + config: + init_data: + instance: + destinations: + - Authentication: NoAuthentication + Name: ABHE_NorthwindProduct_theme + ProxyType: Internet + Type: HTTP + URL: https://api.hana.on.demand + URL.headers.ApiKey: 1234567890abcdefg + existing_destinations_policy: update diff --git a/packages/cf-deploy-config-writer/test/unit/fixtures/mta-ext/multi/mta.yaml b/packages/cf-deploy-config-writer/test/unit/fixtures/mta-ext/multi/mta.yaml new file mode 100644 index 0000000000..8c105ef520 --- /dev/null +++ b/packages/cf-deploy-config-writer/test/unit/fixtures/mta-ext/multi/mta.yaml @@ -0,0 +1,54 @@ +_schema-version: "3.2" +ID: test-mta +description: Destination service configuration for instance based destinations +version: 0.0.1 +modules: +- name: test-mta-app-content + type: com.sap.application.content + path: . + requires: + - name: test-mta-repo-host + parameters: + content-target: true + build-parameters: + build-result: resources + requires: + - artifacts: + - mtaext1.zip + name: mtaext1 + target-path: resources/ +- name: mtaext1 + type: html5 + path: mtaext1 + build-parameters: + build-result: dist + builder: custom + commands: + - npm install + - npm run build:cf + supported-platforms: [] +resources: +- name: qa-destination-service + type: org.cloudfoundry.managed-service + parameters: + config: + HTML5Runtime_enabled: false + init_data: + instance: + destinations: + - Authentication: NoAuthentication + Name: ui5 + ProxyType: Internet + Type: HTTP + URL: https://ui5.sap.com + existing_destinations_policy: update + service: destination + service-plan: lite +- name: test-mta-repo-host + type: org.cloudfoundry.managed-service + parameters: + service: html5-apps-repo + service-name: test-mta-html5-srv + service-plan: app-host +parameters: + deploy_mode: html5-repo diff --git a/packages/cf-deploy-config-writer/test/unit/fixtures/mta-types/abap-service/mta.yaml b/packages/cf-deploy-config-writer/test/unit/fixtures/mta-types/abap-service/mta.yaml new file mode 100644 index 0000000000..33ebede63d --- /dev/null +++ b/packages/cf-deploy-config-writer/test/unit/fixtures/mta-types/abap-service/mta.yaml @@ -0,0 +1,63 @@ +_schema-version: "3.2" +ID: managedApp +version: 0.0.1 +modules: + - name: managedApp-dest-content + type: com.sap.application.content + requires: + - name: managedApp-destination-service + parameters: + content-target: true + - name: managedApp_repo_host + parameters: + service-key: + name: managedApp_repo_host-key + - name: uaa_managedApp + parameters: + service-key: + name: uaa_managedApp-key + parameters: + content: + instance: + destinations: + - Name: myTestApp_managedApp_repo_host + ServiceInstanceName: managedApp-html5-srv + ServiceKeyName: managedApp_repo_host-key + sap.cloud.service: myTestApp + - Authentication: OAuth2UserTokenExchange + Name: myTestApp_uaa_managedApp + ServiceInstanceName: managedApp-xsuaa-srv + ServiceKeyName: uaa_managedApp-key + sap.cloud.service: myTestApp + existing_destinations_policy: ignore + build-parameters: + no-source: true +resources: + - name: managedApp-destination-service + type: org.cloudfoundry.managed-service + parameters: + config: + HTML5Runtime_enabled: true + version: 1.0.0 + service: destination + service-name: managedApp-destination-service + service-plan: lite + - name: managedApp_repo_host + type: org.cloudfoundry.managed-service + parameters: + service: html5-apps-repo + service-name: managedApp-html5-srv + service-plan: app-host + - name: uaa_managedApp + type: org.cloudfoundry.managed-service + parameters: + path: ./xs-security.json + service: xsuaa + service-name: managedApp-xsuaa-srv + service-plan: application + - name: managedApp-abap-service + type: org.cloudfoundry.existing-service + parameters: + service: managedApp-abap-solution + service-name: abap-solution + service-plan: standard diff --git a/packages/cf-deploy-config-writer/test/unit/fixtures/mta-types/managed-apps/mta.yaml b/packages/cf-deploy-config-writer/test/unit/fixtures/mta-types/managed-apps/mta.yaml new file mode 100644 index 0000000000..23c5954113 --- /dev/null +++ b/packages/cf-deploy-config-writer/test/unit/fixtures/mta-types/managed-apps/mta.yaml @@ -0,0 +1,96 @@ +_schema-version: '3.2' +ID: managedApp +version: 0.0.1 +modules: + - name: managedApp-dest-content + type: com.sap.application.content + requires: + - name: managedApp-destination-service + parameters: + content-target: true + - name: managedApp_repo_host + parameters: + service-key: + name: managedApp_repo_host-key + - name: uaa_managedApp + parameters: + service-key: + name: uaa_managedApp-key + parameters: + content: + instance: + destinations: + - Name: myTestApp_managedApp_repo_host + ServiceInstanceName: managedApp-html5-srv + ServiceKeyName: managedApp_repo_host-key + sap.cloud.service: myTestApp + - Authentication: OAuth2UserTokenExchange + Name: myTestApp_uaa_managedApp + ServiceInstanceName: managedApp-xsuaa-srv + ServiceKeyName: uaa_managedApp-key + sap.cloud.service: myTestApp + existing_destinations_policy: ignore + build-parameters: + no-source: true + - name: managedApp-app-content + type: com.sap.application.content + path: . + requires: + - name: managedApp_repo_host + parameters: + content-target: true + build-parameters: + build-result: resources + requires: + - artifacts: + - project1.zip + name: project1 + target-path: resources/ + - name: project1 + type: html5 + path: project1 + build-parameters: + build-result: dist + builder: custom + commands: + - npm install + - npm run build:cf + supported-platforms: [] +resources: + - name: managedApp-destination-service + type: org.cloudfoundry.managed-service + parameters: + config: + HTML5Runtime_enabled: true + init_data: + subaccount: + destinations: + - Name: northwind + WebIDEEnabled: true + WebIDEUsage: odata_gen + HTML5.DynamicDestination: true + Authentication: NoAuthentication + Description: Destination to internet facing host + ProxyType: Internet + Type: HTTP + URL: https://services.odata.org + existing_destinations_policy: update + version: 1.0.0 + service: destination + service-name: managedApp-destination-service + service-plan: lite + - name: managedApp_repo_host + type: org.cloudfoundry.managed-service + parameters: + service: html5-apps-repo + service-name: managedApp-html5-srv + service-plan: app-host + - name: uaa_managedApp + type: org.cloudfoundry.managed-service + parameters: + path: ./xs-security.json + service: xsuaa + service-name: managedApp-xsuaa-srv + service-plan: application +parameters: + deploy_mode: html5-repo diff --git a/packages/cf-deploy-config-writer/test/unit/fixtures/mta-types/managed-basic/mta.yaml b/packages/cf-deploy-config-writer/test/unit/fixtures/mta-types/managed-basic/mta.yaml new file mode 100644 index 0000000000..e2da9d1549 --- /dev/null +++ b/packages/cf-deploy-config-writer/test/unit/fixtures/mta-types/managed-basic/mta.yaml @@ -0,0 +1,10 @@ +_schema-version: '3.2' +ID: basicApp +version: 0.0.1 +parameters: + enable-parallel-deployments: true + deploy_mode: html5-repo + +modules: [] + +resources: [] \ No newline at end of file diff --git a/packages/cf-deploy-config-writer/test/unit/fixtures/mta-types/managed-cap-missing-destinations/mta.yaml b/packages/cf-deploy-config-writer/test/unit/fixtures/mta-types/managed-cap-missing-destinations/mta.yaml new file mode 100644 index 0000000000..daca096f1e --- /dev/null +++ b/packages/cf-deploy-config-writer/test/unit/fixtures/mta-types/managed-cap-missing-destinations/mta.yaml @@ -0,0 +1,83 @@ +_schema-version: '3.2' +ID: managedApp +version: 0.0.1 +modules: + - name: managedApp-dest-content + type: com.sap.application.content + requires: + - name: managedApp-destination-service + parameters: + content-target: true + - name: managedApp_repo_host + parameters: + service-key: + name: managedApp_repo_host-key + - name: uaa_managedApp + parameters: + service-key: + name: uaa_managedApp-key + parameters: + content: + instance: + destinations: + - Name: myTestApp_managedApp_repo_host + ServiceInstanceName: managedApp-html5-srv + ServiceKeyName: managedApp_repo_host-key + sap.cloud.service: myTestApp + - Authentication: OAuth2UserTokenExchange + Name: myTestApp_uaa_managedApp + ServiceInstanceName: managedApp-xsuaa-srv + ServiceKeyName: uaa_managedApp-key + sap.cloud.service: myTestApp + existing_destinations_policy: ignore + build-parameters: + no-source: true + - name: managedApp-app-content + type: com.sap.application.content + path: . + requires: + - name: managedApp_repo_host + parameters: + content-target: true + build-parameters: + build-result: resources + requires: + - artifacts: + - project1.zip + name: project1 + target-path: resources/ + - name: project1 + type: html5 + path: project1 + build-parameters: + build-result: dist + builder: custom + commands: + - npm install + - npm run build:cf + supported-platforms: [] +resources: + - name: managedApp-destination-service + type: org.cloudfoundry.managed-service + parameters: + config: + HTML5Runtime_enabled: true + version: 1.0.0 + service: destination + service-name: managedApp-destination-service + service-plan: lite + - name: managedApp_repo_host + type: org.cloudfoundry.managed-service + parameters: + service: html5-apps-repo + service-name: managedApp-html5-srv + service-plan: app-host + - name: uaa_managedApp + type: org.cloudfoundry.managed-service + parameters: + path: ./xs-security.json + service: xsuaa + service-name: managedApp-xsuaa-srv + service-plan: application +parameters: + deploy_mode: html5-repo diff --git a/packages/cf-deploy-config-writer/test/unit/fixtures/mta-types/managed-cap/mta.yaml b/packages/cf-deploy-config-writer/test/unit/fixtures/mta-types/managed-cap/mta.yaml new file mode 100644 index 0000000000..b4932f1a42 --- /dev/null +++ b/packages/cf-deploy-config-writer/test/unit/fixtures/mta-types/managed-cap/mta.yaml @@ -0,0 +1,43 @@ +_schema-version: '3.1' +ID: managedAppCAPProject +version: 1.0.0 +description: "A simple CAP project." +parameters: + enable-parallel-deployments: true +build-parameters: + before-all: + - builder: custom + commands: + - npm ci + - npx cds build --production +modules: + - name: managedAppCAPProject-srv + type: nodejs + path: gen/srv + parameters: + buildpack: nodejs_buildpack + readiness-health-check-type: http + readiness-health-check-http-endpoint: /health + build-parameters: + builder: npm + provides: + - name: srv-api # required by consumers of CAP services (e.g. approuter) + properties: + srv-url: ${default-url} + requires: + - name: managedAppCAPProject-db + + - name: managedAppCAPProject-db-deployer + type: hdb + path: gen/db + parameters: + buildpack: nodejs_buildpack + requires: + - name: managedAppCAPProject-db + +resources: + - name: managedAppCAPProject-db + type: com.sap.xs.hdi-container + parameters: + service: hana + service-plan: hdi-shared diff --git a/packages/cf-deploy-config-writer/test/unit/fixtures/mta-types/managed-missing-dest/mta.yaml b/packages/cf-deploy-config-writer/test/unit/fixtures/mta-types/managed-missing-dest/mta.yaml new file mode 100644 index 0000000000..58f37404c8 --- /dev/null +++ b/packages/cf-deploy-config-writer/test/unit/fixtures/mta-types/managed-missing-dest/mta.yaml @@ -0,0 +1,57 @@ +_schema-version: "3.2" +ID: managedApp +version: 0.0.1 +modules: + - name: managedApp-destination-content + type: com.sap.application.content + requires: + - name: managedApp-destination-service + parameters: + content-target: true + - name: managedApp_html_repo_host + parameters: + service-key: + name: managedApp_html_repo_host-key + - name: uaa_managedApp + parameters: + service-key: + name: uaa_managedApp-key + parameters: + content: + instance: + destinations: + - Name: uniqueid_managedApp_html_repo_host + ServiceInstanceName: managedApp-html5-app-host-service + ServiceKeyName: managedApp_html_repo_host-key + sap.cloud.service: test2804 + - Authentication: OAuth2UserTokenExchange + Name: test2804_uaa_managedApp + ServiceInstanceName: managedApp-xsuaa-service + ServiceKeyName: uaa_managedApp-key + sap.cloud.service: test2804 + existing_destinations_policy: ignore + build-parameters: + no-source: true +resources: + - name: managedApp-destination-service + type: org.cloudfoundry.managed-service + parameters: + config: + HTML5Runtime_enabled: true + version: 1.0.0 + service: destination + service-name: managedApp-destination-service + service-plan: lite + - name: managedApp_html_repo_host + type: org.cloudfoundry.managed-service + parameters: + service: html5-apps-repo + service-name: managedApp-html5-app-host-service + service-plan: app-host + - name: uaa_managedApp + type: org.cloudfoundry.managed-service + parameters: + path: ./xs-security.json + service: xsuaa + service-name: managedApp-xsuaa-service + service-plan: application diff --git a/packages/cf-deploy-config-writer/test/unit/fixtures/mta-types/managed/mta.yaml b/packages/cf-deploy-config-writer/test/unit/fixtures/mta-types/managed/mta.yaml new file mode 100644 index 0000000000..973b0eac7d --- /dev/null +++ b/packages/cf-deploy-config-writer/test/unit/fixtures/mta-types/managed/mta.yaml @@ -0,0 +1,57 @@ +_schema-version: "3.2" +ID: managedApp +version: 0.0.1 +modules: + - name: managedApp-dest-content + type: com.sap.application.content + requires: + - name: managedApp-destination-service + parameters: + content-target: true + - name: managedApp_repo_host + parameters: + service-key: + name: managedApp_repo_host-key + - name: uaa_managedApp + parameters: + service-key: + name: uaa_managedApp-key + parameters: + content: + instance: + destinations: + - Name: myTestApp_managedApp_repo_host + ServiceInstanceName: managedApp-html5-srv + ServiceKeyName: managedApp_repo_host-key + sap.cloud.service: myTestApp + - Authentication: OAuth2UserTokenExchange + Name: myTestApp_uaa_managedApp + ServiceInstanceName: managedApp-xsuaa-srv + ServiceKeyName: uaa_managedApp-key + sap.cloud.service: myTestApp + existing_destinations_policy: ignore + build-parameters: + no-source: true +resources: + - name: managedApp-destination-service + type: org.cloudfoundry.managed-service + parameters: + config: + HTML5Runtime_enabled: true + version: 1.0.0 + service: destination + service-name: managedApp-destination-service + service-plan: lite + - name: managedApp_repo_host + type: org.cloudfoundry.managed-service + parameters: + service: html5-apps-repo + service-name: managedApp-html5-srv + service-plan: app-host + - name: uaa_managedApp + type: org.cloudfoundry.managed-service + parameters: + path: ./xs-security.json + service: xsuaa + service-name: managedApp-xsuaa-srv + service-plan: application diff --git a/packages/cf-deploy-config-writer/test/unit/fixtures/mta-types/standalone-basic/mta.yaml b/packages/cf-deploy-config-writer/test/unit/fixtures/mta-types/standalone-basic/mta.yaml new file mode 100644 index 0000000000..70e7d07778 --- /dev/null +++ b/packages/cf-deploy-config-writer/test/unit/fixtures/mta-types/standalone-basic/mta.yaml @@ -0,0 +1,17 @@ +_schema-version: "3.2" +ID: standaloneBasic +description: Fiori elements app +version: 0.0.1 +build-parameters: + before-all: + - builder: custom + commands: + - npm install + +parameters: + enable-parallel-deployments: true + deploy_mode: html5-repo + +modules: [] + +resources: [] \ No newline at end of file diff --git a/packages/cf-deploy-config-writer/test/unit/fixtures/mta-types/standalone/mta.yaml b/packages/cf-deploy-config-writer/test/unit/fixtures/mta-types/standalone/mta.yaml new file mode 100644 index 0000000000..c26660fd7d --- /dev/null +++ b/packages/cf-deploy-config-writer/test/unit/fixtures/mta-types/standalone/mta.yaml @@ -0,0 +1,41 @@ +_schema-version: "3.2" +ID: standaloneApp +description: Fiori elements app +version: 0.0.1 +modules: + - name: standaloneApp-router + type: approuter.nodejs + path: router + requires: + - name: standaloneApp-html5-repo-runtime + - name: standaloneApp-uaa + parameters: + disk-quota: 256M + memory: 256M +resources: + - name: standaloneApp-uaa + type: org.cloudfoundry.managed-service + parameters: + config: + tenant-mode: dedicated + xsappname: standaloneApp-${org} + service: xsuaa + service-plan: application + - name: standaloneApp-html5-repo-runtime + type: org.cloudfoundry.managed-service + parameters: + service: html5-apps-repo + service-plan: app-runtime + - name: standaloneApp-destination + type: org.cloudfoundry.managed-service + parameters: + service: destination + service-plan: lite +parameters: + deploy_mode: html5-repo + enable-parallel-deployments: true +build-parameters: + before-all: + - builder: custom + commands: + - npm install diff --git a/packages/cf-deploy-config-writer/test/unit/index-app.test.ts b/packages/cf-deploy-config-writer/test/unit/index-app.test.ts new file mode 100644 index 0000000000..487d6fce17 --- /dev/null +++ b/packages/cf-deploy-config-writer/test/unit/index-app.test.ts @@ -0,0 +1,126 @@ +import { join } from 'path'; +import fsExtra from 'fs-extra'; +import hasbin from 'hasbin'; +import { create as createStorage } from 'mem-fs'; +import { create } from 'mem-fs-editor'; +import { NullTransport, ToolsLogger } from '@sap-ux/logger'; +import * as btp from '@sap-ux/btp-utils'; +import { generateAppConfig } from '../../src'; +import type { Editor } from 'mem-fs-editor'; + +jest.mock('@sap-ux/btp-utils', () => ({ + ...jest.requireActual('@sap-ux/btp-utils'), + isAppStudio: jest.fn(), + listDestinations: jest.fn() +})); + +jest.mock('hasbin', () => { + return { + ...(jest.requireActual('hasbin') as {}), + sync: jest.fn() + }; +}); + +let hasSyncMock: jest.SpyInstance; +let isAppStudioMock: jest.SpyInstance; +let listDestinationsMock: jest.SpyInstance; +let unitTestFs: Editor; + +describe('CF Writer App', () => { + jest.setTimeout(10000); + + const destinationsMock = { + 'TestDestination': { + Name: 'TestDestination', + Type: 'MockType', + Authentication: 'NoAuthentication', + ProxyType: 'NoProxy', + Description: 'MockDestination', + Host: 'MockHost', + WebIDEAdditionalData: btp.WebIDEAdditionalData.FULL_URL, + WebIDEUsage: btp.WebIDEUsage.ODATA_GENERIC + } + }; + const logger = new ToolsLogger({ + transports: [new NullTransport()] + }); + const outputDir = join(__dirname, '../test-output', 'app'); + + beforeEach(() => { + jest.resetAllMocks(); + jest.restoreAllMocks(); + isAppStudioMock = jest.spyOn(btp, 'isAppStudio'); + listDestinationsMock = jest.spyOn(btp, 'listDestinations'); + unitTestFs = create(createStorage()); + hasSyncMock = jest.spyOn(hasbin, 'sync').mockImplementation(() => true); + }); + + beforeAll(() => { + jest.clearAllMocks(); + jest.spyOn(hasbin, 'sync').mockReturnValue(true); + fsExtra.removeSync(outputDir); + jest.mock('hasbin', () => { + return { + ...(jest.requireActual('hasbin') as {}), + sync: hasSyncMock + }; + }); + }); + + afterAll(() => { + jest.resetAllMocks(); + }); + + describe('Generate HTML5 App Config', () => { + test('Generate deployment configs - HTML5 App and destination read from ui5.yaml', async () => { + isAppStudioMock.mockResolvedValue(true); + listDestinationsMock.mockResolvedValue(destinationsMock); + const appName = 'basicapp01'; + const appPath = join(outputDir, appName); + fsExtra.mkdirSync(outputDir, { recursive: true }); + fsExtra.mkdirSync(appPath); + fsExtra.copySync(join(__dirname, '../sample/basicapp'), appPath); + await generateAppConfig({ appPath }, unitTestFs, logger); + expect(isAppStudioMock).toBeCalledTimes(1); + expect(listDestinationsMock).toBeCalledTimes(1); + expect(unitTestFs.dump(appPath)).toMatchSnapshot(); + // Since mta.yaml is not in memfs, read from disk + expect(unitTestFs.read(join(appPath, 'mta.yaml'))).toMatchSnapshot(); + }); + + test('Generate deployment configs - HTML5 App with managed approuter attached with no destination available', async () => { + isAppStudioMock.mockResolvedValue(false); + listDestinationsMock.mockResolvedValue(destinationsMock); + const appName = 'lrop'; + const appPath = join(outputDir, appName); + fsExtra.mkdirSync(outputDir, { recursive: true }); + fsExtra.mkdirSync(appPath); + fsExtra.copySync(join(__dirname, `../sample/lrop`), appPath); + await generateAppConfig({ appPath, addManagedAppRouter: true }, unitTestFs, logger); + expect(listDestinationsMock).toBeCalledTimes(0); + expect(unitTestFs.dump(appPath)).toMatchSnapshot(); + // Since mta.yaml is not in memfs, read from disk + expect(unitTestFs.read(join(appPath, 'mta.yaml'))).toMatchSnapshot(); + }); + + test('Generate deployment configs - HTML5 App with managed approuter attached to a multi target application', async () => { + isAppStudioMock.mockResolvedValue(false); + listDestinationsMock.mockResolvedValue(destinationsMock); + const appName = 'multi'; + const appPath = join(outputDir, appName); + fsExtra.mkdirSync(outputDir, { recursive: true }); + fsExtra.mkdirSync(appPath); + fsExtra.copySync(join(__dirname, `../sample/multi`), appPath); + await generateAppConfig({ appPath, addManagedAppRouter: true }, unitTestFs); + expect(unitTestFs.dump(appPath)).toMatchSnapshot(); + // Since mta.yaml is not in memfs, read from disk + expect(unitTestFs.read(join(appPath, 'mta.yaml'))).toMatchSnapshot(); + }); + + test('Throw an exception if the appPath is not found', async () => { + const appName = 'validate'; + const appPath = join(outputDir, appName); + await expect(generateAppConfig({ appPath }, unitTestFs, logger)).rejects.toThrowError(); + }); + }); +}); diff --git a/packages/cf-deploy-config-writer/test/unit/index-base.test.ts b/packages/cf-deploy-config-writer/test/unit/index-base.test.ts new file mode 100644 index 0000000000..21b819a76f --- /dev/null +++ b/packages/cf-deploy-config-writer/test/unit/index-base.test.ts @@ -0,0 +1,203 @@ +import { join } from 'path'; +import fsExtra from 'fs-extra'; +import * as hasbin from 'hasbin'; +import { create as createStorage } from 'mem-fs'; +import { create } from 'mem-fs-editor'; +import { apiGetInstanceCredentials } from '@sap/cf-tools'; +import { NullTransport, ToolsLogger } from '@sap-ux/logger'; +import { type CFBaseConfig, generateBaseConfig } from '../../src'; +import { RouterModuleType } from '../../src/types'; +import { MTABinNotFound } from '../../src/constants'; +import type { Editor } from 'mem-fs-editor'; + +jest.mock('@sap-ux/btp-utils', () => ({ + ...jest.requireActual('@sap-ux/btp-utils'), + isAppStudio: jest.fn(), + listDestinations: jest.fn() +})); + +jest.mock('hasbin', () => { + return { + ...(jest.requireActual('hasbin') as {}), + sync: jest.fn() + }; +}); + +jest.mock('@sap/cf-tools'); + +let hasSyncMock: jest.SpyInstance; + +describe('CF Writer Base', () => { + jest.setTimeout(10000); + + const logger = new ToolsLogger({ + transports: [new NullTransport()] + }); + const outputDir = join(__dirname, '../test-output', 'base'); + let unitTestFs: Editor; + + beforeEach(() => { + jest.resetAllMocks(); + jest.restoreAllMocks(); + unitTestFs = create(createStorage()); + hasSyncMock = jest.spyOn(hasbin, 'sync').mockImplementation(() => true); + }); + + beforeAll(() => { + jest.clearAllMocks(); + fsExtra.removeSync(outputDir); + jest.mock('hasbin', () => { + return { + ...(jest.requireActual('hasbin') as {}), + sync: hasSyncMock + }; + }); + }); + + afterAll(() => { + jest.resetAllMocks(); + }); + + describe('Generate Base Config - Standalone', () => { + test('Generate deployment configs - standalone with connectivity service', async () => { + const debugSpy = jest.spyOn(logger, 'debug'); + const mtaId = 'standalonewithconnectivityservice'; + const mtaPath = join(outputDir, mtaId); + fsExtra.mkdirSync(outputDir, { recursive: true }); + fsExtra.mkdirSync(mtaPath); + await generateBaseConfig( + { + mtaPath, + mtaId, + routerType: RouterModuleType.Standard, + addConnectivityService: true + }, + unitTestFs, + logger + ); + expect(debugSpy).toBeCalledTimes(1); + expect(unitTestFs.dump(mtaPath)).toMatchSnapshot(); + // Since mta.yaml is not in memfs, read from disk + expect(unitTestFs.read(join(mtaPath, 'mta.yaml'))).toMatchSnapshot(); + }); + + test('Generate deployment configs - standalone with ABAP service provider', async () => { + const apiGetInstanceCredentialsMock = apiGetInstanceCredentials as jest.Mock; + apiGetInstanceCredentialsMock.mockResolvedValue({ + credentials: { + endpoints: { TestEndPoint: '' }, + 'sap.cloud.service': 'TestService' + } + }); + const mtaId = 'standalonewithabapserviceprovider'; + const mtaPath = join(outputDir, mtaId); + fsExtra.mkdirSync(outputDir, { recursive: true }); + fsExtra.mkdirSync(mtaPath); + await generateBaseConfig( + { + mtaPath, + mtaId, + routerType: RouterModuleType.Standard, + abapServiceProvider: { + abapService: 'abap-haas', + abapServiceName: 'Y11_00.0035' + } + }, + unitTestFs, + logger + ); + expect(unitTestFs.dump(mtaPath)).toMatchSnapshot(); + // Since mta.yaml is not in memfs, read from disk + expect(unitTestFs.read(join(mtaPath, 'mta.yaml'))).toMatchSnapshot(); + }); + }); + + describe('Generate Base Config - Managed', () => { + test('Generate deployment configs - managed', async () => { + const debugSpy = jest.spyOn(logger, 'debug'); + const mtaId = 'managed'; + const mtaPath = join(outputDir, mtaId); + fsExtra.mkdirSync(outputDir, { recursive: true }); + fsExtra.mkdirSync(mtaPath); + await generateBaseConfig( + { + mtaPath, + mtaId, + mtaDescription: 'MyManagedDescription', + routerType: RouterModuleType.Managed + }, + unitTestFs, + logger + ); + expect(debugSpy).toBeCalledTimes(1); + expect(unitTestFs.dump(mtaPath)).toMatchSnapshot(); + // Since mta.yaml is not in memfs, read from disk + expect(unitTestFs.read(join(mtaPath, 'mta.yaml'))).toMatchSnapshot(); + }); + }); + + describe('Generate Base Config - Validation', () => { + test('Generate invalid deployment configs', async () => { + const mtaId = 'invalidconfigs02'; + const mtaPath = join(outputDir, mtaId); + const config = { + abapServiceProvider: { + abapService: '~abapService', + abapServiceName: '~abapService' + }, + mtaPath, + mtaId, + mtaDescription: 'MyManagedDescription', + routerType: RouterModuleType.Managed + } as Partial; + jest.spyOn(unitTestFs, 'exists').mockReturnValueOnce(true); + await expect(generateBaseConfig(config as CFBaseConfig, unitTestFs)).rejects.toThrowError( + 'A folder with same name already exists in the target directory' + ); + delete config.abapServiceProvider?.abapService; + await expect(generateBaseConfig(config as CFBaseConfig)).rejects.toThrowError( + 'Missing ABAP service details for direct service binding' + ); + delete config.routerType; + await expect(generateBaseConfig(config as CFBaseConfig)).rejects.toThrowError( + 'Missing required parameters, MTA path, MTA ID or router type' + ); + hasSyncMock.mockReturnValue(false); + await expect(generateBaseConfig(config as CFBaseConfig)).rejects.toThrowError(MTABinNotFound); + }); + it.each([['~sample'], ['111sample'], [' sample'], ['0sample'], ['.sample'], ['s'.repeat(129)]])( + 'Validate length and starting characters %s', + async (mtaId) => { + const config = { + abapServiceProvider: { + abapService: '~abapService', + abapServiceName: '~abapService' + }, + mtaPath: join(outputDir, mtaId), + mtaId, + mtaDescription: 'MyManagedDescription', + routerType: RouterModuleType.Managed + } as Partial; + await expect(generateBaseConfig(config as CFBaseConfig)).rejects.toThrowError( + 'The MTA ID must start with a letter or underscore and be less than 128 characters long' + ); + } + ); + + it.each([['sampl!e'], ['sample two']])('Validate mtaId %s', async (mtaId) => { + const config = { + abapServiceProvider: { + abapService: '~abapService', + abapServiceName: '~abapService' + }, + mtaPath: join(outputDir, mtaId), + mtaId, + mtaDescription: 'MyManagedDescription', + routerType: RouterModuleType.Managed + } as Partial; + await expect(generateBaseConfig(config as CFBaseConfig)).rejects.toThrowError( + 'The MTA ID can only contain letters, numbers, dashes, periods, underscores' + ); + }); + }); +}); diff --git a/packages/cf-deploy-config-writer/test/unit/mockMta.ts b/packages/cf-deploy-config-writer/test/unit/mockMta.ts new file mode 100644 index 0000000000..6140f037bf --- /dev/null +++ b/packages/cf-deploy-config-writer/test/unit/mockMta.ts @@ -0,0 +1,103 @@ +import { join } from 'path'; +import * as fs from 'fs'; +import yaml, { dump } from 'js-yaml'; +import type { Mta, mta } from '@sap/mta-lib'; + +// Cannot directly use the `Mta` class as interface +// We'll need to "implement" private properties and methods as well +// Which means that we'll effectively need to extend the class +// Extending means having to call the super constructor and we +// don't want that, extract the interface instead. +type MtaInterface = Pick; + +/** + * Mocks Mta for testing. + * + * @class MockMta + * @implements {Mta} + */ +export class MockMta implements Partial { + private readonly contents: mta.MtaDescriptor; + public readonly mtaDirPath: string; + private readonly mtaPath: string; + + constructor(mtaDirPath: string) { + this.mtaDirPath = mtaDirPath; + this.mtaPath = join(mtaDirPath, 'mta.yaml'); + this.contents = yaml.load(fs.readFileSync(this.mtaPath).toString()) as mta.MtaDescriptor; + } + create(_descriptor: mta.MtaDescriptor): Promise { + throw new Error('Method not implemented.'); + } + getMtaFilePath(): Promise { + throw new Error('Method not implemented.'); + } + getMtaID(): Promise { + return Promise.resolve(this.contents.ID); + } + addModule(module: mta.Module): Promise { + if (!this.contents.modules) { + this.contents.modules = []; + } + this.contents.modules.push(module); + return Promise.resolve(); + } + addResource(resource: mta.Resource): Promise { + if (!this.contents.modules) { + this.contents.modules = []; + } + this.contents?.resources?.push(resource); + return Promise.resolve(); + } + getModules(): Promise { + return Promise.resolve(this.contents?.modules ?? []); + } + getResources(): Promise { + return Promise.resolve(this.contents.resources ?? []); + } + updateModule(module: mta.Module): Promise { + if (!this.contents.modules) { + throw new Error('No modules'); + } + const moduleIndex = this.contents.modules.findIndex((m) => m.name === module.name); + if (moduleIndex === -1) { + throw new Error(`Module [${module.name} does not exist]`); + } + this.contents.modules[moduleIndex] = module; + return Promise.resolve(); + } + updateResource(resource: mta.Resource): Promise { + if (!this.contents.resources) { + throw new Error('No resources'); + } + const resourceIndex = this.contents.resources.findIndex((m) => m.name === resource.name); + if (resourceIndex === -1) { + throw new Error(`Resource [${resource.name} does not exist]`); + } + this.contents.resources[resourceIndex] = resource; + return Promise.resolve(); + } + updateBuildParameters(_buildParameters: mta.ProjectBuildParameters): Promise { + throw new Error('Method not implemented.'); + } + doesNameExist(_name: string): Promise { + throw new Error('Method not implemented.'); + } + save(): Promise { + fs.writeFileSync(this.mtaPath, dump(this.contents)); + return Promise.resolve(); + } + clean(): Promise { + return Promise.resolve(); + } + resolveModuleProperties(): Promise<{ properties: Record; messages: string[] }> { + throw new Error('Method not implemented.'); + } + getParameters(): Promise { + return Promise.resolve(this.contents.parameters ? this.contents.parameters : {}); + } + updateParameters(parameters: mta.Parameters): Promise { + this.contents.parameters = parameters; + return Promise.resolve(); + } +} diff --git a/packages/cf-deploy-config-writer/test/unit/mta.test.ts b/packages/cf-deploy-config-writer/test/unit/mta.test.ts new file mode 100644 index 0000000000..a476c980e3 --- /dev/null +++ b/packages/cf-deploy-config-writer/test/unit/mta.test.ts @@ -0,0 +1,303 @@ +import { join } from 'path'; +import fs from 'fs'; +import * as memfs from 'memfs'; +import { NullTransport, ToolsLogger } from '@sap-ux/logger'; +import { isMTAFound, useAbapDirectServiceBinding, MtaConfig } from '../../src/'; +import { deployMode, ResourceMTADestination } from '../../src/constants'; +import type { mta } from '@sap/mta-lib'; + +jest.mock('fs', () => { + const fs1 = jest.requireActual('fs'); + // eslint-disable-next-line @typescript-eslint/no-var-requires + const Union = require('unionfs').Union; + // eslint-disable-next-line @typescript-eslint/no-var-requires + const vol = require('memfs').vol; + return new Union().use(fs1).use(vol as unknown as typeof fs); +}); + +jest.mock('@sap/mta-lib', () => { + return { + // eslint-disable-next-line @typescript-eslint/no-var-requires + Mta: require('./mockMta').MockMta + }; +}); + +describe('Validate common functionality', () => { + const nullLogger = new ToolsLogger({ transports: [new NullTransport()] }); + const OUTPUT_DIR_PREFIX = '/abap-service'; + const abapServiceMta = fs.readFileSync(join(__dirname, 'fixtures/mta-types/abap-service/mta.yaml'), 'utf-8'); + + beforeEach(() => { + jest.resetModules(); + memfs.vol.reset(); + }); + + beforeAll(() => { + jest.clearAllMocks(); + }); + + afterAll(() => { + jest.resetAllMocks(); + }); + + it('returns true when there is an mta.yaml in the current directory', () => { + const dirName = 'somedir'; + memfs.vol.fromNestedJSON({ + [`./${dirName}/mta.yaml`]: '' + }); + expect(isMTAFound(dirName)).toBeTruthy(); + }); + + it('returns false when there is no isMTAFound.yaml in the current directory', () => { + const dirName = 'somedir'; + memfs.vol.fromNestedJSON({}); + expect(isMTAFound(dirName)).toBeFalsy(); + }); + + it('Validate isAbapDirectServiceBinding', async () => { + memfs.vol.fromNestedJSON( + { + './mta.yaml': '' + }, + '/' + ); + expect(await useAbapDirectServiceBinding('/testpath', true)).toBeFalsy(); + expect(await useAbapDirectServiceBinding('/testpath', false, '/testpath')).toBeFalsy(); + }); + + it('Validate isAbapDirectServiceBinding is true', async () => { + memfs.vol.fromNestedJSON( + { + [`.${OUTPUT_DIR_PREFIX}/app1/mta.yaml`]: abapServiceMta + }, + '/' + ); + expect(await useAbapDirectServiceBinding(`${OUTPUT_DIR_PREFIX}/app1/`, true)).toBeTruthy(); + }); + + it('Validate isAbapDirectServiceBinding handles exception', async () => { + memfs.vol.fromNestedJSON( + { + [`.${OUTPUT_DIR_PREFIX}/app1/mta.yaml`]: '' + }, + '/' + ); + expect(await useAbapDirectServiceBinding(`/`, false, OUTPUT_DIR_PREFIX, nullLogger)).toBeFalsy(); + }); +}); + +describe('Validate MtaConfig Instance', () => { + const OUTPUT_DIR_PREFIX = '/managed-cap'; + const managedRouterConfigCapMissingDestinations = fs.readFileSync( + join(__dirname, 'fixtures/mta-types/managed-cap-missing-destinations/mta.yaml'), + 'utf-8' + ); + const managedRouterConfigCap = fs.readFileSync(join(__dirname, 'fixtures/mta-types/managed-cap/mta.yaml'), 'utf-8'); + const managedRouterConfig = fs.readFileSync(join(__dirname, 'fixtures/mta-types/managed-apps/mta.yaml'), 'utf-8'); + const appDir = `${OUTPUT_DIR_PREFIX}/app1`; + + beforeEach(() => { + jest.resetModules(); + memfs.vol.reset(); + }); + + beforeAll(() => { + jest.clearAllMocks(); + }); + + afterAll(() => { + jest.resetAllMocks(); + }); + + it('Validate destinations are retrieved for a compliant mta config', async () => { + memfs.vol.fromNestedJSON( + { + [`.${OUTPUT_DIR_PREFIX}/app1/mta.yaml`]: managedRouterConfigCap + }, + '/' + ); + const mtaConfig = await MtaConfig.newInstance(appDir); + expect(mtaConfig.getExposedDestinations()).toMatchInlineSnapshot(`Array []`); + expect(mtaConfig.isABAPServiceFound).toBeFalsy(); + expect(await mtaConfig.getParameters()).toMatchInlineSnapshot(` + Object { + "enable-parallel-deployments": true, + } + `); + }); + + it('Validate destinations are retrieved for an mta config missing destinations', async () => { + memfs.vol.fromNestedJSON( + { + [`.${OUTPUT_DIR_PREFIX}/app1/mta.yaml`]: managedRouterConfigCapMissingDestinations + }, + '/' + ); + const mtaConfig = await MtaConfig.newInstance(appDir); + expect(mtaConfig.getExposedDestinations()).toMatchInlineSnapshot(` + Array [ + "myTestApp_managedApp_repo_host", + "myTestApp_uaa_managedApp", + ] + `); + }); + + it('(Non-CAP) Validate destinations are retrieved for an mta config', async () => { + memfs.vol.fromNestedJSON( + { + [`.${OUTPUT_DIR_PREFIX}/app1/mta.yaml`]: managedRouterConfig + }, + '/' + ); + const mtaConfig = await MtaConfig.newInstance(appDir); + expect(mtaConfig.getExposedDestinations(true)).toMatchInlineSnapshot(` + Array [ + "northwind", + ] + `); + }); + + it.each([ + ['%s.-srv-api', 'managedApp_-srv-api'], + ['%s-srv-api', 'managedApp-srv-api'], + ['%s123!@#&-srv-api', 'managedApp123____-srv-api'] + ])('Format when destination name is %s', async (destinationName, correctDest) => { + memfs.vol.fromNestedJSON({ [`${appDir}/mta.yaml`]: managedRouterConfig }, '/'); + const mtaConfig = await MtaConfig.newInstance(appDir); + const formattedDestinationName = mtaConfig.getFormattedPrefix(destinationName); + expect(formattedDestinationName).toEqual(correctDest); + }); +}); + +describe('Validate common flows', () => { + const nullLogger = new ToolsLogger({ transports: [new NullTransport()] }); + const OUTPUT_DIR_PREFIX = '/managed-cap'; + const managedBasicMTA = fs.readFileSync(join(__dirname, 'fixtures/mta-types/managed-basic/mta.yaml'), 'utf-8'); + const standaloneBasicMTA = fs.readFileSync( + join(__dirname, 'fixtures/mta-types/standalone-basic/mta.yaml'), + 'utf-8' + ); + const standaloneMTA = fs.readFileSync(join(__dirname, 'fixtures/mta-types/standalone/mta.yaml'), 'utf-8'); + const capBasicMTA = fs.readFileSync(join(__dirname, 'fixtures/mta-types/managed-cap/mta.yaml'), 'utf-8'); + const managedMissingDestinationMTA = fs.readFileSync( + join(__dirname, 'fixtures/mta-types/managed-missing-dest/mta.yaml'), + 'utf-8' + ); + + beforeEach(() => { + jest.resetModules(); + memfs.vol.reset(); + }); + + beforeAll(() => { + jest.clearAllMocks(); + }); + + afterAll(() => { + jest.resetAllMocks(); + }); + + it('Validate adding managed approuter', async () => { + memfs.vol.fromNestedJSON( + { + [`.${OUTPUT_DIR_PREFIX}/app1/mta.yaml`]: managedBasicMTA + }, + '/' + ); + const mtaConfig = await MtaConfig.newInstance(`${OUTPUT_DIR_PREFIX}/app1`, nullLogger); + await mtaConfig.addRoutingModules(true); + await mtaConfig.addApp('myhtml5app', './'); + await mtaConfig.addConnectivityResource(); //typical for onpremise destinations + await mtaConfig.save(); + expect(mtaConfig.standaloneRouterPath).toBeUndefined(); + expect(await mtaConfig.getParameters()).toMatchInlineSnapshot(` + Object { + "deploy_mode": "html5-repo", + "enable-parallel-deployments": true, + } + `); + const expectAfterYaml = fs.readFileSync(`${OUTPUT_DIR_PREFIX}/app1/mta.yaml`, 'utf-8'); + expect(expectAfterYaml).toMatchSnapshot(); + }); + + it('Validate destination service is correctly updated if missing instances', async () => { + memfs.vol.fromNestedJSON( + { + [`.${OUTPUT_DIR_PREFIX}/app2/mta.yaml`]: managedMissingDestinationMTA + }, + '/' + ); + const mtaConfig = await MtaConfig.newInstance(`${OUTPUT_DIR_PREFIX}/app2`); + await mtaConfig.addRoutingModules(true); + await mtaConfig.save(); + const expectAfterYaml = fs.readFileSync(`${OUTPUT_DIR_PREFIX}/app2/mta.yaml`, 'utf-8'); + expect(expectAfterYaml).toMatchSnapshot(); + }); + + it('Validate adding standalone approuter', async () => { + memfs.vol.fromNestedJSON( + { + [`.${OUTPUT_DIR_PREFIX}/app3/mta.yaml`]: standaloneBasicMTA + }, + '/' + ); + const mtaConfig = await MtaConfig.newInstance(`${OUTPUT_DIR_PREFIX}/app3`); + await mtaConfig.addStandaloneRouter(true); + await mtaConfig.addRoutingModules(); + await mtaConfig.addApp('myhtml5app', './'); + await mtaConfig.addAbapService('abapservice', 'abapservice'); + await mtaConfig.addConnectivityResource(); //typical for onpremise destinations + await mtaConfig.save(); + expect(mtaConfig.standaloneRouterPath).toEqual('router'); + expect(await mtaConfig.getParameters()).toMatchInlineSnapshot(` + Object { + "deploy_mode": "html5-repo", + "enable-parallel-deployments": true, + } + `); + const expectAfterYaml = fs.readFileSync(`${OUTPUT_DIR_PREFIX}/app3/mta.yaml`, 'utf-8'); + expect(expectAfterYaml).toMatchSnapshot(); + }); + + it('Validate adding standalone approuter with missing module destination', async () => { + memfs.vol.fromNestedJSON( + { + [`.${OUTPUT_DIR_PREFIX}/app4/mta.yaml`]: standaloneMTA + }, + '/' + ); + const mtaConfig = await MtaConfig.newInstance(`${OUTPUT_DIR_PREFIX}/app4`); + await mtaConfig.addRoutingModules(); + await mtaConfig.save(); + const expectAfterYaml = fs.readFileSync(`${OUTPUT_DIR_PREFIX}/app4/mta.yaml`, 'utf-8'); + expect(expectAfterYaml).toMatchSnapshot(); + }); + + it('Validate adding managed approuter and destinations to cds generated mta.yaml', async () => { + memfs.vol.fromNestedJSON( + { + [`.${OUTPUT_DIR_PREFIX}/app5/mta.yaml`]: capBasicMTA + }, + '/' + ); + const mtaConfig = await MtaConfig.newInstance(`${OUTPUT_DIR_PREFIX}/app5`); + await mtaConfig.addRoutingModules(true); + await mtaConfig.addApp('myhtml5app', './'); + const parameters = await mtaConfig.getParameters(); + const params = { ...parameters, ...{} } as mta.Parameters; + params[deployMode] = 'html5-repo'; + await mtaConfig.updateParameters(params); + await mtaConfig.appendInstanceBasedDestination(mtaConfig.getFormattedPrefix(ResourceMTADestination)); + expect(mtaConfig.cloudServiceName).toEqual('managedAppCAPProject'); + expect(mtaConfig.hasManagedXsuaaResource()).toBeTruthy(); + await mtaConfig.save(); + expect(await mtaConfig.getParameters()).toMatchInlineSnapshot(` + Object { + "deploy_mode": "html5-repo", + "enable-parallel-deployments": true, + } + `); + const expectAfterYaml = fs.readFileSync(`${OUTPUT_DIR_PREFIX}/app5/mta.yaml`, 'utf-8'); + expect(expectAfterYaml).toMatchSnapshot(); + }); +}); diff --git a/packages/cf-deploy-config-writer/test/unit/mtaext.test.ts b/packages/cf-deploy-config-writer/test/unit/mtaext.test.ts new file mode 100644 index 0000000000..5f698f0c9d --- /dev/null +++ b/packages/cf-deploy-config-writer/test/unit/mtaext.test.ts @@ -0,0 +1,129 @@ +import { join } from 'path'; +import fs from 'fs'; +import * as memfs from 'memfs'; +import { MtaConfig } from '../../src/'; +import { MTAFileExtension } from '../../src/constants'; + +jest.mock('fs', () => { + const fs1 = jest.requireActual('fs'); + // eslint-disable-next-line @typescript-eslint/no-var-requires + const Union = require('unionfs').Union; + // eslint-disable-next-line @typescript-eslint/no-var-requires + const vol = require('memfs').vol; + return new Union().use(fs1).use(vol as unknown as typeof fs); +}); + +jest.mock('@sap/mta-lib', () => { + return { + // eslint-disable-next-line @typescript-eslint/no-var-requires + Mta: require('./mockMta').MockMta + }; +}); + +describe('Adding and Updating mta extension configuration', () => { + beforeEach(() => { + jest.resetModules(); + memfs.vol.reset(); + }); + + beforeAll(() => { + jest.clearAllMocks(); + }); + + afterAll(() => { + jest.resetAllMocks(); + }); + + it('Creates mta extension in app folder', async () => { + const testAppPath = '/test-app-mta-ext'; + const mtaYaml = fs.readFileSync(join(__dirname, 'fixtures/mta-ext/app/mta.yaml'), 'utf-8'); + memfs.vol.fromNestedJSON( + { + [`.${testAppPath}/mta.yaml`]: mtaYaml + }, + '/' + ); + const mtaConfig = await MtaConfig.newInstance(testAppPath); + await mtaConfig.addMtaExtensionConfig('Instance_Dest_Name1', 'http://somehost:8080', { + key: 'ApiKey', + value: 'key_value_abcd1234' + }); + expect(fs.readFileSync(`${testAppPath}/${MTAFileExtension}`, 'utf-8')).toMatchInlineSnapshot(` + "## SAP UX Tools generated mtaext file + _schema-version: \\"3.2\\" + ID: test-mta-ext + extends: test-mta + version: 0.0.1 + + resources: + - name: test-mta-destination-service + parameters: + config: + init_data: + instance: + destinations: + - Authentication: NoAuthentication + Name: Instance_Dest_Name1 + ProxyType: Internet + Type: HTTP + URL: http://somehost:8080 + URL.headers.ApiKey: key_value_abcd1234 + - Authentication: NoAuthentication + Name: ui5 + Type: HTTP + URL: https://ui5.sap.com + ProxyType: Internet + existing_destinations_policy: update + " + `); + }); + + it('Adds destination entry in existing mta extension', async () => { + const testAppPath = '/test-app-mta-ext1'; + const mtaYaml = fs.readFileSync(join(__dirname, 'fixtures/mta-ext/multi/mta.yaml'), 'utf-8'); + const mtaExtYaml = fs.readFileSync(join(__dirname, 'fixtures/mta-ext/multi/mta-ext.mtaext'), 'utf-8'); + + memfs.vol.fromNestedJSON( + { + [`.${testAppPath}/mta.yaml`]: mtaYaml, + [`.${testAppPath}/mta-ext.mtaext`]: mtaExtYaml + }, + '/' + ); + + const mtaConfig = await MtaConfig.newInstance(testAppPath); + await mtaConfig.addMtaExtensionConfig('Instance_Dest_Name1', 'http://somehost:8080', { + key: 'ApiKey', + value: 'key_value_abcd1234' + }); + expect(fs.readFileSync(`${testAppPath}/${MTAFileExtension}`, 'utf-8')).toMatchInlineSnapshot(` + "## SAP UX Tools generated mtaext file + _schema-version: \\"3.2\\" + ID: test-mta-ext + extends: test-mta + version: 1.0.0 + + resources: + - name: qa-destination-service + parameters: + config: + init_data: + instance: + destinations: + - Authentication: NoAuthentication + Name: ABHE_NorthwindProduct_theme + ProxyType: Internet + Type: HTTP + URL: https://api.hana.on.demand + URL.headers.ApiKey: 1234567890abcdefg + - Authentication: NoAuthentication + Name: Instance_Dest_Name1 + ProxyType: Internet + Type: HTTP + URL: http://somehost:8080 + URL.headers.ApiKey: key_value_abcd1234 + existing_destinations_policy: update + " + `); + }); +}); diff --git a/packages/cf-deploy-config-writer/test/unit/utils.test.ts b/packages/cf-deploy-config-writer/test/unit/utils.test.ts new file mode 100644 index 0000000000..f7d1f18f3f --- /dev/null +++ b/packages/cf-deploy-config-writer/test/unit/utils.test.ts @@ -0,0 +1,22 @@ +import { validateVersion } from '../../src/utils'; +import { MTAVersion } from '../../src/constants'; + +describe('CF utils', () => { + beforeAll(async () => { + jest.clearAllMocks(); + }); + + afterAll(async () => { + jest.resetAllMocks(); + }); + + describe('Utils methods', () => { + test('Validate - validateVersion', async () => { + expect(() => validateVersion('0.0.0')).toThrowError(); + expect(() => validateVersion('~Version')).toThrow(); + expect(() => validateVersion()).not.toThrowError(); + expect(validateVersion(MTAVersion)).toBeTruthy(); + expect(validateVersion('1')).toBeTruthy(); + }); + }); +}); diff --git a/packages/cf-deploy-config-writer/tsconfig.eslint.json b/packages/cf-deploy-config-writer/tsconfig.eslint.json new file mode 100644 index 0000000000..d5f1aa3474 --- /dev/null +++ b/packages/cf-deploy-config-writer/tsconfig.eslint.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "include": ["src", "test", ".eslintrc.js"] +} diff --git a/packages/cf-deploy-config-writer/tsconfig.json b/packages/cf-deploy-config-writer/tsconfig.json new file mode 100644 index 0000000000..6007e34a6a --- /dev/null +++ b/packages/cf-deploy-config-writer/tsconfig.json @@ -0,0 +1,29 @@ +{ + "extends": "../../tsconfig.json", + "include": [ + "../../types/mem-fs-editor.d.ts", + "src/**/*.json", + "src" + ], + "compilerOptions": { + "rootDir": "src", + "outDir": "dist" + }, + "references": [ + { + "path": "../btp-utils" + }, + { + "path": "../logger" + }, + { + "path": "../project-access" + }, + { + "path": "../ui5-config" + }, + { + "path": "../yaml" + } + ] +} diff --git a/packages/telemetry/test/tools-suite-telemetry/index.test.ts b/packages/telemetry/test/tools-suite-telemetry/index.test.ts index df23e5d59f..8f2c6ae433 100644 --- a/packages/telemetry/test/tools-suite-telemetry/index.test.ts +++ b/packages/telemetry/test/tools-suite-telemetry/index.test.ts @@ -37,6 +37,14 @@ describe('Tools Suite Telemetry Tests', () => { memfs.vol.reset(); }); + beforeAll(() => { + jest.clearAllMocks(); + }); + + afterAll(() => { + jest.resetAllMocks(); + }); + it('No additional properties, Not SBAS', async () => { isAppStudioMock.mockReturnValue(false); const commonProperties = await processToolsSuiteTelemetry(undefined); diff --git a/packages/ui5-config/src/ui5config.ts b/packages/ui5-config/src/ui5config.ts index 821b4696d4..4f5d652687 100644 --- a/packages/ui5-config/src/ui5config.ts +++ b/packages/ui5-config/src/ui5config.ts @@ -392,6 +392,78 @@ export class UI5Config { return this; } + /** + * Adds the Cloud Foundry deployment task to the config. + * + * @param archiveName the name of the archive that is to be generated as part of the CF bundling + * @param addModulesTask if true the modules task is added to the deployment configuration + * @param addTranspileTask if true the transpile task is added to the deployment configuration + * @returns this UI5Config instance + * @memberof UI5Config + */ + public addCloudFoundryDeployTask(archiveName: string, addModulesTask = false, addTranspileTask = false): this { + this.document.appendTo({ + path: 'builder.resources.excludes', + value: '/test/**' + }); + this.document.appendTo({ + path: 'builder.resources.excludes', + value: '/localService/**' + }); + + this.document.appendTo({ + path: 'builder.customTasks', + value: { + name: 'webide-extension-task-updateManifestJson', + afterTask: 'replaceVersion', + configuration: { + appFolder: 'webapp', + destDir: 'dist' + } + } + }); + + this.document.appendTo({ + path: 'builder.customTasks', + value: { + name: 'ui5-task-zipper', + afterTask: 'generateCachebusterInfo', + configuration: { + archiveName, + additionalFiles: ['xs-app.json'] + } + } + }); + + if (addModulesTask) { + this.document.appendTo({ + path: 'builder.customTasks', + value: { + name: 'ui5-tooling-modules-task', + afterTask: 'replaceVersion', + configuration: {} + } + }); + } + + if (addTranspileTask) { + this.document.appendTo({ + path: 'builder.customTasks', + value: { + name: 'ui5-tooling-transpile-task', + afterTask: 'replaceVersion', + configuration: { + debug: true, + removeConsoleStatements: true, + transpileAsync: true, + transpileTypeScript: true + } + } + }); + } + return this; + } + /** * Remove a middleware form the UI5 config. * diff --git a/packages/ui5-config/test/__snapshots__/index.test.ts.snap b/packages/ui5-config/test/__snapshots__/index.test.ts.snap index b32ac31dd8..da8ef711df 100644 --- a/packages/ui5-config/test/__snapshots__/index.test.ts.snap +++ b/packages/ui5-config/test/__snapshots__/index.test.ts.snap @@ -186,6 +186,82 @@ exports[`UI5Config addBackendToFioriToolsProxydMiddleware should add comments wi " `; +exports[`UI5Config addCloudFoundryDeployTask add modules task 1`] = ` +"builder: + resources: + excludes: + - /test/** + - /localService/** + customTasks: + - name: webide-extension-task-updateManifestJson + afterTask: replaceVersion + configuration: + appFolder: webapp + destDir: dist + - name: ui5-task-zipper + afterTask: generateCachebusterInfo + configuration: + archiveName: myTestAppId + additionalFiles: + - xs-app.json + - name: ui5-tooling-modules-task + afterTask: replaceVersion + configuration: {} +" +`; + +exports[`UI5Config addCloudFoundryDeployTask add transpile task 1`] = ` +"builder: + resources: + excludes: + - /test/** + - /localService/** + customTasks: + - name: webide-extension-task-updateManifestJson + afterTask: replaceVersion + configuration: + appFolder: webapp + destDir: dist + - name: ui5-task-zipper + afterTask: generateCachebusterInfo + configuration: + archiveName: myTestAppId + additionalFiles: + - xs-app.json + - name: ui5-tooling-modules-task + afterTask: replaceVersion + configuration: {} + - name: ui5-tooling-transpile-task + afterTask: replaceVersion + configuration: + debug: true + removeConsoleStatements: true + transpileAsync: true + transpileTypeScript: true +" +`; + +exports[`UI5Config addCloudFoundryDeployTask minimal settings required 1`] = ` +"builder: + resources: + excludes: + - /test/** + - /localService/** + customTasks: + - name: webide-extension-task-updateManifestJson + afterTask: replaceVersion + configuration: + appFolder: webapp + destDir: dist + - name: ui5-task-zipper + afterTask: generateCachebusterInfo + configuration: + archiveName: myTestAppId + additionalFiles: + - xs-app.json +" +`; + exports[`UI5Config addFioriToolsProxydMiddleware add / get commonly configured backend (and UI5 defaults) 1`] = ` "server: customMiddleware: diff --git a/packages/ui5-config/test/index.test.ts b/packages/ui5-config/test/index.test.ts index 4c36093c3b..5d41ad8970 100644 --- a/packages/ui5-config/test/index.test.ts +++ b/packages/ui5-config/test/index.test.ts @@ -463,4 +463,21 @@ describe('UI5Config', () => { expect(ui5Config.toString()).toMatchSnapshot(); }); }); + + describe('addCloudFoundryDeployTask', () => { + test('minimal settings required', () => { + ui5Config.addCloudFoundryDeployTask('myTestAppId'); + expect(ui5Config.toString()).toMatchSnapshot(); + }); + + test('add modules task', () => { + ui5Config.addCloudFoundryDeployTask('myTestAppId', true); + expect(ui5Config.toString()).toMatchSnapshot(); + }); + + test('add transpile task', () => { + ui5Config.addCloudFoundryDeployTask('myTestAppId', true, true); + expect(ui5Config.toString()).toMatchSnapshot(); + }); + }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1d95f24601..bbde48d139 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -917,6 +917,79 @@ importers: specifier: 8.2.6 version: 8.2.6 + packages/cf-deploy-config-writer: + dependencies: + '@sap-ux/btp-utils': + specifier: workspace:* + version: link:../btp-utils + '@sap-ux/logger': + specifier: workspace:* + version: link:../logger + '@sap-ux/project-access': + specifier: workspace:* + version: link:../project-access + '@sap-ux/ui5-config': + specifier: workspace:* + version: link:../ui5-config + '@sap-ux/yaml': + specifier: workspace:* + version: link:../yaml + '@sap/cf-tools': + specifier: 3.2.0 + version: 3.2.0 + '@sap/mta-lib': + specifier: 1.7.4 + version: 1.7.4 + ejs: + specifier: 3.1.10 + version: 3.1.10 + hasbin: + specifier: 1.2.3 + version: 1.2.3 + i18next: + specifier: 21.10.0 + version: 21.10.0 + mem-fs: + specifier: 2.1.0 + version: 2.1.0 + mem-fs-editor: + specifier: 9.4.0 + version: 9.4.0(mem-fs@2.1.0) + semver: + specifier: 7.5.4 + version: 7.5.4 + devDependencies: + '@types/ejs': + specifier: 3.1.2 + version: 3.1.2 + '@types/fs-extra': + specifier: 9.0.13 + version: 9.0.13 + '@types/hasbin': + specifier: 1.2.2 + version: 1.2.2 + '@types/js-yaml': + specifier: 4.0.9 + version: 4.0.9 + '@types/mem-fs': + specifier: 1.1.2 + version: 1.1.2 + '@types/mem-fs-editor': + specifier: 7.0.1 + version: 7.0.1 + '@types/semver': + specifier: 7.5.2 + version: 7.5.2 + fs-extra: + specifier: 10.0.0 + version: 10.0.0 + js-yaml: + specifier: 3.14.0 + version: 3.14.0 + memfs: + specifier: 3.4.13 + version: 3.4.13 + packages/control-property-editor: devDependencies: '@esbuild-plugins/node-modules-polyfill': @@ -5911,7 +5984,7 @@ packages: resolution: {integrity: sha512-TS/9KG2CdGXS27S+QxbZXgr8uPsP4yNJYb4BC2/NeFUj80Rni3TeD2qwWmabymxmrLo7JEsytXH1FbpKTbvivw==} dependencies: '@changesets/types': 6.0.0 - js-yaml: 3.14.1 + js-yaml: 3.14.0 dev: true /@changesets/pre@2.0.0: @@ -6694,7 +6767,7 @@ packages: camelcase: 5.3.1 find-up: 4.1.0 get-package-type: 0.1.0 - js-yaml: 3.14.1 + js-yaml: 3.14.0 resolve-from: 5.0.0 dev: true @@ -8061,6 +8134,17 @@ packages: url: 0.11.0 dev: false + /@sap/mta-lib@1.7.4: + resolution: {integrity: sha512-NBtcVKipYrw9uCiWwH2QVfaVeJC3OV6spAE29TsUKjGzqUrSzMkFi8FtsikvDDDThtysjmcvWN8SDvkK3eQxmg==} + engines: {node: '>=8.0.0'} + dependencies: + cross-spawn: 7.0.3 + fs-extra: 8.1.0 + mta-local: 1.0.0 + temp-dir: 2.0.0 + which: 2.0.2 + dev: false + /@sap/ux-cds-compiler-facade@1.15.0(@sap-ux/odata-annotation-core-types@packages+odata-annotation-core-types)(@sap-ux/odata-annotation-core@packages+odata-annotation-core)(@sap-ux/project-access@packages+project-access): resolution: {integrity: sha512-WeA/wnF0NoHNod6H/Rcpgbh3ue4TVBSAlHQoupWzdu91yl+RZ4rKS4hARaRH+LV7gTXxJ5k6llJ/wyUN7sMDdw==} engines: {node: '>=18.16.0'} @@ -9257,6 +9341,10 @@ packages: '@types/node': 18.11.9 dev: true + /@types/hasbin@1.2.2: + resolution: {integrity: sha512-+WjYdtDVxfzvyIkVrLmBikOuChFvTE1jEuz51cTqk7dhemLw1A4DYaJjBwsemNhzS9oDiVWr+bqx/Ld9pUVO5Q==} + dev: true + /@types/hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-YIQtIg4PKr7ZyqNPZObpxfHsHEmuB8dXCxd6qVcGuQVDK2bpsF7bYNnBJ4Nn7giuACZg+WewExgrtAJ3XnA4Xw==} dependencies: @@ -9346,6 +9434,10 @@ packages: '@types/sizzle': 2.3.3 dev: true + /@types/js-yaml@4.0.9: + resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} + dev: true + /@types/jsdom@20.0.1: resolution: {integrity: sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==} dependencies: @@ -10527,7 +10619,7 @@ packages: resolution: {integrity: sha512-aiATs7pSutzda/rq8fnuPwTglyVwjM22bNnK2ZgjrpAjQHSSl3lztd2f9evst1W/qnC58DRz7T7QndUDumAR4Q==} engines: {node: '>=14.15.0'} dependencies: - js-yaml: 3.14.1 + js-yaml: 3.14.0 tslib: 2.6.3 dev: true @@ -11148,6 +11240,10 @@ packages: shimmer: 1.2.1 dev: false + /async@1.5.2: + resolution: {integrity: sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==} + dev: false + /async@2.6.4: resolution: {integrity: sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==} dependencies: @@ -13133,6 +13229,7 @@ packages: /ejs@3.1.10: resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} engines: {node: '>=0.10.0'} + hasBin: true dependencies: jake: 10.8.5 @@ -15365,6 +15462,13 @@ packages: function-bind: 1.1.2 dev: true + /hasbin@1.2.3: + resolution: {integrity: sha512-CCd8e/w2w28G8DyZvKgiHnQJ/5XXDz6qiUHnthvtag/6T5acUeN5lqq+HMoBqcmgWueWDhiCplrw0Kb1zDACRg==} + engines: {node: '>=0.10'} + dependencies: + async: 1.5.2 + dev: false + /hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} @@ -15686,7 +15790,7 @@ packages: /i18next@21.10.0: resolution: {integrity: sha512-YeuIBmFsGjUfO3qBmMOc0rQaun4mIpGKET5WDwvu8lU7gvwpcariZLNtL0Fzj+zazcHUrlXHiptcFhBMFaxzfg==} dependencies: - '@babel/runtime': 7.23.2 + '@babel/runtime': 7.25.0 /i18next@21.6.11: resolution: {integrity: sha512-tJ2+o0lVO+fhi8bPkCpBAeY1SgkqmQm5NzgPWCQssBrywJw98/o+Kombhty5nxQOpHtvMmsxcOopczUiH6bJxQ==} @@ -16994,14 +17098,16 @@ packages: /js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - /js-yaml@3.14.1: - resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + /js-yaml@3.14.0: + resolution: {integrity: sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==} + hasBin: true dependencies: argparse: 1.0.10 esprima: 4.0.1 /js-yaml@4.1.0: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true dependencies: argparse: 2.0.1 @@ -17388,7 +17494,7 @@ packages: engines: {node: '>=6'} dependencies: graceful-fs: 4.2.11 - js-yaml: 3.14.1 + js-yaml: 3.14.0 pify: 4.0.1 strip-bom: 3.0.0 @@ -18130,6 +18236,10 @@ packages: /ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + /mta-local@1.0.0: + resolution: {integrity: sha512-+pL8q5wJMyyVUj0gGFwceq7utIjwt744GBmBqYMARK6I4CzPvSv/0akRNTOxcsffXsu8EVyH2NJZ4XS9WcHzZA==} + dev: false + /multimatch@4.0.0: resolution: {integrity: sha512-lDmx79y1z6i7RNx0ZGCPq1bzJ6ZoDDKbvh7jxr9SJcWLkShMzXrHbYVpTdnhNM5MXpDUxCQ4DgqVttVXlBgiBQ==} engines: {node: '>=8'} @@ -18709,7 +18819,7 @@ packages: enquirer: 2.3.6 figures: 3.2.0 flat: 5.0.2 - fs-extra: 11.2.0 + fs-extra: 11.1.1 ignore: 5.3.2 jest-diff: 29.7.0 js-yaml: 4.1.0 @@ -20382,7 +20492,7 @@ packages: engines: {node: '>=6'} dependencies: graceful-fs: 4.2.11 - js-yaml: 3.14.1 + js-yaml: 3.14.0 pify: 4.0.1 strip-bom: 3.0.0 dev: true @@ -21931,7 +22041,6 @@ packages: /temp-dir@2.0.0: resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==} engines: {node: '>=8'} - dev: true /temp-dir@3.0.0: resolution: {integrity: sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw==} @@ -22189,7 +22298,7 @@ packages: json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 - semver: 7.6.3 + semver: 7.5.4 typescript: 5.6.2 yargs-parser: 21.1.1 dev: true diff --git a/sonar-project.properties b/sonar-project.properties index b251f52ceb..613ace3648 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -6,7 +6,8 @@ sonar.exclusions=**/*.test.ts, **/*.test.tsx, **/*.story.tsx, **/examples/**/*, sonar.cpd.exclusions=**/i18n.ts, **/fiori-elements-writer/src/packageConfig.ts, **/fiori-freestyle-writer/src/packageConfig.ts, **/ui5-info/src/ui5-version-fallback.ts, **/odata-vocabularies/src/resources/*, **/ui5-library-reference-inquirer/src/prompts/helpers.ts sonar.tests=. sonar.test.inclusions=**/*.test.ts, **/*.test.tsx -sonar.javascript.lcov.reportPaths=packages/abap-deploy-config-inquirer/coverage/lcov.info, \ +sonar.javascript.lcov.reportPaths=packages/cf-deploy-config-writer/coverage/lcov.info, \ + packages/abap-deploy-config-inquirer/coverage/lcov.info, \ packages/abap-deploy-config-writer/coverage/lcov.info, \ packages/adp-tooling/coverage/lcov.info, \ packages/annotation-generator/coverage/lcov.info, \ diff --git a/tsconfig.json b/tsconfig.json index d6be5dde09..a176a645e9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -65,6 +65,9 @@ { "path": "packages/cf-deploy-config-inquirer" }, + { + "path": "packages/cf-deploy-config-writer" + }, { "path": "packages/control-property-editor-common" },