diff --git a/.changeset/soft-carrots-brush.md b/.changeset/soft-carrots-brush.md new file mode 100644 index 0000000000..79207e43e8 --- /dev/null +++ b/.changeset/soft-carrots-brush.md @@ -0,0 +1,5 @@ +--- +'@sap-ux/cf-deploy-config-writer': minor +--- + +append new logic to create CAP mta diff --git a/packages/cf-deploy-config-writer/README.md b/packages/cf-deploy-config-writer/README.md index fd087ae72e..351f9f1785 100644 --- a/packages/cf-deploy-config-writer/README.md +++ b/packages/cf-deploy-config-writer/README.md @@ -16,8 +16,13 @@ Yarn Pnpm `pnpm add @sap-ux/cf-deploy-config-writer` -## Usage -Calling the `MtaConfig` library to add different types of modules, for example HTML5 resources, routing module 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. +## Example: Reading and writing MTA Configurations +Generate an `MtaConfig` instance to read an `mta.yaml` configuration to allow you append different resources and modules. For example HTML5 resources, routing modules or append mta extension configurations. + +Dependent on the [MTA Tool](https://www.npmjs.com/package/mta) being installed globally on your dev space. +- For VSCode, run the command `npm i -g mta` +- For Business Application Studio, the binary is already installed + ```Typescript import { MtaConfig } from '@sap-ux/cf-deploy-config-writer'; // Create a new instance of MtaConfig @@ -38,17 +43,41 @@ await mtaConfig.addMtaExtensionConfig('mynewdestination', 'https://my-service-ur await mtaConfig.save(); ``` +## Example: Appending a Managed Approuter Configuration to an SAP Fiori UI5 Application + Calling the `generateAppConfig` function to append Cloud Foundry configuration to a HTML5 application, assumes `manifest.json` and `ui5.yaml` configurations are present otherwise the process will exit with an error; ```Typescript import { generateAppConfig, DefaultMTADestination } from '@sap-ux/cf-deploy-config-writer' import { join } from 'path'; const exampleWriter = async () => { - const appPath = join(__dirname, 'testapp'); + const ui5AppPath = join(__dirname, 'testapp'); // Option 1. Append managed approuter configuration, toggle `addManagedAppRouter` to false to ommit the managed approuter configuration being appended to the mta.yaml - const fs = await generateAppConfig({appPath, addManagedAppRouter: true, destinationName: 'SAPBTPDestination'}); + const fs = await generateAppConfig({appPath: ui5AppPath, addManagedAppRouter: true, destinationName: 'SAPBTPDestination'}); + return new Promise((resolve) => { + fs.commit(resolve); // When using with Yeoman it handle the fs commit. + }); +} +// Calling the function +await exampleWriter(); +``` + +## Example: Generate or Append a `Managed` Approuter Configuration to a SAP Fiori UI5 Application + +Calling the `generateAppConfig` function to append Cloud Foundry configuration to a HTML5 application; +- Assumes `manifest.json` and `ui5.yaml` configurations are present in the target folder +- Supports `CAP` projects where an existing `mta.yaml` is already present and you are adding a SAP Fiori UI5 app to it + +```Typescript +import { generateAppConfig, DefaultMTADestination } from '@sap-ux/cf-deploy-config-writer' +import { join } from 'path'; + +const exampleWriter = async () => { + const ui5AppPath = join(__dirname, 'testapp'); + // Option 1. Append managed approuter configuration, toggle `addManagedAppRouter` to false to ommit the managed approuter configuration being appended to the mta.yaml + const fs = await generateAppConfig({appPath: ui5AppPath, addManagedAppRouter: true, destinationName: 'SAPBTPDestination'}); // Option 2. For CAP flows, set the destination to DefaultMTADestination to create a destination instance between the HTML5 app and CAP Project - const fs = await generateAppConfig({appPath, addManagedAppRouter: true, destinationName: DefaultMTADestination}); + const fs = await generateAppConfig({appPath: ui5AppPath, addManagedAppRouter: true, destinationName: DefaultMTADestination}); return new Promise((resolve) => { fs.commit(resolve); // When using with Yeoman it handle the fs commit. }); @@ -57,15 +86,43 @@ const exampleWriter = async () => { await exampleWriter(); ``` +## Example: Generate a Base `Managed` | `Standalone` Approuter Configuration + Calling the `generateBaseConfig` function to generate a `new` Cloud Foundry configuration, supporting `managed` | `standalone` configurations; +- Creates a new `mta.yaml` into the specified `mtaPath`, it will fail if an existing `mta.yaml` is found +- Optional parameters include adding a `connectivity` service if the SAP BTP destination is using an `OnPremise` configuration +- New configuration will include a destination instance to expose a `UI5` endpoint, consumed by SAP Fiori applications when deployed to Cloud Foundry ```Typescript import { generateBaseConfig, RouterModuleType } from '@sap-ux/cf-deploy-config-writer' import { join } from 'path'; const exampleWriter = async () => { - const mtaPath = join(__dirname, 'testapp'); + const mtaPath = join(__dirname, 'testproject'); + // If your SAPUI5 application will be consuming an SAB BTP OnPremise destination, Connectivity serivce is required; Refer to https://discovery-center.cloud.sap/serviceCatalog/connectivity-service?region=all + const addConnectivityService = true; // Generate a managed approuter configuration, toggle the routerType to RouterModuleType.Standard for a standalone configuration - const fs = await generateBaseConfig({ mtaId: 'myapp', routerType: RouterModuleType.Managed, mtaPath }); + const fs = await generateBaseConfig({ mtaId: 'mymtaproject', routerType: RouterModuleType.Managed, mtaPath, addConnectivityService }); + return new Promise((resolve) => { + fs.commit(resolve); // When using with Yeoman it handle the fs commit. + }); +} +// Calling the function +await exampleWriter(); +``` + +## Example: Generate a CAP `Managed` | `Standalone` Approuter Configuration +Calling the `generateCAPConfig` function to generate a `new` Cloud Foundry configuration, supporting `managed` | `standalone` configurations; +- Generate a CAP `mta.yaml` with `destination`, `HTML5-Repo` and `XSUAA` services added by default +- New configuration will include destination instances to expose `UI5` and `CAP` endpoints, consumed by SAP Fiori applications when deployed to Cloud Foundry + +```Typescript +import { generateCAPConfig, RouterModuleType } from '@sap-ux/cf-deploy-config-writer' +import { join } from 'path'; + +const exampleWriter = async () => { + const mtaPath = join(__dirname, 'testcapproject'); + // Generate a managed approuter configuration, toggle the routerType to RouterModuleType.Standard or RouterModuleType.Managed + const fs = await generateCAPConfig({ mtaId: 'mymtaproject', routerType: RouterModuleType.Managed, mtaPath }); return new Promise((resolve) => { fs.commit(resolve); // When using with Yeoman it handle the fs commit. }); @@ -81,4 +138,6 @@ SAP Deployment Cloud Foundry MTA Multi-Target Application +CAP +CDS 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 index 9e7a3f872d..9340ce05a8 100644 --- a/packages/cf-deploy-config-writer/src/cf-writer/app-config.ts +++ b/packages/cf-deploy-config-writer/src/cf-writer/app-config.ts @@ -1,8 +1,6 @@ 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 { type FioriToolsProxyConfig, type UI5Config, UI5Config as UI5ConfigInstance } from '@sap-ux/ui5-config'; import { addPackageDevDependency, @@ -16,16 +14,11 @@ import { import { Authentication } from '@sap-ux/btp-utils'; import { appDeployMTAScript, - CDSAddMtaParams, - CDSBinNotFound, - CDSExecutable, DefaultMTADestination, EmptyDestination, MbtPackage, MbtPackageVersion, - MTABinNotFound, MTABuildScript, - MTAExecutable, MTAFileExtension, ResourceMTADestination, Rimraf, @@ -33,14 +26,12 @@ import { rootDeployMTAScript, UI5DeployBuildScript, undeployMTAScript, - XSAppFile, - XSSecurityFile + WelcomeFile, + XSAppFile } from '../constants'; import { addCommonPackageDependencies, - addGitIgnore, - addRootPackage, - addXSSecurityConfig, + generateSupportingConfig, getDestinationProperties, getTemplatePath, readManifest, @@ -48,7 +39,10 @@ import { } from '../utils'; import { addMtaDeployParameters, + createCAPMTA, createMTA, + doesCDSBinaryExist, + doesMTABinaryExist, getMtaConfig, getMtaId, type MtaConfig, @@ -57,7 +51,7 @@ import { import LoggerHelper from '../logger-helper'; import { t } from '../i18n'; import { type Logger } from '@sap-ux/logger'; -import { ApiHubType, type CFAppConfig, type CFConfig, type MTABaseConfig } from '../types'; +import { type XSAppDocument, ApiHubType, type CFAppConfig, type CFConfig, type MTABaseConfig } from '../types'; /** * Add a managed approuter configuration to an existing HTML5 application. @@ -74,22 +68,11 @@ export async function generateAppConfig(cfAppConfig: CFAppConfig, fs?: Editor, l if (logger) { LoggerHelper.logger = logger; } - validateMtaConfig(); + doesMTABinaryExist(); 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. * @@ -105,6 +88,11 @@ async function getUpdatedConfig(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); + const config = await getUpdatedConfig(cfAppConfig, fs); + + LoggerHelper?.logger?.debug(`Generate app configuration using: \n ${JSON.stringify(config)}`); + + // Generate MTA Config, LCAP will generate the mta.yaml on the fly so we don't care about it! + if (!config.lcapMode) { + generateMTAFile(config); + await generateSupportingConfig(config, fs); + await updateMtaConfig(config, fs); } // Generate HTML5 config - await appendCloudFoundryConfigurations(cfConfig, fs); - await updateManifest(cfConfig, fs); - await updateHTML5AppPackage(cfConfig, fs); - await updateRootPackage(cfConfig, fs); + await appendCloudFoundryConfigurations(config, fs); + await updateManifest(config, fs); + await updateHTML5AppPackage(config, fs); + await updateRootPackage(config, fs); } /** @@ -251,15 +238,10 @@ async function generateDeployConfig(cfAppConfig: CFAppConfig, fs: Editor): Promi * * @param cfConfig writer configuration */ -function createMTAConfig(cfConfig: CFConfig): void { +export function generateMTAFile(cfConfig: CFConfig): void { if (!cfConfig.mtaId) { if (cfConfig.isCap) { - const result = spawnSync(CDSExecutable, CDSAddMtaParams, { - cwd: cfConfig.rootPath - }); - if (result.error) { - throw new Error(CDSBinNotFound); - } + createCAPMTA(cfConfig.rootPath); } else { createMTA({ mtaId: cfConfig.appId, mtaPath: cfConfig.mtaPath ?? cfConfig.rootPath } as MTABaseConfig); } @@ -268,34 +250,13 @@ function createMTAConfig(cfConfig: CFConfig): void { } } -/** - * 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 + * @param fs reference to a mem-fs editor */ -async function updateMtaConfig(cfConfig: CFConfig): Promise { +async function updateMtaConfig(cfConfig: CFConfig, fs: Editor): Promise { const mtaInstance = await getMtaConfig(cfConfig.rootPath); if (mtaInstance) { await mtaInstance.addRoutingModules({ isManagedApp: cfConfig.addManagedAppRouter }); @@ -315,11 +276,37 @@ async function updateMtaConfig(cfConfig: CFConfig): Promise { cfConfig.destinationAuthentication = Authentication.NO_AUTHENTICATION; } } + cleanupStandaloneRoutes(cfConfig, mtaInstance, fs); await saveMta(cfConfig, mtaInstance); cfConfig.cloudServiceName = mtaInstance.cloudServiceName; } } +/** + * + * @param root0 + * @param root0.rootPath + * @param root0.appId + * @param mtaInstance + * @param fs + */ +function cleanupStandaloneRoutes({ rootPath, appId }: CFConfig, mtaInstance: MtaConfig, fs: Editor): void { + // Cleanup standalone xs-app.json to reflect new application + const appRouterPath = mtaInstance.standaloneRouterPath; + if (appRouterPath) { + try { + const xsAppPath = join(appRouterPath, XSAppFile); + const appRouterXsAppObj = fs.readJSON(join(rootPath, xsAppPath)) as unknown as XSAppDocument; + if ((appRouterXsAppObj && !appRouterXsAppObj?.[WelcomeFile]) || appRouterXsAppObj?.[WelcomeFile] === '/') { + appRouterXsAppObj[WelcomeFile] = `/${appId}`; + fs.writeJSON(join(rootPath, xsAppPath), appRouterXsAppObj); + } + } catch (error) { + LoggerHelper.logger?.error(t('error.cannotUpdateRouterXSApp', { error })); + } + } +} + /** * Apply changes to mta.yaml. * @@ -420,7 +407,7 @@ async function updateHTML5AppPackage(cfConfig: CFConfig, fs: Editor): Promise { - const packageExists = fs.exists(join(cfConfig.rootPath, 'package.json')); + const packageExists = fs.exists(join(cfConfig.rootPath, FileName.Package)); // Append mta scripts only if mta.yaml is at a different level to the HTML5 app if (cfConfig.isMtaRoot && packageExists) { await addPackageDevDependency(cfConfig.rootPath, Rimraf, RimrafVersion, fs); 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 index b69ae55e74..3145d718c3 100644 --- a/packages/cf-deploy-config-writer/src/cf-writer/base-config.ts +++ b/packages/cf-deploy-config-writer/src/cf-writer/base-config.ts @@ -1,22 +1,12 @@ -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 { addSupportingConfig, addRoutingConfig } from '../utils'; import LoggerHelper from '../logger-helper'; -import { t } from '../i18n'; -import { MtaConfig, createMTA, addMtaDeployParameters } from '../mta-config'; +import { createMTA, validateMtaConfig } from '../mta-config'; import { type Logger } from '@sap-ux/logger'; -import { type CFBaseConfig, RouterModuleType, type MTABaseConfig } from '../types'; +import { type CFBaseConfig, type MTABaseConfig } from '../types'; +import { join } from 'path'; +import { t } from '../i18n'; /** * Add a standalone | managed approuter to an empty target folder. @@ -33,132 +23,14 @@ export async function generateBaseConfig(config: CFBaseConfig, fs?: Editor, logg if (logger) { LoggerHelper.logger = logger; } - - logger?.debug(`Generate configuration using: \n ${JSON.stringify(config)}`); - - validateMtaConfig(config, fs); - updateBaseConfig(config); + logger?.debug(`Generate base configuration using: \n ${JSON.stringify(config)}`); + validateMtaConfig(config); + if (fs.exists(join(config.mtaPath, config.mtaId))) { + throw new Error(t('error.mtaFolderAlreadyExists')); + } 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({ isManagedApp: true, addMissingModules: false }); - } - await addMtaDeployParameters(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/cap-config.ts b/packages/cf-deploy-config-writer/src/cf-writer/cap-config.ts new file mode 100644 index 0000000000..0a5b5abe1b --- /dev/null +++ b/packages/cf-deploy-config-writer/src/cf-writer/cap-config.ts @@ -0,0 +1,52 @@ +import { create as createStorage } from 'mem-fs'; +import { create, type Editor } from 'mem-fs-editor'; +import { addSupportingConfig, addRoutingConfig } from '../utils'; +import { createCAPMTA, validateMtaConfig, isMTAFound } from '../mta-config'; +import LoggerHelper from '../logger-helper'; +import type { Logger } from '@sap-ux/logger'; +import type { CAPConfig, CFBaseConfig } from '../types'; +import { CDSDestinationService, CDSHTML5RepoService, CDSXSUAAService } from '../constants'; +import { t } from '../i18n'; +import { getCapProjectType } from '@sap-ux/project-access'; + +/** + * Add a standalone | managed approuter to a CAP project. + * + * @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 generateCAPConfig(config: CAPConfig, fs?: Editor, logger?: Logger): Promise { + if (!fs) { + fs = create(createStorage()); + } + if (logger) { + LoggerHelper.logger = logger; + } + logger?.debug(`Generate CAP configuration using: \n ${JSON.stringify(config)}`); + await validateConfig(config); + const cdsOptionalParams: string[] = [CDSXSUAAService, CDSDestinationService, CDSHTML5RepoService]; + createCAPMTA(config.mtaPath, cdsOptionalParams); + await addRoutingConfig(config, fs); + addSupportingConfig(config, fs); + LoggerHelper.logger?.debug(`CF CAP Config ${JSON.stringify(config, null, 2)}`); + return fs; +} + +/** + * Ensure the configuration is valid, target folder exists and is a CAP Node.js app and mta.yaml does not already exist. + * + * @param config writer configuration + */ +async function validateConfig(config: CAPConfig): Promise { + validateMtaConfig(config as CFBaseConfig); + // Check if the target directory contains a CAP Node.js project or exists! + if ((await getCapProjectType(config.mtaPath)) !== 'CAPNodejs') { + throw new Error(t('error.doesNotContainACapApp')); + } + // Check if the target directory contains an existing mta.yaml + if (isMTAFound(config.mtaPath)) { + 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 index 82ffb7df67..bb7188ba21 100644 --- a/packages/cf-deploy-config-writer/src/cf-writer/index.ts +++ b/packages/cf-deploy-config-writer/src/cf-writer/index.ts @@ -1,2 +1,3 @@ export { generateAppConfig } from './app-config'; export { generateBaseConfig } from './base-config'; +export { generateCAPConfig } from './cap-config'; diff --git a/packages/cf-deploy-config-writer/src/constants.ts b/packages/cf-deploy-config-writer/src/constants.ts index fdbc26b412..a5fb565b93 100644 --- a/packages/cf-deploy-config-writer/src/constants.ts +++ b/packages/cf-deploy-config-writer/src/constants.ts @@ -9,6 +9,7 @@ export const MTABuildParams = 'build-parameters'; export const MTAFileExtension = 'mta-ext.mtaext'; export const DefaultServiceURL = '${default-url}'; export const ManagedXSUAA = 'managed:xsuaa'; +export const HTML5RepoHost = 'html5-apps-repo:app-host'; export const SRV_API = 'srv-api'; export const DefaultMTADestination = 'fiori-default-srv-api'; export const EmptyDestination = 'NONE'; @@ -36,6 +37,10 @@ export const UI5TaskZipperPackageVersion = '^3.1.3'; export const UI5Package = '@ui5/cli'; export const UI5PackageVersion = '^3.9.2'; export const CDSAddMtaParams = ['add', 'mta']; +export const CDSXSUAAService = 'xsuaa'; +export const CDSHTML5RepoService = 'html5-repo'; +export const CDSConnectivityService = 'connectivity'; +export const CDSDestinationService = 'destination'; export const MTAAPIDestination = { Name: ResourceMTADestination, Type: 'HTTP', diff --git a/packages/cf-deploy-config-writer/src/mta-config/index.ts b/packages/cf-deploy-config-writer/src/mta-config/index.ts index 03fd46f1d5..49c380da75 100644 --- a/packages/cf-deploy-config-writer/src/mta-config/index.ts +++ b/packages/cf-deploy-config-writer/src/mta-config/index.ts @@ -2,11 +2,25 @@ 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 { getTemplatePath, setMtaDefaults, validateVersion } from '../utils'; +import { + MTAYamlFile, + MTAVersion, + MTADescription, + deployMode, + enableParallelDeployments, + CDSAddMtaParams, + CDSBinNotFound, + CDSExecutable, + MTABinNotFound, + MTAExecutable +} from '../constants'; import type { mta } from '@sap/mta-lib'; -import type { MTABaseConfig } from '../types'; +import { type MTABaseConfig, type CFBaseConfig } from '../types'; import LoggerHelper from '../logger-helper'; +import { sync } from 'hasbin'; +import { spawnSync } from 'child_process'; +import { t } from '../i18n'; /** * Get the MTA ID, read from the root path specified. @@ -93,4 +107,76 @@ export async function addMtaDeployParameters(mtaInstance: MtaConfig): Promise 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')); + } + + setMtaDefaults(config); +} + 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 index 8f32388d31..763c8309c4 100644 --- a/packages/cf-deploy-config-writer/src/mta-config/mta.ts +++ b/packages/cf-deploy-config-writer/src/mta-config/mta.ts @@ -23,7 +23,8 @@ import { MTAAPIDestination, UI5StandaloneModuleDestination, ServiceAPIRequires, - HTMLAppBuildParams + HTMLAppBuildParams, + HTML5RepoHost } from '../constants'; import { t } from '../i18n'; import type { Logger } from '@sap-ux/logger'; @@ -115,7 +116,7 @@ export class MtaConfig { if (resource.parameters?.service === 'html5-apps-repo') { this.resources.set( resource.parameters['service-plan'] === 'app-host' - ? 'html5-apps-repo:app-host' + ? HTML5RepoHost : 'html5-apps-repo:app-runtime', resource ); @@ -139,7 +140,7 @@ export class MtaConfig { 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')) { + } else if (this.targetExists(module.requires ?? [], HTML5RepoHost)) { this.modules.set('com.sap.application.content:resource', module); } else { this.modules.set(module.type as ModuleType, module); // i.e. 'approuter.nodejs' @@ -150,11 +151,11 @@ export class MtaConfig { } private async addAppContent(): Promise { - if (!this.resources.has('html5-apps-repo:app-host')) { + if (!this.resources.has(HTML5RepoHost)) { 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; + const appHostName = this.resources.get(HTML5RepoHost)?.name; if (appHostName) { const deployer: mta.Module = { name: `${this.prefix.slice(0, 100)}-app-content`, @@ -205,6 +206,24 @@ export class MtaConfig { this.dirty = true; } + /** + * + * @param serviceName + * @param resourceName + */ + private async updateServiceName(serviceName: string, resourceName: string): Promise { + const resource = this.resources.get(resourceName); + if (resource && !resource.parameters?.['service-name']) { + resource.parameters = { + ...(resource.parameters ?? {}), + 'service-name': `${this.prefix.slice(0, 100)}-${serviceName}-service` + }; + await this.mta.updateResource(resource); + this.resources.set(resourceName, resource); + this.dirty = true; + } + } + private async addHtml5Host(): Promise { const html5host = `${this.prefix.slice(0, 100)}-repo-host`; // Need to cater for -key being added too! const resource: mta.Resource = { @@ -217,7 +236,7 @@ export class MtaConfig { } }; await this.mta.addResource(resource); - this.resources.set('html5-apps-repo:app-host', resource); + this.resources.set(HTML5RepoHost, resource); this.dirty = true; } @@ -525,7 +544,7 @@ export class MtaConfig { isManagedApp?: boolean; addMissingModules?: boolean; } = {}): Promise { - if (isManagedApp && !this.modules.has('com.sap.application.content:destination')) { + if (isManagedApp) { await this.addManagedAppRouter(); } @@ -822,72 +841,79 @@ export class MtaConfig { if (!this.resources.has(ManagedXSUAA)) { await this.addManagedUaa(); } - if (!this.resources.has('html5-apps-repo:app-host')) { + if (!this.resources.has(HTML5RepoHost)) { 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, 100)}-destination-content`, - type: 'com.sap.application.content', - requires: [ - { - name: destinationName, - parameters: { - 'content-target': true - } - }, - { - name: appHostName, - parameters: { - 'service-key': { - name: `${appHostName}-key` + // Assume these need to be updated for safety! + await this.updateServiceName('html5', HTML5RepoHost); + await this.updateServiceName('xsuaa', ManagedXSUAA); + + // We only want to append a new one, if missing from the existing mta config + if (!this.modules.has('com.sap.application.content:destination')) { + const destinationName = this.resources.get('destination')?.name; + const appHostName = this.resources.get(HTML5RepoHost)?.name; + const appHostServiceName = this.resources.get(HTML5RepoHost)?.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, 100)}-destination-content`, + type: 'com.sap.application.content', + requires: [ + { + name: destinationName, + parameters: { + 'content-target': true } - } - }, - { - name: managedXSUAAName, - parameters: { - 'service-key': { - name: `${managedXSUAAName}-key` + }, + { + name: appHostName, + parameters: { + 'service-key': { + name: `${appHostName}-key` + } } - } - } - ], - parameters: { - content: { - instance: { - destinations: [ - { - Name: `${this.prefix.slice(0, 100)}_html_repo_host`, - ServiceInstanceName: appHostServiceName, - ServiceKeyName: `${appHostName}-key`, - 'sap.cloud.service': `${this.prefix.slice(0, 100)}` - }, - { - Authentication: 'OAuth2UserTokenExchange', - Name: `${this.prefix.slice(0, 100)}_uaa`, - ServiceInstanceName: managedXSUAAServiceName, - ServiceKeyName: `${managedXSUAAName}-key`, - 'sap.cloud.service': `${this.prefix.slice(0, 100)}` + }, + { + name: managedXSUAAName, + parameters: { + 'service-key': { + name: `${managedXSUAAName}-key` } - ], - 'existing_destinations_policy': 'update' + } } + ], + parameters: { + content: { + instance: { + destinations: [ + { + Name: `${this.prefix.slice(0, 100)}_html_repo_host`, + ServiceInstanceName: appHostServiceName, + ServiceKeyName: `${appHostName}-key`, + 'sap.cloud.service': `${this.prefix.slice(0, 100)}` + }, + { + Authentication: 'OAuth2UserTokenExchange', + Name: `${this.prefix.slice(0, 100)}_uaa`, + ServiceInstanceName: managedXSUAAServiceName, + ServiceKeyName: `${managedXSUAAName}-key`, + 'sap.cloud.service': `${this.prefix.slice(0, 100)}` + } + ], + 'existing_destinations_policy': 'update' + } + } + }, + 'build-parameters': { + 'no-source': true } - }, - 'build-parameters': { - 'no-source': true - } - }; - await this.mta.addModule(router); - this.modules.set('com.sap.application.content:destination', router); - this.dirty = true; + }; + await this.mta.addModule(router); + this.modules.set('com.sap.application.content:destination', router); + this.dirty = true; + } } } 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 index c086be3905..ae2d32e90f 100644 --- 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 @@ -5,21 +5,25 @@ "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" + "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.", + "mtaFolderAlreadyExists": "A folder with same name already exists in the target directory.", + "mtaAlreadyExists": "An mta.yaml already exists in the target directory.", + "cannotUpdateRouterXSApp": "Unable to update router xs-app.json welcome location.", + "targetFolderDoesNotExist": "Target folder does not exist, {{targetFolder}}.", + "doesNotContainACapApp": "Target folder does not contain a Node.js CAP project." }, "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" + "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 index 55a887a26e..0392348f02 100644 --- a/packages/cf-deploy-config-writer/src/types/index.ts +++ b/packages/cf-deploy-config-writer/src/types/index.ts @@ -67,6 +67,7 @@ export interface CFConfig extends CFAppConfig, CFBaseConfig { firstServicePathSegment?: string; isMtaRoot?: boolean; } +export interface CAPConfig extends Omit {} export const enum ApiHubType { apiHub = 'API_HUB', apiHubEnterprise = 'API_HUB_ENTERPRISE' diff --git a/packages/cf-deploy-config-writer/src/utils.ts b/packages/cf-deploy-config-writer/src/utils.ts index 9290dc4ac2..afa670dc52 100644 --- a/packages/cf-deploy-config-writer/src/utils.ts +++ b/packages/cf-deploy-config-writer/src/utils.ts @@ -7,7 +7,7 @@ import { type Authentication, type Destinations } from '@sap-ux/btp-utils'; -import { addPackageDevDependency, type Manifest } from '@sap-ux/project-access'; +import { addPackageDevDependency, FileName, type Manifest } from '@sap-ux/project-access'; import { MTAVersion, UI5BuilderWebIdePackage, @@ -16,10 +16,16 @@ import { UI5PackageVersion, UI5TaskZipperPackage, UI5TaskZipperPackageVersion, - XSSecurityFile + XSSecurityFile, + RouterModule, + XSAppFile } from './constants'; import type { Editor } from 'mem-fs-editor'; -import type { MTABaseConfig } from './types'; +import { type MTABaseConfig, type CFConfig, type CFBaseConfig, RouterModuleType } from './types'; +import { getMtaId, MtaConfig, addMtaDeployParameters } from './mta-config'; +import { apiGetInstanceCredentials } from '@sap/cf-tools'; +import LoggerHelper from './logger-helper'; +import { t } from './i18n'; let cachedDestinationsList: Destinations = {}; @@ -41,7 +47,7 @@ export async function readManifest(manifestPath: string, fs: Editor): Promise { + const mtaId: string = config.mtaId ?? (await getMtaId(config.rootPath)); + // Add specific MTA ID configurations + const mtaConfig = { mtaId, 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); + } +} + +/** + * Add supporting configuration to the target folder. + * + * @param config writer configuration + * @param fs reference to a mem-fs editor + */ +export function addSupportingConfig(config: MTABaseConfig, fs: Editor): void { + addRootPackage(config, fs); + addGitIgnore(config.mtaPath, fs); + addXSSecurityConfig(config, fs); +} + +/** + * 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}/${FileName.Package}`)); + + 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 standalone or managed approuter to the target folder. + * + * @param config writer configuration + * @param fs reference to a mem-fs editor + */ +export 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({ isManagedApp: true, addMissingModules: false }); + } + await addMtaDeployParameters(mtaConfigInstance); + await mtaConfigInstance.save(); +} + +/** + * Update the writer configuration with defaults. + * + * @param config writer configuration + */ +export function setMtaDefaults(config: CFBaseConfig): void { + config.mtaPath = config.mtaPath.replace(/\/$/, ''); + config.addConnectivityService ||= false; + config.mtaId = toMtaModuleName(config.mtaId); +} diff --git a/packages/cf-deploy-config-writer/test/sample/capcds/.gitignore b/packages/cf-deploy-config-writer/test/sample/capcds/.gitignore new file mode 100644 index 0000000000..1f6d632351 --- /dev/null +++ b/packages/cf-deploy-config-writer/test/sample/capcds/.gitignore @@ -0,0 +1,34 @@ +# CAP captestproject +_out +*.db +*.sqlite +connection.properties +default-*.json +.cdsrc-private.json +gen/ +node_modules/ +target/ + +# Web IDE, App Studio +.che/ +.gen/ + +# MTA +*_mta_build_tmp +*.mtar +mta_archives/ + +# Other +.DS_Store +*.orig +*.log + +*.iml +*.flattened-pom.xml + +# IDEs +# .vscode +# .idea + +# @cap-js/cds-typer +@cds-models diff --git a/packages/cf-deploy-config-writer/test/sample/capcds/package.json b/packages/cf-deploy-config-writer/test/sample/capcds/package.json new file mode 100644 index 0000000000..bcdb9ea088 --- /dev/null +++ b/packages/cf-deploy-config-writer/test/sample/capcds/package.json @@ -0,0 +1,29 @@ +{ + "name": "captestproject", + "version": "1.0.0", + "description": "A simple CAP project.", + "repository": "", + "license": "UNLICENSED", + "private": true, + "dependencies": { + "@sap/cds": "^8", + "express": "^4", + "@sap/xssec": "^4" + }, + "devDependencies": { + "@cap-js/cds-types": "^0.8.0", + "@cap-js/sqlite": "^1", + "@sap/cds-dk": "^8" + }, + "scripts": { + "start": "cds-serve" + }, + "cds": { + "requires": { + "auth": "xsuaa", + "connectivity": true, + "destinations": true, + "html5-repo": true + } + } +} diff --git a/packages/cf-deploy-config-writer/test/sample/capcds/xs-security.json b/packages/cf-deploy-config-writer/test/sample/capcds/xs-security.json new file mode 100644 index 0000000000..e1d3992300 --- /dev/null +++ b/packages/cf-deploy-config-writer/test/sample/capcds/xs-security.json @@ -0,0 +1,6 @@ +{ + "scopes": [], + "attributes": [], + "role-templates": [], + "authorities-inheritance": false +} diff --git a/packages/cf-deploy-config-writer/test/sample/standalone/.gitignore b/packages/cf-deploy-config-writer/test/sample/standalone/.gitignore new file mode 100644 index 0000000000..647906e4ca --- /dev/null +++ b/packages/cf-deploy-config-writer/test/sample/standalone/.gitignore @@ -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/test/sample/standalone/mta.yaml b/packages/cf-deploy-config-writer/test/sample/standalone/mta.yaml new file mode 100644 index 0000000000..c993a68f40 --- /dev/null +++ b/packages/cf-deploy-config-writer/test/sample/standalone/mta.yaml @@ -0,0 +1,60 @@ +_schema-version: "3.2" +ID: standalone +version: 0.0.1 +modules: +- name: standalone-router + type: approuter.nodejs + path: router + requires: + - name: standalone-html5-repo-runtime + - name: standalone-uaa + - name: standalone-destination-service + group: destinations + properties: + forwardAuthToken: false + name: ui5 + url: https://ui5.sap.com + - name: standalone-connectivity + parameters: + disk-quota: 256M + memory: 256M +resources: +- name: standalone-uaa + type: org.cloudfoundry.managed-service + parameters: + config: + tenant-mode: dedicated + xsappname: standalone-${space-guid} + service: xsuaa + service-plan: application +- name: standalone-html5-repo-runtime + type: org.cloudfoundry.managed-service + parameters: + service: html5-apps-repo + service-plan: app-runtime +- name: standalone-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: standalone-destination-service + service-plan: lite +- name: standalone-connectivity + type: org.cloudfoundry.managed-service + parameters: + service: connectivity + service-plan: lite +parameters: + deploy_mode: html5-repo + enable-parallel-deployments: true diff --git a/packages/cf-deploy-config-writer/test/sample/standalone/package.json b/packages/cf-deploy-config-writer/test/sample/standalone/package.json new file mode 100644 index 0000000000..c459a59e41 --- /dev/null +++ b/packages/cf-deploy-config-writer/test/sample/standalone/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 standalone --delete-services --delete-service-keys --delete-service-brokers" + }, + "devDependencies": { + "mbt": "^1.2.29", + "rimraf": "^5.0.5" + } +} diff --git a/packages/cf-deploy-config-writer/test/sample/standalone/router/package.json b/packages/cf-deploy-config-writer/test/sample/standalone/router/package.json new file mode 100644 index 0000000000..50f3e8353a --- /dev/null +++ b/packages/cf-deploy-config-writer/test/sample/standalone/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/test/sample/standalone/router/xs-app.json b/packages/cf-deploy-config-writer/test/sample/standalone/router/xs-app.json new file mode 100644 index 0000000000..4c7a8957dc --- /dev/null +++ b/packages/cf-deploy-config-writer/test/sample/standalone/router/xs-app.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/test/sample/standalone/xs-security.json b/packages/cf-deploy-config-writer/test/sample/standalone/xs-security.json new file mode 100644 index 0000000000..c31481ff7b --- /dev/null +++ b/packages/cf-deploy-config-writer/test/sample/standalone/xs-security.json @@ -0,0 +1,7 @@ +{ + "xsappname": "standalone", + "tenant-mode": "dedicated", + "description": "Security profile of called application", + "scopes": [], + "role-templates": [] +} diff --git a/packages/cf-deploy-config-writer/test/sample/standalonewithapp/.gitignore b/packages/cf-deploy-config-writer/test/sample/standalonewithapp/.gitignore new file mode 100644 index 0000000000..647906e4ca --- /dev/null +++ b/packages/cf-deploy-config-writer/test/sample/standalonewithapp/.gitignore @@ -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/test/sample/standalonewithapp/mta.yaml b/packages/cf-deploy-config-writer/test/sample/standalonewithapp/mta.yaml new file mode 100644 index 0000000000..c993a68f40 --- /dev/null +++ b/packages/cf-deploy-config-writer/test/sample/standalonewithapp/mta.yaml @@ -0,0 +1,60 @@ +_schema-version: "3.2" +ID: standalone +version: 0.0.1 +modules: +- name: standalone-router + type: approuter.nodejs + path: router + requires: + - name: standalone-html5-repo-runtime + - name: standalone-uaa + - name: standalone-destination-service + group: destinations + properties: + forwardAuthToken: false + name: ui5 + url: https://ui5.sap.com + - name: standalone-connectivity + parameters: + disk-quota: 256M + memory: 256M +resources: +- name: standalone-uaa + type: org.cloudfoundry.managed-service + parameters: + config: + tenant-mode: dedicated + xsappname: standalone-${space-guid} + service: xsuaa + service-plan: application +- name: standalone-html5-repo-runtime + type: org.cloudfoundry.managed-service + parameters: + service: html5-apps-repo + service-plan: app-runtime +- name: standalone-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: standalone-destination-service + service-plan: lite +- name: standalone-connectivity + type: org.cloudfoundry.managed-service + parameters: + service: connectivity + service-plan: lite +parameters: + deploy_mode: html5-repo + enable-parallel-deployments: true diff --git a/packages/cf-deploy-config-writer/test/sample/standalonewithapp/myapp/package.json b/packages/cf-deploy-config-writer/test/sample/standalonewithapp/myapp/package.json new file mode 100644 index 0000000000..0a21b9650f --- /dev/null +++ b/packages/cf-deploy-config-writer/test/sample/standalonewithapp/myapp/package.json @@ -0,0 +1,28 @@ +{ + "name": "myapp", + "version": "0.0.1", + "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#project8-display\"", + "start-local": "fiori run --config ./ui5-local.yaml --open \"test/flpSandbox.html?sap-ui-xx-viewCache=false#project8-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/standalonewithapp/myapp/ui5.yaml b/packages/cf-deploy-config-writer/test/sample/standalonewithapp/myapp/ui5.yaml new file mode 100644 index 0000000000..199b61ba08 --- /dev/null +++ b/packages/cf-deploy-config-writer/test/sample/standalonewithapp/myapp/ui5.yaml @@ -0,0 +1,28 @@ +# yaml-language-server: $schema=https://sap.github.io/ui5-tooling/schema/ui5.yaml.json + +specVersion: "3.1" +metadata: + name: myapp +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 + - name: fiori-tools-appreload + afterMiddleware: compression + configuration: + port: 35729 + path: webapp + delay: 300 + - name: fiori-tools-preview + afterMiddleware: fiori-tools-appreload + configuration: + component: myapp + ui5Theme: sap_horizon diff --git a/packages/cf-deploy-config-writer/test/sample/standalonewithapp/myapp/webapp/manifest.json b/packages/cf-deploy-config-writer/test/sample/standalonewithapp/myapp/webapp/manifest.json new file mode 100644 index 0000000000..ad87bc26a5 --- /dev/null +++ b/packages/cf-deploy-config-writer/test/sample/standalonewithapp/myapp/webapp/manifest.json @@ -0,0 +1,97 @@ +{ + "_version": "1.65.0", + "sap.app": { + "id": "myapp", + "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.16.3-pre-20250120124504-86cdda28f.0", + "toolsId": "0d780c67-3898-4e0f-aab3-44c95943a533" + } + }, + "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.132.0", + "libs": { + "sap.m": {}, + "sap.ui.core": {} + } + }, + "contentDensities": { + "compact": true, + "cozy": true + }, + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "settings": { + "bundleName": "myapp.i18n.i18n" + } + } + }, + "resources": { + "css": [ + { + "uri": "css/style.css" + } + ] + }, + "routing": { + "config": { + "routerClass": "sap.m.routing.Router", + "controlAggregation": "pages", + "controlId": "app", + "transition": "slide", + "type": "View", + "viewType": "XML", + "path": "myapp.view", + "async": true, + "viewPath": "myapp.view" + }, + "routes": [ + { + "name": "RouteView1", + "pattern": ":?query:", + "target": [ + "TargetView1" + ] + } + ], + "targets": { + "TargetView1": { + "id": "View1", + "name": "View1" + } + } + }, + "rootView": { + "viewName": "myapp.view.App", + "type": "XML", + "id": "App" + } + } +} \ No newline at end of file diff --git a/packages/cf-deploy-config-writer/test/sample/standalonewithapp/package.json b/packages/cf-deploy-config-writer/test/sample/standalonewithapp/package.json new file mode 100644 index 0000000000..c459a59e41 --- /dev/null +++ b/packages/cf-deploy-config-writer/test/sample/standalonewithapp/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 standalone --delete-services --delete-service-keys --delete-service-brokers" + }, + "devDependencies": { + "mbt": "^1.2.29", + "rimraf": "^5.0.5" + } +} diff --git a/packages/cf-deploy-config-writer/test/sample/standalonewithapp/router/package.json b/packages/cf-deploy-config-writer/test/sample/standalonewithapp/router/package.json new file mode 100644 index 0000000000..50f3e8353a --- /dev/null +++ b/packages/cf-deploy-config-writer/test/sample/standalonewithapp/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/test/sample/standalonewithapp/router/xs-app.json b/packages/cf-deploy-config-writer/test/sample/standalonewithapp/router/xs-app.json new file mode 100644 index 0000000000..4c7a8957dc --- /dev/null +++ b/packages/cf-deploy-config-writer/test/sample/standalonewithapp/router/xs-app.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/test/sample/standalonewithapp/xs-security.json b/packages/cf-deploy-config-writer/test/sample/standalonewithapp/xs-security.json new file mode 100644 index 0000000000..c31481ff7b --- /dev/null +++ b/packages/cf-deploy-config-writer/test/sample/standalonewithapp/xs-security.json @@ -0,0 +1,7 @@ +{ + "xsappname": "standalone", + "tenant-mode": "dedicated", + "description": "Security profile of called application", + "scopes": [], + "role-templates": [] +} 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 index 4168a0abdb..f10b2938a7 100644 --- 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 @@ -248,100 +248,100 @@ builder: `; exports[`CF Writer App Generate deployment configs - HTML5 App and destination read from ui5.yaml 2`] = ` -"_schema-version: \\"3.2\\" +"_schema-version: '3.2' ID: basicapp description: Generated by Fiori Tools 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 + - 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-target: true - - name: basicapp-repo-host + 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-service + ServiceKeyName: basicapp-uaa-key + sap.cloud.service: basicapp + existing_destinations_policy: update + 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: basicapp + artifacts: + - basicapp.zip + target-path: resources/ + - name: basicapp + 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-key: - name: basicapp-repo-host-key + 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: - 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-service - ServiceKeyName: basicapp-uaa-key - sap.cloud.service: basicapp - existing_destinations_policy: update - build-parameters: - no-source: true -- name: basicapp-app-content - type: com.sap.application.content - path: . - requires: + path: ./xs-security.json + service: xsuaa + service-name: basicapp-xsuaa-service + service-plan: application - name: basicapp-repo-host + type: org.cloudfoundry.managed-service 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-service - 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 + service-name: basicapp-html5-service + service-plan: app-host + service: html5-apps-repo " `; @@ -645,97 +645,97 @@ builder: `; exports[`CF Writer App Generate deployment configs - HTML5 App with managed approuter attached to a multi target application 2`] = ` -"_schema-version: \\"3.1\\" +"_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 + - 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-target: true - - name: multiproject-repo-host + 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-service + ServiceKeyName: multiproject-uaa-key + sap.cloud.service: multiproject + existing_destinations_policy: update + 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: + - name: comfioritoolslrop + artifacts: + - comfioritoolslrop.zip + target-path: resources/ + - name: comfioritoolslrop + type: html5 + path: . + build-parameters: + builder: custom + build-result: dist + commands: + - npm install + - 'npm run build:cf' + supported-platforms: [] +resources: + - name: multiproject-destination-service + type: org.cloudfoundry.managed-service parameters: - service-key: - name: multiproject-repo-host-key + service: destination + service-name: multiproject-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: multiproject-uaa + type: org.cloudfoundry.managed-service 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-service - ServiceKeyName: multiproject-uaa-key - sap.cloud.service: multiproject - existing_destinations_policy: update - build-parameters: - no-source: true -- name: multiproject-app-content - type: com.sap.application.content - path: . - requires: + path: ./xs-security.json + service: xsuaa + service-name: multiproject-xsuaa-service + service-plan: application - name: multiproject-repo-host + type: org.cloudfoundry.managed-service 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-service - 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 + service-name: multiproject-html5-service + service-plan: app-host + service: html5-apps-repo parameters: deploy_mode: html5-repo enable-parallel-deployments: true @@ -1042,99 +1042,312 @@ builder: `; exports[`CF Writer App Generate deployment configs - HTML5 App with managed approuter attached with no destination available 2`] = ` -"_schema-version: \\"3.2\\" +"_schema-version: '3.2' ID: comfioritoolslrop description: Generated by Fiori Tools version: 0.0.1 +parameters: + enable-parallel-deployments: true + deploy_mode: html5-repo modules: -- name: comfioritoolslrop-destination-content - type: com.sap.application.content - requires: - - name: comfioritoolslrop-destination-service + - 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-target: true - - name: comfioritoolslrop-repo-host + 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-service + ServiceKeyName: comfioritoolslrop-uaa-key + sap.cloud.service: comfioritoolslrop + existing_destinations_policy: update + 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: + - name: comfioritoolslrop + artifacts: + - comfioritoolslrop.zip + target-path: resources/ + - name: comfioritoolslrop + type: html5 + path: . + build-parameters: + builder: custom + build-result: dist + commands: + - npm install + - 'npm run build:cf' + supported-platforms: [] +resources: + - name: comfioritoolslrop-destination-service + type: org.cloudfoundry.managed-service parameters: - service-key: - name: comfioritoolslrop-repo-host-key + service: destination + service-name: comfioritoolslrop-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: comfioritoolslrop-uaa + type: org.cloudfoundry.managed-service 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-service - ServiceKeyName: comfioritoolslrop-uaa-key - sap.cloud.service: comfioritoolslrop - existing_destinations_policy: update - build-parameters: - no-source: true -- name: comfioritoolslrop-app-content - type: com.sap.application.content - path: . - requires: + path: ./xs-security.json + service: xsuaa + service-name: comfioritoolslrop-xsuaa-service + service-plan: application - name: comfioritoolslrop-repo-host + type: org.cloudfoundry.managed-service parameters: - content-target: true - build-parameters: - build-result: resources + service-name: comfioritoolslrop-html5-service + service-plan: app-host + service: html5-apps-repo +" +`; + +exports[`CF Writer App Generate deployment configs - generateSupportingConfig read mtaId read from file 1`] = ` +"{ + \\"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 captestproject --delete-services --delete-service-keys --delete-service-brokers\\" + }, + \\"devDependencies\\": { + \\"mbt\\": \\"^1.2.29\\", + \\"rimraf\\": \\"^5.0.5\\" + } +} +" +`; + +exports[`CF Writer App Generate deployment configs - generateSupportingConfig read mtaId read from file 2`] = ` +"node_modules/ +dist/ +.scp/ +.env +Makefile*.mta +mta_archives +mta-* +resources +archive.zip +.*_mta_build_tmp" +`; + +exports[`CF Writer App Generate deployment configs - generateSupportingConfig read mtaId read from file 3`] = ` +"{ + \\"xsappname\\": \\"captestproject\\", + \\"tenant-mode\\": \\"dedicated\\", + \\"description\\": \\"Security profile of called application\\", + \\"scopes\\": [], + \\"role-templates\\": [] +} +" +`; + +exports[`CF Writer App Generate deployment configs - generateSupportingConfig with mtaId passed in 1`] = ` +"{ + \\"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 testMtaId --delete-services --delete-service-keys --delete-service-brokers\\" + }, + \\"devDependencies\\": { + \\"mbt\\": \\"^1.2.29\\", + \\"rimraf\\": \\"^5.0.5\\" + } +} +" +`; + +exports[`CF Writer App Generate deployment configs - generateSupportingConfig with mtaId passed in 2`] = ` +"node_modules/ +dist/ +.scp/ +.env +Makefile*.mta +mta_archives +mta-* +resources +archive.zip +.*_mta_build_tmp" +`; + +exports[`CF Writer App Generate deployment configs - standalone approuter cleanup 1`] = ` +"_schema-version: '3.2' +ID: standalone +version: 0.0.1 +modules: + - name: standalone-router + type: approuter.nodejs + path: router + requires: + - name: standalone-html5-repo-runtime + - name: standalone-uaa + - name: standalone-destination-service + group: destinations + properties: + forwardAuthToken: false + name: ui5 + url: 'https://ui5.sap.com' + - name: standalone-connectivity + parameters: + disk-quota: 256M + memory: 256M + - name: standalone-app-content + type: com.sap.application.content + path: . 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: [] + - name: standalone-repo-host + parameters: + content-target: true + build-parameters: + build-result: resources + requires: + - name: myapp + artifacts: + - myapp.zip + target-path: resources/ + - name: myapp + type: html5 + path: myapp + build-parameters: + builder: custom + build-result: dist + 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-service - 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 + - name: standalone-uaa + type: org.cloudfoundry.managed-service + parameters: + config: + tenant-mode: dedicated + xsappname: 'standalone-\${space-guid}' + service: xsuaa + service-plan: application + - name: standalone-html5-repo-runtime + type: org.cloudfoundry.managed-service + parameters: + service: html5-apps-repo + service-plan: app-runtime + - name: standalone-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: standalone-destination-service + service-plan: lite + - name: standalone-connectivity + type: org.cloudfoundry.managed-service + parameters: + service: connectivity + service-plan: lite + - name: standalone-repo-host + type: org.cloudfoundry.managed-service + parameters: + service-name: standalone-html5-service + service-plan: app-host + service: html5-apps-repo parameters: deploy_mode: html5-repo enable-parallel-deployments: true " `; + +exports[`CF Writer App Generate deployment configs - standalone approuter cleanup 2`] = ` +"{ + \\"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\\" + } +} +" +`; + +exports[`CF Writer App Generate deployment configs - standalone approuter cleanup 3`] = ` +"{ + \\"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\\" + } + ], + \\"welcomeFile\\": \\"/myapp\\" +} +" +`; diff --git a/packages/cf-deploy-config-writer/test/unit/__snapshots__/index-cap.test.ts.snap b/packages/cf-deploy-config-writer/test/unit/__snapshots__/index-cap.test.ts.snap new file mode 100644 index 0000000000..6c0a24ddfb --- /dev/null +++ b/packages/cf-deploy-config-writer/test/unit/__snapshots__/index-cap.test.ts.snap @@ -0,0 +1,306 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CF Writer CAP Validate generation of CAP mta configurations managed 1`] = ` +"_schema-version: 3.3.0 +ID: captestproject +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: captestproject-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: captestproject-db + - name: captestproject-auth + - name: captestproject-connectivity + - name: captestproject-destination + - name: captestproject-db-deployer + type: hdb + path: gen/db + parameters: + buildpack: nodejs_buildpack + requires: + - name: captestproject-db + - name: captestproject-app-deployer + type: com.sap.application.content + path: gen + requires: + - name: captestproject-html5-repo-host + parameters: + content-target: true + build-parameters: + build-result: app/ + requires: null + - name: captestproject-destination-content + type: com.sap.application.content + requires: + - name: captestproject-destination + parameters: + content-target: true + - name: captestproject-html5-repo-host + parameters: + service-key: + name: captestproject-html5-repo-host-key + - name: captestproject-auth + parameters: + service-key: + name: captestproject-auth-key + parameters: + content: + instance: + destinations: + - Name: captestproject_html_repo_host + ServiceInstanceName: captestproject-html5-service + ServiceKeyName: captestproject-html5-repo-host-key + sap.cloud.service: captestproject + - Authentication: OAuth2UserTokenExchange + Name: captestproject_uaa + ServiceInstanceName: captestproject-xsuaa-service + ServiceKeyName: captestproject-auth-key + sap.cloud.service: captestproject + existing_destinations_policy: update + build-parameters: + no-source: true +resources: + - name: captestproject-db + type: com.sap.xs.hdi-container + parameters: + service: hana + service-plan: hdi-shared + - name: captestproject-auth + type: org.cloudfoundry.managed-service + parameters: + service: xsuaa + service-plan: application + path: ./xs-security.json + config: + xsappname: 'captestproject-\${org}-\${space}' + tenant-mode: dedicated + service-name: captestproject-xsuaa-service + - name: captestproject-connectivity + type: org.cloudfoundry.managed-service + parameters: + service: connectivity + service-plan: lite + - name: captestproject-destination + type: org.cloudfoundry.managed-service + parameters: + service: destination + service-plan: lite + config: + HTML5Runtime_enabled: true + init_data: + instance: + existing_destinations_policy: update + destinations: + - Name: captestproject-srv-api + URL: '~{srv-api/srv-url}' + Authentication: NoAuthentication + Type: HTTP + ProxyType: Internet + HTML5.ForwardAuthToken: true + HTML5.DynamicDestination: true + - Name: ui5 + URL: 'https://ui5.sap.com' + Authentication: NoAuthentication + Type: HTTP + ProxyType: Internet + - name: captestproject-html5-repo-host + type: org.cloudfoundry.managed-service + parameters: + service: html5-apps-repo + service-plan: app-host + service-name: captestproject-html5-service +" +`; + +exports[`CF Writer CAP Validate generation of CAP mta configurations standard 1`] = ` +"_schema-version: 3.3.0 +ID: captestproject +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: captestproject-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: captestproject-db + - name: captestproject-auth + - name: captestproject-connectivity + - name: captestproject-destination + - name: captestproject-db-deployer + type: hdb + path: gen/db + parameters: + buildpack: nodejs_buildpack + requires: + - name: captestproject-db + - name: captestproject-app-deployer + type: com.sap.application.content + path: gen + requires: + - name: captestproject-html5-repo-host + parameters: + content-target: true + build-parameters: + build-result: app/ + requires: null + - name: captestproject-router + type: approuter.nodejs + path: router + parameters: + disk-quota: 256M + memory: 256M + requires: + - name: captestproject-html5-repo-runtime + - name: captestproject-uaa + - name: captestproject-destination + group: destinations + properties: + name: ui5 + url: 'https://ui5.sap.com' + forwardAuthToken: false +resources: + - name: captestproject-db + type: com.sap.xs.hdi-container + parameters: + service: hana + service-plan: hdi-shared + - name: captestproject-auth + type: org.cloudfoundry.managed-service + parameters: + service: xsuaa + service-plan: application + path: ./xs-security.json + config: + xsappname: 'captestproject-\${org}-\${space}' + tenant-mode: dedicated + - name: captestproject-connectivity + type: org.cloudfoundry.managed-service + parameters: + service: connectivity + service-plan: lite + - name: captestproject-destination + type: org.cloudfoundry.managed-service + parameters: + service: destination + service-plan: lite + config: + HTML5Runtime_enabled: true + init_data: + instance: + existing_destinations_policy: update + destinations: + - Name: captestproject-srv-api + URL: '~{srv-api/srv-url}' + Authentication: NoAuthentication + Type: HTTP + ProxyType: Internet + HTML5.ForwardAuthToken: true + HTML5.DynamicDestination: true + - Name: ui5 + URL: 'https://ui5.sap.com' + Authentication: NoAuthentication + Type: HTTP + ProxyType: Internet + - name: captestproject-html5-repo-host + type: org.cloudfoundry.managed-service + parameters: + service: html5-apps-repo + service-plan: app-host + - name: captestproject-uaa + type: org.cloudfoundry.managed-service + parameters: + service-plan: application + service: xsuaa + config: + xsappname: 'captestproject-\${space-guid}' + tenant-mode: dedicated + - name: captestproject-html5-repo-runtime + type: org.cloudfoundry.managed-service + parameters: + service-plan: app-runtime + service: html5-apps-repo +" +`; + +exports[`CF Writer CAP Validate generation of CAP mta configurations standard 2`] = ` +"{ + \\"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\\" + } +} +" +`; + +exports[`CF Writer CAP Validate generation of CAP mta configurations standard 3`] = ` +"{ + \\"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/test/unit/cap.test.ts b/packages/cf-deploy-config-writer/test/unit/cap.test.ts index 14612f7b3c..38548fe23a 100644 --- a/packages/cf-deploy-config-writer/test/unit/cap.test.ts +++ b/packages/cf-deploy-config-writer/test/unit/cap.test.ts @@ -86,7 +86,7 @@ describe('CF Writer', () => { await expect(generateAppConfig({ appPath: capPath }, unitTestFs)).rejects.toThrowError(MTABinNotFound); }); - test('Validate dependency on CDS', async () => { + test('Validate error is thrown if cds fails', async () => { spawnMock = jest.spyOn(childProcess, 'spawnSync').mockImplementation(() => ({ error: 1 } as any)); const capPath = join(outputDir, 'capcds'); fsExtra.mkdirSync(outputDir, { recursive: true }); @@ -101,7 +101,7 @@ describe('CF Writer', () => { }, unitTestFs ) - ).rejects.toThrowError(CDSBinNotFound); + ).rejects.toThrowError(/Something went wrong creating mta.yaml!/); expect(spawnMock).not.toHaveBeenCalledWith(''); }); }); diff --git a/packages/cf-deploy-config-writer/test/unit/fixtures/mta-types/cdsmta/mta.yaml b/packages/cf-deploy-config-writer/test/unit/fixtures/mta-types/cdsmta/mta.yaml new file mode 100644 index 0000000000..6baa5d05c8 --- /dev/null +++ b/packages/cf-deploy-config-writer/test/unit/fixtures/mta-types/cdsmta/mta.yaml @@ -0,0 +1,101 @@ +_schema-version: 3.3.0 +ID: captestproject +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: captestproject-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: captestproject-db + - name: captestproject-auth + - name: captestproject-connectivity + - name: captestproject-destination + + - name: captestproject-db-deployer + type: hdb + path: gen/db + parameters: + buildpack: nodejs_buildpack + requires: + - name: captestproject-db + + - name: captestproject-app-deployer + type: com.sap.application.content + path: gen + requires: + - name: captestproject-html5-repo-host + parameters: + content-target: true + build-parameters: + build-result: app/ + requires: + + +resources: + - name: captestproject-db + type: com.sap.xs.hdi-container + parameters: + service: hana + service-plan: hdi-shared + - name: captestproject-auth + type: org.cloudfoundry.managed-service + parameters: + service: xsuaa + service-plan: application + path: ./xs-security.json + config: + xsappname: captestproject-${org}-${space} + tenant-mode: dedicated + - name: captestproject-connectivity + type: org.cloudfoundry.managed-service + parameters: + service: connectivity + service-plan: lite + - name: captestproject-destination + type: org.cloudfoundry.managed-service + parameters: + service: destination + service-plan: lite + config: + HTML5Runtime_enabled: true + init_data: + instance: + existing_destinations_policy: update + destinations: + - Name: captestproject-srv-api + URL: ~{srv-api/srv-url} + Authentication: NoAuthentication + Type: HTTP + ProxyType: Internet + HTML5.ForwardAuthToken: true + HTML5.DynamicDestination: true + - Name: ui5 + URL: https://ui5.sap.com + Authentication: NoAuthentication + Type: HTTP + ProxyType: Internet + - name: captestproject-html5-repo-host + type: org.cloudfoundry.managed-service + parameters: + service: html5-apps-repo + service-plan: app-host 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 index a8447c8282..51bf1aba0b 100644 --- a/packages/cf-deploy-config-writer/test/unit/index-app.test.ts +++ b/packages/cf-deploy-config-writer/test/unit/index-app.test.ts @@ -3,7 +3,11 @@ import fsExtra from 'fs-extra'; import hasbin from 'hasbin'; import { NullTransport, ToolsLogger } from '@sap-ux/logger'; import * as btp from '@sap-ux/btp-utils'; -import { generateAppConfig } from '../../src'; +import { generateAppConfig, DefaultMTADestination } from '../../src'; +import { generateSupportingConfig } from '../../src/utils'; +import type { CFConfig } from '../../src/types'; +import { create } from 'mem-fs-editor'; +import { create as createStorage } from 'mem-fs'; jest.mock('@sap-ux/btp-utils', () => ({ ...jest.requireActual('@sap-ux/btp-utils'), @@ -16,6 +20,13 @@ jest.mock('hasbin', () => ({ sync: jest.fn() })); +jest.mock('@sap/mta-lib', () => { + return { + // eslint-disable-next-line @typescript-eslint/no-var-requires + Mta: require('./mockMta').MockMta + }; +}); + let hasSyncMock: jest.SpyInstance; let isAppStudioMock: jest.SpyInstance; let listDestinationsMock: jest.SpyInstance; @@ -64,6 +75,10 @@ describe('CF Writer App', () => { jest.resetAllMocks(); }); + test('Generate deployment configs - DefaultMTADestination', async () => { + expect(DefaultMTADestination).toEqual('fiori-default-srv-api'); + }); + test('Generate deployment configs - HTML5 App and destination read from ui5.yaml', async () => { isAppStudioMock.mockResolvedValue(true); listDestinationsMock.mockResolvedValue(destinationsMock); @@ -113,4 +128,52 @@ describe('CF Writer App', () => { const appPath = join(outputDir, appName); await expect(generateAppConfig({ appPath }, undefined, logger)).rejects.toThrowError(); }); + + test('Generate deployment configs - should fail if no HTML5 app found', async () => { + const appName = 'standalone'; + const appPath = join(outputDir, appName); + fsExtra.mkdirSync(outputDir, { recursive: true }); + fsExtra.mkdirSync(appPath); + fsExtra.copySync(join(__dirname, `../sample/standalone`), appPath); + await expect(generateAppConfig({ appPath, addManagedAppRouter: false })).rejects.toThrowError( + /No SAP Fiori UI5 application found./ + ); + }); + + test('Generate deployment configs - standalone approuter cleanup', async () => { + const rootPath = join(outputDir, 'standalonewithapp'); + const appPath = join(rootPath, 'myapp'); + fsExtra.mkdirSync(outputDir, { recursive: true }); + fsExtra.mkdirSync(rootPath); + fsExtra.copySync(join(__dirname, `../sample/standalonewithapp`), rootPath); + const localFs = await generateAppConfig({ appPath, addManagedAppRouter: false }); + expect(localFs.read(join(rootPath, 'mta.yaml'))).toMatchSnapshot(); + expect(localFs.read(join(rootPath, 'router', 'package.json'))).toMatchSnapshot(); + expect(localFs.read(join(rootPath, 'router', 'xs-app.json'))).toMatchSnapshot(); + }); + + test('Generate deployment configs - generateSupportingConfig with mtaId passed in', async () => { + const fs = create(createStorage()); + const appPath = join(outputDir, 'supportingconfig'); + fsExtra.mkdirSync(outputDir, { recursive: true }); + fsExtra.mkdirSync(appPath); + await generateSupportingConfig({ appPath, mtaId: 'testMtaId', rootPath: appPath } as unknown as CFConfig, fs); + expect(fs.read(join(appPath, 'package.json'))).toMatchSnapshot(); + expect(fs.read(join(appPath, '.gitignore'))).toMatchSnapshot(); + }); + + test('Generate deployment configs - generateSupportingConfig read mtaId read from file', async () => { + const fs = create(createStorage()); + const appPath = join(outputDir, 'supportingconfigreadmta'); + fsExtra.mkdirSync(outputDir, { recursive: true }); + fsExtra.mkdirSync(appPath); + fsExtra.copySync(join(__dirname, 'fixtures/mta-types/cdsmta'), appPath); + await generateSupportingConfig( + { appPath, rootPath: appPath, addManagedAppRouter: true } as unknown as CFConfig, + fs + ); + expect(fs.read(join(appPath, 'package.json'))).toMatchSnapshot(); + expect(fs.read(join(appPath, '.gitignore'))).toMatchSnapshot(); + expect(fs.read(join(appPath, 'xs-security.json'))).toMatchSnapshot(); + }); }); diff --git a/packages/cf-deploy-config-writer/test/unit/index-cap.test.ts b/packages/cf-deploy-config-writer/test/unit/index-cap.test.ts new file mode 100644 index 0000000000..2b558724bb --- /dev/null +++ b/packages/cf-deploy-config-writer/test/unit/index-cap.test.ts @@ -0,0 +1,136 @@ +import { join } from 'path'; +import fsExtra from 'fs-extra'; +import hasbin from 'hasbin'; +import { NullTransport, ToolsLogger } from '@sap-ux/logger'; +import { generateCAPConfig, RouterModuleType } from '../../src'; +import * as childProcess from 'child_process'; +import * as projectAccess from '@sap-ux/project-access'; +import fs from 'fs'; + +jest.mock('child_process'); +jest.mock('hasbin', () => ({ + ...(jest.requireActual('hasbin') as {}), + sync: jest.fn() +})); + +let hasSyncMock: jest.SpyInstance; +let spawnMock: jest.SpyInstance; +const originalPlatform = process.platform; + +jest.mock('child_process'); + +jest.mock('@sap/mta-lib', () => { + return { + // eslint-disable-next-line @typescript-eslint/no-var-requires + Mta: require('./mockMta').MockMta + }; +}); + +describe('CF Writer CAP', () => { + jest.setTimeout(10000); + const logger = new ToolsLogger({ + transports: [new NullTransport()] + }); + const outputDir = join(__dirname, '../test-output', 'capcds'); + + beforeEach(() => { + jest.resetAllMocks(); + jest.restoreAllMocks(); + hasSyncMock = jest.spyOn(hasbin, 'sync').mockImplementation(() => true); + }); + + beforeAll(() => { + fsExtra.removeSync(outputDir); + jest.clearAllMocks(); + jest.spyOn(hasbin, 'sync').mockReturnValue(true); + jest.mock('hasbin', () => { + return { + ...(jest.requireActual('hasbin') as {}), + sync: hasSyncMock + }; + }); + Object.defineProperty(process, 'platform', { value: 'win32' }); + }); + + afterAll(() => { + jest.resetAllMocks(); + Object.defineProperty(process, 'platform', { + value: originalPlatform + }); + }); + + it.each([[RouterModuleType.Managed], [RouterModuleType.Standard]])( + 'Validate generation of CAP mta configurations %s', + async (routerType: RouterModuleType) => { + const mtaId = 'captestproject'; + const mtaPath = join(outputDir, routerType, mtaId); + fsExtra.mkdirSync(mtaPath, { recursive: true }); + fsExtra.copySync(join(__dirname, `../sample/capcds`), mtaPath); + const getCapProjectTypeMock = jest.spyOn(projectAccess, 'getCapProjectType').mockResolvedValue('CAPNodejs'); + // For testing purposes, an existing mta.yaml is copied to reflect the spawn command; + // `cds add mta xsuaa connectivity destination html5-repo` + spawnMock = jest.spyOn(childProcess, 'spawnSync').mockImplementation(() => { + fsExtra.copySync(join(__dirname, `fixtures/mta-types/cdsmta`), mtaPath); + return { status: 0 } as any; + }); + const localFs = await generateCAPConfig( + { + mtaPath, + mtaId, + routerType + }, + undefined, + logger + ); + expect(localFs.read(join(mtaPath, 'mta.yaml'))).toMatchSnapshot(); + expect(getCapProjectTypeMock).toHaveBeenCalled(); + expect(spawnMock.mock.calls).toHaveLength(2); + expect(spawnMock).toHaveBeenCalledWith( + 'cds', + ['add', 'mta', 'xsuaa', 'destination', 'html5-repo'], + expect.objectContaining({ cwd: expect.stringContaining(mtaId) }) + ); + expect(spawnMock.mock.calls[1][0]).toStrictEqual('npm.cmd'); // Just always test for windows! + expect(spawnMock.mock.calls[1][1]).toStrictEqual(['install', '--ignore-engines']); + if (RouterModuleType.Standard === routerType) { + expect(localFs.read(join(mtaPath, `router`, 'package.json'))).toMatchSnapshot(); + expect(localFs.read(join(mtaPath, `router`, 'xs-app.json'))).toMatchSnapshot(); + } + } + ); + + test('Validate CAP project type is correct when creating mta.yaml', async () => { + const mtaId = 'captestproject'; + const mtaPath = join(outputDir, mtaId); + jest.spyOn(projectAccess, 'getCapProjectType').mockResolvedValue('CAPJava'); + await expect( + generateCAPConfig( + { + mtaPath, + mtaId, + routerType: RouterModuleType.Managed + }, + undefined, + logger + ) + ).rejects.toThrowError(/Target folder does not contain a Node.js CAP project./); + }); + + test('Validate CAP type if target contains mta.yaml', async () => { + const mtaId = 'captestproject'; + const mtaPath = join(outputDir, mtaId); + jest.spyOn(projectAccess, 'getCapProjectType').mockResolvedValue('CAPNodejs'); + jest.spyOn(fs, 'existsSync').mockReturnValue(true); + await expect( + generateCAPConfig( + { + mtaPath, + mtaId, + routerType: RouterModuleType.Managed + }, + undefined, + logger + ) + ).rejects.toThrowError(/An mta.yaml already exists in the target directory./); + }); +});