diff --git a/.changeset/lovely-planets-march.md b/.changeset/lovely-planets-march.md new file mode 100644 index 0000000000..de5eff10ad --- /dev/null +++ b/.changeset/lovely-planets-march.md @@ -0,0 +1,9 @@ +--- +'@sap-ux/cf-deploy-config-sub-generator': minor +'@sap-ux/cf-deploy-config-inquirer': minor +'@sap-ux/abap-deploy-config-sub-generator': patch +'@sap-ux/deploy-config-generator-shared': patch +'@sap-ux/flp-config-sub-generator': patch +--- + +adds new cf generator diff --git a/packages/abap-deploy-config-sub-generator/src/translations/abap-deploy-config-sub-generator.i18n.json b/packages/abap-deploy-config-sub-generator/src/translations/abap-deploy-config-sub-generator.i18n.json index b72b0f3a08..7f468a61dc 100644 --- a/packages/abap-deploy-config-sub-generator/src/translations/abap-deploy-config-sub-generator.i18n.json +++ b/packages/abap-deploy-config-sub-generator/src/translations/abap-deploy-config-sub-generator.i18n.json @@ -11,7 +11,7 @@ "configNotFound": "No existing deployment configuration file found", "indexExists": "'webapp/index.html' already exists and won't be overwritten", "initFailed": "Initializing failed, unable to process project configuration. {{- error}}", - "initTelemetry": "Initializing telemetry", + "initTelemetry": "Initializing telemetry in ABAP deployment configuration generator", "appRootPath": "App loaded from {{- appRootPath}}" } } diff --git a/packages/cf-deploy-config-inquirer/src/index.ts b/packages/cf-deploy-config-inquirer/src/index.ts index 7f30e6d762..8c3aee888c 100644 --- a/packages/cf-deploy-config-inquirer/src/index.ts +++ b/packages/cf-deploy-config-inquirer/src/index.ts @@ -86,5 +86,7 @@ export { getAppRouterPrompts, type CfAppRouterDeployConfigPromptOptions, RouterModuleType, + type CfDeployConfigQuestions, + type CfDeployConfigAnswers, type CfAppRouterDeployConfigAnswers }; diff --git a/packages/cf-deploy-config-inquirer/src/prompts/prompts.ts b/packages/cf-deploy-config-inquirer/src/prompts/prompts.ts index 0a3bda17d6..51854f4e04 100644 --- a/packages/cf-deploy-config-inquirer/src/prompts/prompts.ts +++ b/packages/cf-deploy-config-inquirer/src/prompts/prompts.ts @@ -83,7 +83,7 @@ async function getDestinationNamePrompt( * * @returns {ConfirmQuestion} Returns a confirmation question object for configuring the application router. */ -function getAddManagedRouterPrompt(): CfDeployConfigQuestions { +function getAddManagedAppRouterPrompt(): CfDeployConfigQuestions { return { type: 'confirm', name: promptNames.addManagedAppRouter, @@ -134,7 +134,7 @@ export async function getQuestions( if (addManagedAppRouter) { log?.info(t('info.addManagedAppRouter')); - questions.push(getAddManagedRouterPrompt()); + questions.push(getAddManagedAppRouterPrompt()); } if (addOverwriteQuestion) { diff --git a/packages/cf-deploy-config-inquirer/src/translations/cf-deploy-config-inquirer.i18n.json b/packages/cf-deploy-config-inquirer/src/translations/cf-deploy-config-inquirer.i18n.json index a642aba312..c9c4d3cbd2 100644 --- a/packages/cf-deploy-config-inquirer/src/translations/cf-deploy-config-inquirer.i18n.json +++ b/packages/cf-deploy-config-inquirer/src/translations/cf-deploy-config-inquirer.i18n.json @@ -25,7 +25,7 @@ "emptyDestinationNameError": "You must provide a destination name in order to continue.", "destinationNameError": "The destination name must only contain letters, digits, dashes and underscores.", "destinationNameLengthError": "Destination name cannot contain more than 200 characters", - "folderDoesNotExistError": "Folder path does not exist: {{filePath}}", + "folderDoesNotExistError": "Folder path does not exist: {{- filePath}}", "noMtaIdError": "MTA ID cannot be empty", "invalidMtaIdError": "The ID can only contain letters, numbers, dashes, periods and underscores (but no spaces).", "mtaIdAlreadyExistError": "A folder with same name already exist at {{- mtaPath}}", diff --git a/packages/cf-deploy-config-inquirer/src/types.ts b/packages/cf-deploy-config-inquirer/src/types.ts index 95b79c9c7b..9c5d9d9d4b 100644 --- a/packages/cf-deploy-config-inquirer/src/types.ts +++ b/packages/cf-deploy-config-inquirer/src/types.ts @@ -111,7 +111,7 @@ export interface CfDeployConfigAnswers { /** The selected Cloud Foundry destination. */ destinationName?: string; /** Indicates whether the user opted to include a managed application router. */ - addManagedRouter?: boolean; + addManagedAppRouter?: boolean; /** Indicates whether the user opted to overwrite the destination. */ overwrite?: boolean; } @@ -167,7 +167,7 @@ export interface CfSystemChoice { /** Value associated with the system choice. */ value: string; /** Flag indicating if the system choice is an SCP destination. */ - scp: boolean; + scp?: boolean; /** URL associated with the system choice. */ - url: string; + url?: string; } diff --git a/packages/cf-deploy-config-inquirer/test/index.test.ts b/packages/cf-deploy-config-inquirer/test/index.test.ts index 833841c2e3..27100f56a4 100644 --- a/packages/cf-deploy-config-inquirer/test/index.test.ts +++ b/packages/cf-deploy-config-inquirer/test/index.test.ts @@ -31,7 +31,7 @@ describe('index', () => { it('should prompt with inquirer adapter', async () => { const answers: CfDeployConfigAnswers = { destinationName: 'testDestination', - addManagedRouter: true, + addManagedAppRouter: true, overwrite: true }; diff --git a/packages/cf-deploy-config-inquirer/test/prompts.test.ts b/packages/cf-deploy-config-inquirer/test/prompts.test.ts index 0bc4c13da1..12bcdd944e 100644 --- a/packages/cf-deploy-config-inquirer/test/prompts.test.ts +++ b/packages/cf-deploy-config-inquirer/test/prompts.test.ts @@ -187,7 +187,7 @@ describe('Prompt Generation Tests', () => { }); }); - describe('getAddManagedRouterPrompt', () => { + describe('getaddManagedAppRouterPrompt', () => { beforeEach(() => { promptOptions = { ...promptOptions, diff --git a/packages/cf-deploy-config-sub-generator/package.json b/packages/cf-deploy-config-sub-generator/package.json index 61739f3ab6..28ffa6b823 100644 --- a/packages/cf-deploy-config-sub-generator/package.json +++ b/packages/cf-deploy-config-sub-generator/package.json @@ -11,7 +11,7 @@ "url": "https://github.com/SAP/open-ux-tools/issues?q=is%3Aopen+is%3Aissue" }, "license": "Apache-2.0", - "main": "generators/app-router/index.js", + "main": "generators/app/index.js", "scripts": { "build": "tsc --build", "clean": "rimraf --glob generators test/test-output coverage *.tsbuildinfo", @@ -31,11 +31,16 @@ ], "dependencies": { "@sap-devx/yeoman-ui-types": "1.14.4", + "@sap-ux/btp-utils": "workspace:*", "@sap-ux/cf-deploy-config-writer": "workspace:*", "@sap-ux/cf-deploy-config-inquirer": "workspace:*", "@sap-ux/deploy-config-generator-shared": "workspace:*", + "@sap-ux/feature-toggle": "workspace:*", + "@sap-ux/fiori-generator-shared": "workspace:*", "@sap-ux/i18n": "workspace:*", "@sap-ux/inquirer-common": "workspace:*", + "@sap-ux/project-access": "workspace:*", + "@sap-ux/ui5-config": "workspace:*", "hasbin": "1.2.3", "i18next": "23.5.1", "yeoman-generator": "5.10.0" @@ -45,6 +50,8 @@ "@types/hasbin": "1.2.2", "@types/inquirer": "8.2.6", "@types/js-yaml": "4.0.9", + "@types/mem-fs": "1.1.2", + "@types/mem-fs-editor": "7.0.1", "@types/yeoman-generator": "5.2.11", "@types/yeoman-environment": "2.10.11", "@types/yeoman-test": "4.0.6", diff --git a/packages/cf-deploy-config-sub-generator/src/app/index.ts b/packages/cf-deploy-config-sub-generator/src/app/index.ts new file mode 100644 index 0000000000..a5b89fbc65 --- /dev/null +++ b/packages/cf-deploy-config-sub-generator/src/app/index.ts @@ -0,0 +1,322 @@ +import { join, dirname } from 'path'; +import { platform } from 'os'; +import hasbin = require('hasbin'); +import { AppWizard, MessageType } from '@sap-devx/yeoman-ui-types'; +import { + sendTelemetry, + TelemetryHelper, + isExtensionInstalled, + YUI_EXTENSION_ID, + YUI_MIN_VER_FILES_GENERATED_MSG +} 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 { + DeploymentGenerator, + showOverwriteQuestion, + bail, + handleErrorMessage, + ErrorHandler, + ERROR_TYPE, + mtaExecutable, + cdsExecutable, + generateDestinationName, + getDestination +} 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 type { Logger } from '@sap-ux/logger'; +import type { CfDeployConfigOptions } from './types'; +import type { CfDeployConfigAnswers } from '@sap-ux/cf-deploy-config-inquirer/dist/types'; +import type { YeomanEnvironment } from '@sap-ux/fiori-generator-shared'; + +/** + * Cloud Foundry deployment configuration generator. + */ +export default class extends DeploymentGenerator { + private readonly appWizard: AppWizard; + private readonly vscode: unknown; + private readonly launchDeployConfigAsSubGenerator: boolean; + private readonly launchStandaloneFromYui?: boolean; + private readonly appPath: string; + private readonly addMtaDestination: boolean; + private readonly apiHubConfig?: ApiHubConfig; + private readonly cloudServiceName?: string; + private readonly serviceBase?: string; + + private answers: CfDeployConfigAnswers & Partial = {}; + private projectRoot: string; + private mtaPath?: string; + private isCap = false; + private isAbapDirectServiceBinding = false; + private lcapModeOnly = false; + private servicePath?: string; + private destinationName: string; + + private abort = false; + private deployConfigExists = false; + + /** + * Constructor for the CF deployment configuration generator. + * + * @param args - arguments + * @param opts - cf deploy config options + */ + constructor(args: string | string[], opts: CfDeployConfigOptions) { + super(args, opts); + + this.launchDeployConfigAsSubGenerator = opts.launchDeployConfigAsSubGenerator ?? false; + this.launchStandaloneFromYui = opts.launchStandaloneFromYui; + this.appWizard = opts.appWizard ?? AppWizard.create(opts); + this.vscode = opts.vscode; + 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.lcapModeOnly = opts.lcapModeOnly ?? false; + this.cloudServiceName = opts.cloudServiceName || undefined; + this.apiHubConfig = opts.apiHubConfig; + this.servicePath = opts.appGenServicePath; + this.serviceBase = opts.appGenServiceHost; + this.appPath = opts.appRootPath ?? this.destinationRoot(); + this.projectRoot = opts.projectRoot ?? this.destinationRoot(); + } + + public async initializing(): Promise { + await super.initializing(); + await initI18n(); + + if ((this.env as unknown as YeomanEnvironment).conflicter) { + (this.env as unknown as YeomanEnvironment).conflicter.force = this.options.force ?? true; + } + + DeploymentGenerator.logger?.debug(t('cfGen.debug.initTelemetry')); + + await TelemetryHelper.initTelemetrySettings({ + consumerModule: { + name: '@sap-ux/cf-deploy-config-sub-generator', + version: this.rootGeneratorVersion() + }, + internalFeature: isInternalFeaturesSettingEnabled(), + watchTelemetrySettingStore: false + }); + + if (!this.launchDeployConfigAsSubGenerator) { + await this._init(); + } + } + + private async _init(): Promise { + // mta executable is required as mta-lib is used + if (!hasbin.sync(mtaExecutable)) { + 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; + } + } + + private async _processProjectPaths(): Promise { + const mtaPathResult = await getMtaPath(this.appPath); + this.mtaPath = mtaPathResult?.mtaPath; + const capRoot = await findCapProjectRoot(this.appPath); + if (capRoot) { + if (!hasbin.sync(cdsExecutable)) { + bail(ErrorHandler.getErrorMsgFromType(ERROR_TYPE.NO_CDS_BIN)); + } + this.isCap = true; + this.projectRoot = capRoot; + } else if (this.mtaPath) { + this.projectRoot = dirname(this.mtaPath); + } + } + + private async _processProjectConfigs(): Promise { + const baseConfigFile = this.options.base ?? FileName.Ui5Yaml; + const baseConfigExists = this.fs.exists(join(this.appPath, baseConfigFile)); + if (!baseConfigExists) { + bail(ErrorHandler.noBaseConfig(baseConfigFile)); + } + + this.deployConfigExists = this.fs.exists(join(this.appPath, this.options.config ?? FileName.Ui5Yaml)); + } + + public async prompting(): Promise { + 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._handleApiHubConfig(); + + const questions = await getCFQuestions({ + projectRoot: this.projectRoot, + isAbapDirectServiceBinding: this.isAbapDirectServiceBinding, + cfDestination: this.destinationName, + isCap: this.isCap, + addOverwrite: showOverwriteQuestion( + this.deployConfigExists, + this.launchDeployConfigAsSubGenerator, + this.launchStandaloneFromYui, + this.options.overwrite + ), + apiHubConfig: this.apiHubConfig + }); + + this.answers = await this.prompt(questions); + } + + await this._reconcileAnswersWithOptions(); + } + + private async _handleApiHubConfig(): Promise { + // generate a new instance dest name for api hub + if (this.apiHubConfig && this.apiHubConfig.apiHubType === ApiHubType.apiHubEnterprise) { + // full service path is only available from the manifest.json + if (!this.servicePath) { + const manifest = await loadManifest(this.fs, this.appPath); + this.servicePath = manifest?.['sap.app']?.dataSources?.mainService?.uri; + } + this.destinationName = generateDestinationName(API_BUSINESS_HUB_ENTERPRISE_PREFIX, this.servicePath); + } + } + + private async _reconcileAnswersWithOptions(): Promise { + const destinationName = this.destinationName || this.answers.destinationName; + const destination = await getDestination(destinationName); + const addManagedAppRouter = this.options.addManagedAppRouter ?? false; + const isDestinationFullUrl = + this.options.isFullUrlDest ?? (destination && isFullUrlDestination(destination)) ?? false; + const destinationAuthentication = + this.options.destinationAuthType ?? destination?.Authentication ?? DESTINATION_AUTHTYPE_NOTFOUND; + const overwrite = this.options.overwrite ?? this.answers.overwrite; + + this.answers = { + destinationName, + addManagedAppRouter, + isDestinationFullUrl, + destinationAuthentication, + overwrite + }; + } + + public async writing(): Promise { + if (this.abort || this.options.overwrite === false) { + return; + } + + if (!this.launchDeployConfigAsSubGenerator) { + await this._writing(); + } + } + + private async _writing(): Promise { + try { + const appConfig = { + appPath: this.appPath, + addManagedAppRouter: this.answers.addManagedAppRouter, + destinationName: this.answers.destinationName, + destinationAuthentication: this.answers.destinationAuthentication, + isDestinationFullUrl: this.answers.isDestinationFullUrl, + apiHubConfig: this.apiHubConfig, + serviceHost: this.serviceBase, + lcapMode: this.lcapModeOnly, + addMtaDestination: this.addMtaDestination, + cloudServiceName: this.cloudServiceName + } satisfies CFAppConfig; + await generateAppConfig(appConfig, this.fs, DeploymentGenerator.logger as unknown as Logger); + } catch (error) { + this.abort = true; + handleErrorMessage(this.appWizard, { errorMsg: t('cfGen.error.writing', { error }) }); + } + } + + public async install(): Promise { + if (!this.launchDeployConfigAsSubGenerator && this.options.overwrite !== false && !this.abort) { + await this._install(); + } + } + + private async _install(): Promise { + if (!this.options.skipInstall) { + const npm = platform() === 'win32' ? 'npm.cmd' : 'npm'; + try { + // install dependencies in project root + await this.spawnCommand( + npm, + ['install', '--no-audit', '--no-fund', '--silent', '--prefer-offline', '--no-progress'], + { + cwd: this.projectRoot + } + ); + + // prevent installing twice if the project root is the same as the app path + if (this.projectRoot !== this.appPath) { + // install dependencies in the application folder + await this.spawnCommand( + npm, + ['install', '--no-audit', '--no-fund', '--silent', '--prefer-offline', '--no-progress'], + { + cwd: this.appPath + } + ); + } + } catch (error) { + handleErrorMessage(this.appWizard, { errorMsg: t('cfGen.error.install', { error }) }); + } + } else { + DeploymentGenerator.logger?.info(t('cfGen.info.skippedInstallation')); + } + } + + public async end(): Promise { + try { + if ((this.launchDeployConfigAsSubGenerator && !this.abort) || this.options.overwrite === true) { + await this._init(); + await this._writing(); + await this._install(); + } + if ( + this.options.launchStandaloneFromYui && + isExtensionInstalled(this.vscode, YUI_EXTENSION_ID, YUI_MIN_VER_FILES_GENERATED_MSG) + ) { + this.appWizard?.showInformation(t('cfGen.info.filesGenerated'), MessageType.notification); + } + + const telemetryData = + TelemetryHelper.createTelemetryData({ + DeployTarget: 'CF', + ManagedApprouter: this.answers.addManagedAppRouter, + MTA: this.mtaPath ? 'true' : 'false', + ...this.options.telemetryData + }) ?? {}; + await sendTelemetry(EventName.DEPLOY_CONFIG, telemetryData, this.appPath); + } catch (error) { + DeploymentGenerator.logger?.error(t('cfGen.error.end', { error })); + } + } +} + +export { getCFQuestions, loadManifest }; +export { API_BUSINESS_HUB_ENTERPRISE_PREFIX, DESTINATION_AUTHTYPE_NOTFOUND }; +export { CfDeployConfigOptions, CfDeployConfigAnswers }; diff --git a/packages/cf-deploy-config-sub-generator/src/app/questions.ts b/packages/cf-deploy-config-sub-generator/src/app/questions.ts new file mode 100644 index 0000000000..91439f7ea3 --- /dev/null +++ b/packages/cf-deploy-config-sub-generator/src/app/questions.ts @@ -0,0 +1,62 @@ +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 { 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. + * + * @param options - the options required for retrieving the prompts. + * @param options.projectRoot - the root path of the project. + * @param options.isAbapDirectServiceBinding - whether the destination is an ABAP direct service binding. + * @param options.cfDestination - the Cloud Foundry destination. + * @param options.isCap - whether the project is a CAP project. + * @param options.addOverwrite - whether to add the overwrite prompt. + * @param options.apiHubConfig - the API Hub configuration. + * @returns the cf deploy config questions. + */ +export async function getCFQuestions({ + projectRoot, + isAbapDirectServiceBinding, + cfDestination, + isCap, + addOverwrite, + apiHubConfig +}: { + projectRoot: string; + isAbapDirectServiceBinding: boolean; + cfDestination: string; + isCap: boolean; + addOverwrite: boolean; + apiHubConfig?: ApiHubConfig; +}): Promise { + const isBAS = isAppStudio(); + const mtaYamlExists = !!(await getMtaPath(projectRoot)); + const cfChoices = await getCFChoices({ + projectRoot, + isCap, + cfDestination, + isAbapDirectServiceBinding, + apiHubConfigType: apiHubConfig?.apiHubType + }); + + const options: CfDeployConfigPromptOptions = { + [promptNames.destinationName]: { + defaultValue: destinationQuestionDefaultOption(isAbapDirectServiceBinding, isBAS, cfDestination), + hint: !!isAbapDirectServiceBinding, + useAutocomplete: getHostEnvironment() === hostEnvironment.cli, + addBTPDestinationList: isBAS ? !isAbapDirectServiceBinding : false, + additionalChoiceList: cfChoices + }, + [promptNames.addManagedAppRouter]: !mtaYamlExists && !isCap, + [promptNames.overwrite]: addOverwrite + }; + + DeploymentGenerator.logger?.debug(t('cfGen.debug.promptOptions', { options: JSON.stringify(options) })); + return getPrompts(options); +} diff --git a/packages/cf-deploy-config-sub-generator/src/app/types.ts b/packages/cf-deploy-config-sub-generator/src/app/types.ts new file mode 100644 index 0000000000..3f64b968d2 --- /dev/null +++ b/packages/cf-deploy-config-sub-generator/src/app/types.ts @@ -0,0 +1,87 @@ +import type { AppWizard } from '@sap-devx/yeoman-ui-types'; +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'; + +export interface CfDeployConfigOptions extends CfDeployConfigAnswers { + /** + * VSCode instance + */ + vscode?: unknown; + /** + * AppWizard instance + */ + appWizard?: AppWizard; + /** + * Whether the generator is launched as a subgenerator + */ + launchDeployConfigAsSubGenerator?: boolean; + /** + * Whether the generator is launched as a standalone generator in a YUI context + */ + launchStandaloneFromYui?: boolean; + /** + * Path to the project root - this could be the base Fiori app, CAP project or App Router + */ + projectRoot: string; + /** + * Path to the application + */ + appRootPath: string; + /** + * The name of the base config file e.g. ui5.yaml + */ + base?: string; + /** + * The name of the deploy config file e.g. ui5-deploy.yaml + */ + config?: string; + /** + * The destination authentication type + */ + destinationAuthType?: string; + /** + * Whether the destination is a full url destination + */ + isFullUrlDest?: boolean; + /** + * Add CAP destination + */ + addMTADestination: boolean; + /** + * Add cloud service name + */ + cloudServiceName: string; + /** + * Only make local Fiori app changes when parent project is a CAP project + */ + lcapModeOnly: boolean; + /** + * The API Hub configuration + */ + apiHubConfig?: ApiHubConfig; + /** + * The service host used to connect during app generation + */ + appGenServiceHost?: string; + /** + * The service path used to connect during app generation + */ + appGenServicePath?: string; + /** + * Option to silentyl overwrite the existing deployment configuration + */ + overwrite?: boolean; + /** + * Option to skip the installation of dependencies + */ + skipInstall?: boolean; + /** + * Option to force the conflicter property of the yeoman environment (prevents additional prompt for overwriting files) + */ + force?: boolean; + /** + * Telemetry data to be send after deployment configuration has been added + */ + telemetryData?: TelemetryData; +} diff --git a/packages/cf-deploy-config-sub-generator/src/app/utils.ts b/packages/cf-deploy-config-sub-generator/src/app/utils.ts new file mode 100644 index 0000000000..46487beb27 --- /dev/null +++ b/packages/cf-deploy-config-sub-generator/src/app/utils.ts @@ -0,0 +1,170 @@ +import { isAppStudio } from '@sap-ux/btp-utils'; +import { FileName, getMtaPath, getWebappPath } from '@sap-ux/project-access'; +import { DeploymentGenerator, bail, ErrorHandler, ERROR_TYPE } from '@sap-ux/deploy-config-generator-shared'; +import { ApiHubType, MtaConfig } from '@sap-ux/cf-deploy-config-writer'; +import { join } from 'path'; +import { + t, + DEFAULT_MTA_DESTINATION, + DESTINATION_CHOICE_NONE, + DESTINATION_CHOICE_DIRECT_SERVICE_BINDING +} from '../utils'; +import type { Manifest } from '@sap-ux/project-access'; +import type { Editor } from 'mem-fs-editor'; +import type { CfSystemChoice } from '@sap-ux/cf-deploy-config-inquirer'; + +/** + * Get the destination choices from API Hub | Local Store | mta.yaml. + * + * @param options - the options required for retrieving the questions. + * @param options.projectRoot - the root path of the project. + * @param options.isAbapDirectServiceBinding - whether the destination is an ABAP direct service binding. + * @param options.cfDestination - the Cloud Foundry destination. + * @param options.isCap - whether the project is a CAP project. + * @param options.apiHubConfigType - the API Hub configuration. + * @returns the cf deploy config questions. + */ +export async function getCFChoices({ + projectRoot, + isAbapDirectServiceBinding, + cfDestination, + isCap, + apiHubConfigType +}: { + projectRoot: string; + isAbapDirectServiceBinding: boolean; + cfDestination: string; + isCap: boolean; + apiHubConfigType?: string; +}): Promise { + let choices: CfSystemChoice[] = []; + const abapBindingChoice = [ + { + name: t('cfGen.prompts.abapBinding.name'), + value: DESTINATION_CHOICE_DIRECT_SERVICE_BINDING + } + ]; + + // If API Hub Enterprise configuration is used, return the CF destination + if (apiHubConfigType === ApiHubType.apiHubEnterprise) { + choices = [ + { + name: cfDestination, + value: cfDestination + } + ]; + } else { + const cfChoices = + !isAbapDirectServiceBinding && isAppStudio() + ? await getCFSystemChoices(projectRoot, isCap, cfDestination) + : []; + choices = isAbapDirectServiceBinding ? abapBindingChoice : cfChoices; + } + return choices; +} + +/** + * Generate a systems choice list. + * + * @param projectRoot - the root path of the project. + * @param isCap - whether the project is a CAP project. + * @param cfDestination - the Cloud Foundry destination. + * @returns the cf deploy config questions. + */ +async function getCFSystemChoices( + projectRoot: string, + isCap: boolean, + cfDestination?: string +): Promise { + const choices: CfSystemChoice[] = []; + try { + let mtaDestinations: string[] = []; + // Append mta destinations to support instance based destination flows + if (isCap) { + const mtaConfig = await MtaConfig.newInstance(projectRoot); + mtaDestinations = mtaConfig.getExposedDestinations(); + if (mtaDestinations?.length) { + // Add default option + choices.push({ + name: t('cfGen.prompts.capInstanceBasedDest.name'), + value: DEFAULT_MTA_DESTINATION + }); + } + } else { + const mtaResult = await getMtaPath(projectRoot); + const mtaDir = mtaResult?.mtaPath?.split(FileName.MtaYaml)[0]; + if (mtaDir) { + const mtaConfig = await MtaConfig.newInstance(mtaDir); + mtaDestinations = mtaConfig.getExposedDestinations(true); + } + } + + // Add MTA destinations + if (mtaDestinations?.length) { + // Load additional destinations exposed by the mta.yaml + mtaDestinations.forEach((dest) => { + choices.push({ + name: t('cfGen.prompts.instanceBasedDest.name', { destination: dest }), + value: dest + }); + }); + } + } catch (error) { + DeploymentGenerator.logger?.debug(t('cfGen.error.mtaDestinations', { error })); + } + + if (!cfDestination) { + choices.splice(0, 0, { name: t('cfGen.prompts.none.name'), value: DESTINATION_CHOICE_NONE }); + } + return choices; +} + +/** + * Determines the default option for the destination question based on the project environment. + * + * @param isAbapDirectServiceBinding - Indicates if ABAP direct service binding is used. + * @param isBAS - Whether the environment is SAP Business Application Studio (BAS). + * @param cfDestination - The pre-configured Cloud Foundry destination (if available). + * @returns {string} The default destination option. + */ +export function destinationQuestionDefaultOption( + isAbapDirectServiceBinding: boolean, + isBAS: boolean, + cfDestination?: string +): string { + let defaultDestination = ''; + + if (!isBAS) { + defaultDestination = cfDestination ?? ''; + } else if (cfDestination) { + defaultDestination = cfDestination; + } else if (isAbapDirectServiceBinding) { + defaultDestination = DESTINATION_CHOICE_DIRECT_SERVICE_BINDING; + } else if (isBAS) { + defaultDestination = DESTINATION_CHOICE_NONE; + } + + return defaultDestination; +} + +/** + * Load the manifest file from the project. + * + * @param fs - editor instance + * @param appPath - path to the project + * @returns manifest object + */ +export async function loadManifest(fs: Editor, appPath: string): Promise { + const manifestPath = join(await getWebappPath(appPath), FileName.Manifest); + const manifest = fs.readJSON(manifestPath) as unknown as Manifest; + + if (!manifest) { + bail(ErrorHandler.getErrorMsgFromType(ERROR_TYPE.NO_MANIFEST)); + } + + if (!manifest['sap.app']?.id) { + bail(ErrorHandler.getErrorMsgFromType(ERROR_TYPE.NO_APP_NAME)); + } + + return manifest; +} diff --git a/packages/cf-deploy-config-sub-generator/src/telemetryEvents/index.ts b/packages/cf-deploy-config-sub-generator/src/telemetryEvents/index.ts new file mode 100644 index 0000000000..ebb12a2086 --- /dev/null +++ b/packages/cf-deploy-config-sub-generator/src/telemetryEvents/index.ts @@ -0,0 +1,3 @@ +export enum EventName { + DEPLOY_CONFIG = 'DEPLOY_CONFIG' +} 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 06e9a20020..77eb9bdabf 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 @@ -1,4 +1,34 @@ { + "cfGen": { + "prompts": { + "abapBinding": { + "name": "Direct Service Binding" + }, + "capInstanceBasedDest": { + "name": "Local CAP Project API (Instance Based Destination)" + }, + "instanceBasedDest": { + "name": "{{- destination}} (Instance Based Destination)" + }, + "none": { + "name": "None" + } + }, + "info": { + "filesGenerated": "The files have been generated.", + "skippedInstallation": "Option `--skipInstall` was specified. Installation of dependencies will be skipped." + }, + "error": { + "mtaDestinations": "No destinations loaded from mta.yaml. {{- error}}", + "writing": "Error in writing phase: {{- error}}", + "install": "Error in install phase: {{- error}}", + "end": "Error in end phase: {{- error}}" + }, + "debug": { + "promptOptions": "Retrieving CF prompts using: \n {{- options}}", + "initTelemetry": "Initializing telemetry in CF deployment configuration generator" + } + }, "appRouterGen": { "debug": { "projectPath": "Project loaded from {{- destinationPath}}" diff --git a/packages/cf-deploy-config-sub-generator/src/utils/constants.ts b/packages/cf-deploy-config-sub-generator/src/utils/constants.ts new file mode 100644 index 0000000000..59184d041d --- /dev/null +++ b/packages/cf-deploy-config-sub-generator/src/utils/constants.ts @@ -0,0 +1,5 @@ +export const API_BUSINESS_HUB_ENTERPRISE_PREFIX = 'ABHE'; +export const DEFAULT_MTA_DESTINATION = 'fiori-default-srv-api'; +export const DESTINATION_AUTHTYPE_NOTFOUND = 'NotAvailable'; +export const DESTINATION_CHOICE_NONE = 'NONE'; +export const DESTINATION_CHOICE_DIRECT_SERVICE_BINDING = 'DIRECT_SERVICE_BINDING'; diff --git a/packages/cf-deploy-config-sub-generator/src/utils/index.ts b/packages/cf-deploy-config-sub-generator/src/utils/index.ts index e82230f1bf..afeddaea3e 100644 --- a/packages/cf-deploy-config-sub-generator/src/utils/index.ts +++ b/packages/cf-deploy-config-sub-generator/src/utils/index.ts @@ -1 +1,2 @@ +export * from './constants'; export * from './i18n'; diff --git a/packages/cf-deploy-config-sub-generator/test/__snapshots__/app.test.ts.snap b/packages/cf-deploy-config-sub-generator/test/__snapshots__/app.test.ts.snap new file mode 100644 index 0000000000..49ac68269e --- /dev/null +++ b/packages/cf-deploy-config-sub-generator/test/__snapshots__/app.test.ts.snap @@ -0,0 +1,419 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Cloud foundry generator tests Generate CF deployment to an app within a managed app router 4`] = ` +[ + { + "name": "managedApp-destination-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": "managedApp-destination-service", + "service-plan": "lite", + }, + "type": "org.cloudfoundry.managed-service", + }, + { + "name": "managedApp_repo_host", + "parameters": { + "service": "html5-apps-repo", + "service-name": "managedApp-html5-srv", + "service-plan": "app-host", + }, + "type": "org.cloudfoundry.managed-service", + }, + { + "name": "uaa_managedApp", + "parameters": { + "path": "./xs-security.json", + "service": "xsuaa", + "service-name": "managedApp-xsuaa-srv", + "service-plan": "application", + }, + "type": "org.cloudfoundry.managed-service", + }, +] +`; + +exports[`Cloud foundry generator tests Generate CF deployment to an app within a managed app router 6`] = ` +"{ + "welcomeFile": "/index.html", + "authenticationMethod": "route", + "routes": [ + { + "source": "^/resources/(.*)$", + "target": "/resources/$1", + "authenticationType": "none", + "destination": "ui5" + }, + { + "source": "^/test-resources/(.*)$", + "target": "/test-resources/$1", + "authenticationType": "none", + "destination": "ui5" + }, + { + "source": "^(.*)$", + "target": "$1", + "service": "html5-apps-repo-rt", + "authenticationType": "xsuaa" + } + ] +} +" +`; + +exports[`Cloud foundry generator tests Validate app is added and configured for standalone approuter 5`] = ` +{ + "devDependencies": { + "@sap/ui5-builder-webide-extension": "^1.1.9", + "@ui5/cli": "^3.9.2", + "mbt": "^1.2.29", + "rimraf": "^5.0.5", + "ui5-task-zipper": "^3.1.3", + }, + "scripts": { + "build:cf": "ui5 build preload --clean-dest --config ui5-deploy.yaml --include-task=generateCachebusterInfo", + "build:mta": "rimraf resources mta_archives && mbt build", + "deploy": "fiori cfDeploy", + "undeploy": "cf undeploy standaloneApp --delete-services --delete-service-keys --delete-service-brokers", + }, +} +`; + +exports[`Cloud foundry generator tests Validate app is added and configured for standalone approuter 6`] = ` +{ + "authenticationMethod": "route", + "routes": [ + { + "authenticationType": "xsuaa", + "csrfProtection": false, + "destination": "TestDestination", + "source": "^/sap/(.*)$", + "target": "/sap/$1", + }, + { + "authenticationType": "none", + "destination": "ui5", + "source": "^/resources/(.*)$", + "target": "/resources/$1", + }, + { + "authenticationType": "none", + "destination": "ui5", + "source": "^/test-resources/(.*)$", + "target": "/test-resources/$1", + }, + { + "authenticationType": "xsuaa", + "service": "html5-apps-repo-rt", + "source": "^(.*)$", + "target": "$1", + }, + ], + "welcomeFile": "/index.html", +} +`; + +exports[`Cloud foundry generator tests Validate app is added to an existing managed approuter project with an existing FE app 6`] = ` +"{ + "welcomeFile": "/index.html", + "authenticationMethod": "route", + "routes": [ + { + "source": "^/sap/(.*)$", + "target": "/sap/$1", + "destination": "testDestination", + "authenticationType": "xsuaa", + "csrfProtection": false + }, + { + "source": "^/resources/(.*)$", + "target": "/resources/$1", + "authenticationType": "none", + "destination": "ui5" + }, + { + "source": "^/test-resources/(.*)$", + "target": "/test-resources/$1", + "authenticationType": "none", + "destination": "ui5" + }, + { + "source": "^(.*)$", + "target": "$1", + "service": "html5-apps-repo-rt", + "authenticationType": "xsuaa" + } + ] +} +" +`; + +exports[`Cloud foundry generator tests Validate new managed approuter is added when there is no existing mta.yaml 2`] = ` +{ + "build-parameters": { + "no-source": true, + }, + "name": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-destination-content", + "parameters": { + "content": { + "instance": { + "destinations": [ + { + "Name": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_html_repo_host", + "ServiceInstanceName": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-html5-service", + "ServiceKeyName": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-repo-host-key", + "sap.cloud.service": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + }, + { + "Authentication": "OAuth2UserTokenExchange", + "Name": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_uaa", + "ServiceInstanceName": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-xsuaa-service", + "ServiceKeyName": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-uaa-key", + "sap.cloud.service": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + }, + ], + "existing_destinations_policy": "update", + }, + }, + }, + "requires": [ + { + "name": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-destination-service", + "parameters": { + "content-target": true, + }, + }, + { + "name": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-repo-host", + "parameters": { + "service-key": { + "name": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-repo-host-key", + }, + }, + }, + { + "name": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-uaa", + "parameters": { + "service-key": { + "name": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-uaa-key", + }, + }, + }, + ], + "type": "com.sap.application.content", +} +`; + +exports[`Cloud foundry generator tests Validate new managed approuter is added when there is no existing mta.yaml 3`] = ` +{ + "build-parameters": { + "build-result": "resources", + "requires": [ + { + "artifacts": [ + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.zip", + ], + "name": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "target-path": "resources/", + }, + ], + }, + "name": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-app-content", + "path": ".", + "requires": [ + { + "name": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-repo-host", + "parameters": { + "content-target": true, + }, + }, + ], + "type": "com.sap.application.content", +} +`; + +exports[`Cloud foundry generator tests Validate new managed approuter is added when there is no existing mta.yaml 4`] = ` +{ + "build-parameters": { + "build-result": "dist", + "builder": "custom", + "commands": [ + "npm install", + "npm run build:cf", + ], + "supported-platforms": [], + }, + "name": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "path": ".", + "type": "html5", +} +`; + +exports[`Cloud foundry generator tests Validate new managed approuter is added when there is no existing mta.yaml 5`] = ` +[ + { + "name": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-destination-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": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-destination-service", + "service-plan": "lite", + }, + "type": "org.cloudfoundry.managed-service", + }, + { + "name": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-uaa", + "parameters": { + "path": "./xs-security.json", + "service": "xsuaa", + "service-name": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-xsuaa-service", + "service-plan": "application", + }, + "type": "org.cloudfoundry.managed-service", + }, + { + "name": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-repo-host", + "parameters": { + "service": "html5-apps-repo", + "service-name": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-html5-service", + "service-plan": "app-host", + }, + "type": "org.cloudfoundry.managed-service", + }, +] +`; + +exports[`Cloud foundry generator tests Validate new managed approuter is added when there is no existing mta.yaml 6`] = ` +{ + "description": "Security profile of called application", + "role-templates": [], + "scopes": [], + "tenant-mode": "dedicated", + "xsappname": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", +} +`; + +exports[`Cloud foundry generator tests Validate new managed approuter is added when there is no existing mta.yaml 7`] = ` +"node_modules/ +dist/ +.scp/ +.env +Makefile*.mta +mta_archives +mta-* +resources +archive.zip +.*_mta_build_tmp" +`; + +exports[`Cloud foundry generator tests Validate new managed approuter is added when there is no existing mta.yaml 9`] = ` +{ + "authenticationMethod": "route", + "routes": [ + { + "authenticationType": "none", + "csrfProtection": false, + "destination": "testDestination", + "source": "^/sap/(.*)$", + "target": "/sap/$1", + }, + { + "authenticationType": "none", + "destination": "ui5", + "source": "^/resources/(.*)$", + "target": "/resources/$1", + }, + { + "authenticationType": "none", + "destination": "ui5", + "source": "^/test-resources/(.*)$", + "target": "/test-resources/$1", + }, + { + "authenticationType": "xsuaa", + "service": "html5-apps-repo-rt", + "source": "^(.*)$", + "target": "$1", + }, + ], + "welcomeFile": "/index.html", +} +`; + +exports[`Cloud foundry generator tests Validate new managed approuter is added when there is no existing mta.yaml 10`] = ` +{ + "builder": { + "customTasks": [ + { + "afterTask": "replaceVersion", + "configuration": { + "appFolder": "webapp", + "destDir": "dist", + }, + "name": "webide-extension-task-updateManifestJson", + }, + { + "afterTask": "generateCachebusterInfo", + "configuration": { + "additionalFiles": [ + "xs-app.json", + ], + "archiveName": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + }, + "name": "ui5-task-zipper", + }, + ], + "resources": { + "excludes": [ + "/test/**", + "/localService/**", + ], + }, + }, + "metadata": { + "name": "travel", + }, + "resources": { + "configuration": { + "propertiesFileSourceEncoding": "UTF-8", + }, + }, + "specVersion": "2.4", + "type": "application", +} +`; diff --git a/packages/cf-deploy-config-sub-generator/test/app.test.ts b/packages/cf-deploy-config-sub-generator/test/app.test.ts new file mode 100644 index 0000000000..b9d64459be --- /dev/null +++ b/packages/cf-deploy-config-sub-generator/test/app.test.ts @@ -0,0 +1,1150 @@ +import hasbin from 'hasbin'; +import CFGenerator from '../src/app'; +import yeomanTest from 'yeoman-test'; +import { load, dump } from 'js-yaml'; +import { join } from 'path'; +import { TestFixture } from './fixtures'; +import { Manifest } from '@sap-ux/project-access'; +import { initI18n, t } from '../src/utils'; +import { MessageType } from '@sap-devx/yeoman-ui-types'; +import { hostEnvironment } from '@sap-ux/fiori-generator-shared'; +import { MockMta } from './utils/mock-mta'; +import { ApiHubType } 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 questions from '../src/app/questions'; +import * as cfConfigWriter from '@sap-ux/cf-deploy-config-writer'; + +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 readJson = (path: string) => { + return JSON.parse(fs.readFileSync(path).toString()); +}; + +const mockShowInformation = jest.fn(); +const mockShowError = jest.fn(); +const mockAppWizard = { + showInformation: mockShowInformation, + showError: mockShowError +}; + +describe('Cloud foundry generator tests', () => { + let cwd: string; + 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; + }); + }); + + beforeAll(async () => { + await initI18n(); + }); + + afterAll(() => { + jest.resetAllMocks(); + }); + + it('Generate CF deployment to an app within a managed app router', async () => { + hasbinSyncMock.mockReturnValue(true); + + jest.spyOn(fioriGenShared, 'isExtensionInstalled').mockImplementation(() => { + return true; + }); + const managedRouterConfig = load(testFixture.getContents('mta-types/managed/mta.yaml')); + + memfs.vol.fromNestedJSON( + { + [`.${OUTPUT_DIR_PREFIX}/app1/webapp/manifest.json`]: + testFixture.getContents('app1/webapp/manifest.json'), + [`.${OUTPUT_DIR_PREFIX}/app1/package.json`]: JSON.stringify({ scripts: {} }), + [`.${OUTPUT_DIR_PREFIX}/app1/mta.yaml`]: dump(managedRouterConfig), + [`.${OUTPUT_DIR_PREFIX}/app1/ui5.yaml`]: testFixture.getContents('app1/ui5.yaml') + }, + '/' + ); + const appDir = join(OUTPUT_DIR_PREFIX, 'app1'); + + await expect( + yeomanTest + .create( + CFGenerator, + { + resolved: cfGenPath + }, + { cwd: appDir } + ) + .withOptions({ + skipInstall: true, + appWizard: mockAppWizard, + launchStandaloneFromYui: true, + launchDeployConfigAsSubGenerator: true + }) + .withPrompts({}) + .run() + ).resolves.not.toThrow(); + + // Before + const mtaBeforeYaml = new MockMta(join(__dirname, '/fixtures/mta-types/managed/')); + const modulesBefore = await mtaBeforeYaml.getModules(); + const resourcesBefore = await mtaBeforeYaml.getResources(); + const parametersBefore = await mtaBeforeYaml.getParameters(); + + expect(modulesBefore.length).toEqual(1); + expect(resourcesBefore.length).toEqual(3); + expect(parametersBefore).toStrictEqual({}); + + // After + const mtaAfterYaml = new MockMta(`${OUTPUT_DIR_PREFIX}/app1/`); + const modulesAfter = await mtaAfterYaml.getModules(); + const resourcesAfter = await mtaAfterYaml.getResources(); + const parametersAfter = await mtaAfterYaml.getParameters(); + + expect(modulesAfter.length).toEqual(3); + expect(resourcesAfter.length).toEqual(3); + + const appDestination = modulesAfter.find((m: { name: string }) => m.name === 'managedApp-dest-content'); + // This ensures the `myTestApp` is also the name of the service added to the manifest + expect(appDestination).toMatchInlineSnapshot(` + { + "build-parameters": { + "no-source": true, + }, + "name": "managedApp-dest-content", + "parameters": { + "content": { + "instance": { + "destinations": [ + { + "Name": "myTestApp_managedApp_repo_host", + "ServiceInstanceName": "managedApp-html5-srv", + "ServiceKeyName": "managedApp_repo_host-key", + "sap.cloud.service": "myTestApp", + }, + { + "Authentication": "OAuth2UserTokenExchange", + "Name": "myTestApp_uaa_managedApp", + "ServiceInstanceName": "managedApp-xsuaa-srv", + "ServiceKeyName": "uaa_managedApp-key", + "sap.cloud.service": "myTestApp", + }, + ], + "existing_destinations_policy": "update", + }, + }, + }, + "requires": [ + { + "name": "managedApp-destination-service", + "parameters": { + "content-target": true, + }, + }, + { + "name": "managedApp_repo_host", + "parameters": { + "service-key": { + "name": "managedApp_repo_host-key", + }, + }, + }, + { + "name": "uaa_managedApp", + "parameters": { + "service-key": { + "name": "uaa_managedApp-key", + }, + }, + }, + ], + "type": "com.sap.application.content", + } + `); + expect(parametersAfter).toMatchInlineSnapshot(` + { + "deploy_mode": "html5-repo", + "enable-parallel-deployments": true, + } + `); + const appContent = modulesAfter.find((m: { name: string }) => m.name === 'managedApp-app-content'); + expect(appContent).toMatchInlineSnapshot(` + { + "build-parameters": { + "build-result": "resources", + "requires": [ + { + "artifacts": [ + "comfioritoolstravel.zip", + ], + "name": "comfioritoolstravel", + "target-path": "resources/", + }, + ], + }, + "name": "managedApp-app-content", + "path": ".", + "requires": [ + { + "name": "managedApp_repo_host", + "parameters": { + "content-target": true, + }, + }, + ], + "type": "com.sap.application.content", + } + `); + expect(resourcesAfter).toMatchSnapshot(); // Shows the ui5 destination being added to an existing resource + const changedManifest: Manifest = readJson(`${OUTPUT_DIR_PREFIX}/app1/webapp/manifest.json`); + expect(changedManifest['sap.cloud']).toMatchInlineSnapshot(` + { + "public": true, + "service": "myTestApp", + } + `); + const xsApp = fs.readFileSync(`${OUTPUT_DIR_PREFIX}/app1/xs-app.json`, 'utf-8'); + expect(xsApp).toMatchSnapshot(); // Uses the xs-app-nodestination config + expect(mockShowInformation).toHaveBeenCalledWith(t('cfGen.info.filesGenerated'), MessageType.notification); + }); + + it('Validate app is added to an existing managed approuter project with an existing FE app', async () => { + hasbinSyncMock.mockReturnValue(true); + const managedRouterConfig = load(testFixture.getContents('mta-types/managed-apps/mta.yaml')); + + memfs.vol.fromNestedJSON( + { + [`.${OUTPUT_DIR_PREFIX}/app1/webapp/manifest.json`]: + testFixture.getContents('/app1/webapp/manifest.json'), + [`.${OUTPUT_DIR_PREFIX}/app1/package.json`]: JSON.stringify({ scripts: {} }), + [`.${OUTPUT_DIR_PREFIX}/app1/mta.yaml`]: dump(managedRouterConfig), + [`.${OUTPUT_DIR_PREFIX}/app1/ui5.yaml`]: testFixture.getContents('app1/ui5.yaml') + }, + '/' + ); + const appDir = (cwd = `${OUTPUT_DIR_PREFIX}/app1`); + + await expect( + yeomanTest + .create( + CFGenerator, + { + resolved: cfGenPath + }, + { cwd: appDir } + ) + .withOptions({ skipInstall: true, appRootPath: appDir }) + .withPrompts({ destinationName: 'testDestination' }) + .run() + ).resolves.not.toThrow(); + + // Before + const mtaBeforeYaml = new MockMta(join(__dirname, '/fixtures/mta-types/managed-apps/')); + const modulesBefore = await mtaBeforeYaml.getModules(); + const resourcesBefore = await mtaBeforeYaml.getResources(); + const parametersBefore = await mtaBeforeYaml.getParameters(); + + expect(modulesBefore.length).toEqual(3); + expect(resourcesBefore.length).toEqual(3); + expect(parametersBefore).toBeDefined(); + + // After + const mtaAfterYaml = new MockMta(`${OUTPUT_DIR_PREFIX}/app1/`); + const modulesAfter = await mtaAfterYaml.getModules(); + const resourcesAfter = await mtaAfterYaml.getResources(); + const parametersAfter = await mtaAfterYaml.getParameters(); + + expect(modulesAfter.length).toEqual(4); + expect(resourcesAfter.length).toEqual(3); + + const appDestination = modulesAfter.find((m) => m.name === 'managedApp-dest-content'); + // This ensures the `myTestApp` is also the name of the service added to the manifest + expect(appDestination).toMatchInlineSnapshot(` + { + "build-parameters": { + "no-source": true, + }, + "name": "managedApp-dest-content", + "parameters": { + "content": { + "instance": { + "destinations": [ + { + "Name": "myTestApp_managedApp_repo_host", + "ServiceInstanceName": "managedApp-html5-srv", + "ServiceKeyName": "managedApp_repo_host-key", + "sap.cloud.service": "myTestApp", + }, + { + "Authentication": "OAuth2UserTokenExchange", + "Name": "myTestApp_uaa_managedApp", + "ServiceInstanceName": "managedApp-xsuaa-srv", + "ServiceKeyName": "uaa_managedApp-key", + "sap.cloud.service": "myTestApp", + }, + ], + "existing_destinations_policy": "update", + }, + }, + }, + "requires": [ + { + "name": "managedApp-destination-service", + "parameters": { + "content-target": true, + }, + }, + { + "name": "managedApp_repo_host", + "parameters": { + "service-key": { + "name": "managedApp_repo_host-key", + }, + }, + }, + { + "name": "uaa_managedApp", + "parameters": { + "service-key": { + "name": "uaa_managedApp-key", + }, + }, + }, + ], + "type": "com.sap.application.content", + } + `); + expect(parametersAfter).toMatchInlineSnapshot(` + { + "deploy_mode": "html5-repo", + "enable-parallel-deployments": true, + } + `); + const appContent = modulesAfter.find((m) => m.name === 'managedApp-app-content'); + // Will now contain two apps for deployment + expect(appContent).toMatchInlineSnapshot(` + { + "build-parameters": { + "build-result": "resources", + "requires": [ + { + "artifacts": [ + "project1.zip", + ], + "name": "project1", + "target-path": "resources/", + }, + { + "artifacts": [ + "comfioritoolstravel.zip", + ], + "name": "comfioritoolstravel", + "target-path": "resources/", + }, + ], + }, + "name": "managedApp-app-content", + "path": ".", + "requires": [ + { + "name": "managedApp_repo_host", + "parameters": { + "content-target": true, + }, + }, + ], + "type": "com.sap.application.content", + } + `); + const appHtml5 = modulesAfter.filter((m) => m.type === 'html5'); + expect(appHtml5).toMatchInlineSnapshot(` + [ + { + "build-parameters": { + "build-result": "dist", + "builder": "custom", + "commands": [ + "npm install", + "npm run build:cf", + ], + "supported-platforms": [], + }, + "name": "project1", + "path": "project1", + "type": "html5", + }, + { + "build-parameters": { + "build-result": "dist", + "builder": "custom", + "commands": [ + "npm install", + "npm run build:cf", + ], + "supported-platforms": [], + }, + "name": "comfioritoolstravel", + "path": ".", + "type": "html5", + }, + ] + `); + + const changedManifest: Manifest = readJson(`${OUTPUT_DIR_PREFIX}/app1/webapp/manifest.json`); + + expect(changedManifest['sap.cloud']).toMatchInlineSnapshot(` + { + "public": true, + "service": "myTestApp", + } + `); + const xsApp = fs.readFileSync(`${OUTPUT_DIR_PREFIX}/app1/xs-app.json`, 'utf-8'); + expect(xsApp).toMatchSnapshot(); + }); + + it('Validate new managed approuter is added when there is no existing mta.yaml', async () => { + hasbinSyncMock.mockReturnValue(true); + const projectName = 'TestApp'; + const manifestId = 'a'.repeat(200); + const manifestConfig = JSON.parse(testFixture.getContents('/app1/webapp/manifest.json')); + manifestConfig['sap.app'].id = manifestId; + memfs.vol.fromNestedJSON( + { + [`.${OUTPUT_DIR_PREFIX}/app1/${projectName}/webapp/manifest.json`]: JSON.stringify(manifestConfig), + [`.${OUTPUT_DIR_PREFIX}/app1/${projectName}/package.json`]: JSON.stringify({ scripts: {} }), + [`.${OUTPUT_DIR_PREFIX}/app1/${projectName}/ui5.yaml`]: testFixture.getContents('/app1/ui5.yaml') + }, + '/' + ); + const appDir = `${OUTPUT_DIR_PREFIX}/app1`; + + await expect( + yeomanTest + .create( + CFGenerator, + { + resolved: cfGenPath + }, + { cwd: appDir } + ) + .withOptions({ + skipInstall: true, + appRootPath: join(appDir, projectName), + addManagedAppRouter: true, + launchDeployConfigAsSubGenerator: true, + destinationName: 'testDestination', + destinationAuthType: 'NoAuthentication' // Validating SH4 + }) + .withPrompts({ addManagedAppRouter: true }) + .run() + ).resolves.not.toThrow(); + + // After + expect(fs.existsSync(`${OUTPUT_DIR_PREFIX}/app1/mta.yaml`)).toBeFalsy(); // Ensure nothing is added to the root folder + + const mtaAfterYaml = new MockMta(`${OUTPUT_DIR_PREFIX}/app1/${projectName}/`); + const idAfter = await mtaAfterYaml.getMtaID(); + const modulesAfter = await mtaAfterYaml.getModules(); + const resourcesAfter = await mtaAfterYaml.getResources(); + const parametersAfter = await mtaAfterYaml.getParameters(); + + expect(idAfter.length).toEqual(128); + expect(modulesAfter.length).toEqual(3); + expect(resourcesAfter.length).toEqual(3); + expect(parametersAfter).toMatchInlineSnapshot(` + { + "deploy_mode": "html5-repo", + "enable-parallel-deployments": true, + } + `); + const managedApprouter = modulesAfter.find((m) => m.name.includes('-destination-content')); + expect(managedApprouter).toMatchSnapshot(); + const appContent = modulesAfter.find((m) => m.name.includes('-app-content')); + expect(appContent).toMatchSnapshot(); + const appHtml5 = modulesAfter.find((m) => m.type === 'html5'); + expect(appHtml5).toMatchSnapshot(); + expect(resourcesAfter).toMatchSnapshot(); + const xsSecurity = readJson(`${OUTPUT_DIR_PREFIX}/app1/${projectName}/xs-security.json`); + expect(xsSecurity).toMatchSnapshot(); + const gitIgnore = fs.readFileSync(`${OUTPUT_DIR_PREFIX}/app1/${projectName}/.gitignore`, 'utf-8'); + expect(gitIgnore).toMatchSnapshot(); + const changedManifest: Manifest = readJson(`${OUTPUT_DIR_PREFIX}/app1/${projectName}/webapp/manifest.json`); + expect(changedManifest['sap.cloud']).toMatchInlineSnapshot(` + { + "public": true, + "service": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + } + `); + const xsApp = readJson(`${OUTPUT_DIR_PREFIX}/app1/${projectName}/xs-app.json`); + expect(xsApp).toMatchSnapshot(); + const ui5Deploy = fs.readFileSync(`${OUTPUT_DIR_PREFIX}/app1/${projectName}/ui5-deploy.yaml`, 'utf-8'); + const ui5DeployYaml = load(ui5Deploy); + expect(ui5DeployYaml).toMatchSnapshot(); // Validates the archiveName is shortened + }); + + it('Validate app is added and configured for standalone approuter', async () => { + hasbinSyncMock.mockReturnValue(true); + const standaloneRouterConfig = load(testFixture.getContents('mta-types/standalone/mta.yaml')); + + memfs.vol.fromNestedJSON( + { + [`.${OUTPUT_DIR_PREFIX}/app1/webapp/manifest.json`]: + testFixture.getContents('/app1/webapp/manifest.json'), + [`.${OUTPUT_DIR_PREFIX}/app1/ui5.yaml`]: testFixture.getContents('/app1/ui5-client-value.yaml'), + [`.${OUTPUT_DIR_PREFIX}/app1/package.json`]: JSON.stringify({ scripts: {} }), + [`.${OUTPUT_DIR_PREFIX}/app1/mta.yaml`]: dump(standaloneRouterConfig) + }, + '/' + ); + const appDir = `${OUTPUT_DIR_PREFIX}/app1`; + + await expect( + yeomanTest + .create( + CFGenerator, + { + resolved: cfGenPath + }, + { cwd: appDir } + ) + .withOptions({ + skipInstall: true + }) + .withPrompts({ + destinationName: 'TestDestination', + addManagedAppRouter: false + }) + .run() + ).resolves.not.toThrow(); + + // Before + const mtaBeforeYaml = new MockMta(join(__dirname, '/fixtures/mta-types/standalone/')); + const modulesBefore = await mtaBeforeYaml.getModules(); + const resourcesBefore = await mtaBeforeYaml.getResources(); + const parametersBefore = await mtaBeforeYaml.getParameters(); + + expect(modulesBefore.length).toEqual(1); + expect(resourcesBefore.length).toEqual(3); + expect(parametersBefore).toBeDefined(); + + // After + const mtaAfterYaml = new MockMta(`${OUTPUT_DIR_PREFIX}/app1/`); + const modulesAfter = await mtaAfterYaml.getModules(); + const resourcesAfter = await mtaAfterYaml.getResources(); + const parametersAfter = await mtaAfterYaml.getParameters(); + + expect(modulesAfter.length).toEqual(3); + expect(resourcesAfter.length).toEqual(4); + + const standaloneRouter = modulesAfter.find((m) => m.type === 'approuter.nodejs'); + expect(standaloneRouter).toMatchInlineSnapshot(` + { + "name": "standaloneApp-router", + "parameters": { + "disk-quota": "256M", + "memory": "256M", + }, + "path": "router", + "requires": [ + { + "name": "standaloneApp-html5-repo-runtime", + }, + { + "name": "standaloneApp-uaa", + }, + { + "group": "destinations", + "name": "standaloneApp-destination", + "properties": { + "forwardAuthToken": false, + "name": "ui5", + "url": "https://ui5.sap.com", + }, + }, + ], + "type": "approuter.nodejs", + } + `); + expect(parametersAfter).toMatchInlineSnapshot(` + { + "deploy_mode": "html5-repo", + "enable-parallel-deployments": true, + } + `); + const appContent = modulesAfter.find((m) => m.type === 'com.sap.application.content'); + expect(appContent).toMatchInlineSnapshot(` + { + "build-parameters": { + "build-result": "resources", + "requires": [ + { + "artifacts": [ + "comfioritoolstravel.zip", + ], + "name": "comfioritoolstravel", + "target-path": "resources/", + }, + ], + }, + "name": "standaloneApp-app-content", + "path": ".", + "requires": [ + { + "name": "standaloneApp-repo-host", + "parameters": { + "content-target": true, + }, + }, + ], + "type": "com.sap.application.content", + } + `); + const appHtml5 = modulesAfter.find((m) => m.type === 'html5'); + expect(appHtml5).toMatchInlineSnapshot(` + { + "build-parameters": { + "build-result": "dist", + "builder": "custom", + "commands": [ + "npm install", + "npm run build:cf", + ], + "supported-platforms": [], + }, + "name": "comfioritoolstravel", + "path": ".", + "type": "html5", + } + `); + + const packagejson: Manifest = readJson(`${OUTPUT_DIR_PREFIX}/app1/package.json`); + expect(packagejson).toMatchSnapshot(); + + const changedManifest: Manifest = readJson(`${OUTPUT_DIR_PREFIX}/app1/webapp/manifest.json`); + expect(changedManifest['sap.cloud']).toBeUndefined(); + + // Validates ui5-deploy template being copied with comment + const ui5DeployYamlContent = fs.readFileSync(`${OUTPUT_DIR_PREFIX}/app1/ui5-deploy.yaml`, 'utf-8'); + const regExArr = ui5DeployYamlContent.match(/#.*/g); + + if (regExArr) { + const comments = [...regExArr]; + expect(comments.length).toEqual(1); + expect(comments[0]).toEqual( + '# yaml-language-server: $schema=https://sap.github.io/ui5-tooling/schema/ui5.yaml.json' + ); + } else { + fail('No comments found in the ui5.yaml file'); + } + + const xsApp = readJson(`${OUTPUT_DIR_PREFIX}/app1/xs-app.json`); + expect(xsApp).toMatchSnapshot(); // authenticationType should equal `none` + }); + + it('Validate destination name is generated for for ApiHub Enterprise', async () => { + hasbinSyncMock.mockReturnValue(true); + const managedRouterConfig = load(testFixture.getContents('mta-types/managed/mta.yaml')); + const getCFQuestionsSpy = jest.spyOn(questions, 'getCFQuestions'); + + memfs.vol.fromNestedJSON( + { + [`.${OUTPUT_DIR_PREFIX}/app1/webapp/manifest.json`]: + testFixture.getContents('app1/webapp/manifest.json'), + [`.${OUTPUT_DIR_PREFIX}/app1/package.json`]: JSON.stringify({ scripts: {} }), + [`.${OUTPUT_DIR_PREFIX}/app1/mta.yaml`]: dump(managedRouterConfig), + [`.${OUTPUT_DIR_PREFIX}/app1/ui5.yaml`]: testFixture.getContents('app1/ui5.yaml') + }, + '/' + ); + const appDir = join(OUTPUT_DIR_PREFIX, 'app1'); + + await expect( + yeomanTest + .create( + CFGenerator, + { + resolved: cfGenPath + }, + { cwd: appDir } + ) + .withOptions({ + skipInstall: true, + apiHubConfig: { + apiHubKey: 'mockApiHubKey', + apiHubType: ApiHubType.apiHubEnterprise + } + }) + .withPrompts({}) + .run() + ).resolves.not.toThrow(); + + expect(getCFQuestionsSpy).toHaveBeenCalledWith({ + addOverwrite: true, + apiHubConfig: { + apiHubKey: 'mockApiHubKey', + apiHubType: 'API_HUB_ENTERPRISE' + }, + cfDestination: 'ABHE_sap_opu_odata_sap_ZUI_RAP_TRAVEL_M_U025', + isAbapDirectServiceBinding: false, + isCap: false, + projectRoot: '/output/app1' + }); + }); + + it('Validate target path is updated if already exists', async () => { + hasbinSyncMock.mockReturnValue(true); + mockIsAppStudio.mockReturnValue(true); + const standaloneRouterConfig = load(testFixture.getContents('mta-types/standalone-with-ui/mta.yaml')); + + memfs.vol.fromNestedJSON( + { + [`.${OUTPUT_DIR_PREFIX}/app1/webapp/manifest.json`]: + testFixture.getContents('/app1/webapp/manifest.json'), + [`.${OUTPUT_DIR_PREFIX}/app1/ui5.yaml`]: testFixture.getContents('/app1/ui5.yaml'), + [`.${OUTPUT_DIR_PREFIX}/app1/package.json`]: JSON.stringify({ scripts: {} }), + [`.${OUTPUT_DIR_PREFIX}/app1/mta.yaml`]: dump(standaloneRouterConfig) + }, + '/' + ); + const appDir = `${OUTPUT_DIR_PREFIX}/app1`; + + await expect( + yeomanTest + .create( + CFGenerator, + { + resolved: cfGenPath + }, + { cwd: appDir } + ) + .withOptions({ + skipInstall: true + }) + .withPrompts({ destinationName: 'TestDestination', addManagedAppRouter: false }) + .run() + ).resolves.not.toThrow(); + + // Before + const mtaBeforeYaml = new MockMta(join(__dirname, '/fixtures/mta-types/standalone-with-ui/')); + const modulesBefore = await mtaBeforeYaml.getModules(); + const resourcesBefore = await mtaBeforeYaml.getResources(); + const parametersBefore = await mtaBeforeYaml.getParameters(); + + expect(modulesBefore.length).toEqual(2); + expect(resourcesBefore.length).toEqual(2); + expect(parametersBefore).toStrictEqual({}); + + // After + const mtaAfterYaml = new MockMta(`${OUTPUT_DIR_PREFIX}/app1/`); + const modulesAfter = await mtaAfterYaml.getModules(); + const resourcesAfter = await mtaAfterYaml.getResources(); + + const destinationResource = resourcesAfter.find((m) => m.name.includes('-dest-srv')); + // Validates standalone has HTML5Runtime_enabled set to false + expect(destinationResource).toMatchInlineSnapshot(`undefined`); + const appContent = modulesAfter.find((m) => m.type === 'com.sap.application.content'); + expect(appContent).toMatchInlineSnapshot(` + { + "build-parameters": { + "build-result": "resources", + "requires": [ + { + "artifacts": [ + "comfioritoolstravel.zip", + ], + "name": "comfioritoolstravel", + "target-path": "resources/", + }, + ], + }, + "name": "standalonewithui_ui_deployer", + "path": ".", + "requires": [ + { + "name": "standalonewithui_html_repo_host", + "parameters": { + "content-target": true, + }, + }, + ], + "type": "com.sap.application.content", + } + `); + const appHtml5 = modulesAfter.find((m) => m.type === 'html5'); + expect(appHtml5).toMatchInlineSnapshot(` + { + "build-parameters": { + "build-result": "dist", + "builder": "custom", + "commands": [ + "npm install", + "npm run build:cf", + ], + "supported-platforms": [], + }, + "name": "comfioritoolstravel", + "path": ".", + "type": "html5", + } + `); + }); + + it('Should throw error when mta executable is not found (CLI)', async () => { + hasbinSyncMock.mockReturnValue(false); + mockGetHostEnvironment.mockReturnValue(hostEnvironment.cli); + const managedRouterConfig = load(testFixture.getContents('mta-types/managed/mta.yaml')); + + memfs.vol.fromNestedJSON( + { + [`.${OUTPUT_DIR_PREFIX}/app1/webapp/manifest.json`]: + testFixture.getContents('app1/webapp/manifest.json'), + [`.${OUTPUT_DIR_PREFIX}/app1/package.json`]: JSON.stringify({ scripts: {} }), + [`.${OUTPUT_DIR_PREFIX}/app1/mta.yaml`]: dump(managedRouterConfig), + [`.${OUTPUT_DIR_PREFIX}/app1/ui5.yaml`]: testFixture.getContents('app1/ui5.yaml') + }, + '/' + ); + const appDir = join(OUTPUT_DIR_PREFIX, 'app1'); + + await expect( + yeomanTest + .create( + CFGenerator, + { + resolved: cfGenPath + }, + { cwd: appDir } + ) + .withOptions({ + skipInstall: true + }) + .withPrompts({}) + .run() + ).rejects.toThrowError( + `Cannot find the \"mta\" executable. Please add it to the path or use \"npm i -g mta\" to install it.` + ); + }); + + it('Should show error when mta executable is not found (YUI) and skip lifecycle methods', async () => { + hasbinSyncMock.mockReturnValue(false); + mockGetHostEnvironment.mockReturnValue(hostEnvironment.vscode); + const managedRouterConfig = load(testFixture.getContents('mta-types/managed/mta.yaml')); + + memfs.vol.fromNestedJSON( + { + [`.${OUTPUT_DIR_PREFIX}/app1/webapp/manifest.json`]: + testFixture.getContents('app1/webapp/manifest.json'), + [`.${OUTPUT_DIR_PREFIX}/app1/package.json`]: JSON.stringify({ scripts: {} }), + [`.${OUTPUT_DIR_PREFIX}/app1/mta.yaml`]: dump(managedRouterConfig), + [`.${OUTPUT_DIR_PREFIX}/app1/ui5.yaml`]: testFixture.getContents('app1/ui5.yaml') + }, + '/' + ); + const appDir = join(OUTPUT_DIR_PREFIX, 'app1'); + + await expect( + yeomanTest + .create( + CFGenerator, + { + resolved: cfGenPath + }, + { cwd: appDir } + ) + .withOptions({ + skipInstall: true, + appWizard: mockAppWizard + }) + .withPrompts({}) + .run() + ).resolves.not.toThrow(); + + expect(mockShowError).toHaveBeenCalledWith( + `Cannot find the \"mta\" executable. Please add it to the path or use \"npm i -g mta\" to install it.`, + MessageType.notification + ); + }); + + it('Should throw error when cds executable is not found for CAP project', async () => { + hasbinSyncMock.mockReturnValueOnce(true).mockReturnValueOnce(false); + mockGetHostEnvironment.mockReturnValue(hostEnvironment.cli); + mockFindCapProjectRoot.mockReturnValueOnce('/capRoot'); + const managedRouterConfig = load(testFixture.getContents('mta-types/managed/mta.yaml')); + + memfs.vol.fromNestedJSON( + { + [`.${OUTPUT_DIR_PREFIX}/app1/webapp/manifest.json`]: + testFixture.getContents('app1/webapp/manifest.json'), + [`.${OUTPUT_DIR_PREFIX}/app1/package.json`]: JSON.stringify({ scripts: {} }), + [`.${OUTPUT_DIR_PREFIX}/app1/mta.yaml`]: dump(managedRouterConfig), + [`.${OUTPUT_DIR_PREFIX}/app1/ui5.yaml`]: testFixture.getContents('app1/ui5.yaml') + }, + '/' + ); + const appDir = join(OUTPUT_DIR_PREFIX, 'app1'); + + await expect( + yeomanTest + .create( + CFGenerator, + { + resolved: cfGenPath + }, + { cwd: appDir } + ) + .withOptions({ + skipInstall: true + }) + .withPrompts({}) + .run() + ).rejects.toThrowError( + `Cannot find the \"cds\" executable. Please add it to the path or use \"npm i -g @sap/cds-dk\" to install it.` + ); + }); + + it('Should throw error when base config is not found', async () => { + hasbinSyncMock.mockReturnValue(true); + mockGetHostEnvironment.mockReturnValue(hostEnvironment.cli); + + const managedRouterConfig = load(testFixture.getContents('mta-types/managed/mta.yaml')); + + memfs.vol.fromNestedJSON( + { + [`.${OUTPUT_DIR_PREFIX}/app1/webapp/manifest.json`]: + testFixture.getContents('app1/webapp/manifest.json'), + [`.${OUTPUT_DIR_PREFIX}/app1/package.json`]: JSON.stringify({ scripts: {} }), + [`.${OUTPUT_DIR_PREFIX}/app1/mta.yaml`]: dump(managedRouterConfig) + }, + '/' + ); + const appDir = join(OUTPUT_DIR_PREFIX, 'app1'); + + await expect( + yeomanTest + .create( + CFGenerator, + { + resolved: cfGenPath + }, + { cwd: appDir } + ) + .withOptions({ + skipInstall: true, + base: 'missing-base.yaml' + }) + .withPrompts({}) + .run() + ).rejects.toThrowError(`Error: could not read missing-base.yaml`); + }); + + it('Should throw error when manifest is not found', async () => { + hasbinSyncMock.mockReturnValue(true); + mockGetHostEnvironment.mockReturnValue(hostEnvironment.cli); + const managedRouterConfig = load(testFixture.getContents('mta-types/managed/mta.yaml')); + + memfs.vol.fromNestedJSON( + { + [`.${OUTPUT_DIR_PREFIX}/app1/package.json`]: JSON.stringify({ scripts: {} }), + [`.${OUTPUT_DIR_PREFIX}/app1/mta.yaml`]: dump(managedRouterConfig), + [`.${OUTPUT_DIR_PREFIX}/app1/ui5.yaml`]: testFixture.getContents('app1/ui5.yaml') + }, + '/' + ); + const appDir = join(OUTPUT_DIR_PREFIX, 'app1'); + + await expect( + yeomanTest + .create( + CFGenerator, + { + resolved: cfGenPath + }, + { cwd: appDir } + ) + .withOptions({ + skipInstall: true, + apiHubConfig: { + apiHubKey: 'mockApiHubKey', + apiHubType: ApiHubType.apiHubEnterprise + } + }) + .withPrompts({}) + .run() + ).rejects.toThrowError(`Error: could not read webapp/manifest.json`); + }); + + it('Should throw error when not app name is found in manifest', async () => { + hasbinSyncMock.mockReturnValue(true); + mockGetHostEnvironment.mockReturnValue(hostEnvironment.cli); + const managedRouterConfig = load(testFixture.getContents('mta-types/managed/mta.yaml')); + + memfs.vol.fromNestedJSON( + { + [`.${OUTPUT_DIR_PREFIX}/app1/webapp/manifest.json`]: JSON.stringify({ + 'sap.app': { fakeid: 'myTestApp' } + }), + [`.${OUTPUT_DIR_PREFIX}/app1/package.json`]: JSON.stringify({ scripts: {} }), + [`.${OUTPUT_DIR_PREFIX}/app1/mta.yaml`]: dump(managedRouterConfig), + [`.${OUTPUT_DIR_PREFIX}/app1/ui5.yaml`]: testFixture.getContents('app1/ui5.yaml') + }, + '/' + ); + const appDir = join(OUTPUT_DIR_PREFIX, 'app1'); + + await expect( + yeomanTest + .create( + CFGenerator, + { + resolved: cfGenPath + }, + { cwd: appDir } + ) + .withOptions({ + skipInstall: true, + apiHubConfig: { + apiHubKey: 'mockApiHubKey', + apiHubType: ApiHubType.apiHubEnterprise + } + }) + .withPrompts({}) + .run() + ).rejects.toThrowError(`Could not determine app name from manifest`); + }); + + it('Should throw error if config writing fails', async () => { + hasbinSyncMock.mockReturnValue(true); + mockGetHostEnvironment.mockReturnValue(hostEnvironment.cli); + jest.spyOn(cfConfigWriter, 'generateAppConfig').mockImplementation(() => { + throw new Error('MTA Error'); + }); + const managedRouterConfig = load(testFixture.getContents('mta-types/managed/mta.yaml')); + + memfs.vol.fromNestedJSON( + { + [`.${OUTPUT_DIR_PREFIX}/app1/webapp/manifest.json`]: + testFixture.getContents('app1/webapp/manifest.json'), + [`.${OUTPUT_DIR_PREFIX}/app1/package.json`]: JSON.stringify({ scripts: {} }), + [`.${OUTPUT_DIR_PREFIX}/app1/mta.yaml`]: dump(managedRouterConfig), + [`.${OUTPUT_DIR_PREFIX}/app1/ui5.yaml`]: testFixture.getContents('app1/ui5.yaml') + }, + '/' + ); + const appDir = join(OUTPUT_DIR_PREFIX, 'app1'); + + await expect( + yeomanTest + .create( + CFGenerator, + { + resolved: cfGenPath + }, + { cwd: appDir } + ) + .withOptions({ + skipInstall: true, + appWizard: mockAppWizard + }) + .withPrompts({}) + .run() + ).rejects.toThrowError(); + }); + + it('Should not throw error in end phase if telemetry fails', async () => { + hasbinSyncMock.mockReturnValue(true); + mockSendTelemetry.mockImplementation(() => { + throw new Error('Telemetry Error'); + }); + + const managedRouterConfig = load(testFixture.getContents('mta-types/managed/mta.yaml')); + + memfs.vol.fromNestedJSON( + { + [`.${OUTPUT_DIR_PREFIX}/app1/webapp/manifest.json`]: + testFixture.getContents('app1/webapp/manifest.json'), + [`.${OUTPUT_DIR_PREFIX}/app1/package.json`]: JSON.stringify({ scripts: {} }), + [`.${OUTPUT_DIR_PREFIX}/app1/mta.yaml`]: dump(managedRouterConfig), + [`.${OUTPUT_DIR_PREFIX}/app1/ui5.yaml`]: testFixture.getContents('app1/ui5.yaml') + }, + '/' + ); + const appDir = join(OUTPUT_DIR_PREFIX, 'app1'); + + await expect( + yeomanTest + .create( + CFGenerator, + { + resolved: cfGenPath + }, + { cwd: appDir } + ) + .withOptions({ + skipInstall: true, + appWizard: mockAppWizard, + launchStandaloneFromYui: true, + launchDeployConfigAsSubGenerator: true + }) + .withPrompts({}) + .run() + ).resolves.not.toThrow(); + }); +}); diff --git a/packages/cf-deploy-config-sub-generator/test/app/utils.test.ts b/packages/cf-deploy-config-sub-generator/test/app/utils.test.ts new file mode 100644 index 0000000000..ea71895f2d --- /dev/null +++ b/packages/cf-deploy-config-sub-generator/test/app/utils.test.ts @@ -0,0 +1,24 @@ +import { destinationQuestionDefaultOption } from '../../src/app/utils'; +import { DESTINATION_CHOICE_DIRECT_SERVICE_BINDING, DESTINATION_CHOICE_NONE } from '../../src/utils'; + +describe('test utils', () => { + it('should return correct default destination', () => { + let defaultDestination = destinationQuestionDefaultOption(true, true, 'test'); + expect(defaultDestination).toBe('test'); + + defaultDestination = destinationQuestionDefaultOption(false, true, 'test'); + expect(defaultDestination).toBe('test'); + + defaultDestination = destinationQuestionDefaultOption(false, false, 'test'); + expect(defaultDestination).toBe('test'); + + defaultDestination = destinationQuestionDefaultOption(true, true); + expect(defaultDestination).toBe(DESTINATION_CHOICE_DIRECT_SERVICE_BINDING); + + defaultDestination = destinationQuestionDefaultOption(false, true); + expect(defaultDestination).toBe(DESTINATION_CHOICE_NONE); + + defaultDestination = destinationQuestionDefaultOption(false, false); + expect(defaultDestination).toBe(''); + }); +}); diff --git a/packages/cf-deploy-config-sub-generator/test/fixtures/app1/mta.yaml b/packages/cf-deploy-config-sub-generator/test/fixtures/app1/mta.yaml new file mode 100644 index 0000000000..d64a95959e --- /dev/null +++ b/packages/cf-deploy-config-sub-generator/test/fixtures/app1/mta.yaml @@ -0,0 +1,6 @@ +_schema-version: "3.1" +ID: existing-id-deploy +description: Fiori elements app +version: 0.0.1 +modules: [] +resources: [] diff --git a/packages/cf-deploy-config-sub-generator/test/fixtures/app1/package.json b/packages/cf-deploy-config-sub-generator/test/fixtures/app1/package.json new file mode 100644 index 0000000000..5f2b205122 --- /dev/null +++ b/packages/cf-deploy-config-sub-generator/test/fixtures/app1/package.json @@ -0,0 +1,3 @@ +{ + "scripts": {} +} \ No newline at end of file diff --git a/packages/cf-deploy-config-sub-generator/test/fixtures/app1/ui5-client-value.yaml b/packages/cf-deploy-config-sub-generator/test/fixtures/app1/ui5-client-value.yaml new file mode 100644 index 0000000000..fbfbb02e6d --- /dev/null +++ b/packages/cf-deploy-config-sub-generator/test/fixtures/app1/ui5-client-value.yaml @@ -0,0 +1,26 @@ +specVersion: '2.4' +metadata: + name: 'travel' +type: application +server: + customMiddleware: + - name: fiori-tools-proxy + afterMiddleware: compression + configuration: + ignoreCertError: false # If set to true, certificate errors will be ignored. E.g. self-signed certificates will be accepted + backend: + - path: /sap + url: https://abap.staging.hana.ondemand.com + client: 100 + scp: true + ui5: + path: + - /resources + - /test-resources + url: https://ui5.sap.com + version: # The UI5 version, for instance, 1.78.1. Empty means latest version + - name: fiori-tools-appreload + afterMiddleware: compression + configuration: + port: 35729 + path: webapp diff --git a/packages/cf-deploy-config-sub-generator/test/fixtures/app1/ui5.yaml b/packages/cf-deploy-config-sub-generator/test/fixtures/app1/ui5.yaml new file mode 100644 index 0000000000..cab98e8d32 --- /dev/null +++ b/packages/cf-deploy-config-sub-generator/test/fixtures/app1/ui5.yaml @@ -0,0 +1,25 @@ +specVersion: '2.4' +metadata: + name: 'travel' +type: application +server: + customMiddleware: + - name: fiori-tools-proxy + afterMiddleware: compression + configuration: + ignoreCertError: false # If set to true, certificate errors will be ignored. E.g. self-signed certificates will be accepted + backend: + - path: /sap + url: https://abap.staging.hana.ondemand.com + scp: true + ui5: + path: + - /resources + - /test-resources + url: https://ui5.sap.com + version: # The UI5 version, for instance, 1.78.1. Empty means latest version + - name: fiori-tools-appreload + afterMiddleware: compression + configuration: + port: 35729 + path: webapp diff --git a/packages/cf-deploy-config-sub-generator/test/fixtures/app1/webapp/manifest.json b/packages/cf-deploy-config-sub-generator/test/fixtures/app1/webapp/manifest.json new file mode 100755 index 0000000000..4203720be8 --- /dev/null +++ b/packages/cf-deploy-config-sub-generator/test/fixtures/app1/webapp/manifest.json @@ -0,0 +1,186 @@ +{ + "_version": "1.8.0", + "sap.app": { + "id": "com.fiori.tools.travel", + "type": "application", + "i18n": "i18n/i18n.properties", + "applicationVersion": { + "version": "1.0.0" + }, + "title": "{{appTitle}}", + "description": "{{appDescription}}", + "tags": { + "keywords": [] + }, + "ach": "", + "resources": "resources.json", + "dataSources": { + "mainService": { + "uri": "/sap/opu/odata/sap/ZUI_RAP_TRAVEL_M_U025/", + "type": "OData", + "settings": { + "annotations": [ + "ZUI_RAP_TRAVEL_M_U025_VAN", + "annotation" + ], + "localUri": "localService/metadata.xml" + } + }, + "ZUI_RAP_TRAVEL_M_U025_VAN": { + "uri": "/sap/opu/odata/IWFND/CATALOGSERVICE;v=2/Annotations(TechnicalName='ZUI_RAP_TRAVEL_M_U025_VAN',Version='0001')/$value/", + "type": "ODataAnnotation", + "settings": { + "localUri": "localService/ZUI_RAP_TRAVEL_M_U025_VAN.xml" + } + }, + "annotation": { + "type": "ODataAnnotation", + "uri": "annotations/annotation.xml", + "settings": { + "localUri": "annotations/annotation.xml" + } + } + }, + "offline": false, + "sourceTemplate": { + "id": "ui5template.smartTemplate", + "version": "1.40.12" + }, + "crossNavigation": { + "inbounds": { + "com-fiori-tools-travel-inbound": { + "signature": { + "parameters": {}, + "additionalParameters": "allowed" + }, + "semanticObject": "Travel", + "action": "display", + "title": "Travel", + "subTitle": "", + "icon": "" + } + } + } + }, + "sap.ui": { + "technology": "UI5", + "icons": { + "icon": "", + "favIcon": "", + "phone": "", + "phone@2": "", + "tablet": "", + "tablet@2": "" + }, + "deviceTypes": { + "desktop": true, + "tablet": true, + "phone": true + }, + "supportedThemes": [ + "sap_hcb", + "sap_belize" + ] + }, + "sap.ui5": { + "resources": { + "js": [], + "css": [] + }, + "dependencies": { + "minUI5Version": "1.65.0", + "libs": {}, + "components": {} + }, + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "uri": "i18n/i18n.properties" + }, + "@i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "uri": "i18n/i18n.properties" + }, + "i18n|sap.suite.ui.generic.template.ListReport|Travel": { + "type": "sap.ui.model.resource.ResourceModel", + "uri": "i18n/ListReport/Travel/i18n.properties" + }, + "i18n|sap.suite.ui.generic.template.ObjectPage|Travel": { + "type": "sap.ui.model.resource.ResourceModel", + "uri": "i18n/ObjectPage/Travel/i18n.properties" + }, + "i18n|sap.suite.ui.generic.template.ObjectPage|Booking": { + "type": "sap.ui.model.resource.ResourceModel", + "uri": "i18n/ObjectPage/Booking/i18n.properties" + }, + "": { + "dataSource": "mainService", + "preload": true, + "settings": { + "defaultBindingMode": "TwoWay", + "defaultCountMode": "Inline", + "refreshAfterChange": false, + "metadataUrlParams": { + "sap-value-list": "none" + } + } + } + }, + "extends": { + "extensions": {} + }, + "contentDensities": { + "compact": true, + "cozy": true + } + }, + "sap.ui.generic.app": { + "_version": "1.3.0", + "settings": { + "forceGlobalRefresh": false, + "objectPageHeaderType": "Dynamic", + "showDraftToggle": false + }, + "pages": { + "ListReport|Travel": { + "entitySet": "Travel", + "component": { + "name": "sap.suite.ui.generic.template.ListReport", + "list": true, + "settings": { + "condensedTableLayout": true, + "smartVariantManagement": true, + "enableTableFilterInPageVariant": true + } + }, + "pages": { + "ObjectPage|Travel": { + "entitySet": "Travel", + "component": { + "name": "sap.suite.ui.generic.template.ObjectPage" + }, + "pages": { + "ObjectPage|to_Booking": { + "navigationProperty": "to_Booking", + "entitySet": "Booking", + "component": { + "name": "sap.suite.ui.generic.template.ObjectPage" + } + } + } + } + } + } + } + }, + "sap.platform.abap": { + "uri": "" + }, + "sap.fiori": { + "registrationIds": [], + "archeType": "transactional" + }, + "sap.platform.hcp": { + "uri": "" + } +} diff --git a/packages/cf-deploy-config-sub-generator/test/fixtures/app1/xs-app.json b/packages/cf-deploy-config-sub-generator/test/fixtures/app1/xs-app.json new file mode 100644 index 0000000000..666c6012b6 --- /dev/null +++ b/packages/cf-deploy-config-sub-generator/test/fixtures/app1/xs-app.json @@ -0,0 +1,18 @@ +{ + "welcomeFile": "/manifest.json", + "authenticationMethod": "route", + "routes": [ + { + "source": "^/sap/(.*)$", + "target": "/sap/$1", + "destination": "not_going_to_work", + "authenticationType": "xsuaa" + }, + { + "source": "^(.*)$", + "target": "$1", + "service": "html5-apps-repo-rt", + "authenticationType": "none" + } + ] + } diff --git a/packages/cf-deploy-config-sub-generator/test/fixtures/mta-types/managed-apps/mta.yaml b/packages/cf-deploy-config-sub-generator/test/fixtures/mta-types/managed-apps/mta.yaml new file mode 100644 index 0000000000..afee01c4ff --- /dev/null +++ b/packages/cf-deploy-config-sub-generator/test/fixtures/mta-types/managed-apps/mta.yaml @@ -0,0 +1,96 @@ +_schema-version: '3.2' +ID: managedApp +version: 0.0.1 +modules: + - name: managedApp-dest-content + type: com.sap.application.content + requires: + - name: managedApp-destination-service + parameters: + content-target: true + - name: managedApp_repo_host + parameters: + service-key: + name: managedApp_repo_host-key + - name: uaa_managedApp + parameters: + service-key: + name: uaa_managedApp-key + parameters: + content: + instance: + destinations: + - Name: myTestApp_managedApp_repo_host + ServiceInstanceName: managedApp-html5-srv + ServiceKeyName: managedApp_repo_host-key + sap.cloud.service: myTestApp + - Authentication: OAuth2UserTokenExchange + Name: myTestApp_uaa_managedApp + ServiceInstanceName: managedApp-xsuaa-srv + ServiceKeyName: uaa_managedApp-key + sap.cloud.service: myTestApp + existing_destinations_policy: update + build-parameters: + no-source: true + - name: managedApp-app-content + type: com.sap.application.content + path: . + requires: + - name: managedApp_repo_host + parameters: + content-target: true + build-parameters: + build-result: resources + requires: + - artifacts: + - project1.zip + name: project1 + target-path: resources/ + - name: project1 + type: html5 + path: project1 + build-parameters: + build-result: dist + builder: custom + commands: + - npm install + - npm run build:cf + supported-platforms: [] +resources: + - name: managedApp-destination-service + type: org.cloudfoundry.managed-service + parameters: + config: + HTML5Runtime_enabled: true + init_data: + subaccount: + destinations: + - Name: northwind + WebIDEEnabled: true + WebIDEUsage: odata_gen + HTML5.DynamicDestination: true + Authentication: NoAuthentication + Description: Destination to internet facing host + ProxyType: Internet + Type: HTTP + URL: https://services.odata.org + existing_destinations_policy: update + version: 1.0.0 + service: destination + service-name: managedApp-destination-service + service-plan: lite + - name: managedApp_repo_host + type: org.cloudfoundry.managed-service + parameters: + service: html5-apps-repo + service-name: managedApp-html5-srv + service-plan: app-host + - name: uaa_managedApp + type: org.cloudfoundry.managed-service + parameters: + path: ./xs-security.json + service: xsuaa + service-name: managedApp-xsuaa-srv + service-plan: application +parameters: + deploy_mode: html5-repo diff --git a/packages/cf-deploy-config-sub-generator/test/fixtures/mta-types/managed/mta.yaml b/packages/cf-deploy-config-sub-generator/test/fixtures/mta-types/managed/mta.yaml new file mode 100644 index 0000000000..5e18327877 --- /dev/null +++ b/packages/cf-deploy-config-sub-generator/test/fixtures/mta-types/managed/mta.yaml @@ -0,0 +1,57 @@ +_schema-version: "3.2" +ID: managedApp +version: 0.0.1 +modules: + - name: managedApp-dest-content + type: com.sap.application.content + requires: + - name: managedApp-destination-service + parameters: + content-target: true + - name: managedApp_repo_host + parameters: + service-key: + name: managedApp_repo_host-key + - name: uaa_managedApp + parameters: + service-key: + name: uaa_managedApp-key + parameters: + content: + instance: + destinations: + - Name: myTestApp_managedApp_repo_host + ServiceInstanceName: managedApp-html5-srv + ServiceKeyName: managedApp_repo_host-key + sap.cloud.service: myTestApp + - Authentication: OAuth2UserTokenExchange + Name: myTestApp_uaa_managedApp + ServiceInstanceName: managedApp-xsuaa-srv + ServiceKeyName: uaa_managedApp-key + sap.cloud.service: myTestApp + existing_destinations_policy: update + build-parameters: + no-source: true +resources: + - name: managedApp-destination-service + type: org.cloudfoundry.managed-service + parameters: + config: + HTML5Runtime_enabled: true + version: 1.0.0 + service: destination + service-name: managedApp-destination-service + service-plan: lite + - name: managedApp_repo_host + type: org.cloudfoundry.managed-service + parameters: + service: html5-apps-repo + service-name: managedApp-html5-srv + service-plan: app-host + - name: uaa_managedApp + type: org.cloudfoundry.managed-service + parameters: + path: ./xs-security.json + service: xsuaa + service-name: managedApp-xsuaa-srv + service-plan: application diff --git a/packages/cf-deploy-config-sub-generator/test/fixtures/mta-types/standalone-with-ui/mta.yaml b/packages/cf-deploy-config-sub-generator/test/fixtures/mta-types/standalone-with-ui/mta.yaml new file mode 100644 index 0000000000..78df7dc0ef --- /dev/null +++ b/packages/cf-deploy-config-sub-generator/test/fixtures/mta-types/standalone-with-ui/mta.yaml @@ -0,0 +1,30 @@ +_schema-version: "3.2" +ID: standalonewithui +version: 0.0.1 +modules: + - name: standalonewithui-approuter + type: approuter.nodejs + path: standalonewithui-approuter + requires: + - name: standalonewithui_html_repo_runtime + parameters: + disk-quota: 256M + memory: 256M + - name: standalonewithui_ui_deployer + type: com.sap.application.content + path: . + requires: + - name: standalonewithui_html_repo_host + parameters: + content-target: true +resources: + - name: standalonewithui_html_repo_runtime + type: org.cloudfoundry.managed-service + parameters: + service: html5-apps-repo + service-plan: app-runtime + - name: standalonewithui_html_repo_host + type: org.cloudfoundry.managed-service + parameters: + service: html5-apps-repo + service-plan: app-host diff --git a/packages/cf-deploy-config-sub-generator/test/fixtures/mta-types/standalone/mta.yaml b/packages/cf-deploy-config-sub-generator/test/fixtures/mta-types/standalone/mta.yaml new file mode 100644 index 0000000000..353bdb3a14 --- /dev/null +++ b/packages/cf-deploy-config-sub-generator/test/fixtures/mta-types/standalone/mta.yaml @@ -0,0 +1,42 @@ +_schema-version: "3.2" +ID: standaloneApp +description: Fiori elements app +version: 0.0.1 +modules: + - name: standaloneApp-router + type: approuter.nodejs + path: router + requires: + - name: standaloneApp-html5-repo-runtime + - name: standaloneApp-uaa + - name: standaloneApp-destination + group: destinations + properties: + forwardAuthToken: false + name: ui5 + url: https://ui5.sap.com + parameters: + disk-quota: 256M + memory: 256M +resources: + - name: standaloneApp-uaa + type: org.cloudfoundry.managed-service + parameters: + config: + tenant-mode: dedicated + xsappname: standaloneApp-${org} + service: xsuaa + service-plan: application + - name: standaloneApp-html5-repo-runtime + type: org.cloudfoundry.managed-service + parameters: + service: html5-apps-repo + service-plan: app-runtime + - name: standaloneApp-destination + type: org.cloudfoundry.managed-service + parameters: + service: destination + service-plan: lite +parameters: + deploy_mode: html5-repo + enable-parallel-deployments: true diff --git a/packages/cf-deploy-config-sub-generator/tsconfig.json b/packages/cf-deploy-config-sub-generator/tsconfig.json index 82981f63ba..bec49d732b 100644 --- a/packages/cf-deploy-config-sub-generator/tsconfig.json +++ b/packages/cf-deploy-config-sub-generator/tsconfig.json @@ -9,6 +9,9 @@ "outDir": "generators" }, "references": [ + { + "path": "../btp-utils" + }, { "path": "../cf-deploy-config-inquirer" }, @@ -18,6 +21,12 @@ { "path": "../deploy-config-generator-shared" }, + { + "path": "../feature-toggle" + }, + { + "path": "../fiori-generator-shared" + }, { "path": "../i18n" }, @@ -26,6 +35,12 @@ }, { "path": "../logger" + }, + { + "path": "../project-access" + }, + { + "path": "../ui5-config" } ] } diff --git a/packages/deploy-config-generator-shared/package.json b/packages/deploy-config-generator-shared/package.json index d919595e75..911fbe0783 100644 --- a/packages/deploy-config-generator-shared/package.json +++ b/packages/deploy-config-generator-shared/package.json @@ -28,6 +28,7 @@ ], "dependencies": { "@sap-devx/yeoman-ui-types": "1.14.4", + "@sap-ux/btp-utils": "workspace:*", "@sap-ux/fiori-generator-shared": "workspace:*", "@sap-ux/nodejs-utils": "workspace:*", "@vscode-logging/logger": "2.0.0", @@ -39,7 +40,6 @@ "@types/vscode": "1.73.1", "@types/yeoman-generator": "5.2.11", "@sap-ux/axios-extension": "workspace:*", - "@sap-ux/btp-utils": "workspace:*", "@sap-ux/store": "workspace:*", "typescript": "5.3.3" }, diff --git a/packages/deploy-config-generator-shared/src/index.ts b/packages/deploy-config-generator-shared/src/index.ts index f72384fdb2..981eda20a4 100644 --- a/packages/deploy-config-generator-shared/src/index.ts +++ b/packages/deploy-config-generator-shared/src/index.ts @@ -1,12 +1,3 @@ export { DeploymentGenerator } from './base/generator'; -export { - initI18n, - bail, - handleErrorMessage, - showOverwriteQuestion, - ErrorHandler, - ERROR_TYPE, - ConnectedSystem, - mtaExecutable -} from './utils'; +export * from './utils'; export { getConfirmConfigUpdatePrompt } from './prompts'; 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 e97058dcd0..30cd4dde76 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 @@ -10,7 +10,8 @@ "fileDoesNotExist": "File does not exist: {{- filePath}}", "folderDoesNotExist": "Folder path does not exist: {{- filePath}}", "noAppName": "Could not determine app name from manifest", - "noBinary": "Cannot find the \"{{bin}}\" executable. Please add it to the path or use \"npm i -g {{pkg}}\" to install it.", + "noBaseConfig": "Error: could not read {{- baseConfig}}", + "noBinary": "Cannot find the \"{{bin}}\" executable. Please add it to the path or use \"npm i -g {{- pkg}}\" to install it.", "noManifest": "Error: could not read webapp/manifest.json", "unrecognizedTarget": "Unrecognized target: {{target}}", "unknownError": "Unknown error" diff --git a/packages/deploy-config-generator-shared/src/utils/constants.ts b/packages/deploy-config-generator-shared/src/utils/constants.ts index 1923379201..88c3664eb8 100644 --- a/packages/deploy-config-generator-shared/src/utils/constants.ts +++ b/packages/deploy-config-generator-shared/src/utils/constants.ts @@ -3,3 +3,8 @@ export const cdsPkg = '@sap/cds-dk'; export const mtaExecutable = 'mta'; export const mtaPkg = 'mta'; export const mtaYaml = 'mta.yaml'; + +export enum TargetName { + ABAP = 'abap', + CF = 'cf' +} diff --git a/packages/deploy-config-generator-shared/src/utils/destination.ts b/packages/deploy-config-generator-shared/src/utils/destination.ts new file mode 100644 index 0000000000..495f941ba1 --- /dev/null +++ b/packages/deploy-config-generator-shared/src/utils/destination.ts @@ -0,0 +1,29 @@ +import { isAppStudio, listDestinations } from '@sap-ux/btp-utils'; +import type { Destination } from '@sap-ux/btp-utils'; + +/** + * Generate a destination name based on the service path. + * Remove leading & trailing '/'. Substitute '/' for '_'. + * + * @param prefix - prefixes the dest name + * @param servicePath - used to create a meaningful dest name + * @returns destination name + */ +export function generateDestinationName(prefix: string, servicePath?: string): string { + return `${prefix}_${servicePath?.replace(/(^\/)|(\/$)/g, '').replace(/\//g, '_')}`; +} + +/** + * Get the destination with the specified name. + * + * @param destName - name of the destination + * @returns destination object + */ +export async function getDestination(destName?: string): Promise { + let destination; + if (isAppStudio() && destName) { + const destinations = await listDestinations({ stripS4HCApiHosts: true }); + destination = destinations[destName]; + } + return destination; +} 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 532f192754..cc76d8cfed 100644 --- a/packages/deploy-config-generator-shared/src/utils/error-handler.ts +++ b/packages/deploy-config-generator-shared/src/utils/error-handler.ts @@ -8,7 +8,6 @@ export enum ERROR_TYPE { ABORT_SIGNAL = 'ABORT_SIGNAL', NO_MANIFEST = 'NO_MANIFEST', NO_APP_NAME = 'NO_APP_NAME', - NO_UI5_CONFIG = 'NO_UI5_CONFIG', NO_CDS_BIN = 'NO_CDS_BIN', NO_MTA_BIN = 'NO_MTA_BIN', CAP_DEPLOYMENT_NO_MTA = 'CAP_DEPLOYMENT_NO_MTA' @@ -37,12 +36,12 @@ export class ErrorHandler { [ERROR_TYPE.ABORT_SIGNAL]: () => t('errors.abortSignal'), [ERROR_TYPE.NO_MANIFEST]: () => t('errors.noManifest'), [ERROR_TYPE.NO_APP_NAME]: () => t('errors.noAppName'), - [ERROR_TYPE.NO_UI5_CONFIG]: () => t('errors.noUi5Config'), [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') }; + public static readonly noBaseConfig = (baseConfig: string): string => t('errors.noBaseConfig', { baseConfig }); public static readonly unrecognizedTarget = (target: string): string => t('errors.unrecognizedTarget', { target }); public static readonly fileDoesNotExist = (filePath: string): string => t('errors.fileDoesNotExist', { filePath }); public static readonly folderDoesNotExist = (filePath: string): string => diff --git a/packages/deploy-config-generator-shared/src/utils/index.ts b/packages/deploy-config-generator-shared/src/utils/index.ts index a52266c623..2223163c47 100644 --- a/packages/deploy-config-generator-shared/src/utils/index.ts +++ b/packages/deploy-config-generator-shared/src/utils/index.ts @@ -1,5 +1,6 @@ export { t, initI18n } from './i18n'; export { showOverwriteQuestion } from './conditions'; export { ErrorHandler, ERROR_TYPE, bail, handleErrorMessage } from './error-handler'; -export { mtaExecutable } from './constants'; +export * from './constants'; +export * from './destination'; export * from './types'; 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 d2a57c5e10..deca567cb7 100644 --- a/packages/deploy-config-generator-shared/test/error-handler.test.ts +++ b/packages/deploy-config-generator-shared/test/error-handler.test.ts @@ -35,11 +35,16 @@ describe('Error Message Methods', () => { expect(result).toBe(t('errors.folderDoesNotExist', { filePath })); }); + it('noBaseConfig should return the correct error message', () => { + const baseConfig = 'mockConfig.yaml'; + const result = ErrorHandler.noBaseConfig(baseConfig); + expect(result).toBe(t('errors.noBaseConfig', { baseConfig })); + }); + it('should return correct error message for each error type', () => { expect(ErrorHandler.getErrorMsgFromType(ERROR_TYPE.ABORT_SIGNAL)).toBe(t('errors.abortSignal')); expect(ErrorHandler.getErrorMsgFromType(ERROR_TYPE.NO_MANIFEST)).toBe(t('errors.noManifest')); expect(ErrorHandler.getErrorMsgFromType(ERROR_TYPE.NO_APP_NAME)).toBe(t('errors.noAppName')); - expect(ErrorHandler.getErrorMsgFromType(ERROR_TYPE.NO_UI5_CONFIG)).toBe(t('errors.noUi5Config')); expect(ErrorHandler.getErrorMsgFromType(ERROR_TYPE.NO_CDS_BIN)).toBe( t('errors.noBinary', { bin: cdsExecutable, pkg: cdsPkg }) ); diff --git a/packages/deploy-config-generator-shared/test/fixtures/destinations.ts b/packages/deploy-config-generator-shared/test/fixtures/destinations.ts new file mode 100644 index 0000000000..2610270a0b --- /dev/null +++ b/packages/deploy-config-generator-shared/test/fixtures/destinations.ts @@ -0,0 +1,18 @@ +export const mockDestinations = { + Dest1: { + Name: 'Dest1', + Type: 'HTTP', + Authentication: 'BasicAuthentication', + Description: 'Mock destination', + Host: 'https://mock.url.dest1.com', + ProxyType: 'OnPremise' + }, + Dest2: { + Name: 'Dest2', + Type: 'HTTP', + Authentication: 'NoAuthentication', + Description: 'Mock destination 2', + Host: 'https://mock.url.dest2.com', + ProxyType: 'OnPremise' + } +}; diff --git a/packages/deploy-config-generator-shared/test/utils/destintation.test.ts b/packages/deploy-config-generator-shared/test/utils/destintation.test.ts new file mode 100644 index 0000000000..c7f8a48e90 --- /dev/null +++ b/packages/deploy-config-generator-shared/test/utils/destintation.test.ts @@ -0,0 +1,25 @@ +import { generateDestinationName, getDestination } from '../../src'; +import { isAppStudio, listDestinations } from '@sap-ux/btp-utils'; +import { mockDestinations } from '../fixtures/destinations'; + +jest.mock('@sap-ux/btp-utils', () => ({ + isAppStudio: jest.fn(), + listDestinations: jest.fn() +})); + +const mockIsAppStudio = isAppStudio as jest.Mock; +const mockListDestinations = listDestinations as jest.Mock; + +describe('destination utils', () => { + it('should generate destination name', () => { + const destName = generateDestinationName('ABCD', 'service/path'); + expect(destName).toBe('ABCD_service_path'); + }); + + it('should find destination', async () => { + mockIsAppStudio.mockReturnValueOnce(true); + mockListDestinations.mockResolvedValueOnce(mockDestinations); + const destResult = await getDestination(mockDestinations.Dest1.Name); + expect(destResult).toStrictEqual(mockDestinations.Dest1); + }); +}); diff --git a/packages/flp-config-sub-generator/src/app/index.ts b/packages/flp-config-sub-generator/src/app/index.ts index 6fe82c05e6..0c0f1e96c1 100644 --- a/packages/flp-config-sub-generator/src/app/index.ts +++ b/packages/flp-config-sub-generator/src/app/index.ts @@ -252,6 +252,7 @@ export default class extends Generator { try { if ( !this.options.launchFlpConfigAsSubGenerator && + this.abort !== true && isExtensionInstalled(this.vscode, YUI_EXTENSION_ID, YUI_MIN_VER_FILES_GENERATED_MSG) ) { this.appWizard?.showInformation(t('info.filesGenerated'), MessageType.notification); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4f00d8d451..19df7b2215 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1017,6 +1017,9 @@ importers: '@sap-devx/yeoman-ui-types': specifier: 1.14.4 version: 1.14.4 + '@sap-ux/btp-utils': + specifier: workspace:* + version: link:../btp-utils '@sap-ux/cf-deploy-config-inquirer': specifier: workspace:* version: link:../cf-deploy-config-inquirer @@ -1026,12 +1029,24 @@ importers: '@sap-ux/deploy-config-generator-shared': specifier: workspace:* version: link:../deploy-config-generator-shared + '@sap-ux/feature-toggle': + specifier: workspace:* + version: link:../feature-toggle + '@sap-ux/fiori-generator-shared': + specifier: workspace:* + version: link:../fiori-generator-shared '@sap-ux/i18n': specifier: workspace:* version: link:../i18n '@sap-ux/inquirer-common': specifier: workspace:* version: link:../inquirer-common + '@sap-ux/project-access': + specifier: workspace:* + version: link:../project-access + '@sap-ux/ui5-config': + specifier: workspace:* + version: link:../ui5-config hasbin: specifier: 1.2.3 version: 1.2.3 @@ -1060,6 +1075,12 @@ importers: '@types/js-yaml': specifier: 4.0.9 version: 4.0.9 + '@types/mem-fs': + specifier: 1.1.2 + version: 1.1.2 + '@types/mem-fs-editor': + specifier: 7.0.1 + version: 7.0.1 '@types/yeoman-environment': specifier: 2.10.11 version: 2.10.11 @@ -1389,6 +1410,9 @@ importers: '@sap-devx/yeoman-ui-types': specifier: 1.14.4 version: 1.14.4 + '@sap-ux/btp-utils': + specifier: workspace:* + version: link:../btp-utils '@sap-ux/fiori-generator-shared': specifier: workspace:* version: link:../fiori-generator-shared @@ -1408,9 +1432,6 @@ importers: '@sap-ux/axios-extension': specifier: workspace:* version: link:../axios-extension - '@sap-ux/btp-utils': - specifier: workspace:* - version: link:../btp-utils '@sap-ux/store': specifier: workspace:* version: link:../store