diff --git a/.changeset/tasty-cats-search.md b/.changeset/tasty-cats-search.md new file mode 100644 index 0000000000..bf4ff2d788 --- /dev/null +++ b/.changeset/tasty-cats-search.md @@ -0,0 +1,8 @@ +--- +'@sap-ux/cf-deploy-config-sub-generator': patch +'@sap-ux/deploy-config-generator-shared': patch +'@sap-ux/cf-deploy-config-inquirer': patch +'@sap-ux/cf-deploy-config-writer': patch +--- + +Changes to support adding CAP MTA prompt to allow user generate MTA diff --git a/README.md b/README.md index 23a28cc948..c8e4b87ff0 100644 --- a/README.md +++ b/README.md @@ -127,7 +127,7 @@ When analyzing a problem, it is helpful to be able to debug the modules. How to Each of the packages has an extensive set of unit tests covering as many as possible different scenarios, therefore, as a starting point for debugging, it is a good idea to use the tests. The easiest (but not the only) way to debug a specific test in VSCode is to open a `JavaScript Debug Terminal` and then go to the package that needs to be debugged. Using the debug terminal, execute all tests with `pnpm test` or a specific one, e.g. execute `pnpm test -- test/basic.test.ts` in the `fiori-freestyle-writer` directory (`./packages/fiori-freestyle-writer`). When running either of the commands in the debug terminal, breakpoints set in VSCode will be active. -Additionally for the `*-writer` modules it is sometimes helpful to manually inspect the generated output of the unit tests on the filesystem. This can be achieved by setting the variable `UX_DEBUG` before running the tests e.g. in `fiori-freestyle-writer` run `UX_DEBUG=true pnpm test` and after the tests finish, the generated files can be found at `./test/test-output`. +Additionally, for the `*-writer` modules it is sometimes helpful to manually inspect the generated output of the unit tests on the filesystem. This can be achieved by setting the variable `UX_DEBUG` before running the tests e.g. in `fiori-freestyle-writer` run `UX_DEBUG=true pnpm test` and after the tests finish, the generated files can be found at `./test/test-output`. Additional checks can be performed on the generated projects by also setting `UX_DEBUG_FULL` e.g. `UX_DEBUG=true UX_DEBUG_FULL=true pnpm test`. This includes checks such as `npm install`, `npm run ts-typecheck`, `npm run lint` as appropriate to the project. diff --git a/packages/cf-deploy-config-inquirer/src/index.ts b/packages/cf-deploy-config-inquirer/src/index.ts index 8c3aee888c..4e3c4a3720 100644 --- a/packages/cf-deploy-config-inquirer/src/index.ts +++ b/packages/cf-deploy-config-inquirer/src/index.ts @@ -88,5 +88,6 @@ export { RouterModuleType, type CfDeployConfigQuestions, type CfDeployConfigAnswers, - type CfAppRouterDeployConfigAnswers + type CfAppRouterDeployConfigAnswers, + type CfAppRouterDeployConfigQuestions }; diff --git a/packages/cf-deploy-config-sub-generator/src/app/index.ts b/packages/cf-deploy-config-sub-generator/src/app/index.ts index ed1950b36b..a6fb4d56e3 100644 --- a/packages/cf-deploy-config-sub-generator/src/app/index.ts +++ b/packages/cf-deploy-config-sub-generator/src/app/index.ts @@ -11,7 +11,13 @@ import { } from '@sap-ux/fiori-generator-shared'; import { isInternalFeaturesSettingEnabled } from '@sap-ux/feature-toggle'; import { isFullUrlDestination } from '@sap-ux/btp-utils'; -import { generateAppConfig, ApiHubType, useAbapDirectServiceBinding } from '@sap-ux/cf-deploy-config-writer'; +import { + generateAppConfig, + generateCAPConfig, + ApiHubType, + useAbapDirectServiceBinding, + DefaultMTADestination +} from '@sap-ux/cf-deploy-config-writer'; import { DeploymentGenerator, showOverwriteQuestion, @@ -22,18 +28,25 @@ import { mtaExecutable, cdsExecutable, generateDestinationName, - getDestination + getDestination, + getConfirmMtaContinuePrompt } from '@sap-ux/deploy-config-generator-shared'; import { t, initI18n, DESTINATION_AUTHTYPE_NOTFOUND, API_BUSINESS_HUB_ENTERPRISE_PREFIX } from '../utils'; import { loadManifest } from './utils'; import { getMtaPath, findCapProjectRoot, FileName } from '@sap-ux/project-access'; import { EventName } from '../telemetryEvents'; -import { getCFQuestions } from './questions'; -import type { ApiHubConfig, CFAppConfig } from '@sap-ux/cf-deploy-config-writer'; +import { getCFApprouterQuestionsForCap, getCFQuestions } from './questions'; +import type { ApiHubConfig, CFAppConfig, CAPConfig } from '@sap-ux/cf-deploy-config-writer'; import type { Logger } from '@sap-ux/logger'; -import type { CfDeployConfigOptions } from './types'; -import type { CfDeployConfigAnswers, CfDeployConfigQuestions } from '@sap-ux/cf-deploy-config-inquirer'; +import { CfDeployConfigOptions } from './types'; +import { + type CfAppRouterDeployConfigAnswers, + type CfDeployConfigQuestions, + CfDeployConfigAnswers +} from '@sap-ux/cf-deploy-config-inquirer'; import type { YeomanEnvironment } from '@sap-ux/fiori-generator-shared'; +import { withCondition } from '@sap-ux/inquirer-common'; +import type { Answers, Question } from 'inquirer'; /** * Cloud Foundry deployment configuration generator. @@ -49,6 +62,7 @@ export default class extends DeploymentGenerator { private readonly cloudServiceName?: string; private readonly serviceBase?: string; private answers: CfDeployConfigAnswers & Partial = {}; + private appRouterAnswers: CfAppRouterDeployConfigAnswers; private projectRoot: string; private mtaPath?: string; private isCap = false; @@ -75,7 +89,7 @@ export default class extends DeploymentGenerator { this.options = opts; this.destinationName = opts.destinationName ?? ''; - this.addMtaDestination = opts.addMTADestination ?? false; // by default it's false unless passed in i.e. headless flow + this.addMtaDestination = opts.addMTADestination ?? false; // by default, it's false unless passed in i.e. headless flow this.lcapModeOnly = opts.lcapModeOnly ?? false; this.cloudServiceName = opts.cloudServiceName || undefined; this.apiHubConfig = opts.apiHubConfig; @@ -106,6 +120,8 @@ export default class extends DeploymentGenerator { if (!this.launchDeployConfigAsSubGenerator) { await this._init(); + } else { + await this._processProjectConfigs(); } } @@ -115,12 +131,10 @@ export default class extends DeploymentGenerator { this.abort = true; handleErrorMessage(this.appWizard, { errorType: ERROR_TYPE.NO_MTA_BIN }); } - await this._processProjectPaths(); await this._processProjectConfigs(); this.isAbapDirectServiceBinding = await useAbapDirectServiceBinding(this.appPath, false, this.mtaPath); - // restricting local changes is only applicable for CAP flows if (!this.isCap) { this.lcapModeOnly = false; @@ -134,7 +148,7 @@ export default class extends DeploymentGenerator { private async _processProjectPaths(): Promise { const mtaPathResult = await getMtaPath(this.appPath); this.mtaPath = mtaPathResult?.mtaPath; - const capRoot = await findCapProjectRoot(this.appPath); + const capRoot = await findCapProjectRoot(this.appPath, true, this.fs); if (capRoot) { if (!hasbin.sync(cdsExecutable)) { bail(ErrorHandler.getErrorMsgFromType(ERROR_TYPE.NO_CDS_BIN)); @@ -156,7 +170,6 @@ export default class extends DeploymentGenerator { if (!baseConfigExists) { bail(ErrorHandler.noBaseConfig(baseConfigFile)); } - this.deployConfigExists = this.fs.exists(join(this.appPath, this.options.config ?? FileName.Ui5Yaml)); } @@ -164,21 +177,38 @@ export default class extends DeploymentGenerator { if (this.abort) { return; } - - if (this.isCap && this.projectRoot && !this.mtaPath) { - // if the user is adding deploy config to a CAP project and there is no mta.yaml in the root, then log error and exit - this.abort = true; - handleErrorMessage(this.appWizard, { errorType: ERROR_TYPE.CAP_DEPLOYMENT_NO_MTA }); - return; + if (!this.launchDeployConfigAsSubGenerator) { + await this._prompting(); } + await this._reconcileAnswersWithOptions(); + } - if (!this.launchDeployConfigAsSubGenerator) { + private async _prompting(): Promise { + const isCAPMissingMTA = this.isCap && this.projectRoot && !this.mtaPath; + if (isCAPMissingMTA) { + DeploymentGenerator.logger?.debug(t('cfGen.debug.capMissingMTA')); + // If launched as root generator, add a prompt to allow user decide if they want to add an MTA config + let questions = (await getCFApprouterQuestionsForCap({ + projectRoot: this.projectRoot ?? process.cwd() + })) as Question[]; + questions = withCondition(questions, (answers: Answers) => answers.addCapMtaContinue === true); + questions.unshift(...getConfirmMtaContinuePrompt()); + this.appRouterAnswers = (await this.prompt(questions)) as CfAppRouterDeployConfigAnswers; + if ((this.appRouterAnswers as Answers).addCapMtaContinue !== true) { + this.abort = true; + return; + } + // Configure defaults + this.destinationName = DefaultMTADestination; + this.options.overwrite = true; // Don't prompt the user to overwrite files we've just written! + this.answers = {}; + this.answers.destinationName = this.destinationName; + this.answers.addManagedAppRouter = false; + } else { await this._handleApiHubConfig(); const questions = await this._getCFQuestions(); this.answers = await this.prompt(questions); } - - await this._reconcileAnswersWithOptions(); } /** @@ -251,8 +281,16 @@ export default class extends DeploymentGenerator { private async _writing(): Promise { try { - const appConfig = this._getAppConfig(); - await generateAppConfig(appConfig, this.fs, DeploymentGenerator.logger as unknown as Logger); + // Step1. (Optional) Generate CAP MTA with specific approuter type managed | standalone + if (this.appRouterAnswers) { + await generateCAPConfig( + this.appRouterAnswers as CAPConfig, + this.fs, + DeploymentGenerator.logger as unknown as Logger + ); + } + // Step2. Append HTML5 app to MTA + await generateAppConfig(this._getAppConfig(), this.fs, DeploymentGenerator.logger as unknown as Logger); } catch (error) { this.abort = true; handleErrorMessage(this.appWizard, { errorMsg: t('cfGen.error.writing', { error }) }); diff --git a/packages/cf-deploy-config-sub-generator/src/app/questions.ts b/packages/cf-deploy-config-sub-generator/src/app/questions.ts index 91439f7ea3..7bd7efffdd 100644 --- a/packages/cf-deploy-config-sub-generator/src/app/questions.ts +++ b/packages/cf-deploy-config-sub-generator/src/app/questions.ts @@ -1,12 +1,20 @@ import { isAppStudio } from '@sap-ux/btp-utils'; import { DeploymentGenerator } from '@sap-ux/deploy-config-generator-shared'; import { getMtaPath } from '@sap-ux/project-access'; -import { getPrompts, promptNames } from '@sap-ux/cf-deploy-config-inquirer'; +import { + appRouterPromptNames, + type CfAppRouterDeployConfigPromptOptions, + type CfAppRouterDeployConfigQuestions, + type CfDeployConfigPromptOptions, + type CfDeployConfigQuestions, + getAppRouterPrompts, + getPrompts, + promptNames +} from '@sap-ux/cf-deploy-config-inquirer'; import { getHostEnvironment, hostEnvironment } from '@sap-ux/fiori-generator-shared'; import { destinationQuestionDefaultOption, getCFChoices } from './utils'; import { t } from '../utils'; import type { ApiHubConfig } from '@sap-ux/cf-deploy-config-writer'; -import type { CfDeployConfigPromptOptions, CfDeployConfigQuestions } from '@sap-ux/cf-deploy-config-inquirer'; /** * Fetches the Cloud Foundry deployment configuration questions. @@ -60,3 +68,29 @@ export async function getCFQuestions({ DeploymentGenerator.logger?.debug(t('cfGen.debug.promptOptions', { options: JSON.stringify(options) })); return getPrompts(options); } + +/** + * Retrieve the CF Approuter questions, certain prompts are restricted to support CAP project. + * + * @param options - the options required for retrieving the prompts. + * @param options.projectRoot - the root path of the project. + * @returns the cf approuter config questions. + */ +export async function getCFApprouterQuestionsForCap({ + projectRoot +}: { + projectRoot: string; +}): Promise { + // Disable some prompts, not required for CAP flow + const appRouterPromptOptions: CfAppRouterDeployConfigPromptOptions = { + [appRouterPromptNames.mtaPath]: projectRoot, + [appRouterPromptNames.mtaId]: true, + [appRouterPromptNames.mtaDescription]: false, + [appRouterPromptNames.mtaVersion]: false, + [appRouterPromptNames.routerType]: true, + [appRouterPromptNames.addConnectivityService]: true, + [appRouterPromptNames.addABAPServiceBinding]: false + }; + + return getAppRouterPrompts(appRouterPromptOptions); +} diff --git a/packages/cf-deploy-config-sub-generator/src/app/types.ts b/packages/cf-deploy-config-sub-generator/src/app/types.ts index 3f64b968d2..71cefea601 100644 --- a/packages/cf-deploy-config-sub-generator/src/app/types.ts +++ b/packages/cf-deploy-config-sub-generator/src/app/types.ts @@ -1,5 +1,5 @@ import type { AppWizard } from '@sap-devx/yeoman-ui-types'; -import type { CfDeployConfigAnswers } from '@sap-ux/cf-deploy-config-inquirer'; +import { type CfDeployConfigAnswers } from '@sap-ux/cf-deploy-config-inquirer'; import type { ApiHubConfig } from '@sap-ux/cf-deploy-config-writer'; import type { TelemetryData } from '@sap-ux/fiori-generator-shared'; @@ -84,4 +84,8 @@ export interface CfDeployConfigOptions extends CfDeployConfigAnswers { * Telemetry data to be send after deployment configuration has been added */ telemetryData?: TelemetryData; + /** + * Option to invoke the getConfirmMtaContinue prompt + */ + addCapMtaContinue?: boolean; } diff --git a/packages/cf-deploy-config-sub-generator/src/translations/cf-deploy-config-sub-generator.i18n.json b/packages/cf-deploy-config-sub-generator/src/translations/cf-deploy-config-sub-generator.i18n.json index 77eb9bdabf..26b51f4b6d 100644 --- a/packages/cf-deploy-config-sub-generator/src/translations/cf-deploy-config-sub-generator.i18n.json +++ b/packages/cf-deploy-config-sub-generator/src/translations/cf-deploy-config-sub-generator.i18n.json @@ -26,7 +26,8 @@ }, "debug": { "promptOptions": "Retrieving CF prompts using: \n {{- options}}", - "initTelemetry": "Initializing telemetry in CF deployment configuration generator" + "initTelemetry": "Initializing telemetry in CF deployment configuration generator", + "capMissingMTA": "CAP project detected with no MTA configuration" } }, "appRouterGen": { diff --git a/packages/cf-deploy-config-sub-generator/test/cap-app.test.ts b/packages/cf-deploy-config-sub-generator/test/cap-app.test.ts new file mode 100644 index 0000000000..4b434c2a43 --- /dev/null +++ b/packages/cf-deploy-config-sub-generator/test/cap-app.test.ts @@ -0,0 +1,226 @@ +import hasbin from 'hasbin'; +import CFGenerator from '../src/app'; +import yeomanTest from 'yeoman-test'; +import { join } from 'path'; +import { TestFixture } from './fixtures'; +import { initI18n, t } from '../src/utils'; +import { RouterModuleType } from '@sap-ux/cf-deploy-config-writer'; +import * as fs from 'fs'; +import * as fioriGenShared from '@sap-ux/fiori-generator-shared'; +import * as memfs from 'memfs'; +import * as cfDeployWriter from '@sap-ux/cf-deploy-config-writer'; +import type { Editor } from 'mem-fs-editor'; + +const mockIsAppStudio = jest.fn(); +jest.mock('@sap-ux/btp-utils', () => { + return { + ...(jest.requireActual('@sap-ux/btp-utils') as {}), + isAppStudio: () => mockIsAppStudio(), + listDestinations: () => jest.fn() + }; +}); + +const mockFindCapProjectRoot = jest.fn(); +jest.mock('@sap-ux/project-access', () => { + return { + ...(jest.requireActual('@sap-ux/project-access') as {}), + findCapProjectRoot: () => mockFindCapProjectRoot() + }; +}); + +jest.mock('fs', () => { + const fsLib = jest.requireActual('fs'); + const Union = require('unionfs').Union; + const vol = require('memfs').vol; + const _fs = new Union().use(fsLib); + _fs.constants = fsLib.constants; + return _fs.use(vol as unknown as typeof fs); +}); + +jest.mock('hasbin', () => ({ + sync: jest.fn() +})); + +jest.mock('@sap/mta-lib', () => { + return { + Mta: require('./utils/mock-mta').MockMta + }; +}); + +const mockGetHostEnvironment = jest.fn(); +const mockSendTelemetry = jest.fn(); +jest.mock('@sap-ux/fiori-generator-shared', () => ({ + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + ...(jest.requireActual('@sap-ux/fiori-generator-shared') as {}), + sendTelemetry: () => mockSendTelemetry(), + isExtensionInstalled: jest.fn().mockReturnValue(true), + getHostEnvironment: () => mockGetHostEnvironment(), + TelemetryHelper: { + initTelemetrySettings: jest.fn(), + createTelemetryData: jest.fn() + } +})); + +const hasbinSyncMock = hasbin.sync as jest.MockedFunction; + +const mockShowInformation = jest.fn(); +const mockShowError = jest.fn(); +const mockAppWizard = { + showInformation: mockShowInformation, + showError: mockShowError +}; + +describe('Cloud foundry generator tests', () => { + jest.setTimeout(10000); + let cwd: string; + let fsMock: Editor; + const cfGenPath = join(__dirname, '../src/app'); + const OUTPUT_DIR_PREFIX = join('/output'); + const testFixture = new TestFixture(); + + beforeEach(() => { + jest.clearAllMocks(); + memfs.vol.reset(); + const mockChdir = jest.spyOn(process, 'chdir'); + mockChdir.mockImplementation((dir): void => { + cwd = dir; + }); + fsMock = { + dump: jest.fn(), + commit: jest.fn().mockImplementation((callback) => callback()) + } as Partial as Editor; + }); + + beforeAll(async () => { + await initI18n(); + }); + + afterAll(() => { + jest.resetAllMocks(); + }); + + it('Validate Approuter prompting aborts if user doesnt want to proceed', async () => { + hasbinSyncMock.mockReturnValue(true); + mockFindCapProjectRoot.mockReturnValueOnce('/capmissingmta'); + const mockGenerateCAPConfig = jest.spyOn(cfDeployWriter, 'generateCAPConfig').mockResolvedValue(fsMock); + const mockGenerateAppConfig = jest.spyOn(cfDeployWriter, 'generateAppConfig').mockResolvedValue(fsMock); + jest.spyOn(fioriGenShared, 'isExtensionInstalled').mockImplementation(() => { + return true; + }); + + memfs.vol.fromNestedJSON( + { + [`.${OUTPUT_DIR_PREFIX}/capmissingmta/app/testui5app/webapp/manifest.json`]: testFixture.getContents( + 'cap/app/testui5app/webapp/manifest.json' + ), + [`.${OUTPUT_DIR_PREFIX}/capmissingmta/app/testui5app/package.json`]: testFixture.getContents( + 'cap/app/testui5app/package.json' + ), + [`.${OUTPUT_DIR_PREFIX}/capmissingmta/app/testui5app/ui5.yaml`]: + testFixture.getContents('cap/app/testui5app/ui5.yaml'), + [`.${OUTPUT_DIR_PREFIX}/capmissingmta/app/services.cds`]: + testFixture.getContents('cap/app/services.cds'), + [`.${OUTPUT_DIR_PREFIX}/capmissingmta/db/schmea.cds`]: testFixture.getContents('cap/db/schema.cds'), + [`.${OUTPUT_DIR_PREFIX}/capmissingmta/srv/cat-service.cds`]: + testFixture.getContents('cap/srv/cat-service.cds') + }, + '/' + ); + const appDir = join(OUTPUT_DIR_PREFIX, 'capmissingmta', 'app', 'testui5app'); + + await expect( + yeomanTest + .create( + CFGenerator, + { + resolved: cfGenPath + }, + { cwd: appDir } + ) + .withOptions({ + skipInstall: true, + appWizard: mockAppWizard, + launchStandaloneFromYui: true, + launchDeployConfigAsSubGenerator: false + }) + .withPrompts({ + addCapMtaContinue: false, + routerType: RouterModuleType.Managed, + mtaPath: join(OUTPUT_DIR_PREFIX, 'capmissingmta'), + mtaId: 'capmtaid' + }) + .run() + ).resolves.not.toThrow(); + expect(mockGenerateCAPConfig).not.toHaveBeenCalled(); + expect(mockGenerateAppConfig).not.toHaveBeenCalled(); + expect(mockFindCapProjectRoot).toHaveBeenCalled(); + expect(mockSendTelemetry).toHaveBeenCalled(); + }); + + it('Validate Approuter prompting is shown if HTML5 is being added to a CAP project with missing mta', async () => { + hasbinSyncMock.mockReturnValue(true); + mockFindCapProjectRoot.mockReturnValueOnce('/capmissingmta'); + const mockGenerateCAPConfig = jest.spyOn(cfDeployWriter, 'generateCAPConfig').mockResolvedValue(fsMock); + const mockGenerateAppConfig = jest.spyOn(cfDeployWriter, 'generateAppConfig').mockResolvedValue(fsMock); + jest.spyOn(fioriGenShared, 'isExtensionInstalled').mockImplementation(() => { + return true; + }); + + memfs.vol.fromNestedJSON( + { + [`.${OUTPUT_DIR_PREFIX}/capmissingmta/app/testui5app/webapp/manifest.json`]: testFixture.getContents( + 'cap/app/testui5app/webapp/manifest.json' + ), + [`.${OUTPUT_DIR_PREFIX}/capmissingmta/app/testui5app/package.json`]: testFixture.getContents( + 'cap/app/testui5app/package.json' + ), + [`.${OUTPUT_DIR_PREFIX}/capmissingmta/app/testui5app/ui5.yaml`]: + testFixture.getContents('cap/app/testui5app/ui5.yaml'), + [`.${OUTPUT_DIR_PREFIX}/capmissingmta/app/services.cds`]: + testFixture.getContents('cap/app/services.cds'), + [`.${OUTPUT_DIR_PREFIX}/capmissingmta/db/schmea.cds`]: testFixture.getContents('cap/db/schema.cds'), + [`.${OUTPUT_DIR_PREFIX}/capmissingmta/srv/cat-service.cds`]: + testFixture.getContents('cap/srv/cat-service.cds') + }, + '/' + ); + const appDir = join(OUTPUT_DIR_PREFIX, 'capmissingmta', 'app', 'testui5app'); + + await expect( + yeomanTest + .create( + CFGenerator, + { + resolved: cfGenPath + }, + { cwd: appDir } + ) + .withOptions({ + skipInstall: true, + appWizard: mockAppWizard, + launchStandaloneFromYui: true, + launchDeployConfigAsSubGenerator: false + }) + .withPrompts({ + addCapMtaContinue: true, + routerType: RouterModuleType.Managed, + mtaPath: join(OUTPUT_DIR_PREFIX, 'capmissingmta'), + mtaId: 'capmtaid' + }) + .run() + ).resolves.not.toThrow(); + expect(mockGenerateCAPConfig).toHaveBeenCalledWith( + expect.objectContaining({ + addCapMtaContinue: true, + mtaId: 'capmtaid', + mtaPath: '/output/capmissingmta', + routerType: 'managed' + }), + expect.anything(), + expect.anything() + ); + expect(mockGenerateAppConfig).toHaveBeenCalled(); + expect(mockFindCapProjectRoot).toHaveBeenCalled(); + expect(mockSendTelemetry).toHaveBeenCalled(); + }); +}); diff --git a/packages/cf-deploy-config-sub-generator/test/fixtures/cap/README.md b/packages/cf-deploy-config-sub-generator/test/fixtures/cap/README.md new file mode 100644 index 0000000000..dbac29eb15 --- /dev/null +++ b/packages/cf-deploy-config-sub-generator/test/fixtures/cap/README.md @@ -0,0 +1,25 @@ +# Getting Started + +Welcome to your new project. + +It contains these folders and files, following our recommended project layout: + +File or Folder | Purpose +---------|---------- +`app/` | content for UI frontends goes here +`db/` | your domain models and data go here +`srv/` | your service models and code go here +`package.json` | project metadata and configuration +`readme.md` | this getting started guide + + +## Next Steps + +- Open a new terminal and run `cds watch` +- (in VS Code simply choose _**Terminal** > Run Task > cds watch_) +- Start adding content, for example, a [db/schema.cds](db/schema.cds). + + +## Learn More + +Learn more at https://cap.cloud.sap/docs/get-started/. diff --git a/packages/cf-deploy-config-sub-generator/test/fixtures/cap/app/services.cds b/packages/cf-deploy-config-sub-generator/test/fixtures/cap/app/services.cds new file mode 100644 index 0000000000..fbb0384ad3 --- /dev/null +++ b/packages/cf-deploy-config-sub-generator/test/fixtures/cap/app/services.cds @@ -0,0 +1,2 @@ + +using from './project1/annotations'; \ No newline at end of file diff --git a/packages/cf-deploy-config-sub-generator/test/fixtures/cap/app/testui5app/README.md b/packages/cf-deploy-config-sub-generator/test/fixtures/cap/app/testui5app/README.md new file mode 100644 index 0000000000..8f3d6e8319 --- /dev/null +++ b/packages/cf-deploy-config-sub-generator/test/fixtures/cap/app/testui5app/README.md @@ -0,0 +1,34 @@ +## Application Details +| | +| ------------- | +|**Generation Date and Time**
Wed Feb 05 2025 15:24:28 GMT+0000 (Greenwich Mean Time)| +|**App Generator**
@sap/generator-fiori-freestyle| +|**App Generator Version**
1.16.3-pre-20250120124504-86cdda28f.0| +|**Generation Platform**
Visual Studio Code| +|**Template Used**
simple| +|**Service Type**
Local Cap| +|**Service URL**
http://localhost:4004/odata/v4/catalog/| +|**Module Name**
project1| +|**Application Title**
App Title| +|**Namespace**
| +|**UI5 Theme**
sap_horizon| +|**UI5 Version**
1.132.1| +|**Enable Code Assist Libraries**
False| +|**Enable TypeScript**
False| +|**Add Eslint configuration**
False| + +## project1 + +An SAP Fiori application. + +### Starting the generated app + +- This app has been generated using the SAP Fiori tools - App Generator, as part of the SAP Fiori tools suite. In order to launch the generated app, simply start your CAP project and navigate to the following location in your browser: + +http://localhost:4004/project1/webapp/index.html + +#### Pre-requisites: + +1. Active NodeJS LTS (Long Term Support) version and associated supported NPM version. (See https://nodejs.org) + + diff --git a/packages/cf-deploy-config-sub-generator/test/fixtures/cap/app/testui5app/annotations.cds b/packages/cf-deploy-config-sub-generator/test/fixtures/cap/app/testui5app/annotations.cds new file mode 100644 index 0000000000..57192cc1d8 --- /dev/null +++ b/packages/cf-deploy-config-sub-generator/test/fixtures/cap/app/testui5app/annotations.cds @@ -0,0 +1 @@ +using CatalogService as service from '../../srv/cat-service'; \ No newline at end of file diff --git a/packages/cf-deploy-config-sub-generator/test/fixtures/cap/app/testui5app/package.json b/packages/cf-deploy-config-sub-generator/test/fixtures/cap/app/testui5app/package.json new file mode 100644 index 0000000000..ba84f4b94e --- /dev/null +++ b/packages/cf-deploy-config-sub-generator/test/fixtures/cap/app/testui5app/package.json @@ -0,0 +1,19 @@ +{ + "name": "project1", + "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": { + "deploy-config": "npx -p @sap/ux-ui5-tooling fiori add deploy-config cf" + } +} diff --git a/packages/cf-deploy-config-sub-generator/test/fixtures/cap/app/testui5app/ui5.yaml b/packages/cf-deploy-config-sub-generator/test/fixtures/cap/app/testui5app/ui5.yaml new file mode 100644 index 0000000000..bb0057645a --- /dev/null +++ b/packages/cf-deploy-config-sub-generator/test/fixtures/cap/app/testui5app/ui5.yaml @@ -0,0 +1,23 @@ +# yaml-language-server: $schema=https://sap.github.io/ui5-tooling/schema/ui5.yaml.json + +specVersion: "3.1" +metadata: + name: project1 +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://sapui5.hana.ondemand.com + - name: fiori-tools-appreload + afterMiddleware: compression + configuration: + port: 35729 + path: webapp + delay: 300 diff --git a/packages/cf-deploy-config-sub-generator/test/fixtures/cap/app/testui5app/webapp/Component.js b/packages/cf-deploy-config-sub-generator/test/fixtures/cap/app/testui5app/webapp/Component.js new file mode 100644 index 0000000000..2cc94dd09f --- /dev/null +++ b/packages/cf-deploy-config-sub-generator/test/fixtures/cap/app/testui5app/webapp/Component.js @@ -0,0 +1,26 @@ +sap.ui.define([ + "sap/ui/core/UIComponent", + "project1/model/models" +], (UIComponent, models) => { + "use strict"; + + return UIComponent.extend("project1.Component", { + metadata: { + manifest: "json", + interfaces: [ + "sap.ui.core.IAsyncContentCreation" + ] + }, + + init() { + // call the base component's init function + UIComponent.prototype.init.apply(this, arguments); + + // set the device model + this.setModel(models.createDeviceModel(), "device"); + + // enable routing + this.getRouter().initialize(); + } + }); +}); \ No newline at end of file diff --git a/packages/cf-deploy-config-sub-generator/test/fixtures/cap/app/testui5app/webapp/controller/App.controller.js b/packages/cf-deploy-config-sub-generator/test/fixtures/cap/app/testui5app/webapp/controller/App.controller.js new file mode 100644 index 0000000000..3b9355dda6 --- /dev/null +++ b/packages/cf-deploy-config-sub-generator/test/fixtures/cap/app/testui5app/webapp/controller/App.controller.js @@ -0,0 +1,10 @@ +sap.ui.define([ + "sap/ui/core/mvc/Controller" +], (BaseController) => { + "use strict"; + + return BaseController.extend("project1.controller.App", { + onInit() { + } + }); +}); \ No newline at end of file diff --git a/packages/cf-deploy-config-sub-generator/test/fixtures/cap/app/testui5app/webapp/controller/View1.controller.js b/packages/cf-deploy-config-sub-generator/test/fixtures/cap/app/testui5app/webapp/controller/View1.controller.js new file mode 100644 index 0000000000..491f3309bb --- /dev/null +++ b/packages/cf-deploy-config-sub-generator/test/fixtures/cap/app/testui5app/webapp/controller/View1.controller.js @@ -0,0 +1,10 @@ +sap.ui.define([ + "sap/ui/core/mvc/Controller" +], (Controller) => { + "use strict"; + + return Controller.extend("project1.controller.View1", { + onInit() { + } + }); +}); \ No newline at end of file diff --git a/packages/cf-deploy-config-sub-generator/test/fixtures/cap/app/testui5app/webapp/css/style.css b/packages/cf-deploy-config-sub-generator/test/fixtures/cap/app/testui5app/webapp/css/style.css new file mode 100644 index 0000000000..f280a0e771 --- /dev/null +++ b/packages/cf-deploy-config-sub-generator/test/fixtures/cap/app/testui5app/webapp/css/style.css @@ -0,0 +1 @@ +/* Enter your custom styles here */ \ No newline at end of file diff --git a/packages/cf-deploy-config-sub-generator/test/fixtures/cap/app/testui5app/webapp/i18n/i18n.properties b/packages/cf-deploy-config-sub-generator/test/fixtures/cap/app/testui5app/webapp/i18n/i18n.properties new file mode 100644 index 0000000000..f64210fbdd --- /dev/null +++ b/packages/cf-deploy-config-sub-generator/test/fixtures/cap/app/testui5app/webapp/i18n/i18n.properties @@ -0,0 +1,11 @@ +# This is the resource bundle for project1 + +#Texts for manifest.json + +#XTIT: Application name +appTitle=App Title + +#YDES: Application description +appDescription=An SAP Fiori application. +#XTIT: Main view title +title=App Title \ No newline at end of file diff --git a/packages/cf-deploy-config-sub-generator/test/fixtures/cap/app/testui5app/webapp/index.html b/packages/cf-deploy-config-sub-generator/test/fixtures/cap/app/testui5app/webapp/index.html new file mode 100644 index 0000000000..9df17fc0fe --- /dev/null +++ b/packages/cf-deploy-config-sub-generator/test/fixtures/cap/app/testui5app/webapp/index.html @@ -0,0 +1,35 @@ + + + + + + + App Title + + + + +
+ + \ No newline at end of file diff --git a/packages/cf-deploy-config-sub-generator/test/fixtures/cap/app/testui5app/webapp/manifest.json b/packages/cf-deploy-config-sub-generator/test/fixtures/cap/app/testui5app/webapp/manifest.json new file mode 100644 index 0000000000..ff30f03a03 --- /dev/null +++ b/packages/cf-deploy-config-sub-generator/test/fixtures/cap/app/testui5app/webapp/manifest.json @@ -0,0 +1,114 @@ +{ + "_version": "1.65.0", + "sap.app": { + "id": "project1", + "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": "6cf48606-c416-45d4-976b-8f858c1d4fe4" + }, + "dataSources": { + "mainService": { + "uri": "/odata/v4/catalog/", + "type": "OData", + "settings": { + "annotations": [], + "odataVersion": "4.0" + } + } + } + }, + "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.1", + "libs": { + "sap.m": {}, + "sap.ui.core": {} + } + }, + "contentDensities": { + "compact": true, + "cozy": true + }, + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "settings": { + "bundleName": "project1.i18n.i18n" + } + }, + "": { + "dataSource": "mainService", + "preload": true, + "settings": { + "operationMode": "Server", + "autoExpandSelect": true, + "earlyRequests": true + } + } + }, + "resources": { + "css": [ + { + "uri": "css/style.css" + } + ] + }, + "routing": { + "config": { + "routerClass": "sap.m.routing.Router", + "controlAggregation": "pages", + "controlId": "app", + "transition": "slide", + "type": "View", + "viewType": "XML", + "path": "project1.view" + }, + "routes": [ + { + "name": "RouteView1", + "pattern": ":?query:", + "target": [ + "TargetView1" + ] + } + ], + "targets": { + "TargetView1": { + "id": "View1", + "name": "View1" + } + } + }, + "rootView": { + "viewName": "project1.view.App", + "type": "XML", + "id": "App" + } + } +} diff --git a/packages/cf-deploy-config-sub-generator/test/fixtures/cap/app/testui5app/webapp/model/models.js b/packages/cf-deploy-config-sub-generator/test/fixtures/cap/app/testui5app/webapp/model/models.js new file mode 100644 index 0000000000..47b027fbc0 --- /dev/null +++ b/packages/cf-deploy-config-sub-generator/test/fixtures/cap/app/testui5app/webapp/model/models.js @@ -0,0 +1,20 @@ +sap.ui.define([ + "sap/ui/model/json/JSONModel", + "sap/ui/Device" +], +function (JSONModel, Device) { + "use strict"; + + return { + /** + * Provides runtime information for the device the UI5 app is running on as a JSONModel. + * @returns {sap.ui.model.json.JSONModel} The device model. + */ + createDeviceModel: function () { + var oModel = new JSONModel(Device); + oModel.setDefaultBindingMode("OneWay"); + return oModel; + } + }; + +}); \ No newline at end of file diff --git a/packages/cf-deploy-config-sub-generator/test/fixtures/cap/app/testui5app/webapp/test/flpSandbox.html b/packages/cf-deploy-config-sub-generator/test/fixtures/cap/app/testui5app/webapp/test/flpSandbox.html new file mode 100644 index 0000000000..6f8d5d2a49 --- /dev/null +++ b/packages/cf-deploy-config-sub-generator/test/fixtures/cap/app/testui5app/webapp/test/flpSandbox.html @@ -0,0 +1,84 @@ + + + + + + + + {{appTitle}} + + + + + + + + + + + + + + + + diff --git a/packages/cf-deploy-config-sub-generator/test/fixtures/cap/app/testui5app/webapp/view/App.view.xml b/packages/cf-deploy-config-sub-generator/test/fixtures/cap/app/testui5app/webapp/view/App.view.xml new file mode 100644 index 0000000000..adf4758721 --- /dev/null +++ b/packages/cf-deploy-config-sub-generator/test/fixtures/cap/app/testui5app/webapp/view/App.view.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/packages/cf-deploy-config-sub-generator/test/fixtures/cap/app/testui5app/webapp/view/View1.view.xml b/packages/cf-deploy-config-sub-generator/test/fixtures/cap/app/testui5app/webapp/view/View1.view.xml new file mode 100644 index 0000000000..d2c15055a3 --- /dev/null +++ b/packages/cf-deploy-config-sub-generator/test/fixtures/cap/app/testui5app/webapp/view/View1.view.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/packages/cf-deploy-config-sub-generator/test/fixtures/cap/db/data/my.bookshop-Books.csv b/packages/cf-deploy-config-sub-generator/test/fixtures/cap/db/data/my.bookshop-Books.csv new file mode 100644 index 0000000000..0210c0909d --- /dev/null +++ b/packages/cf-deploy-config-sub-generator/test/fixtures/cap/db/data/my.bookshop-Books.csv @@ -0,0 +1,3 @@ +ID,title,stock +1,Wuthering Heights,100 +2,Jane Eyre,500 diff --git a/packages/cf-deploy-config-sub-generator/test/fixtures/cap/db/schema.cds b/packages/cf-deploy-config-sub-generator/test/fixtures/cap/db/schema.cds new file mode 100644 index 0000000000..653cc58f79 --- /dev/null +++ b/packages/cf-deploy-config-sub-generator/test/fixtures/cap/db/schema.cds @@ -0,0 +1,7 @@ +namespace my.bookshop; + +entity Books { + key ID : Integer; + title : String; + stock : Integer; +} diff --git a/packages/cf-deploy-config-sub-generator/test/fixtures/cap/eslint.config.mjs b/packages/cf-deploy-config-sub-generator/test/fixtures/cap/eslint.config.mjs new file mode 100644 index 0000000000..2fdb4320ca --- /dev/null +++ b/packages/cf-deploy-config-sub-generator/test/fixtures/cap/eslint.config.mjs @@ -0,0 +1,2 @@ +import cds from '@sap/cds/eslint.config.mjs' +export default [ ...cds.recommended ] diff --git a/packages/cf-deploy-config-sub-generator/test/fixtures/cap/package.json b/packages/cf-deploy-config-sub-generator/test/fixtures/cap/package.json new file mode 100644 index 0000000000..b3c13ce05c --- /dev/null +++ b/packages/cf-deploy-config-sub-generator/test/fixtures/cap/package.json @@ -0,0 +1,20 @@ +{ + "name": "captestproject", + "version": "1.0.0", + "description": "A simple CAP project.", + "repository": "", + "license": "UNLICENSED", + "private": true, + "dependencies": { + "@sap/cds": "^8", + "express": "^4" + }, + "devDependencies": { + "@cap-js/sqlite": "^1", + "@cap-js/cds-types": "^0.7.0" + }, + "scripts": { + "start": "cds-serve", + "watch-project1": "cds watch --open project1/webapp/index.html?sap-ui-xx-viewCache=false" + } +} diff --git a/packages/cf-deploy-config-sub-generator/test/fixtures/cap/srv/cat-service.cds b/packages/cf-deploy-config-sub-generator/test/fixtures/cap/srv/cat-service.cds new file mode 100644 index 0000000000..dd8a434031 --- /dev/null +++ b/packages/cf-deploy-config-sub-generator/test/fixtures/cap/srv/cat-service.cds @@ -0,0 +1,5 @@ +using my.bookshop as my from '../db/schema'; + +service CatalogService { + @readonly entity Books as projection on my.Books; +} 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 2a0f4689e4..a246ee1bdd 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 @@ -23,7 +23,6 @@ import { ResourceMTADestination, Rimraf, RimrafVersion, - rootDeployMTAScript, UI5DeployBuildScript, undeployMTAScript, WelcomeFile, @@ -35,7 +34,8 @@ import { getDestinationProperties, getTemplatePath, readManifest, - toPosixPath + toPosixPath, + updateRootPackage } from '../utils'; import { addMtaDeployParameters, @@ -230,7 +230,9 @@ async function generateDeployConfig(cfAppConfig: CFAppConfig, fs: Editor): Promi await appendCloudFoundryConfigurations(config, fs); await updateManifest(config, fs); await updateHTML5AppPackage(config, fs); - await updateRootPackage(config, fs); + if (config.isMtaRoot) { + await updateRootPackage({ mtaId: config.mtaId ?? config.appId, rootPath: config.rootPath }, fs); + } } /** @@ -408,31 +410,6 @@ async function updateHTML5AppPackage(cfConfig: CFConfig, fs: Editor): Promise { - 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); - await addPackageDevDependency(cfConfig.rootPath, MbtPackage, MbtPackageVersion, fs); - let deployArgs: string[] = []; - if (fs.exists(join(cfConfig.rootPath, MTAFileExtension))) { - deployArgs = ['-e', MTAFileExtension]; - } - for (const script of [ - { name: 'undeploy', run: undeployMTAScript(cfConfig.mtaId ?? cfConfig.appId) }, - { name: 'build', run: `${MTABuildScript} --mtar archive` }, - { name: 'deploy', run: rootDeployMTAScript(deployArgs) } - ]) { - await updatePackageScript(cfConfig.rootPath, script.name, script.run, fs); - } - } -} /** * Generate UI5 deploy config. * 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 0a5b5abe1b..e76f5f4ec1 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,6 +1,6 @@ import { create as createStorage } from 'mem-fs'; import { create, type Editor } from 'mem-fs-editor'; -import { addSupportingConfig, addRoutingConfig } from '../utils'; +import { updateRootPackage, addRoutingConfig } from '../utils'; import { createCAPMTA, validateMtaConfig, isMTAFound } from '../mta-config'; import LoggerHelper from '../logger-helper'; import type { Logger } from '@sap-ux/logger'; @@ -29,8 +29,7 @@ export async function generateCAPConfig(config: CAPConfig, fs?: Editor, logger?: 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)}`); + await updateRootPackage({ mtaId: config.mtaId, rootPath: config.mtaPath }, fs); return fs; } diff --git a/packages/cf-deploy-config-writer/src/index.ts b/packages/cf-deploy-config-writer/src/index.ts index d52fea8e20..61257e7ad1 100644 --- a/packages/cf-deploy-config-writer/src/index.ts +++ b/packages/cf-deploy-config-writer/src/index.ts @@ -1,4 +1,4 @@ export * from './mta-config'; export * from './cf-writer'; export { DefaultMTADestination } from './constants'; -export { CFBaseConfig, CFAppConfig, RouterModuleType, ApiHubConfig, ApiHubType } from './types'; +export { CFBaseConfig, CFAppConfig, CAPConfig, RouterModuleType, ApiHubConfig, ApiHubType } from './types'; 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 7608650462..62acaca75d 100644 --- a/packages/cf-deploy-config-writer/src/mta-config/index.ts +++ b/packages/cf-deploy-config-writer/src/mta-config/index.ts @@ -147,6 +147,7 @@ export function createCAPMTA(cwd: string, options?: string[]): void { if (result?.error) { throw new Error(`Something went wrong installing node modules! ${result.error}`); } + LoggerHelper.logger?.debug(t('debug.capMtaCreated')); } /** 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 8cf7bf4b15..4223b7c7fc 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 @@ -3,12 +3,13 @@ "logError": "{{method}} error found {{ error }}", "mtaLoaded": "MTA {{ type }} loaded", "ui5YamlDoesNotExist": "File ui5.yaml does not exist in the project", + "capMtaCreated": "CAP MTA Configuration created", "mtaCreated": "MTA Configuration created {{mtaPath}}.", "mtaSaved": "MTA Configuration has been saved.", "mtaSavedFailed": "MTA saved failed with error {{- error }}" }, "error": { - "unableToLoadMTA": "Unable to load mta.yaml configuration {{ mtaDir }}, error thrown {{ error }}.", + "unableToLoadMTA": "Unable to load mta.yaml configuration {{- mtaDir }}, error thrown {{- error }}.", "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}}.", diff --git a/packages/cf-deploy-config-writer/src/types/index.ts b/packages/cf-deploy-config-writer/src/types/index.ts index 0392348f02..22a5fd8f08 100644 --- a/packages/cf-deploy-config-writer/src/types/index.ts +++ b/packages/cf-deploy-config-writer/src/types/index.ts @@ -27,10 +27,12 @@ export type MTADestinationType = Destination & { ServiceKeyName: string; 'sap.cloud.service': string; }; -export enum RouterModuleType { - Standard = 'standard', - Managed = 'managed' -} +export const RouterModuleType = { + Standard: 'standard', + Managed: 'managed' +} as const; + +export type RouterModuleType = (typeof RouterModuleType)[keyof typeof RouterModuleType]; export interface MTABaseConfig { mtaId: string; mtaPath: string; diff --git a/packages/cf-deploy-config-writer/src/utils.ts b/packages/cf-deploy-config-writer/src/utils.ts index afa670dc52..251736ac7a 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, FileName, type Manifest } from '@sap-ux/project-access'; +import { addPackageDevDependency, FileName, type Manifest, updatePackageScript } from '@sap-ux/project-access'; import { MTAVersion, UI5BuilderWebIdePackage, @@ -18,7 +18,15 @@ import { UI5TaskZipperPackageVersion, XSSecurityFile, RouterModule, - XSAppFile + XSAppFile, + rootDeployMTAScript, + undeployMTAScript, + MTAFileExtension, + Rimraf, + RimrafVersion, + MbtPackageVersion, + MbtPackage, + MTABuildScript } from './constants'; import type { Editor } from 'mem-fs-editor'; import { type MTABaseConfig, type CFConfig, type CFBaseConfig, RouterModuleType } from './types'; @@ -271,3 +279,34 @@ export function setMtaDefaults(config: CFBaseConfig): void { config.addConnectivityService ||= false; config.mtaId = toMtaModuleName(config.mtaId); } + +/** + * Update the root package.json with scripts to deploy the MTA. + * + * @param {object} Options + * @param {string} Options.mtaId - MTA ID to be written to package.json + * @param {string} Options.rootPath - MTA project path + * @param fs + */ +export async function updateRootPackage( + { mtaId, rootPath }: { mtaId: string; rootPath: string }, + fs: Editor +): Promise { + const packageExists = fs.exists(join(rootPath, FileName.Package)); + // Append mta scripts only if mta.yaml is at a different level to the HTML5 app + if (packageExists) { + await addPackageDevDependency(rootPath, Rimraf, RimrafVersion, fs); + await addPackageDevDependency(rootPath, MbtPackage, MbtPackageVersion, fs); + let deployArgs: string[] = []; + if (fs.exists(join(rootPath, MTAFileExtension))) { + deployArgs = ['-e', MTAFileExtension]; + } + for (const script of [ + { name: 'undeploy', run: undeployMTAScript(mtaId) }, + { name: 'build', run: `${MTABuildScript} --mtar archive` }, + { name: 'deploy', run: rootDeployMTAScript(deployArgs) } + ]) { + await updatePackageScript(rootPath, script.name, script.run, fs); + } + } +} 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 index 7070fa0ae6..2463976646 100644 --- 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 @@ -133,6 +133,44 @@ resources: " `; +exports[`CF Writer CAP Validate generation of CAP mta configurations managed 2`] = ` +"{ + \\"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\\", + \\"rimraf\\": \\"^5.0.5\\", + \\"mbt\\": \\"^1.2.29\\" + }, + \\"scripts\\": { + \\"start\\": \\"cds-serve\\", + \\"undeploy\\": \\"cf undeploy captestproject --delete-services --delete-service-keys --delete-service-brokers\\", + \\"build\\": \\"rimraf resources mta_archives && mbt build --mtar archive\\", + \\"deploy\\": \\"cf deploy mta_archives/archive.mtar --retries 1\\" + }, + \\"cds\\": { + \\"requires\\": { + \\"auth\\": \\"xsuaa\\", + \\"connectivity\\": true, + \\"destinations\\": true, + \\"html5-repo\\": true + } + } +} +" +`; + exports[`CF Writer CAP Validate generation of CAP mta configurations standard 1`] = ` "_schema-version: 3.3.0 ID: captestproject @@ -263,6 +301,44 @@ resources: `; exports[`CF Writer CAP Validate generation of CAP mta configurations standard 2`] = ` +"{ + \\"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\\", + \\"rimraf\\": \\"^5.0.5\\", + \\"mbt\\": \\"^1.2.29\\" + }, + \\"scripts\\": { + \\"start\\": \\"cds-serve\\", + \\"undeploy\\": \\"cf undeploy captestproject --delete-services --delete-service-keys --delete-service-brokers\\", + \\"build\\": \\"rimraf resources mta_archives && mbt build --mtar archive\\", + \\"deploy\\": \\"cf deploy mta_archives/archive.mtar --retries 1\\" + }, + \\"cds\\": { + \\"requires\\": { + \\"auth\\": \\"xsuaa\\", + \\"connectivity\\": true, + \\"destinations\\": true, + \\"html5-repo\\": true + } + } +} +" +`; + +exports[`CF Writer CAP Validate generation of CAP mta configurations standard 3`] = ` "{ \\"name\\": \\"app-router\\", \\"private\\": true, @@ -284,7 +360,7 @@ exports[`CF Writer CAP Validate generation of CAP mta configurations standard 2` " `; -exports[`CF Writer CAP Validate generation of CAP mta configurations standard 3`] = ` +exports[`CF Writer CAP Validate generation of CAP mta configurations standard 4`] = ` "{ \\"authenticationMethod\\": \\"route\\", \\"routes\\": [ 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 2b558724bb..e0c9b625a7 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 @@ -83,6 +83,7 @@ describe('CF Writer CAP', () => { logger ); expect(localFs.read(join(mtaPath, 'mta.yaml'))).toMatchSnapshot(); + expect(localFs.read(join(mtaPath, 'package.json'))).toMatchSnapshot(); // Ensure it hasn't changed! expect(getCapProjectTypeMock).toHaveBeenCalled(); expect(spawnMock.mock.calls).toHaveLength(2); expect(spawnMock).toHaveBeenCalledWith( diff --git a/packages/deploy-config-generator-shared/src/index.ts b/packages/deploy-config-generator-shared/src/index.ts index 981eda20a4..30c7edfe73 100644 --- a/packages/deploy-config-generator-shared/src/index.ts +++ b/packages/deploy-config-generator-shared/src/index.ts @@ -1,3 +1,3 @@ export { DeploymentGenerator } from './base/generator'; export * from './utils'; -export { getConfirmConfigUpdatePrompt } from './prompts'; +export { getConfirmConfigUpdatePrompt, getConfirmMtaContinuePrompt } from './prompts'; diff --git a/packages/deploy-config-generator-shared/src/prompts/index.ts b/packages/deploy-config-generator-shared/src/prompts/index.ts index 8d821d3c7c..da7ad6dc8a 100644 --- a/packages/deploy-config-generator-shared/src/prompts/index.ts +++ b/packages/deploy-config-generator-shared/src/prompts/index.ts @@ -24,3 +24,19 @@ export function getConfirmConfigUpdatePrompt(configType?: string): Question[] { } ]; } + +/** + * Generate a new prompt asking if the user wants to create an approuter configuration within a CAP project. + * + * @returns the CAP MTA continue question. + */ +export function getConfirmMtaContinuePrompt(): Question[] { + return [ + { + type: 'confirm', + name: 'addCapMtaContinue', + message: t('prompts.confirmCAPMtaContinue.message'), + default: false + } + ]; +} diff --git a/packages/deploy-config-generator-shared/src/translations/deploy-config-generator-shared.i18n.json b/packages/deploy-config-generator-shared/src/translations/deploy-config-generator-shared.i18n.json index 4a4e2f4e1b..343f3e3154 100644 --- a/packages/deploy-config-generator-shared/src/translations/deploy-config-generator-shared.i18n.json +++ b/packages/deploy-config-generator-shared/src/translations/deploy-config-generator-shared.i18n.json @@ -2,11 +2,13 @@ "prompts": { "confirmConfigUpdate": { "message": "{{- configType}} configuration is managed centrally as part of the CI pipeline, local updates to the configuration will not be for productive use. Are you sure you want to continue?" + }, + "confirmCAPMtaContinue": { + "message": "There is no mta.yaml file defined for this project. In order to add deployment configuration for this application, this file must be present. Do you want to create an mta.yaml to continue?" } }, "errors": { "abortSignal": "Generator aborted", - "capDeploymentNoMta": "The SAP Fiori application is within a CAP project and deployment should be configured as part of the CAP project. Please ensure you have a mta.yaml file defined for this project.", "fileDoesNotExist": "File does not exist: {{- filePath}}", "folderDoesNotExist": "Folder path does not exist: {{- filePath}}", "noAppName": "Could not determine app name from manifest", diff --git a/packages/deploy-config-generator-shared/src/utils/error-handler.ts b/packages/deploy-config-generator-shared/src/utils/error-handler.ts index cc76d8cfed..8504115f0f 100644 --- a/packages/deploy-config-generator-shared/src/utils/error-handler.ts +++ b/packages/deploy-config-generator-shared/src/utils/error-handler.ts @@ -9,8 +9,7 @@ export enum ERROR_TYPE { NO_MANIFEST = 'NO_MANIFEST', NO_APP_NAME = 'NO_APP_NAME', NO_CDS_BIN = 'NO_CDS_BIN', - NO_MTA_BIN = 'NO_MTA_BIN', - CAP_DEPLOYMENT_NO_MTA = 'CAP_DEPLOYMENT_NO_MTA' + NO_MTA_BIN = 'NO_MTA_BIN' } /** @@ -37,8 +36,7 @@ export class ErrorHandler { [ERROR_TYPE.NO_MANIFEST]: () => t('errors.noManifest'), [ERROR_TYPE.NO_APP_NAME]: () => t('errors.noAppName'), [ERROR_TYPE.NO_CDS_BIN]: () => ErrorHandler.cannotFindBinary(cdsExecutable, cdsPkg), - [ERROR_TYPE.NO_MTA_BIN]: () => ErrorHandler.cannotFindBinary(mtaExecutable, mtaPkg), - [ERROR_TYPE.CAP_DEPLOYMENT_NO_MTA]: () => t('errors.capDeploymentNoMta') + [ERROR_TYPE.NO_MTA_BIN]: () => ErrorHandler.cannotFindBinary(mtaExecutable, mtaPkg) }; public static readonly noBaseConfig = (baseConfig: string): string => t('errors.noBaseConfig', { baseConfig }); diff --git a/packages/deploy-config-generator-shared/test/error-handler.test.ts b/packages/deploy-config-generator-shared/test/error-handler.test.ts index deca567cb7..f683737dbc 100644 --- a/packages/deploy-config-generator-shared/test/error-handler.test.ts +++ b/packages/deploy-config-generator-shared/test/error-handler.test.ts @@ -51,7 +51,6 @@ describe('Error Message Methods', () => { expect(ErrorHandler.getErrorMsgFromType(ERROR_TYPE.NO_MTA_BIN)).toBe( t('errors.noBinary', { bin: mtaExecutable, pkg: mtaPkg }) ); - expect(ErrorHandler.getErrorMsgFromType(ERROR_TYPE.CAP_DEPLOYMENT_NO_MTA)).toBe(t('errors.capDeploymentNoMta')); }); });