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 683aa07479..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 @@ -282,6 +282,14 @@ async function updateMtaConfig(cfConfig: CFConfig, fs: Editor): Promise { } } +/** + * + * @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; @@ -399,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 3713870e87..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 @@ -5,6 +5,8 @@ import LoggerHelper from '../logger-helper'; import { createMTA, validateMtaConfig } from '../mta-config'; import { type Logger } from '@sap-ux/logger'; 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. @@ -22,7 +24,10 @@ export async function generateBaseConfig(config: CFBaseConfig, fs?: Editor, logg LoggerHelper.logger = logger; } logger?.debug(`Generate base configuration using: \n ${JSON.stringify(config)}`); - validateMtaConfig(config, fs); + 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); 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 index 713c2e8911..0a5b5abe1b 100644 --- a/packages/cf-deploy-config-writer/src/cf-writer/cap-config.ts +++ b/packages/cf-deploy-config-writer/src/cf-writer/cap-config.ts @@ -1,11 +1,13 @@ import { create as createStorage } from 'mem-fs'; import { create, type Editor } from 'mem-fs-editor'; import { addSupportingConfig, addRoutingConfig } from '../utils'; -import { createCAPMTA, validateMtaConfig } from '../mta-config'; +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. @@ -23,7 +25,7 @@ export async function generateCAPConfig(config: CAPConfig, fs?: Editor, logger?: LoggerHelper.logger = logger; } logger?.debug(`Generate CAP configuration using: \n ${JSON.stringify(config)}`); - validateMtaConfig(config as CFBaseConfig, fs); + await validateConfig(config); const cdsOptionalParams: string[] = [CDSXSUAAService, CDSDestinationService, CDSHTML5RepoService]; createCAPMTA(config.mtaPath, cdsOptionalParams); await addRoutingConfig(config, fs); @@ -31,3 +33,20 @@ export async function generateCAPConfig(config: CAPConfig, fs?: Editor, logger?: 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/mta-config/index.ts b/packages/cf-deploy-config-writer/src/mta-config/index.ts index 7bf74b45d4..49c380da75 100644 --- a/packages/cf-deploy-config-writer/src/mta-config/index.ts +++ b/packages/cf-deploy-config-writer/src/mta-config/index.ts @@ -20,7 +20,6 @@ import { type MTABaseConfig, type CFBaseConfig } from '../types'; import LoggerHelper from '../logger-helper'; import { sync } from 'hasbin'; import { spawnSync } from 'child_process'; -import type { Editor } from 'mem-fs-editor'; import { t } from '../i18n'; /** @@ -137,9 +136,15 @@ export function doesCDSBinaryExist(): void { * @param options */ export function createCAPMTA(cwd: string, options?: string[]): void { - const result = spawnSync(CDSExecutable, [...CDSAddMtaParams, ...(options ?? [])], { cwd }); - if (result.error) { - throw new Error(CDSBinNotFound); + let result = spawnSync(CDSExecutable, [...CDSAddMtaParams, ...(options ?? [])], { cwd }); + if (result?.error) { + throw new Error(`Something went wrong creating mta.yaml! ${result.error}`); + } + // Ensure the package-lock is created otherwise mta build will fail + const cmd = process.platform === 'win32' ? `npm.cmd` : 'npm'; + result = spawnSync(cmd, ['install', '--ignore-engines'], { cwd }); + if (result?.error) { + throw new Error(`Something went wrong installing node modules! ${result.error}`); } } @@ -147,9 +152,8 @@ export function createCAPMTA(cwd: string, options?: string[]): void { * Validate the writer configuration to ensure all required parameters are present. * * @param config writer configuration - * @param fs reference to a mem-fs editor */ -export function validateMtaConfig(config: CFBaseConfig, fs: Editor): void { +export function validateMtaConfig(config: CFBaseConfig): void { // We use mta-lib, which in turn relies on the mta executable being installed and available in the path doesMTABinaryExist(); @@ -172,9 +176,6 @@ export function validateMtaConfig(config: CFBaseConfig, fs: Editor): void { throw new Error(t('error.missingABAPServiceBindingDetails')); } - if (fs.exists(join(config.mtaPath, config.mtaId))) { - throw new Error(t('error.mtaAlreadyExists')); - } setMtaDefaults(config); } 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 a15e793e5a..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,22 +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", - "cannotUpdateRouterXSApp": "Unable to update router xs-app.json welcome location." + "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/utils.ts b/packages/cf-deploy-config-writer/src/utils.ts index 707cb88c1a..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, @@ -151,7 +151,7 @@ export function addGitIgnore(targetPath: string, fs: Editor): void { * @param fs reference to a mem-fs editor */ export function addRootPackage({ mtaPath, mtaId }: MTABaseConfig, fs: Editor): void { - fs.copyTpl(getTemplatePath('package.json'), join(mtaPath, 'package.json'), { + fs.copyTpl(getTemplatePath('package.json'), join(mtaPath, FileName.Package), { mtaId: mtaId }); } @@ -219,7 +219,7 @@ async function addStandaloneRouter(cfConfig: CFBaseConfig, mtaInstance: MtaConfi await mtaInstance.addAbapService(abapServiceName, abapService); } - fs.copyTpl(getTemplatePath(`router/package.json`), join(cfConfig.mtaPath, `${RouterModule}/package.json`)); + fs.copyTpl(getTemplatePath(`router/package.json`), join(cfConfig.mtaPath, `${RouterModule}/${FileName.Package}`)); if (abapServiceName) { let serviceKey; 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/index-cap.test.ts b/packages/cf-deploy-config-writer/test/unit/index-cap.test.ts index 0bec5ff617..2b558724bb 100644 --- a/packages/cf-deploy-config-writer/test/unit/index-cap.test.ts +++ b/packages/cf-deploy-config-writer/test/unit/index-cap.test.ts @@ -4,8 +4,10 @@ import hasbin from 'hasbin'; import { NullTransport, ToolsLogger } from '@sap-ux/logger'; import { generateCAPConfig, RouterModuleType } from '../../src'; import * as childProcess from 'child_process'; -jest.mock('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() @@ -13,6 +15,9 @@ jest.mock('hasbin', () => ({ let hasSyncMock: jest.SpyInstance; let spawnMock: jest.SpyInstance; +const originalPlatform = process.platform; + +jest.mock('child_process'); jest.mock('@sap/mta-lib', () => { return { @@ -44,11 +49,14 @@ describe('CF Writer CAP', () => { sync: hasSyncMock }; }); + Object.defineProperty(process, 'platform', { value: 'win32' }); }); afterAll(() => { jest.resetAllMocks(); - // fsExtra.removeSync(outputDir); + Object.defineProperty(process, 'platform', { + value: originalPlatform + }); }); it.each([[RouterModuleType.Managed], [RouterModuleType.Standard]])( @@ -58,13 +66,13 @@ describe('CF Writer CAP', () => { 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, @@ -75,15 +83,54 @@ describe('CF Writer CAP', () => { 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./); + }); });