From 12927d28f133b4a6cac28f881a3f160491cd258c Mon Sep 17 00:00:00 2001 From: I743583 Date: Tue, 18 Mar 2025 15:16:34 +0000 Subject: [PATCH 01/41] Handle Base64 ZIP Archive Conversion to Buffer for Data Processing --- .../axios-extension/src/abap/ui5-abap-repository-service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/axios-extension/src/abap/ui5-abap-repository-service.ts b/packages/axios-extension/src/abap/ui5-abap-repository-service.ts index 982920d1c8..034d0114ca 100644 --- a/packages/axios-extension/src/abap/ui5-abap-repository-service.ts +++ b/packages/axios-extension/src/abap/ui5-abap-repository-service.ts @@ -156,7 +156,7 @@ export class Ui5AbapRepositoryService extends ODataService { } }); const data = response.odata(); - return data.ZipArchive ? Buffer.from(data.ZipArchive) : undefined; + return data.ZipArchive ? Buffer.from(data.ZipArchive, 'base64') : undefined; } catch (error) { this.log.debug(`Retrieving application ${app}, ${error}`); if (isAxiosError(error) && error.response?.status === 404) { From a3a17036f985d406b12db10001062810834793bc Mon Sep 17 00:00:00 2001 From: I743583 Date: Tue, 25 Mar 2025 14:18:45 +0000 Subject: [PATCH 02/41] add app download sub generator --- .../.eslintignore | 2 + .../.eslintrc.js | 7 + .../bsp-app-download-sub-generator/LICENSE | 201 ++++++++++++++ .../bsp-app-download-sub-generator/README.md | 16 ++ .../jest.config.js | 6 + .../package.json | 78 ++++++ .../questions.md | 30 +++ .../src/app/index.ts | 254 ++++++++++++++++++ .../src/app/prompts.ts | 105 ++++++++ .../src/app/types.ts | 76 ++++++ .../src/telemetryEvents/index.ts | 6 + .../bsp-app-download-sub-generator.i18n.json | 20 ++ .../src/utils/app-config.ts | 162 +++++++++++ .../src/utils/constants.ts | 20 ++ .../src/utils/eventHook.ts | 25 ++ .../src/utils/i18n.ts | 32 +++ .../src/utils/index.ts | 67 +++++ .../src/utils/logger.ts | 50 ++++ .../src/utils/prompt-state.ts | 33 +++ .../src/utils/utils.ts | 127 +++++++++ .../tsconfig.eslint.json | 4 + .../tsconfig.json | 64 +++++ .../src/applicationInfoHandler.ts | 8 +- .../src/launch-config-crud/create.ts | 1 + pnpm-lock.yaml | 123 ++++++++- tsconfig.json | 3 + 26 files changed, 1517 insertions(+), 3 deletions(-) create mode 100644 packages/bsp-app-download-sub-generator/.eslintignore create mode 100644 packages/bsp-app-download-sub-generator/.eslintrc.js create mode 100644 packages/bsp-app-download-sub-generator/LICENSE create mode 100644 packages/bsp-app-download-sub-generator/README.md create mode 100644 packages/bsp-app-download-sub-generator/jest.config.js create mode 100644 packages/bsp-app-download-sub-generator/package.json create mode 100644 packages/bsp-app-download-sub-generator/questions.md create mode 100644 packages/bsp-app-download-sub-generator/src/app/index.ts create mode 100644 packages/bsp-app-download-sub-generator/src/app/prompts.ts create mode 100644 packages/bsp-app-download-sub-generator/src/app/types.ts create mode 100644 packages/bsp-app-download-sub-generator/src/telemetryEvents/index.ts create mode 100644 packages/bsp-app-download-sub-generator/src/translations/bsp-app-download-sub-generator.i18n.json create mode 100644 packages/bsp-app-download-sub-generator/src/utils/app-config.ts create mode 100644 packages/bsp-app-download-sub-generator/src/utils/constants.ts create mode 100644 packages/bsp-app-download-sub-generator/src/utils/eventHook.ts create mode 100644 packages/bsp-app-download-sub-generator/src/utils/i18n.ts create mode 100644 packages/bsp-app-download-sub-generator/src/utils/index.ts create mode 100644 packages/bsp-app-download-sub-generator/src/utils/logger.ts create mode 100644 packages/bsp-app-download-sub-generator/src/utils/prompt-state.ts create mode 100644 packages/bsp-app-download-sub-generator/src/utils/utils.ts create mode 100644 packages/bsp-app-download-sub-generator/tsconfig.eslint.json create mode 100644 packages/bsp-app-download-sub-generator/tsconfig.json diff --git a/packages/bsp-app-download-sub-generator/.eslintignore b/packages/bsp-app-download-sub-generator/.eslintignore new file mode 100644 index 0000000000..89ff260405 --- /dev/null +++ b/packages/bsp-app-download-sub-generator/.eslintignore @@ -0,0 +1,2 @@ +/test/ +generators diff --git a/packages/bsp-app-download-sub-generator/.eslintrc.js b/packages/bsp-app-download-sub-generator/.eslintrc.js new file mode 100644 index 0000000000..b717f83ae9 --- /dev/null +++ b/packages/bsp-app-download-sub-generator/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['../../.eslintrc'], + parserOptions: { + project: './tsconfig.eslint.json', + tsconfigRootDir: __dirname + } +}; diff --git a/packages/bsp-app-download-sub-generator/LICENSE b/packages/bsp-app-download-sub-generator/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/packages/bsp-app-download-sub-generator/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/bsp-app-download-sub-generator/README.md b/packages/bsp-app-download-sub-generator/README.md new file mode 100644 index 0000000000..9921252c3b --- /dev/null +++ b/packages/bsp-app-download-sub-generator/README.md @@ -0,0 +1,16 @@ +# @sap-ux/bsp-app-download-sub-generator + +## Features + +The SAP App download sub-generator enables users to download a basic LROP App from BSP repository. The sub-generator will download the app from the BSP repository and add it to the workspace. + +## Installation + +The SAP App download sub-generator sub-generator is installed as part of the [@sap/generator-fiori](https://www.npmjs.com/package/@sap/generator-fiori) generator and cannot be used stand alone. + +## Launch the SAP Reuse Library sub-generator + +Open the Command Palette in MS Visual Studio Code ( CMD/CTRL + Shift + P ) and execute the Fiori: Download Fiori app from BSP repository. + +## Keywords +SAP Fiori Generator diff --git a/packages/bsp-app-download-sub-generator/jest.config.js b/packages/bsp-app-download-sub-generator/jest.config.js new file mode 100644 index 0000000000..2f0a4db758 --- /dev/null +++ b/packages/bsp-app-download-sub-generator/jest.config.js @@ -0,0 +1,6 @@ +const config = require('../../jest.base'); +config.snapshotFormat = { + escapeString: false, + printBasicPrototype: false +}; +module.exports = config; diff --git a/packages/bsp-app-download-sub-generator/package.json b/packages/bsp-app-download-sub-generator/package.json new file mode 100644 index 0000000000..5d77f4a329 --- /dev/null +++ b/packages/bsp-app-download-sub-generator/package.json @@ -0,0 +1,78 @@ +{ + "name": "@sap-ux/bsp-app-download-sub-generator", + "description": "Generator for downloading Fiori LROP Apps from BSP", + "version": "0.1.40", + "repository": { + "type": "git", + "url": "https://github.com/SAP/open-ux-tools.git", + "directory": "packages/bsp-app-download-sub-generator" + }, + "bugs": { + "url": "https://github.com/SAP/open-ux-tools/issues?q=is%3Aopen+is%3Aissue" + }, + "license": "Apache-2.0", + "main": "generators/app/index.js", + "scripts": { + "build": "tsc --build", + "clean": "rimraf --glob generators test/test-output coverage *.tsbuildinfo", + "watch": "tsc --watch", + "lint": "eslint . --ext .ts", + "lint:fix": "eslint . --ext .ts --fix", + "test": "jest --ci --forceExit --detectOpenHandles --colors --passWithNoTests", + "test-u": "jest --ci --forceExit --detectOpenHandles --colors -u", + "link": "pnpm link --global", + "unlink": "pnpm unlink --global" + }, + "files": [ + "LICENSE", + "generators", + "!generators/*.map", + "!generators/**/*.map" + ], + "dependencies": { + "@sap-devx/yeoman-ui-types": "1.14.4", + "@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/odata-service-inquirer": "workspace:*", + "@sap-ux/ui5-application-writer": "workspace:*", + "@sap-ux/fiori-elements-writer": "workspace:*", + "@sap-ux/logger": "workspace:*", + "@sap-ux/project-input-validator": "workspace:*", + "@sap-ux/ui5-application-inquirer": "workspace:*", + "@sap-ux/launch-config": "workspace:*", + "@sap-ux/fiori-tools-settings": "workspace:*", + "@sap-ux/abap-deploy-config-writer": "workspace:*", + "@sap-ux/btp-utils": "workspace:*", + "@sap-ux/ui5-info": "workspace:*", + "adm-zip": "0.5.10", + "i18next": "23.5.1", + "inquirer": "8.2.6", + "yeoman-generator": "5.10.0" + }, + "devDependencies": { + "@jest/types": "29.6.3", + "@types/inquirer": "8.2.6", + "@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", + "@sap-ux/nodejs-utils": "workspace:*", + "@vscode-logging/logger": "2.0.0", + "@types/adm-zip": "0.5.5", + "memfs": "3.4.13", + "mem-fs-editor": "9.4.0", + "lodash": "4.17.21", + "@types/lodash": "4.14.202", + "rimraf": "5.0.5", + "typescript": "5.3.3", + "unionfs": "4.4.0", + "yeoman-test": "6.3.0" + }, + "engines": { + "node": ">=18.x" + } +} diff --git a/packages/bsp-app-download-sub-generator/questions.md b/packages/bsp-app-download-sub-generator/questions.md new file mode 100644 index 0000000000..516d9cb1ef --- /dev/null +++ b/packages/bsp-app-download-sub-generator/questions.md @@ -0,0 +1,30 @@ + +- for Extracting Downloaded Files - Where would be an appropriate location to extract downloaded files? I’m considering extracting them to a temporary directory defined by: +const tempFilePath = join(homedir(), '.fioritools'); +zip.extractAllTo(tempFilePath, true); +Once writing the app is auccessful, I plan to delete the directory. Does this approach sound reasonable, or do you have a better suggestion for a temporary extraction path? + +- Service Metadata: say we have a service URL like: +https://ldciuia.wdf.sap.corp:44300/sap/opu/odata4/sap/test_service_bindings_07/srvd/sap/test_srvb_01/0001/ from extracted manifest json. +I’m assuming the metadata for this service will always be populated, this is a basic lrop app support correct - so I think its jst gna be an edmx project know? + +- When setting the project path, do we provide an option for the user to specify a name? And if a user selects a name that already exists, how should we handle that scenario? Should we prompt the user for a new name, or overwrite the existing one? + +- sourceTemplates id + +- services.entityConfig.mainEntity - I have double entitties + +- questions about local uri - is it okay to use annotation file manager ? + +- how to get annotations form v2 and v4 ? v2 vs v4 + +- ts or js enabled for tests ? does it matter? + + +// things to do - use zip.entries mem-fs and then then use fe writers and finally use the fs to modify some stuff - +// get edmx https://ldciuia.wdf.sap.corp:44300/sap/opu/odata4/sap/test_service_bindings_07/srvd/sap/test_srvb_01/0001/$metadata + + + +discussion with adp +- diff --git a/packages/bsp-app-download-sub-generator/src/app/index.ts b/packages/bsp-app-download-sub-generator/src/app/index.ts new file mode 100644 index 0000000000..b2fcf7ef45 --- /dev/null +++ b/packages/bsp-app-download-sub-generator/src/app/index.ts @@ -0,0 +1,254 @@ +import Generator from 'yeoman-generator'; +import BspAppDownloadLogger from '../utils/logger'; +import { AppWizard, Prompts } from '@sap-devx/yeoman-ui-types'; +import { isInternalFeaturesSettingEnabled } from '@sap-ux/feature-toggle'; +import type { Logger } from '@sap-ux/logger'; +import { sendTelemetry, TelemetryHelper } from '@sap-ux/fiori-generator-shared'; +import { generatorTitle, extractedFilePath, generatorName } from '../utils/constants'; +import { t } from '../utils'; +import { getYUIDetails, downloadApp } from '../utils/utils'; +import { EventName } from '../telemetryEvents'; +import type { YeomanEnvironment, VSCodeInstance } from '@sap-ux/fiori-generator-shared'; +import { getDefaultTargetFolder } from '@sap-ux/fiori-generator-shared'; +import type { BspAppDownloadOptions, BspAppDownloadAnswers } from './types'; +import { promptNames } from '@sap-ux/odata-service-inquirer'; +import { getQuestions } from './prompts'; +import { generate, TemplateType, type FioriElementsApp, type LROPSettings } from '@sap-ux/fiori-elements-writer'; +import { getAppConfig } from '../utils/app-config'; +import { join } from 'path'; +import { platform } from 'os'; +import { generateReadMe, type ReadMe } from '@sap-ux/fiori-generator-shared'; +import { runPostAppGenHook } from '../utils/eventHook'; +import { getDefaultUI5Theme } from '@sap-ux/ui5-info'; +import type { DebugOptions, FioriOptions } from '@sap-ux/launch-config'; +import { createLaunchConfig } from '@sap-ux/launch-config'; +import { isAppStudio } from '@sap-ux/btp-utils'; +import { OdataVersion } from '@sap-ux/odata-service-inquirer'; +import { writeApplicationInfoSettings } from '@sap-ux/fiori-tools-settings'; +import { generate as generateDeployConfig } from '@sap-ux/abap-deploy-config-writer'; +import { PromptState } from '../utils/prompt-state'; + +/** + * + */ +export default class extends Generator { + private readonly appWizard: AppWizard; + private readonly vscode?: VSCodeInstance; + private readonly launchAppDownloaderAsSubGenerator: boolean; + private readonly appRootPath: string; + private readonly prompts: Prompts; + private answers: BspAppDownloadAnswers; + public options: BspAppDownloadOptions; + private fioriOptions: FioriOptions; + // re visit this + private projectPath: string; + private extractedProjectPath: string; + setPromptsCallback: (fn: object) => void; + + /** + * Constructor for Downloading App. + * + * @param args - arguments passed to the generator + * @param opts - options passed to the generator + */ + constructor(args: string | string[], opts: BspAppDownloadOptions) { + super(args, opts); + + this.appWizard = opts.appWizard ?? AppWizard.create(opts); + this.vscode = opts.vscode; + this.launchAppDownloaderAsSubGenerator = opts.launchAppDownloaderAsSubGenerator ?? false; + this.appRootPath = opts?.appRootPath ?? getDefaultTargetFolder(this.vscode) ?? this.destinationRoot(); + this.options = opts; + + BspAppDownloadLogger.configureLogging( + this.rootGeneratorName(), + this.log, + this.options.logWrapper, + this.options.logLevel, + this.options.logger, + this.vscode + ); + + // If launched standalone, set the header, title and description + if (!this.launchAppDownloaderAsSubGenerator) { + this.appWizard.setHeaderTitle(generatorTitle); + this.prompts = new Prompts(getYUIDetails()); + this.setPromptsCallback = (fn): void => { + if (this.prompts) { + this.prompts.setCallback(fn); + } + }; + } + } + + public async initializing(): Promise { + if ((this.env as unknown as YeomanEnvironment).conflicter) { + (this.env as unknown as YeomanEnvironment).conflicter.force = this.options.force ?? true; + } + + await TelemetryHelper.initTelemetrySettings({ + consumerModule: { + name: generatorName, + version: this.rootGeneratorVersion() + }, + internalFeature: isInternalFeaturesSettingEnabled(), + watchTelemetrySettingStore: false + }); + } + + public async prompting(): Promise { + const questions = await getQuestions(this.appRootPath); + const { selectedApp, targetFolder } = (await this.prompt(questions)) as BspAppDownloadAnswers; + if (PromptState.systemSelection.connectedSystem?.serviceProvider && selectedApp?.appId && targetFolder) { + Object.assign(this.answers, { + selectedApp, + targetFolder, + serviceProvider: PromptState.systemSelection.connectedSystem?.serviceProvider + }); + + this.projectPath = join(targetFolder, selectedApp.appId); + this.extractedProjectPath = join(this.projectPath, extractedFilePath); + // Trigger app download + await downloadApp( + this.answers, + this.extractedProjectPath, + this.fs, + BspAppDownloadLogger.logger as unknown as Logger + ); + } + } + + public async writing(): Promise { + const config = await getAppConfig( + this.answers, + this.extractedProjectPath, + this.fs, + BspAppDownloadLogger.logger as unknown as Logger + ); + await generate(this.projectPath, config, this.fs); + await generateDeployConfig( + this.projectPath, + { + // todo: get from json file + target: { + url: `TEST_URL`, + destination: `TEST_DESTINATION` + }, + app: { + name: this.answers.selectedApp.appId, + package: `TEST_PACKAGE`, + description: this.answers.selectedApp.description, + transport: `TEST_TRANSPORT_REQ` + } + }, + undefined, + this.fs + ); + const readMeConfig = this._getReadMeConfig(config); + generateReadMe(this.projectPath, readMeConfig, this.fs); + this.fioriOptions = this._getLaunchConfig(config); + writeApplicationInfoSettings(this.projectPath); + } + + /** + * + * @param config + */ + private _getReadMeConfig(config: FioriElementsApp): ReadMe { + const readMeConfig: ReadMe = { + appName: config.app.id, + appTitle: config.app.title ?? '', + appNamespace: '', // todo: cant find namespace in manifest json - default? + appDescription: t('readMe.appDescription'), + ui5Theme: getDefaultUI5Theme(config.ui5?.version), + generatorName: generatorName, // todo: check if this is correct + generatorVersion: this.rootGeneratorVersion(), + ui5Version: config.ui5?.version ?? '', + template: TemplateType.ListReportObjectPage, + serviceUrl: config.service.url, + launchText: t('readMe.launchText') + }; + return readMeConfig; + } + + /** + * + * @param config + */ + private _getLaunchConfig(config: FioriElementsApp): FioriOptions { + const debugOptions: DebugOptions = { + vscode: this.vscode, + addStartCmd: true, + sapClientParam: '', // todo: check if sap-client info is available + flpAppId: config.app.flpAppId ?? config.app.id, + flpSandboxAvailable: true, + isAppStudio: isAppStudio(), + odataVersion: config.service.version === OdataVersion.v2 ? '2.0' : '4.0' + }; + const fioriOptions: FioriOptions = { + name: config.app.id, + projectRoot: this.projectPath, + debugOptions + }; + return fioriOptions; + } + + public async install(): Promise { + if (!this.options.skipInstall) { + try { + await this._runNpmInstall(this.projectPath); + } catch (error) { + BspAppDownloadLogger.logger?.error(t('error.npmInstall', { error })); + } + } else { + BspAppDownloadLogger.logger?.info(t('info.skippedInstallation')); + } + } + /** + * Runs npm install in the specified path. + * + * @param path - the path to run npm install + */ + private async _runNpmInstall(path: string): Promise { + const npm = platform() === 'win32' ? 'npm.cmd' : 'npm'; + // install dependencies + await this.spawnCommand( + npm, + ['install', '--no-audit', '--no-fund', '--silent', '--prefer-offline', '--no-progress'], + { + cwd: path + } + ); + } + + async end() { + sendTelemetry( + EventName.GENERATION_SUCCESS, + TelemetryHelper.createTelemetryData({ + appType: 'bsp-app-download-sub-generator', + ...this.options.telemetryData + }) ?? {} + ).catch((error) => { + BspAppDownloadLogger.logger.error(t('error.telemetry', { error })); + }); + debugger; + await createLaunchConfig( + this.projectPath, + this.fioriOptions, + this.fs, + BspAppDownloadLogger.logger as unknown as Logger + ); + // delete extracted path before commiting the changes + // this.fs.delete(this.extractedProjectPath); + if (this.options.data?.postGenCommands) { + await runPostAppGenHook({ + path: this.projectPath, + vscodeInstance: this.vscode, + postGenCommand: this.options.data?.postGenCommands + }); + } + } +} + +export { promptNames }; +export type { BspAppDownloadOptions }; diff --git a/packages/bsp-app-download-sub-generator/src/app/prompts.ts b/packages/bsp-app-download-sub-generator/src/app/prompts.ts new file mode 100644 index 0000000000..5a83ae39e9 --- /dev/null +++ b/packages/bsp-app-download-sub-generator/src/app/prompts.ts @@ -0,0 +1,105 @@ +import type { AbapServiceProvider, AppIndex } from '@sap-ux/axios-extension'; +import { getSystemSelectionQuestions, promptNames } from '@sap-ux/odata-service-inquirer'; +import type { BspAppDownloadAnswers } from './types'; +import { PromptNames } from './types'; +import { Severity } from '@sap-devx/yeoman-ui-types'; +import { t } from '../utils/i18n'; +import type { FileBrowserQuestion } from '@sap-ux/inquirer-common'; +import type { Logger } from '@sap-ux/logger'; +import type { Question } from 'inquirer'; +import { getAppList } from '../utils/utils'; +import { validateTargetFolderForFioriApp } from '@sap-ux/project-input-validator'; +import { PromptState } from '../utils/prompt-state'; + +/** + * Gets the target folder prompt. + * + * @param appRootPath - The application root path. + * @returns - The target folder prompt. + */ +const getTargetFolderPrompt = (appRootPath?: string) => { + return { + type: 'input', + name: PromptNames.targetFolder, + message: t('prompts.targetPath.targetPathMessage'), + guiType: 'folder-browser', + when: (answers: BspAppDownloadAnswers) => Boolean(answers?.selectedApp?.appId), + guiOptions: { + applyDefaultWhenDirty: true, + mandatory: true, + breadcrumb: t('prompts.targetPath.targetPathBreadcrumb') + }, + validate: async (target, answers: BspAppDownloadAnswers): Promise => { + return await validateTargetFolderForFioriApp(target, answers.selectedApp.appId, true); + }, + default: () => appRootPath + } as FileBrowserQuestion; +}; + +/** + * + * @param appRootPath + * @param log + */ +export async function getQuestions(appRootPath?: string, log?: Logger): Promise[]> { + PromptState.reset(); + const systemQuestions = await getSystemSelectionQuestions({ serviceSelection: { hide: true } }, true); + let appList: AppIndex = []; + let result: Question[] = []; + + const appSelectionPrompt = [ + { + when: async (answers: BspAppDownloadAnswers): Promise => { + if (answers[promptNames.systemSelection] && systemQuestions.answers.connectedSystem?.serviceProvider) { + PromptState.systemSelection = { + connectedSystem: { + serviceProvider: systemQuestions.answers.connectedSystem + .serviceProvider as unknown as AbapServiceProvider + } + }; + appList = await getAppList( + PromptState.systemSelection?.connectedSystem?.serviceProvider as AbapServiceProvider + ); + return !!appList.length; + } + return false; + }, + type: 'list', + name: PromptNames.selectedApp, + guiOptions: { + mandatory: true, + breadcrumb: true + }, + message: t('prompts.appSelection.message'), + choices: async () => + appList.length + ? appList.map((app: any) => ({ + name: `${app['sap.app/id']}`, + value: { + appId: app['sap.app/id'], + title: app['sap.app/title'], + description: app['sap.app/description'], + repoName: app['repoName'], + url: app['url'] + } + })) + : [], + additionalMessages: async () => + appList.length === 0 + ? { + message: t('prompts.appSelection.noAppsDeployed'), + severity: Severity.warning + } + : undefined + } + ]; + + const targetFolderPrompts = getTargetFolderPrompt(appRootPath); + result = [ + ...systemQuestions.prompts, + ...appSelectionPrompt, + targetFolderPrompts + ] as Question[]; + + return result; +} diff --git a/packages/bsp-app-download-sub-generator/src/app/types.ts b/packages/bsp-app-download-sub-generator/src/app/types.ts new file mode 100644 index 0000000000..d22b168bf6 --- /dev/null +++ b/packages/bsp-app-download-sub-generator/src/app/types.ts @@ -0,0 +1,76 @@ +import type Generator from 'yeoman-generator'; +import type { AppWizard } from '@sap-devx/yeoman-ui-types'; +import type { VSCodeInstance, TelemetryData, LogWrapper } from '@sap-ux/fiori-generator-shared'; +import type { promptNames } from '@sap-ux/odata-service-inquirer'; +import type { Destination } from '@sap-ux/btp-utils'; +import type { BackendSystem } from '@sap-ux/store'; +import type { AbapServiceProvider } from '@sap-ux/axios-extension'; + +export interface BspAppDownloadOptions extends Generator.GeneratorOptions { + /** + * VSCode instance + */ + vscode?: VSCodeInstance; + /** + * AppWizard instance + */ + appWizard?: AppWizard; + /** + * Whether the generator is launched as a subgenerator + */ + launchAppDownloaderAsSubGenerator?: boolean; //todo: check this option + /** + * Path to the application root where the Fiori launchpad configuration will be added. + */ + appRootPath?: string; + /** + * Telemetry data to be send after deployment configuration has been added + */ + telemetryData?: TelemetryData; + /** + * Logger instance + */ + logWrapper?: LogWrapper; +} + +export interface BspAppDownloadAnswers { + [promptNames.systemSelection]: SystemSelectionAnswers; + selectedApp: { + appId: string; + title: string; + description: string; + repoName: string; + url: string; + }; + targetFolder: string; +} + +export const PromptNames = { + selectedApp: 'selectedApp', + systemSelection: 'systemSelection', + targetFolder: 'targetFolder' +}; + +export interface SystemSelectionAnswers { + /** + * The connected system will allow downstream consumers to access the connected system without creating new connections. + * + */ + connectedSystem?: { + /** + * Convienence property to pass the connected system + */ + serviceProvider: AbapServiceProvider; + + /** + * The persistable backend system representation of the connected service provider + * `newOrUpdated` is set to true if the system was newly created or updated during the connection validation process and should be considered for storage. + */ + backendSystem?: BackendSystem & { newOrUpdated?: boolean }; + + /** + * The destination information for the connected system + */ + destination?: Destination; + }; +} diff --git a/packages/bsp-app-download-sub-generator/src/telemetryEvents/index.ts b/packages/bsp-app-download-sub-generator/src/telemetryEvents/index.ts new file mode 100644 index 0000000000..61fa53f5ab --- /dev/null +++ b/packages/bsp-app-download-sub-generator/src/telemetryEvents/index.ts @@ -0,0 +1,6 @@ +/** + * Event names for telemetry for the generator when downloading an app from BSP + */ +export enum EventName { + GENERATION_SUCCESS = 'GENERATION_SUCCESS' +} diff --git a/packages/bsp-app-download-sub-generator/src/translations/bsp-app-download-sub-generator.i18n.json b/packages/bsp-app-download-sub-generator/src/translations/bsp-app-download-sub-generator.i18n.json new file mode 100644 index 0000000000..b96b884c90 --- /dev/null +++ b/packages/bsp-app-download-sub-generator/src/translations/bsp-app-download-sub-generator.i18n.json @@ -0,0 +1,20 @@ +{ + "error": { + "telemetry": "Failed to send telemetry data after downloading app from BSP. {{- error}}", + "noAppsDeployed": "No basic applications deployed to this system can be downloaded. Please see for more details" + }, + "prompts": { + "appSelection": { + "message": "App", + "hint": "Select the app to download" + }, + "targetPath": { + "targetPathMessage": "Project folder path", + "targetPathBreadcrumb": "Project Path" + } + }, + "readMe": { + "appDescription" : "This application was converted from an ABAP basic app that was deployed from ADT", + "launchText": "In order to launch the generated app, simply run the following from the generated app root folder:\n\n```\n npm start\n```" + } +} diff --git a/packages/bsp-app-download-sub-generator/src/utils/app-config.ts b/packages/bsp-app-download-sub-generator/src/utils/app-config.ts new file mode 100644 index 0000000000..99cf8a1add --- /dev/null +++ b/packages/bsp-app-download-sub-generator/src/utils/app-config.ts @@ -0,0 +1,162 @@ +import { TemplateType, type FioriElementsApp, type LROPSettings } from '@sap-ux/fiori-elements-writer'; +import { type Manifest } from '@sap-ux/project-access'; +import { OdataVersion } from '@sap-ux/odata-service-inquirer'; +import type { AbapServiceProvider, ServiceDocument } from '@sap-ux/axios-extension'; +import type { Logger } from '@sap-ux/logger'; +import type { Editor } from 'mem-fs-editor'; +import { t } from './i18n'; +import type { BspAppDownloadAnswers } from '../app/types'; +import { readManifest } from './utils'; +import { getLatestUI5Version } from '@sap-ux/ui5-info'; +import { getMinimumUI5Version } from '@sap-ux/project-access'; +import { adtSourceTemplateId } from './constants'; +import { PromptState } from '../utils/prompt-state'; + +/** + * Retrieves metadata for the provided service URL. + * + * @param {AbapServiceProvider} provider - The ABAP service provider. + * @param {string} serviceUrl - The URL of the service to fetch metadata for. + * @param {Logger} [log] - The logger instance. + * @returns {Promise} - The retrieved metadata. + */ +const fetchMetadata = async (provider: AbapServiceProvider, serviceUrl: string, log?: Logger): Promise => { + try { + return await provider.service(serviceUrl).metadata(); + } catch (err) { + log?.error(`Error fetching metadata: ${err.message}`); + throw err; + } +}; + +// /** +// * Retrieves the main entity name from the manifest routing targets. +// * +// * @param {string} entity - The entity to check for in the manifest. +// * @param {Manifest} manifest - The manifest.json object. +// * @returns {string} - The valid main entity name if found, otherwise throws an error. +// * @throws {Error} - If the routing configuration is invalid or the entity is not found. +// */ +// const getMainEntityName = (entity: string, manifest: Manifest): string => { +// const entityList = `${entity}List`; +// const targetConfig = manifest['sap.ui5']?.routing?.targets?.[entityList]?.options as +// | Record +// | undefined; + +// if (!targetConfig) { +// throw new Error(`Could not find entity name: "${entityList}" in manifest.json!`); +// } +// const { settings } = targetConfig; +// if (!settings) { +// throw new Error(`Invalid or missing 'settings' in navigation source: "${entityList}"`); +// } +// // Extract the contextPath and entitySet from the settings +// const contextPath = settings.contextPath; +// const entitySet = settings.entitySet; +// // Validate the extracted paths +// if (contextPath && contextPath === `/${entity}`) { +// return entity; +// } else if (entitySet && entitySet === entity) { +// return entity; +// } +// throw new Error( +// `Invalid routing configuration for navigation source: "${entityList}". Neither contextPath nor entitySet matches the entity: "${entity}".` +// ); +// }; + +const getEntity = async (provider: AbapServiceProvider, manifest: Manifest): Promise => { + const entities: ServiceDocument = await provider + .service(manifest?.['sap.app']?.dataSources?.mainService.uri ?? '') + .document(); + if (!entities.EntitySets || entities.EntitySets.length === 0) { + throw Error(t('error.entitySetsNotFound')); + } + return entities.EntitySets[0]; +}; + +/** + * Generates the application configuration based on the manifest.json file. + * + * @param {any} answers - The user inputs containing appId. + * @param extractedProjectPath + * @param {Editor} fs - The file system editor. + * @param {Logger} [log] - The logger instance. + * @returns {FioriElementsApp} - The generated application configuration. + */ +export async function getAppConfig( + answers: BspAppDownloadAnswers, + extractedProjectPath: string, + fs: Editor, + log?: Logger +): Promise> { + try { + const { selectedApp } = answers; + const manifest = await readManifest(extractedProjectPath, fs); + const serviceProvider = PromptState.systemSelection?.connectedSystem?.serviceProvider as AbapServiceProvider; + if (!manifest?.['sap.app']?.dataSources) { + throw Error(t('error.dataSourcesNotFound')); + } + + const odataVersion = + manifest?.['sap.app']?.dataSources?.mainService?.settings?.odataVersion === '4.0' + ? OdataVersion.v4 + : OdataVersion.v2; + const metadata = await fetchMetadata(serviceProvider, manifest?.['sap.app']?.dataSources?.mainService.uri, log); + const entity = await getEntity(serviceProvider, manifest); + //const mainEntityName = getMainEntityName(entity, manifest); + const routes = (manifest?.['sap.ui5']?.routing?.routes ?? []) as Array<{ + pattern: string; + name: string; + target: string; + }>; + const mainEntityName = routes.find((route) => route.pattern === ':?query:')?.name; + + const appConfig: FioriElementsApp = { + app: { + id: selectedApp.appId, + title: selectedApp.title, + description: selectedApp.description, + sourceTemplate: { + id: adtSourceTemplateId, + version: manifest?.['sap.app']?.sourceTemplate?.version, + toolsId: manifest?.['sap.app']?.sourceTemplate?.toolsId + }, + projectType: 'EDMXBackend', + flpAppId: `${selectedApp.appId.replace(/[-_.]/g, '')}-tile` // todo: check if flpAppId is correct + }, + package: { + name: selectedApp.appId, + description: selectedApp.description, + devDependencies: {}, + sapuxLayer: 'VENDOR', // todo: add internal feature enabled check, + scripts: {}, + version: manifest?.['sap.app']?.applicationVersion?.version ?? '0.0.1' + }, + template: { + type: TemplateType.ListReportObjectPage, + settings: { + entityConfig: { + mainEntityName: mainEntityName ?? entity ?? '' // todo: add check for v2 service + } + } + }, + service: { + path: manifest?.['sap.app']?.dataSources?.mainService.uri, + version: odataVersion, + metadata, + url: serviceProvider.defaults.baseURL + }, + appOptions: { + addAnnotations: odataVersion === OdataVersion.v4, + addTests: true + }, + ui5: { + version: getMinimumUI5Version(manifest) ?? (await getLatestUI5Version()) + } + }; + return appConfig; + } catch (error) { + log?.error(`Error generating application configuration: ${error.message}`); + throw error; + } +} diff --git a/packages/bsp-app-download-sub-generator/src/utils/constants.ts b/packages/bsp-app-download-sub-generator/src/utils/constants.ts new file mode 100644 index 0000000000..eaf0812ece --- /dev/null +++ b/packages/bsp-app-download-sub-generator/src/utils/constants.ts @@ -0,0 +1,20 @@ +export const generatorTitle = 'Fiori App Download from BSP'; +export const generatorDescription = 'Download a Fiori LROP app from a BSP reapository'; +export const extractedFilePath = 'extractedFiles'; + +export const generatorName = '@sap-ux/bsp-app-download-sub-generator'; +export const adtSourceTemplateId = '@sap.adt.sevicebinding.deploy:lrop'; + +// filter using source template id +export const appListSearchParams = { + 'sap.app/sourceTemplate/id': adtSourceTemplateId +}; +export const appListResultFields = [ + 'sap.app/id', + 'sap.app/title', + 'sap.app/description', + 'sap.app/sourceTemplate/id', + 'repoName', + 'fileType', + 'url' +]; diff --git a/packages/bsp-app-download-sub-generator/src/utils/eventHook.ts b/packages/bsp-app-download-sub-generator/src/utils/eventHook.ts new file mode 100644 index 0000000000..fa3699c1d9 --- /dev/null +++ b/packages/bsp-app-download-sub-generator/src/utils/eventHook.ts @@ -0,0 +1,25 @@ +import ReuseLibGenLogger from './logger'; +import { t } from './i18n'; +import type { VSCodeInstance } from '@sap-ux/fiori-generator-shared'; + +export interface BspAppGenContext { + path: string; + postGenCommand: string; + vscodeInstance?: VSCodeInstance; +} +export const POST_LIB_GEN_COMMAND = 'sap.ux.library.generated.handler'; + +/** + * Executes post app generation command. + * + * @param context BspAppGenContext + */ +export async function runPostAppGenHook(context: BspAppGenContext): Promise { + try { + await context.vscodeInstance?.commands?.executeCommand?.(context.postGenCommand, { + fsPath: context.path + }); + } catch (e) { + ReuseLibGenLogger.logger.error(t('error.postLibGenHook', { error: e })); + } +} diff --git a/packages/bsp-app-download-sub-generator/src/utils/i18n.ts b/packages/bsp-app-download-sub-generator/src/utils/i18n.ts new file mode 100644 index 0000000000..6912814463 --- /dev/null +++ b/packages/bsp-app-download-sub-generator/src/utils/i18n.ts @@ -0,0 +1,32 @@ +import type { TOptions } from 'i18next'; +import i18next from 'i18next'; +import translations from '../translations/bsp-app-download-sub-generator.i18n.json'; + +const bspAppDownloadGeneratorNs = 'bsp-app-download-sub-generator'; + +/** + * Initialize i18next with the translations for this module. + */ +export async function initI18n(): Promise { + await i18next.init({ lng: 'en', fallbackLng: 'en' }, () => + i18next.addResourceBundle('en', bspAppDownloadGeneratorNs, translations) + ); +} + +/** + * Helper function facading the call to i18next. Unless a namespace option is provided the local namespace will be used. + * + * @param key i18n key + * @param options additional options + * @returns {string} localized string stored for the given key + */ +export function t(key: string, options?: TOptions): string { + if (!options?.ns) { + options = Object.assign(options ?? {}, { ns: bspAppDownloadGeneratorNs }); + } + return i18next.t(key, options); +} + +initI18n().catch(() => { + // Needed for lint +}); diff --git a/packages/bsp-app-download-sub-generator/src/utils/index.ts b/packages/bsp-app-download-sub-generator/src/utils/index.ts new file mode 100644 index 0000000000..3135b79c23 --- /dev/null +++ b/packages/bsp-app-download-sub-generator/src/utils/index.ts @@ -0,0 +1,67 @@ +import { getHostEnvironment, hostEnvironment } from '@sap-ux/fiori-generator-shared'; +import { t } from './i18n'; +import { MessageType, type AppWizard } from '@sap-devx/yeoman-ui-types'; +import BspAppDownloadLogger from './logger'; + +export enum ERROR_TYPE {} +// ABORT_SIGNAL = 'ABORT_SIGNAL', +// NO_MANIFEST = 'NO_MANIFEST', +// NO_APP_NAME = 'NO_APP_NAME', +// NO_CDS_BIN = 'NO_CDS_BIN', +// NO_MTA_BIN = 'NO_MTA_BIN' + +/** + * Error messages for the deploy configuration generator. + */ +export class ErrorHandler { + /** + * Get the error message for the specified error type. + * + * @param errorType The error type for which the message may be returned + * @returns The error message for the specified error type + */ + public static getErrorMsgFromType(errorType?: ERROR_TYPE): string { + if (errorType) { + return ErrorHandler._errorTypeToMsg[errorType](); + } + return t('errors.unknownError'); + } + + private static readonly _errorTypeToMsg: Record string> = { + // [ERROR_TYPE.ABORT_SIGNAL]: () => t('errors.abortSignal'), + // [ERROR_TYPE.NO_MANIFEST]: () => t('errors.noManifest'), + // [ERROR_TYPE.NO_APP_NAME]: () => t('errors.noAppName') + }; +} + +/** + * Bail out with an error message. + * + * @param errorMessage - Error message to be displayed + */ +export function bail(errorMessage: string): void { + throw new Error(errorMessage); +} + +/** + * Handle error message, display it in the UI or throws an error in CLI. + * + * @param appWizard - AppWizard instance + * @param error - error type or message + * @param error.errorType - error type + * @param error.errorMsg - error message + */ +export function handleErrorMessage( + appWizard: AppWizard, + { errorType, errorMsg }: { errorType?: ERROR_TYPE; errorMsg?: string } +): void { + const error = errorMsg ?? ErrorHandler.getErrorMsgFromType(errorType); + if (getHostEnvironment() === hostEnvironment.cli) { + bail(error); + } else { + BspAppDownloadLogger.logger?.debug(error); + appWizard?.showError(error, MessageType.notification); + } +} + +export { t } from './i18n'; diff --git a/packages/bsp-app-download-sub-generator/src/utils/logger.ts b/packages/bsp-app-download-sub-generator/src/utils/logger.ts new file mode 100644 index 0000000000..d7b267483a --- /dev/null +++ b/packages/bsp-app-download-sub-generator/src/utils/logger.ts @@ -0,0 +1,50 @@ +import { DefaultLogger, LogWrapper, type ILogWrapper } from '@sap-ux/fiori-generator-shared'; +import type { Logger } from 'yeoman-environment'; +import type { IVSCodeExtLogger, LogLevel } from '@vscode-logging/logger'; + +/** + * Static logger prevents passing of logger references through all functions, as this is a cross-cutting concern. + */ +export default class BspAppDownloadLogger { + private static _logger: ILogWrapper = DefaultLogger; + + /** + * Get the logger. + * + * @returns the logger + */ + public static get logger(): ILogWrapper { + return BspAppDownloadLogger._logger; + } + + /** + * Set the logger. + * + * @param value the logger to set + */ + public static set logger(value: ILogWrapper) { + BspAppDownloadLogger._logger = value; + } + + /** + * Configures the logger. + * + * @param loggerName - the logger name + * @param yoLogger - the yeoman logger + * @param logWrapper - log wrapper instance + * @param logLevel - the log level + * @param vscLogger - the vscode logger + * @param vscode - the vscode instance + */ + static configureLogging( + loggerName: string, + yoLogger: Logger, + logWrapper?: LogWrapper, + logLevel?: LogLevel, + vscLogger?: IVSCodeExtLogger, + vscode?: unknown + ): void { + const logger = logWrapper ?? new LogWrapper(loggerName, yoLogger, logLevel, vscLogger, vscode); + BspAppDownloadLogger.logger = logger; + } +} diff --git a/packages/bsp-app-download-sub-generator/src/utils/prompt-state.ts b/packages/bsp-app-download-sub-generator/src/utils/prompt-state.ts new file mode 100644 index 0000000000..cdb327feab --- /dev/null +++ b/packages/bsp-app-download-sub-generator/src/utils/prompt-state.ts @@ -0,0 +1,33 @@ +import type { SystemSelectionAnswers } from '../app/types'; + +/** + * Much of the values returned by the bsp app downloader prompting are derived from prompt answers and are not direct answer values. + * Since inquirer does not provide a way to return values that are not direct answers from prompts, this class will maintain the derived values + * across prompts statically for the lifespan of the prompting session. + * + */ +export class PromptState { + private static _systemSelection: SystemSelectionAnswers = {}; + + /** + * Returns the current state of the service config. + * + * @returns {ServiceConfig} service config + */ + public static get systemSelection(): SystemSelectionAnswers { + return this._systemSelection; + } + + /** + * Set the state of the system selection. + * + * @param {SystemSelectionAnswers} value - system selection value + */ + public static set systemSelection(value: Partial) { + this._systemSelection = value; + } + + static reset(): void { + PromptState.systemSelection = {}; + } +} diff --git a/packages/bsp-app-download-sub-generator/src/utils/utils.ts b/packages/bsp-app-download-sub-generator/src/utils/utils.ts new file mode 100644 index 0000000000..3144216017 --- /dev/null +++ b/packages/bsp-app-download-sub-generator/src/utils/utils.ts @@ -0,0 +1,127 @@ +import { + generatorTitle, + generatorDescription, + appListSearchParams, + appListResultFields, + adtSourceTemplateId +} from './constants'; +import type { AbapServiceProvider, AppIndex } from '@sap-ux/axios-extension'; +import AdmZip from 'adm-zip'; +import type { Logger } from '@sap-ux/logger'; +import { join } from 'path'; +import type { Editor } from 'mem-fs-editor'; +import type { BspAppDownloadAnswers } from '../app/types'; +import { FileName, type Manifest } from '@sap-ux/project-access'; +import { t } from './i18n'; +import { PromptState } from './prompt-state'; + +/** + * Returns the details for the YUI prompt. + * + * @returns step details + */ +export function getYUIDetails(): { name: string; description: string }[] { + return [ + { + name: generatorTitle, + description: generatorDescription + } + ]; +} + +/** + * Retrieves a list of deployed applications from abap respository. + * + * @param {AbapServiceProvider} provider - The ABAP service provider. + * @param {Logger} log - The logger instance. + * @returns {Promise>} - List of applications filtered by source template. + */ +export async function getAppList(provider: AbapServiceProvider, log?: Logger): Promise { + try { + const appIndexService = provider.getAppIndex(); + return await appIndexService.search(appListSearchParams, appListResultFields); + } catch (error) { + log?.error(`Error fetching application list: ${error.message}`); + return []; + } +} + +/** + * Extracts ZIP archive to a temporary directory. + * + * @param extractedProjectPath + * @param {Buffer} archive - The ZIP archive buffer. + * @param fs + * @param {Logger} log - The logger instance. + */ +async function extractZip(extractedProjectPath: string, archive: Buffer, fs: Editor, log?: Logger): Promise { + try { + const zip = new AdmZip(archive); + //zip.extractAllTo(join(fioriToolsExtractionPath, appId), true); + const zipEntries = zip.getEntries(); // an array of ZipEntry records + zipEntries.forEach(function (zipEntry) { + if (!zipEntry.isDirectory) { + // Extract the file content + const fileContent = zipEntry.getData().toString('utf8'); + // Add the file content to mem-fs at a virtual path + fs.write(join(extractedProjectPath, zipEntry.entryName), fileContent); + } + }); + } catch (error) { + log?.error(`Error extracting zip: ${error.message}`); + } +} + +/** + * Downloads application files from the server. + * + * @param {AbapServiceProvider} provider - The ABAP service provider. + * @param answers + * @param extractedProjectPath + * @param fs + * @param {Logger} log - The logger instance. + */ +export async function downloadApp( + answers: BspAppDownloadAnswers, + extractedProjectPath: string, + fs: Editor, + log?: Logger +): Promise { + try { + const { selectedApp } = answers; + const ui5AbapRepositoryService = ( + PromptState.systemSelection?.connectedSystem?.serviceProvider as AbapServiceProvider + ).getUi5AbapRepository(); + const archive = await ui5AbapRepositoryService.downloadFiles(selectedApp.repoName); + + if (Buffer.isBuffer(archive)) { + await extractZip(extractedProjectPath, archive, fs, log); + } else { + log?.error('Error: The downloaded file is not a Buffer.'); + } + } catch (error) { + throw Error(`Error downloading file: ${error.message}`); + } +} + +/** + * Reads and validates the manifest.json file. + * + * @param {string} extractedProjectPath - The path to the extracted project. + * @param {Editor} fs - The file system editor. + * @returns {Promise} - The manifest object. + */ +export async function readManifest(extractedProjectPath: string, fs: Editor): Promise { + const manifestPath = join(extractedProjectPath, FileName.Manifest); + const manifest = fs.readJSON(manifestPath) as unknown as Manifest; + if (!manifest) { + throw Error(t('error.manifestNotFound')); + } + if (!manifest['sap.app']) { + throw Error(t('error.sapAppNotDefined')); + } + if (manifest['sap.app'].sourceTemplate?.id !== adtSourceTemplateId) { + throw Error(t('error.sourceTemplateNotSupported')); + } + return manifest; +} diff --git a/packages/bsp-app-download-sub-generator/tsconfig.eslint.json b/packages/bsp-app-download-sub-generator/tsconfig.eslint.json new file mode 100644 index 0000000000..d5f1aa3474 --- /dev/null +++ b/packages/bsp-app-download-sub-generator/tsconfig.eslint.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "include": ["src", "test", ".eslintrc.js"] +} diff --git a/packages/bsp-app-download-sub-generator/tsconfig.json b/packages/bsp-app-download-sub-generator/tsconfig.json new file mode 100644 index 0000000000..b6597d08a7 --- /dev/null +++ b/packages/bsp-app-download-sub-generator/tsconfig.json @@ -0,0 +1,64 @@ +{ + "extends": "../../tsconfig.json", + "include": [ + "src", + "src/**/*.json" + ], + "compilerOptions": { + "rootDir": "src", + "outDir": "generators" + }, + "references": [ + { + "path": "../abap-deploy-config-writer" + }, + { + "path": "../btp-utils" + }, + { + "path": "../feature-toggle" + }, + { + "path": "../fiori-elements-writer" + }, + { + "path": "../fiori-generator-shared" + }, + { + "path": "../fiori-tools-settings" + }, + { + "path": "../i18n" + }, + { + "path": "../inquirer-common" + }, + { + "path": "../launch-config" + }, + { + "path": "../logger" + }, + { + "path": "../nodejs-utils" + }, + { + "path": "../odata-service-inquirer" + }, + { + "path": "../project-access" + }, + { + "path": "../project-input-validator" + }, + { + "path": "../ui5-application-inquirer" + }, + { + "path": "../ui5-application-writer" + }, + { + "path": "../ui5-info" + } + ] +} diff --git a/packages/fiori-tools-settings/src/applicationInfoHandler.ts b/packages/fiori-tools-settings/src/applicationInfoHandler.ts index 87bbe129d8..66fb60c7da 100644 --- a/packages/fiori-tools-settings/src/applicationInfoHandler.ts +++ b/packages/fiori-tools-settings/src/applicationInfoHandler.ts @@ -50,7 +50,9 @@ export function writeApplicationInfoSettings(path: string, fs?: Editor) { appInfoContents.latestGeneratedFiles.push(path); fs.write(appInfoFilePath, JSON.stringify(appInfoContents, null, 2)); fs.commit((err) => { - console.log('Error in writting to AppInfo.json file', err); + if(err){ + console.log('Error in writting to AppInfo.json file', err); + } }); } @@ -68,7 +70,9 @@ export function deleteAppInfoSettings(fs?: Editor) { try { fs.delete(appInfoFilePath); fs.commit((err) => { - console.log('Failed to commit the deletion of the AppInfo.json file: ', err); + if(err){ + console.log('Failed to commit the deletion of the AppInfo.json file: ', err); + } }); } catch (err) { throw new Error(`Error deleting appInfo.json file: ${err}`); diff --git a/packages/launch-config/src/launch-config-crud/create.ts b/packages/launch-config/src/launch-config-crud/create.ts index 4147d6c03f..93f45c5a1b 100644 --- a/packages/launch-config/src/launch-config-crud/create.ts +++ b/packages/launch-config/src/launch-config-crud/create.ts @@ -158,6 +158,7 @@ async function handleDebugOptions( } else { writeLaunchJsonFile(fs, launchJsonWritePath, configurations); } + debugger; // The `workspaceFolderUri` is a URI obtained from VS Code that specifies the path to the workspace folder. // This URI is populated when a reload of the workspace is required. It allows us to identify and update diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dbd64de094..2dc1f14fbd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -901,6 +901,127 @@ importers: specifier: 2.2.2 version: 2.2.2 + packages/bsp-app-download-sub-generator: + dependencies: + '@sap-devx/yeoman-ui-types': + specifier: 1.14.4 + version: 1.14.4 + '@sap-ux/abap-deploy-config-writer': + specifier: workspace:* + version: link:../abap-deploy-config-writer + '@sap-ux/btp-utils': + specifier: workspace:* + version: link:../btp-utils + '@sap-ux/feature-toggle': + specifier: workspace:* + version: link:../feature-toggle + '@sap-ux/fiori-elements-writer': + specifier: workspace:* + version: link:../fiori-elements-writer + '@sap-ux/fiori-generator-shared': + specifier: workspace:* + version: link:../fiori-generator-shared + '@sap-ux/fiori-tools-settings': + specifier: workspace:* + version: link:../fiori-tools-settings + '@sap-ux/i18n': + specifier: workspace:* + version: link:../i18n + '@sap-ux/inquirer-common': + specifier: workspace:* + version: link:../inquirer-common + '@sap-ux/launch-config': + specifier: workspace:* + version: link:../launch-config + '@sap-ux/logger': + specifier: workspace:* + version: link:../logger + '@sap-ux/odata-service-inquirer': + specifier: workspace:* + version: link:../odata-service-inquirer + '@sap-ux/project-access': + specifier: workspace:* + version: link:../project-access + '@sap-ux/project-input-validator': + specifier: workspace:* + version: link:../project-input-validator + '@sap-ux/ui5-application-inquirer': + specifier: workspace:* + version: link:../ui5-application-inquirer + '@sap-ux/ui5-application-writer': + specifier: workspace:* + version: link:../ui5-application-writer + '@sap-ux/ui5-info': + specifier: workspace:* + version: link:../ui5-info + adm-zip: + specifier: 0.5.10 + version: 0.5.10 + i18next: + specifier: 23.5.1 + version: 23.5.1 + inquirer: + specifier: 8.2.6 + version: 8.2.6 + yeoman-generator: + specifier: 5.10.0 + version: 5.10.0(mem-fs@2.1.0)(yeoman-environment@3.19.3) + devDependencies: + '@jest/types': + specifier: 29.6.3 + version: 29.6.3 + '@sap-ux/nodejs-utils': + specifier: workspace:* + version: link:../nodejs-utils + '@types/adm-zip': + specifier: 0.5.5 + version: 0.5.5 + '@types/inquirer': + specifier: 8.2.6 + version: 8.2.6 + '@types/lodash': + specifier: 4.14.202 + version: 4.14.202 + '@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 + '@types/yeoman-generator': + specifier: 5.2.11 + version: 5.2.11 + '@types/yeoman-test': + specifier: 4.0.6 + version: 4.0.6 + '@vscode-logging/logger': + specifier: 2.0.0 + version: 2.0.0 + lodash: + specifier: 4.17.21 + version: 4.17.21 + mem-fs-editor: + specifier: 9.4.0 + version: 9.4.0(mem-fs@2.1.0) + memfs: + specifier: 3.4.13 + version: 3.4.13 + rimraf: + specifier: 5.0.5 + version: 5.0.5 + typescript: + specifier: 5.3.3 + version: 5.3.3 + unionfs: + specifier: 4.4.0 + version: 4.4.0 + yeoman-test: + specifier: 6.3.0 + version: 6.3.0(mem-fs@2.1.0)(yeoman-environment@3.19.3)(yeoman-generator@5.10.0) + packages/btp-utils: dependencies: '@sap/bas-sdk': @@ -23434,7 +23555,7 @@ packages: pacote: 15.2.0 read-pkg-up: 7.0.1 run-async: 2.4.1 - semver: 7.5.4 + semver: 7.6.3 shelljs: 0.8.5 sort-keys: 4.2.0 text-table: 0.2.0 diff --git a/tsconfig.json b/tsconfig.json index eac773b50d..81194d36f8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -50,6 +50,9 @@ { "path": "packages/backend-proxy-middleware" }, + { + "path": "packages/bsp-app-download-sub-generator" + }, { "path": "packages/btp-utils" }, From f661d4e03470b8181c03036501440513bfcd838a Mon Sep 17 00:00:00 2001 From: Austin Devine Date: Wed, 26 Mar 2025 01:14:16 +0000 Subject: [PATCH 03/41] Updates for debugging --- .vscode/launch.json | 16 +- .../package.json | 5 +- .../tsconfig.json | 6 + pnpm-lock.yaml | 1572 ++++++++++++++++- 4 files changed, 1589 insertions(+), 10 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 103482a9a5..8c7761c1da 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -429,6 +429,20 @@ "program": "${workspaceFolder}/node_modules/jest/bin/jest" }, "cwd": "${workspaceFolder}/packages/system-access" - } + }, + { + "type": "node", + "request": "launch", + "name": "bsp-app-download-sub-generator: Launch Yeoman generators/app", + "program": "${workspaceFolder}/packages/bsp-app-download-sub-generator/node_modules/yo/lib/cli.js", + "args": [ + "${workspaceFolder}/packages/bsp-app-download-sub-generator/generators/app/index.js" + ], + "env": { + }, + "stopOnEntry": true, + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen" + }, ] } diff --git a/packages/bsp-app-download-sub-generator/package.json b/packages/bsp-app-download-sub-generator/package.json index 5d77f4a329..7ace1e97b2 100644 --- a/packages/bsp-app-download-sub-generator/package.json +++ b/packages/bsp-app-download-sub-generator/package.json @@ -60,7 +60,9 @@ "@types/yeoman-generator": "5.2.11", "@types/yeoman-environment": "2.10.11", "@types/yeoman-test": "4.0.6", + "@sap-ux/axios-extension": "workspace:*", "@sap-ux/nodejs-utils": "workspace:*", + "@sap-ux/store": "workspace:*", "@vscode-logging/logger": "2.0.0", "@types/adm-zip": "0.5.5", "memfs": "3.4.13", @@ -70,7 +72,8 @@ "rimraf": "5.0.5", "typescript": "5.3.3", "unionfs": "4.4.0", - "yeoman-test": "6.3.0" + "yeoman-test": "6.3.0", + "yo": "4" }, "engines": { "node": ">=18.x" diff --git a/packages/bsp-app-download-sub-generator/tsconfig.json b/packages/bsp-app-download-sub-generator/tsconfig.json index b6597d08a7..e0b353d861 100644 --- a/packages/bsp-app-download-sub-generator/tsconfig.json +++ b/packages/bsp-app-download-sub-generator/tsconfig.json @@ -12,6 +12,9 @@ { "path": "../abap-deploy-config-writer" }, + { + "path": "../axios-extension" + }, { "path": "../btp-utils" }, @@ -51,6 +54,9 @@ { "path": "../project-input-validator" }, + { + "path": "../store" + }, { "path": "../ui5-application-inquirer" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2dc1f14fbd..61ba1f8148 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -970,9 +970,15 @@ importers: '@jest/types': specifier: 29.6.3 version: 29.6.3 + '@sap-ux/axios-extension': + specifier: workspace:* + version: link:../axios-extension '@sap-ux/nodejs-utils': specifier: workspace:* version: link:../nodejs-utils + '@sap-ux/store': + specifier: workspace:* + version: link:../store '@types/adm-zip': specifier: 0.5.5 version: 0.5.5 @@ -1021,6 +1027,9 @@ importers: yeoman-test: specifier: 6.3.0 version: 6.3.0(mem-fs@2.1.0)(yeoman-environment@3.19.3)(yeoman-generator@5.10.0) + yo: + specifier: '4' + version: 4.3.1(mem-fs@2.1.0) packages/btp-utils: dependencies: @@ -9023,6 +9032,21 @@ packages: /@sinclair/typebox@0.27.8: resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + /@sindresorhus/is@0.14.0: + resolution: {integrity: sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==} + engines: {node: '>=6'} + dev: true + + /@sindresorhus/is@0.7.0: + resolution: {integrity: sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==} + engines: {node: '>=4'} + dev: true + + /@sindresorhus/is@4.6.0: + resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} + engines: {node: '>=10'} + dev: true + /@sindresorhus/is@5.6.0: resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==} engines: {node: '>=14.16'} @@ -9436,6 +9460,20 @@ packages: dependencies: tslib: 2.6.3 + /@szmarczak/http-timer@1.1.2: + resolution: {integrity: sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==} + engines: {node: '>=6'} + dependencies: + defer-to-connect: 1.1.3 + dev: true + + /@szmarczak/http-timer@4.0.6: + resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} + engines: {node: '>=10'} + dependencies: + defer-to-connect: 2.0.1 + dev: true + /@szmarczak/http-timer@5.0.1: resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==} engines: {node: '>=14.16'} @@ -9627,6 +9665,15 @@ packages: '@types/connect': 3.4.38 '@types/node': 18.11.9 + /@types/cacheable-request@6.0.3: + resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} + dependencies: + '@types/http-cache-semantics': 4.0.1 + '@types/keyv': 3.1.4 + '@types/node': 18.11.9 + '@types/responselike': 1.0.3 + dev: true + /@types/cheerio@0.22.31: resolution: {integrity: sha512-Kt7Cdjjdi2XWSfrZ53v4Of0wG3ZcmaegFXjMmz9tfNrZSkzzo36G0AL1YqSdcIA78Etjt6E609pt5h1xnQkPUw==} dependencies: @@ -9872,6 +9919,12 @@ packages: '@types/node': 18.11.9 dev: false + /@types/keyv@3.1.4: + resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} + dependencies: + '@types/node': 18.11.9 + dev: true + /@types/linkify-it@3.0.3: resolution: {integrity: sha512-pTjcqY9E4nOI55Wgpz7eiI8+LzdYnw3qxXCfHyBDdPbYvbyLgWLJGh8EdPvqawwMK1Uo1794AUkkR38Fr0g+2g==} dev: true @@ -10073,6 +10126,12 @@ packages: resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} dev: true + /@types/responselike@1.0.3: + resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} + dependencies: + '@types/node': 18.11.9 + dev: true + /@types/sanitize-html@2.11.0: resolution: {integrity: sha512-7oxPGNQHXLHE48r/r/qjn7q0hlrs3kL7oZnGj0Wf/h9tj/6ibFyRkNbsDxaBBZ4XUZ0Dx5LGCyDJ04ytSofacQ==} dependencies: @@ -11247,6 +11306,11 @@ packages: engines: {node: '>=6'} dev: true + /ansi-escapes@1.4.0: + resolution: {integrity: sha512-wiXutNjDUlNEDWHcYH3jtZUhd3c4/VojassD8zHdHCY13xbZy2XbW+NKQwA0tWGBVzDA9qEzYwfoSsWmviidhw==} + engines: {node: '>=0.10.0'} + dev: true + /ansi-escapes@4.3.2: resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} engines: {node: '>=8'} @@ -11263,6 +11327,11 @@ packages: engines: {node: '>=0.10.0'} dev: true + /ansi-regex@3.0.1: + resolution: {integrity: sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==} + engines: {node: '>=4'} + dev: true + /ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -11271,6 +11340,11 @@ packages: resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} engines: {node: '>=12'} + /ansi-styles@2.2.1: + resolution: {integrity: sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==} + engines: {node: '>=0.10.0'} + dev: true + /ansi-styles@3.2.1: resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} engines: {node: '>=4'} @@ -11291,6 +11365,10 @@ packages: resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} engines: {node: '>=12'} + /ansi@0.3.1: + resolution: {integrity: sha512-iFY7JCgHbepc0b82yLaw4IMortylNb6wG4kL+4R0C3iv6i+RHGHux/yUX5BTiRvSX/shMnngjR1YyNMnXEFh5A==} + dev: true + /antlr4@4.9.3: resolution: {integrity: sha512-qNy2odgsa0skmNMCuxzXhM4M8J1YDaPv3TI+vCdnOAanu0N982wBrSqziDKRDctEZLZy9VffqIZXc0UGjjSP/g==} engines: {node: '>=14'} @@ -11557,6 +11635,11 @@ packages: is-shared-array-buffer: 1.0.3 dev: true + /arrify@1.0.1: + resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} + engines: {node: '>=0.10.0'} + dev: true + /arrify@2.0.1: resolution: {integrity: sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==} engines: {node: '>=8'} @@ -11976,6 +12059,23 @@ packages: rimraf: 3.0.2 write-file-atomic: 4.0.2 + /bin-version-check@4.0.0: + resolution: {integrity: sha512-sR631OrhC+1f8Cvs8WyVWOA33Y8tgwjETNPyyD/myRBXLkfS/vl74FmH/lFcRl9KY3zwGh7jFhvyk9vV3/3ilQ==} + engines: {node: '>=6'} + dependencies: + bin-version: 3.1.0 + semver: 5.7.2 + semver-truncate: 1.1.2 + dev: true + + /bin-version@3.1.0: + resolution: {integrity: sha512-Mkfm4iE1VFt4xd4vH+gx+0/71esbfus2LsnCGe8Pi4mndSPyT+NGES/Eg99jx8/lUGWfu3z2yuB/bt5UB+iVbQ==} + engines: {node: '>=6'} + dependencies: + execa: 1.0.0 + find-versions: 3.2.0 + dev: true + /binary-extensions@2.2.0: resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} engines: {node: '>=8'} @@ -12026,6 +12126,25 @@ packages: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} dev: true + /boolean@3.2.0: + resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + dev: true + + /boxen@5.1.2: + resolution: {integrity: sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==} + engines: {node: '>=10'} + dependencies: + ansi-align: 3.0.1 + camelcase: 6.3.0 + chalk: 4.1.2 + cli-boxes: 2.2.1 + string-width: 4.2.3 + type-fest: 0.20.2 + widest-line: 3.1.0 + wrap-ansi: 7.0.0 + dev: true + /boxen@7.1.1: resolution: {integrity: sha512-2hCgjEmP8YLWQ130n2FerGv7rYpfBmnmp9Uy2Le1vge6X3gZIfSmEzP5QTDElFxcvVcXlEn8Aq6MU/PZygIOog==} engines: {node: '>=14.16'} @@ -12270,6 +12389,11 @@ packages: unique-filename: 3.0.0 dev: true + /cacheable-lookup@5.0.4: + resolution: {integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==} + engines: {node: '>=10.6.0'} + dev: true + /cacheable-lookup@7.0.0: resolution: {integrity: sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==} engines: {node: '>=14.16'} @@ -12288,6 +12412,44 @@ packages: responselike: 3.0.0 dev: true + /cacheable-request@2.1.4: + resolution: {integrity: sha512-vag0O2LKZ/najSoUwDbVlnlCFvhBE/7mGTY2B5FgCBDcRD+oVV1HYTOwM6JZfMg/hIcM6IwnTZ1uQQL5/X3xIQ==} + dependencies: + clone-response: 1.0.2 + get-stream: 3.0.0 + http-cache-semantics: 3.8.1 + keyv: 3.0.0 + lowercase-keys: 1.0.0 + normalize-url: 2.0.1 + responselike: 1.0.2 + dev: true + + /cacheable-request@6.1.0: + resolution: {integrity: sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==} + engines: {node: '>=8'} + dependencies: + clone-response: 1.0.3 + get-stream: 5.2.0 + http-cache-semantics: 4.1.1 + keyv: 3.1.0 + lowercase-keys: 2.0.0 + normalize-url: 4.5.1 + responselike: 1.0.2 + dev: true + + /cacheable-request@7.0.4: + resolution: {integrity: sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==} + engines: {node: '>=8'} + dependencies: + clone-response: 1.0.3 + get-stream: 5.2.0 + http-cache-semantics: 4.1.1 + keyv: 4.5.4 + lowercase-keys: 2.0.0 + normalize-url: 6.1.0 + responselike: 2.0.1 + dev: true + /call-bind@1.0.7: resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} engines: {node: '>= 0.4'} @@ -12309,6 +12471,20 @@ packages: tslib: 2.6.3 dev: true + /camelcase-keys@4.2.0: + resolution: {integrity: sha512-Ej37YKYbFUI8QiYlvj9YHb6/Z60dZyPJW0Cs8sFilMbd2lP0bw3ylAq9yJkK4lcTA2dID5fG8LjmJYbO7kWb7Q==} + engines: {node: '>=4'} + dependencies: + camelcase: 4.1.0 + map-obj: 2.0.0 + quick-lru: 1.1.0 + dev: true + + /camelcase@4.1.0: + resolution: {integrity: sha512-FxAv7HpHrXbh3aPo4o2qxHay2lkLY3x5Mw3KeE4KQE8ysVfziWeRZDwcjauvwBSGEC/nXUPzZy8zeh4HokqOnw==} + engines: {node: '>=4'} + dev: true + /camelcase@5.3.1: resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} engines: {node: '>=6'} @@ -12335,6 +12511,11 @@ packages: /caniuse-lite@1.0.30001677: resolution: {integrity: sha512-fmfjsOlJUpMWu+mAAtZZZHz7UEwsUxIIvu1TJfO1HqFQvB/B+ii0xr9B5HpbZY/mC4XZ8SvjHJqtAY6pDPQEog==} + /capture-stack-trace@1.0.2: + resolution: {integrity: sha512-X/WM2UQs6VMHUtjUDnZTRI+i1crWteJySFzr9UpGoQa4WQffXVTTXuekjl7TjZRlcF2XfjgITT0HxZ9RnxeT0w==} + engines: {node: '>=0.10.0'} + dev: true + /case-sensitive-paths-webpack-plugin@2.4.0: resolution: {integrity: sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw==} engines: {node: '>=4'} @@ -12353,6 +12534,17 @@ packages: traverse: 0.3.9 dev: false + /chalk@1.1.3: + resolution: {integrity: sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==} + engines: {node: '>=0.10.0'} + dependencies: + ansi-styles: 2.2.1 + escape-string-regexp: 1.0.5 + has-ansi: 2.0.0 + strip-ansi: 3.0.1 + supports-color: 2.0.0 + dev: true + /chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} @@ -12483,6 +12675,10 @@ packages: zod: 3.23.8 dev: true + /ci-info@2.0.0: + resolution: {integrity: sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==} + dev: true + /ci-info@3.9.0: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} @@ -12506,17 +12702,38 @@ packages: resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} engines: {node: '>=6'} + /cli-boxes@1.0.0: + resolution: {integrity: sha512-3Fo5wu8Ytle8q9iCzS4D2MWVL2X7JVWRiS1BnXbTFDhS9c/REkM9vd1AmabsoZoY5/dGi5TT9iKL8Kb6DeBRQg==} + engines: {node: '>=0.10.0'} + dev: true + + /cli-boxes@2.2.1: + resolution: {integrity: sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==} + engines: {node: '>=6'} + dev: true + /cli-boxes@3.0.0: resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} engines: {node: '>=10'} dev: true + /cli-cursor@1.0.2: + resolution: {integrity: sha512-25tABq090YNKkF6JH7lcwO0zFJTRke4Jcq9iX2nr/Sz0Cjjv4gckmwlW6Ty/aoyFd6z3ysR2hMGC2GFugmBo6A==} + engines: {node: '>=0.10.0'} + dependencies: + restore-cursor: 1.0.1 + dev: true + /cli-cursor@3.1.0: resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} engines: {node: '>=8'} dependencies: restore-cursor: 3.1.0 + /cli-list@0.2.0: + resolution: {integrity: sha512-+3MlQHdTSiT7e3Uxco/FL1MjuIYLmvDEhCAekRLCrGimHGfAR1LbJwCrKGceVp95a4oDFVB9CtLWiw2MT8NDXw==} + dev: true + /cli-progress@3.12.0: resolution: {integrity: sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==} engines: {node: '>=4'} @@ -12534,6 +12751,10 @@ packages: dependencies: colors: 1.0.3 + /cli-width@2.2.1: + resolution: {integrity: sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==} + dev: true + /cli-width@3.0.0: resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} engines: {node: '>= 10'} @@ -12559,6 +12780,26 @@ packages: resolution: {integrity: sha512-KLLTJWrvwIP+OPfMn0x2PheDEP20RPUcGXj/ERegTgdmPEZylALQldygiqrPPu8P45uNuPs7ckmReLY6v/iA5g==} engines: {node: '>= 0.10'} + /clone-regexp@1.0.1: + resolution: {integrity: sha512-Fcij9IwRW27XedRIJnSOEupS7RVcXtObJXbcUOX93UCLqqOdRpkvzKywOOSizmEK/Is3S/RHX9dLdfo6R1Q1mw==} + engines: {node: '>=0.10.0'} + dependencies: + is-regexp: 1.0.0 + is-supported-regexp-flag: 1.0.1 + dev: true + + /clone-response@1.0.2: + resolution: {integrity: sha512-yjLXh88P599UOyPTFX0POsd7WxnbsVsGohcwzHOLspIhhpalPw1BcqED8NblyZLKcGrL8dTgMlcaZxV2jAD41Q==} + dependencies: + mimic-response: 1.0.1 + dev: true + + /clone-response@1.0.3: + resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} + dependencies: + mimic-response: 1.0.1 + dev: true + /clone-stats@1.0.0: resolution: {integrity: sha512-au6ydSpg6nsrigcZ4m8Bc9hxjeW+GJ8xh5G3BJCMt4WXe1H10UNaVOamqQTmrx1kjVuxAHIQSNU6hY4Nsn9/ag==} @@ -12771,6 +13012,16 @@ packages: /concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + /concat-stream@1.6.2: + resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==} + engines: {'0': node >= 0.8} + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 2.3.7 + typedarray: 0.0.6 + dev: true + /config-chain@1.1.13: resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} dependencies: @@ -12778,6 +13029,18 @@ packages: proto-list: 1.2.4 dev: true + /configstore@5.0.1: + resolution: {integrity: sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==} + engines: {node: '>=8'} + dependencies: + dot-prop: 5.3.0 + graceful-fs: 4.2.11 + make-dir: 3.1.0 + unique-string: 2.0.0 + write-file-atomic: 3.0.3 + xdg-basedir: 4.0.0 + dev: true + /configstore@6.0.0: resolution: {integrity: sha512-cD31W1v3GqUlQvbBCGcXmd2Nj9SvLDOP1oQ0YFuLETufzSPaKp11rYBsSOm7rCsW3OnIRAFM3OxRhceaXNYHkA==} engines: {node: '>=12'} @@ -12871,6 +13134,11 @@ packages: browserslist: 4.23.3 dev: true + /core-js@3.41.0: + resolution: {integrity: sha512-SJ4/EHwS36QMJd6h/Rg+GyR4A5xE0FSI3eZ+iBVpfqf1x0eTSg1smWLHrA+2jQThZSh97fmSgFSU8B61nxosxA==} + requiresBuild: true + dev: true + /core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -12905,6 +13173,13 @@ packages: readable-stream: 4.5.2 dev: false + /create-error-class@3.0.2: + resolution: {integrity: sha512-gYTKKexFO3kh200H1Nit76sRwRtOY32vQd3jpAQKpLtZqyNsSQNfI4N7o3eP2wUjV35pTWKRYqFUDBvUha/Pkw==} + engines: {node: '>=0.10.0'} + dependencies: + capture-stack-trace: 1.0.2 + dev: true + /create-jest@29.7.0(@types/node@18.11.9)(ts-node@10.9.2): resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -12953,6 +13228,11 @@ packages: shebang-command: 2.0.0 which: 2.0.2 + /crypto-random-string@2.0.0: + resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} + engines: {node: '>=8'} + dev: true + /crypto-random-string@4.0.0: resolution: {integrity: sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==} engines: {node: '>=12'} @@ -13173,9 +13453,41 @@ packages: resolution: {integrity: sha512-syBZ+rnAK3EgMsH2aYEOLUW7mZSY9Gb+0wUMCFsZvcmiz+HigA0LOcq/HoQqVuGG+EKykunc7QG2bzrponfaSw==} deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + /decamelize-keys@1.1.1: + resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} + engines: {node: '>=0.10.0'} + dependencies: + decamelize: 1.2.0 + map-obj: 1.0.1 + dev: true + + /decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + dev: true + + /decamelize@2.0.0: + resolution: {integrity: sha512-Ikpp5scV3MSYxY39ymh45ZLEecsTdv/Xj2CaQfI8RLMuwi7XvjX9H/fhraiSuU+C5w5NTDu4ZU72xNiZnurBPg==} + engines: {node: '>=4'} + dependencies: + xregexp: 4.0.0 + dev: true + /decimal.js@10.4.3: resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} + /decode-uri-component@0.2.2: + resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} + engines: {node: '>=0.10'} + dev: true + + /decompress-response@3.3.0: + resolution: {integrity: sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA==} + engines: {node: '>=4'} + dependencies: + mimic-response: 1.0.1 + dev: true + /decompress-response@6.0.0: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} @@ -13249,11 +13561,20 @@ packages: titleize: 3.0.0 dev: true + /default-uid@1.0.0: + resolution: {integrity: sha512-KqOPKqX9VLrCfdKK/zMll+xb9kZOP4QyguB6jyN4pKaPoedk1bMFIfyTCFhVdrHb3GU7aJvKjd8myKxFRRDwCg==} + engines: {node: '>=0.10.0'} + dev: true + /defaults@1.0.3: resolution: {integrity: sha512-s82itHOnYrN0Ib8r+z7laQz3sdE+4FP3d9Q7VLO7U+KRT+CR0GsWuyHxzdAY82I7cXv0G/twrqomTJLOssO5HA==} dependencies: clone: 1.0.4 + /defer-to-connect@1.1.3: + resolution: {integrity: sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==} + dev: true + /defer-to-connect@2.0.1: resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} engines: {node: '>=10'} @@ -13500,6 +13821,13 @@ packages: tslib: 2.6.3 dev: true + /dot-prop@5.3.0: + resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} + engines: {node: '>=8'} + dependencies: + is-obj: 2.0.0 + dev: true + /dot-prop@6.0.1: resolution: {integrity: sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==} engines: {node: '>=10'} @@ -13523,6 +13851,18 @@ packages: engines: {node: '>=12'} dev: true + /downgrade-root@1.2.2: + resolution: {integrity: sha512-K/QnPfqybcxP6rriuM17fnaQ/zDnG0hh8ISbm9szzIqZSI4wtfaj4D5oL6WscT2xVFQ3kDISZrrgeUtd+rW8pQ==} + engines: {node: '>=0.10.0'} + dependencies: + default-uid: 1.0.0 + is-root: 1.0.0 + dev: true + + /duplexer3@0.1.5: + resolution: {integrity: sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==} + dev: true + /duplexer@0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} dev: true @@ -13935,6 +14275,10 @@ packages: is-symbol: 1.0.4 dev: true + /es6-error@4.1.1: + resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} + dev: true + /esbuild-android-64@0.14.49: resolution: {integrity: sha512-vYsdOTD+yi+kquhBiFWl3tyxnj2qZJsl4tAqwhT90ktUdnyTizgle7TjNx6Ar1bN7wcwWqZ9QInfdk2WVagSww==} engines: {node: '>=12'} @@ -14227,6 +14571,11 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + /escape-goat@2.1.1: + resolution: {integrity: sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==} + engines: {node: '>=8'} + dev: true + /escape-goat@4.0.0: resolution: {integrity: sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==} engines: {node: '>=12'} @@ -14675,6 +15024,19 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} + /execa@1.0.0: + resolution: {integrity: sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==} + engines: {node: '>=6'} + dependencies: + cross-spawn: 6.0.6 + get-stream: 4.1.0 + is-stream: 1.1.0 + npm-run-path: 2.0.2 + p-finally: 1.0.0 + signal-exit: 3.0.7 + strip-eof: 1.0.0 + dev: true + /execa@4.1.0: resolution: {integrity: sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==} engines: {node: '>=10'} @@ -14718,6 +15080,18 @@ packages: strip-final-newline: 3.0.0 dev: true + /execall@1.0.0: + resolution: {integrity: sha512-/J0Q8CvOvlAdpvhfkD/WnTQ4H1eU0exze2nFGPj/RSC7jpQ0NkKe2r28T5eMkhEEs+fzepMZNy1kVRKNlC04nQ==} + engines: {node: '>=0.10.0'} + dependencies: + clone-regexp: 1.0.1 + dev: true + + /exit-hook@1.1.1: + resolution: {integrity: sha512-MsG3prOVw1WtLXAZbM3KiYtooKR1LvxHh3VHsVtIy0uiUu8usxgB/94DP2HxtD/661lLdB6yzQ09lGJSQr6nkg==} + engines: {node: '>=0.10.0'} + dev: true + /exit@0.1.2: resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} engines: {node: '>= 0.8.0'} @@ -14790,12 +15164,19 @@ packages: /extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} - dev: false /extendable-error@0.1.7: resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} dev: true + /external-editor@1.1.1: + resolution: {integrity: sha512-0XYlP43jzxMgJjugDJ85Z0UDPnowkUbfFztNvsSGC9sJVIk97MZbGEb9WAhIVH0UgNxoLj/9ZQgB4CHJyz2GGQ==} + dependencies: + extend: 3.0.2 + spawn-sync: 1.0.15 + tmp: 0.0.29 + dev: true + /external-editor@3.1.0: resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} engines: {node: '>=4'} @@ -14885,6 +15266,14 @@ packages: /fecha@4.2.1: resolution: {integrity: sha512-MMMQ0ludy/nBs1/o0zVOiKTpG7qMbonKUzjJgQFEuvq6INZ1OraKPRAWkBq5vlKLOUMpmNYG1JoN3oDPUQ9m3Q==} + /figures@1.7.0: + resolution: {integrity: sha512-UxKlfCRuCBxSXU4C6t9scbDyWZ4VlaFFdojKtzJuSkuOBQ5CNFum+zZXFwHjo+CxBC1t6zlYPgHIgFjL8ggoEQ==} + engines: {node: '>=0.10.0'} + dependencies: + escape-string-regexp: 1.0.5 + object-assign: 4.1.1 + dev: true + /figures@3.2.0: resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} engines: {node: '>=8'} @@ -14937,6 +15326,11 @@ packages: dependencies: to-regex-range: 5.0.1 + /filter-obj@2.0.2: + resolution: {integrity: sha512-lO3ttPjHZRfjMcxWKb1j1eDhTFsu4meeR3lnMcnBFhk6RuLhvEiuALu2TlfL310ph4lCYYwgF/ElIjdP739tdg==} + engines: {node: '>=8'} + dev: true + /finalhandler@1.1.2: resolution: {integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==} engines: {node: '>= 0.8'} @@ -15013,6 +15407,13 @@ packages: resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} dev: false + /find-up@2.1.0: + resolution: {integrity: sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==} + engines: {node: '>=4'} + dependencies: + locate-path: 2.0.0 + dev: true + /find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -15035,6 +15436,13 @@ packages: path-exists: 5.0.0 dev: true + /find-versions@3.2.0: + resolution: {integrity: sha512-P8WRou2S+oe222TOCHitLy8zj+SIsVJh52VP4lvXkaFVnOFFdoWv1H1Jjvel1aI6NCFOAaeAVm8qrI0odiLcww==} + engines: {node: '>=6'} + dependencies: + semver-regex: 2.0.0 + dev: true + /find-yarn-workspace-root2@1.2.16: resolution: {integrity: sha512-hr6hb1w8ePMpPVUK39S4RlwJzi+xPLuVuG8XlwXU3KD5Yn3qgBWVfy3AzNlDhWvE1EORCE65/Qm26rFQt3VLVA==} dependencies: @@ -15095,6 +15503,10 @@ packages: is-callable: 1.2.7 dev: true + /foreachasync@3.0.0: + resolution: {integrity: sha512-J+ler7Ta54FwwNcx6wQRDhTIbNeyDcARMkOcguEqnEdtm0jKvN3Li3PDAb2Du3ubJYEWfYL83XMROXdsXAXycw==} + dev: true + /foreground-child@3.3.0: resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} engines: {node: '>=14'} @@ -15161,6 +15573,13 @@ packages: engines: {node: '>= 0.6'} dev: true + /from2@2.3.0: + resolution: {integrity: sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==} + dependencies: + inherits: 2.0.4 + readable-stream: 2.3.7 + dev: true + /front-matter@4.0.2: resolution: {integrity: sha512-I8ZuJ/qG92NWX8i5x1Y8qyj3vizhXS31OxjKDu3LKP+7/qBgfIKValiZIEwoVoJKUHlhWtYrktkxV1XsX+pPlg==} dependencies: @@ -15274,6 +15693,18 @@ packages: requiresBuild: true optional: true + /fullname@4.0.1: + resolution: {integrity: sha512-jVT8q9Ah9JwqfIGKwKzTdbRRthdPpIjEe9kgvxM104Tv+q6SgOAQqJMVP90R0DBRAqejGMHDRWJtl3Ats6BjfQ==} + engines: {node: '>=8'} + dependencies: + execa: 1.0.0 + filter-obj: 2.0.2 + mem: 5.1.1 + p-any: 2.1.0 + passwd-user: 3.0.0 + rc: 1.2.8 + dev: true + /function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} @@ -15306,6 +15737,17 @@ packages: engines: {node: '>= 0.6.0'} dev: false + /gauge@1.2.7: + resolution: {integrity: sha512-fVbU2wRE91yDvKUnrIaQlHKAWKY5e08PmztCrwuH5YVQ+Z/p3d0ny2T48o6uvAAXHIUnfaQdHkmxYbQft1eHVA==} + deprecated: This package is no longer supported. + dependencies: + ansi: 0.3.1 + has-unicode: 2.0.1 + lodash.pad: 4.5.1 + lodash.padend: 4.6.1 + lodash.padstart: 4.6.1 + dev: true + /gauge@2.7.4: resolution: {integrity: sha512-14x4kjc6lkD3ltw589k0NrPD6cCNTD6CWoVUNpB85+DrtONoZn+Rug6xZU5RvSC4+TZPxA5AnBibQYAvZn41Hg==} deprecated: This package is no longer supported. @@ -15384,13 +15826,30 @@ packages: engines: {node: '>=4'} dev: true - /get-stream@5.2.0: - resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} - engines: {node: '>=8'} - dependencies: - pump: 3.0.0 + /get-stdin@4.0.1: + resolution: {integrity: sha512-F5aQMywwJ2n85s4hJPTT9RPxGmubonuB10MNYo17/xph174n2MIR33HRguhzVag10O/npM7SPk73LMZNP+FaWw==} + engines: {node: '>=0.10.0'} + dev: true - /get-stream@6.0.1: + /get-stream@3.0.0: + resolution: {integrity: sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==} + engines: {node: '>=4'} + dev: true + + /get-stream@4.1.0: + resolution: {integrity: sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==} + engines: {node: '>=6'} + dependencies: + pump: 3.0.0 + dev: true + + /get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + dependencies: + pump: 3.0.0 + + /get-stream@6.0.1: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} @@ -15511,6 +15970,31 @@ packages: minimatch: 5.1.6 once: 1.4.0 + /global-agent@2.2.0: + resolution: {integrity: sha512-+20KpaW6DDLqhG7JDiJpD1JvNvb8ts+TNl7BPOYcURqCrXqnN1Vf+XVOrkKJAFPqfX+oEhsdzOj1hLWkBTdNJg==} + engines: {node: '>=10.0'} + dependencies: + boolean: 3.2.0 + core-js: 3.41.0 + es6-error: 4.1.1 + matcher: 3.0.0 + roarr: 2.15.4 + semver: 7.6.3 + serialize-error: 7.0.1 + dev: true + + /global-agent@3.0.0: + resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==} + engines: {node: '>=10.0'} + dependencies: + boolean: 3.2.0 + es6-error: 4.1.1 + matcher: 3.0.0 + roarr: 2.15.4 + semver: 7.6.3 + serialize-error: 7.0.1 + dev: true + /global-dirs@3.0.1: resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==} engines: {node: '>=10'} @@ -15536,6 +16020,16 @@ packages: which: 1.3.1 dev: false + /global-tunnel-ng@2.7.1: + resolution: {integrity: sha512-4s+DyciWBV0eK148wqXxcmVAbFVPqtc3sEtUE/GTQfuU80rySLcMhUmHKSHI7/LDj8q0gDYI1lIhRRB7ieRAqg==} + engines: {node: '>=0.10'} + dependencies: + encodeurl: 1.0.2 + lodash: 4.17.21 + npm-conf: 1.1.3 + tunnel: 0.0.6 + dev: true + /globals@11.12.0: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} engines: {node: '>=4'} @@ -15586,6 +16080,23 @@ packages: dependencies: get-intrinsic: 1.2.4 + /got@11.8.6: + resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==} + engines: {node: '>=10.19.0'} + dependencies: + '@sindresorhus/is': 4.6.0 + '@szmarczak/http-timer': 4.0.6 + '@types/cacheable-request': 6.0.3 + '@types/responselike': 1.0.3 + cacheable-lookup: 5.0.4 + cacheable-request: 7.0.4 + decompress-response: 6.0.0 + http2-wrapper: 1.0.3 + lowercase-keys: 2.0.0 + p-cancelable: 2.1.1 + responselike: 2.0.1 + dev: true + /got@12.6.1: resolution: {integrity: sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==} engines: {node: '>=14.16'} @@ -15603,6 +16114,69 @@ packages: responselike: 3.0.0 dev: true + /got@6.7.1: + resolution: {integrity: sha512-Y/K3EDuiQN9rTZhBvPRWMLXIKdeD1Rj0nzunfoi0Yyn5WBEbzxXKU9Ub2X41oZBagVWOBU3MuDonFMgPWQFnwg==} + engines: {node: '>=4'} + dependencies: + '@types/keyv': 3.1.4 + '@types/responselike': 1.0.3 + create-error-class: 3.0.2 + duplexer3: 0.1.5 + get-stream: 3.0.0 + is-redirect: 1.0.0 + is-retry-allowed: 1.2.0 + is-stream: 1.1.0 + lowercase-keys: 1.0.1 + safe-buffer: 5.2.1 + timed-out: 4.0.1 + unzip-response: 2.0.1 + url-parse-lax: 1.0.0 + dev: true + + /got@8.3.2: + resolution: {integrity: sha512-qjUJ5U/hawxosMryILofZCkm3C84PLJS/0grRIpjAwu+Lkxxj5cxeCU25BG0/3mDSpXKTyZr8oh8wIgLaH0QCw==} + engines: {node: '>=4'} + dependencies: + '@sindresorhus/is': 0.7.0 + '@types/keyv': 3.1.4 + '@types/responselike': 1.0.3 + cacheable-request: 2.1.4 + decompress-response: 3.3.0 + duplexer3: 0.1.5 + get-stream: 3.0.0 + into-stream: 3.1.0 + is-retry-allowed: 1.2.0 + isurl: 1.0.0 + lowercase-keys: 1.0.1 + mimic-response: 1.0.1 + p-cancelable: 0.4.1 + p-timeout: 2.0.1 + pify: 3.0.0 + safe-buffer: 5.2.1 + timed-out: 4.0.1 + url-parse-lax: 3.0.0 + url-to-options: 1.0.1 + dev: true + + /got@9.6.0: + resolution: {integrity: sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==} + engines: {node: '>=8.6'} + dependencies: + '@sindresorhus/is': 0.14.0 + '@szmarczak/http-timer': 1.1.2 + '@types/keyv': 3.1.4 + '@types/responselike': 1.0.3 + cacheable-request: 6.1.0 + decompress-response: 3.3.0 + duplexer3: 0.1.5 + get-stream: 4.1.0 + lowercase-keys: 1.0.1 + mimic-response: 1.0.1 + p-cancelable: 1.1.0 + to-readable-stream: 1.0.0 + url-parse-lax: 3.0.0 + dev: true + /graceful-fs@4.2.10: resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} dev: true @@ -15621,10 +16195,22 @@ packages: resolution: {integrity: sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==} dev: true + /has-ansi@2.0.0: + resolution: {integrity: sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==} + engines: {node: '>=0.10.0'} + dependencies: + ansi-regex: 2.1.1 + dev: true + /has-bigints@1.0.2: resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} dev: true + /has-flag@1.0.0: + resolution: {integrity: sha512-DyYHfIYwAJmjAjSSPKANxI8bFY9YtFrgkAfinBojQ8YJTOuOuav64tMUJv584SES4xl74PmuaevIyaLESHdTAA==} + engines: {node: '>=0.10.0'} + dev: true + /has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} @@ -15646,10 +16232,20 @@ packages: resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==} engines: {node: '>= 0.4'} + /has-symbol-support-x@1.4.2: + resolution: {integrity: sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw==} + dev: true + /has-symbols@1.0.3: resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} engines: {node: '>= 0.4'} + /has-to-string-tag-x@1.4.1: + resolution: {integrity: sha512-vdbKfmw+3LoOYVr+mtxHaX5a96+0f3DljYd8JOqvOLsf5mw2Otda2qCDT9qRqLAhrjyQ0h7ual5nOiASpsGNFw==} + dependencies: + has-symbol-support-x: 1.4.2 + dev: true + /has-tostringtag@1.0.2: resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} engines: {node: '>= 0.4'} @@ -15660,6 +16256,11 @@ packages: /has-unicode@2.0.1: resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} + /has-yarn@2.1.0: + resolution: {integrity: sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==} + engines: {node: '>=8'} + dev: true + /has-yarn@3.0.0: resolution: {integrity: sha512-IrsVwUHhEULx3R8f/aA8AHuEzAorplsab/v8HBzEiIukwq5i/EC+xmOW+HfP1OaDP+2JkgT1yILHN2O3UFIbcA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -15828,6 +16429,10 @@ packages: entities: 4.5.0 dev: false + /http-cache-semantics@3.8.1: + resolution: {integrity: sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w==} + dev: true + /http-cache-semantics@4.1.1: resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} @@ -15903,6 +16508,14 @@ packages: transitivePeerDependencies: - debug + /http2-wrapper@1.0.3: + resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==} + engines: {node: '>=10.19.0'} + dependencies: + quick-lru: 5.1.1 + resolve-alpn: 1.2.1 + dev: true + /http2-wrapper@2.2.0: resolution: {integrity: sha512-kZB0wxMo0sh1PehyjJUWRFEd99KC5TLjZ2cULC4f9iqJBAmKQQXEICjxl5iPJRwP40dpeHFqqhm7tYCvODpqpQ==} engines: {node: '>=10.19.0'} @@ -15951,6 +16564,13 @@ packages: dependencies: ms: 2.1.3 + /humanize-string@2.1.0: + resolution: {integrity: sha512-sQ+hqmxyXW8Cj7iqxcQxD7oSy3+AXnIZXdUF9lQMkzaG8dtbKAB8U7lCtViMnwQ+MpdCKsO2Kiij3G6UUXq/Xg==} + engines: {node: '>=6'} + dependencies: + decamelize: 2.0.0 + dev: true + /husky@8.0.3: resolution: {integrity: sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==} engines: {node: '>=14'} @@ -16079,6 +16699,11 @@ packages: module-details-from-path: 1.0.3 dev: false + /import-lazy@2.1.0: + resolution: {integrity: sha512-m7ZEHgtw69qOGw+jwxXkHlrlIPdTGkyh66zXZ1ajZbxkDBNjSY/LGbmjc7h0s2ELsUDTAhFr55TrPSSqJGPG0A==} + engines: {node: '>=4'} + dev: true + /import-lazy@4.0.0: resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} engines: {node: '>=8'} @@ -16096,6 +16721,11 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + /indent-string@3.2.0: + resolution: {integrity: sha512-BYqTHXTGUIvg7t1r4sJNKcbDZkL92nkXA8YtRpbjFHRHGDL/NtUeiBJMeE60kIFN/Mg8ESaWQvftaYMGJzQZCQ==} + engines: {node: '>=4'} + dev: true + /indent-string@4.0.0: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} @@ -16140,6 +16770,25 @@ packages: rxjs: 7.8.1 dev: false + /inquirer@1.2.3: + resolution: {integrity: sha512-diSnpgfv/Ozq6QKuV2mUcwZ+D24b03J3W6EVxzvtkCWJTPrH2gKLsqgSW0vzRMZZFhFdhnvzka0RUJxIm7AOxQ==} + dependencies: + ansi-escapes: 1.4.0 + chalk: 1.1.3 + cli-cursor: 1.0.2 + cli-width: 2.2.1 + external-editor: 1.1.1 + figures: 1.7.0 + lodash: 4.17.21 + mute-stream: 0.0.6 + pinkie-promise: 2.0.1 + run-async: 2.4.1 + rx: 4.1.0 + string-width: 1.0.2 + strip-ansi: 3.0.1 + through: 2.3.8 + dev: true + /inquirer@8.2.6: resolution: {integrity: sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==} engines: {node: '>=12.0.0'} @@ -16189,6 +16838,14 @@ packages: tslib: 2.6.3 dev: true + /into-stream@3.1.0: + resolution: {integrity: sha512-TcdjPibTksa1NQximqep2r17ISRiNE9fwlfbg3F8ANdvP5/yrFTew86VcO//jk4QTaMlbjypPBq76HN2zaKfZQ==} + engines: {node: '>=4'} + dependencies: + from2: 2.3.0 + p-is-promise: 1.1.0 + dev: true + /ip-address@9.0.5: resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==} engines: {node: '>= 12'} @@ -16291,6 +16948,13 @@ packages: engines: {node: '>= 0.4'} dev: true + /is-ci@2.0.0: + resolution: {integrity: sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==} + hasBin: true + dependencies: + ci-info: 2.0.0 + dev: true + /is-ci@3.0.1: resolution: {integrity: sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==} dependencies: @@ -16321,6 +16985,11 @@ packages: resolution: {integrity: sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==} dev: false + /is-docker@1.1.0: + resolution: {integrity: sha512-ZEpopPu+bLIb/x3IF9wXxRdAW74e/ity1XGRxpznAaABKhc8mmtRamRB2l71CSs1YMS8FQxDK/vPK10XlhzG2A==} + engines: {node: '>=0.10.0'} + dev: true + /is-docker@2.2.1: resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} engines: {node: '>=8'} @@ -16347,6 +17016,11 @@ packages: number-is-nan: 1.0.1 dev: true + /is-fullwidth-code-point@2.0.0: + resolution: {integrity: sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==} + engines: {node: '>=4'} + dev: true + /is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} @@ -16413,6 +17087,11 @@ packages: engines: {node: '>= 0.4'} dev: true + /is-npm@5.0.0: + resolution: {integrity: sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA==} + engines: {node: '>=10'} + dev: true + /is-npm@6.0.0: resolution: {integrity: sha512-JEjxbSmtPSt1c8XTkVrlujcXdKV1/tvuQ7GwKcAlyiVLeYFQ2VHat8xfrDJsIkhCdF/tZ7CiIR3sy141c6+gPQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -16440,10 +17119,19 @@ packages: engines: {node: '>=8'} dev: true + /is-object@1.0.2: + resolution: {integrity: sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==} + dev: true + /is-path-inside@3.0.3: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} engines: {node: '>=8'} + /is-plain-obj@1.1.0: + resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} + engines: {node: '>=0.10.0'} + dev: true + /is-plain-obj@2.1.0: resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} engines: {node: '>=8'} @@ -16459,6 +17147,11 @@ packages: /is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + /is-redirect@1.0.0: + resolution: {integrity: sha512-cr/SlUEe5zOGmzvj9bUyC4LVvkNVAXu4GytXLNMr1pny+a65MpQ9IJzFHD5vi7FyJgb4qt27+eS3TuQnqB+RQw==} + engines: {node: '>=0.10.0'} + dev: true + /is-reference@1.2.1: resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} dependencies: @@ -16473,6 +17166,21 @@ packages: has-tostringtag: 1.0.2 dev: true + /is-regexp@1.0.0: + resolution: {integrity: sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==} + engines: {node: '>=0.10.0'} + dev: true + + /is-retry-allowed@1.2.0: + resolution: {integrity: sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==} + engines: {node: '>=0.10.0'} + dev: true + + /is-root@1.0.0: + resolution: {integrity: sha512-1d50EJ7ipFxb9bIx213o6KPaJmHN8f+nR48UZWxWVzDx+NA3kpscxi02oQX3rGkEaLBi9m3ZayHngQc3+bBX9w==} + engines: {node: '>=0.10.0'} + dev: true + /is-scoped@2.1.0: resolution: {integrity: sha512-Cv4OpPTHAK9kHYzkzCrof3VJh7H/PrG2MBUMvvJebaaUMbqhm0YAtXnvh0I3Hnj2tMZWwrRROWLSgfJrKqWmlQ==} engines: {node: '>=8'} @@ -16496,6 +17204,11 @@ packages: call-bind: 1.0.7 dev: true + /is-stream@1.1.0: + resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==} + engines: {node: '>=0.10.0'} + dev: true + /is-stream@2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} @@ -16523,6 +17236,11 @@ packages: resolution: {integrity: sha512-6Ybun0IkarhmEqxXCNw/C0bna6Zb/TkfUX9UbwJtK6ObwAVCxmAP308WWTHviM/zAqXk05cdhYsUsZeGQh99iw==} dev: true + /is-supported-regexp-flag@1.0.1: + resolution: {integrity: sha512-3vcJecUUrpgCqc/ca0aWeNu64UGgxcvO60K/Fkr1N6RSvfGCTU60UKN68JDmKokgba0rFFJs12EnzOQa14ubKQ==} + engines: {node: '>=0.10.0'} + dev: true + /is-symbol@1.0.4: resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} engines: {node: '>= 0.4'} @@ -16597,6 +17315,10 @@ packages: dependencies: is-docker: 2.2.1 + /is-yarn-global@0.3.0: + resolution: {integrity: sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==} + dev: true + /is-yarn-global@0.4.1: resolution: {integrity: sha512-/kppl+R+LO5VmhYSEWARUFjodS25D68gvj8W7z0I7OWhUla5xWu8KL6CtB2V0R6yqhnRgbcaREMr4EEM6htLPQ==} engines: {node: '>=12'} @@ -16684,6 +17406,14 @@ packages: istanbul-lib-report: 3.0.1 dev: true + /isurl@1.0.0: + resolution: {integrity: sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w==} + engines: {node: '>= 4'} + dependencies: + has-to-string-tag-x: 1.4.1 + is-object: 1.0.2 + dev: true + /iterator.prototype@1.1.0: resolution: {integrity: sha512-rjuhAk1AJ1fssphHD0IFV6TWL40CwRZ53FrztKx43yk2v6rguBYsY4Bj1VU4HmoMmKwZUlx7mfnhDf9cOp4YTw==} dependencies: @@ -17326,6 +18056,10 @@ packages: resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} engines: {node: '>=4'} + /json-buffer@3.0.0: + resolution: {integrity: sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==} + dev: true + /json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} @@ -17426,6 +18160,18 @@ packages: engines: {node: '>=18'} dev: false + /keyv@3.0.0: + resolution: {integrity: sha512-eguHnq22OE3uVoSYG0LVWNP+4ppamWr9+zWBe1bsNcovIMy6huUJFPgy4mGwCd/rnl3vOLGW1MTlu4c57CT1xA==} + dependencies: + json-buffer: 3.0.0 + dev: true + + /keyv@3.1.0: + resolution: {integrity: sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==} + dependencies: + json-buffer: 3.0.0 + dev: true + /keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} dependencies: @@ -17444,6 +18190,20 @@ packages: /kuler@2.0.0: resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} + /latest-version@3.1.0: + resolution: {integrity: sha512-Be1YRHWWlZaSsrz2U+VInk+tO0EwLIyV+23RhWLINJYwg/UIikxjlj3MhH37/6/EDCAusjajvMkMMUXRaMWl/w==} + engines: {node: '>=4'} + dependencies: + package-json: 4.0.1 + dev: true + + /latest-version@5.1.0: + resolution: {integrity: sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==} + engines: {node: '>=8'} + dependencies: + package-json: 6.5.0 + dev: true + /latest-version@7.0.0: resolution: {integrity: sha512-KvNT4XqAMzdcL6ka6Tl3i2lYeFDgXNCuIX+xNx6ZMVR1dFq+idXd9FLKNMOIx0t9mJ9/HudyX4oZWXZQ0UJHeg==} engines: {node: '>=14.16'} @@ -17561,6 +18321,14 @@ packages: engines: {node: '>= 12.13.0'} dev: true + /locate-path@2.0.0: + resolution: {integrity: sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==} + engines: {node: '>=4'} + dependencies: + p-locate: 2.0.0 + path-exists: 3.0.0 + dev: true + /locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} @@ -17586,6 +18354,11 @@ packages: signal-exit: 3.0.7 dev: true + /locutus@2.0.32: + resolution: {integrity: sha512-fr7OCpbE4xeefhHqfh6hM2/l9ZB3XvClHgtgFnQNImrM/nqL950o6FO98vmUH8GysfQRCcyBYtZ4C8GcY52Edw==} + engines: {node: '>= 10', yarn: '>= 1'} + dev: true + /lodash-es@4.17.21: resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} dev: true @@ -17629,6 +18402,18 @@ packages: /lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + /lodash.pad@4.5.1: + resolution: {integrity: sha512-mvUHifnLqM+03YNzeTBS1/Gr6JRFjd3rRx88FHWUvamVaT9k2O/kXha3yBSOwB9/DTQrSTLJNHvLBBt2FdX7Mg==} + dev: true + + /lodash.padend@4.6.1: + resolution: {integrity: sha512-sOQs2aqGpbl27tmCS1QNZA09Uqp01ZzWfDUoD+xzTii0E7dSQfRKcRetFwa+uXaxaqL+TKm7CgD2JdKP7aZBSw==} + dev: true + + /lodash.padstart@4.6.1: + resolution: {integrity: sha512-sW73O6S8+Tg66eY56DBk85aQzzUJDtpoXFBgELMd5P/SotAguo+1kYO6RuYgXxA4HJH3LFTFPASX6ET6bjfriw==} + dev: true + /lodash.startcase@4.4.0: resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} dev: true @@ -17640,6 +18425,13 @@ packages: /lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + /log-symbols@2.2.0: + resolution: {integrity: sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==} + engines: {node: '>=4'} + dependencies: + chalk: 2.4.2 + dev: true + /log-symbols@4.1.0: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} engines: {node: '>=10'} @@ -17673,6 +18465,14 @@ packages: dependencies: js-tokens: 4.0.0 + /loud-rejection@1.6.0: + resolution: {integrity: sha512-RPNliZOFkqFumDhvYqOaNY4Uz9oJM2K9tC6JWsJJsNdhuONW4LQHRBpb0qf4pJApVffI5N39SwzWZJuEhfd7eQ==} + engines: {node: '>=0.10.0'} + dependencies: + currently-unhandled: 0.4.1 + signal-exit: 3.0.7 + dev: true + /loud-rejection@2.2.0: resolution: {integrity: sha512-S0FayMXku80toa5sZ6Ro4C+s+EtFDCsyJNG/AzFMfX3AxD5Si4dZsgzm/kKnbOxHl5Cv8jBlno8+3XYIh2pNjQ==} engines: {node: '>=8'} @@ -17687,6 +18487,21 @@ packages: tslib: 2.6.3 dev: true + /lowercase-keys@1.0.0: + resolution: {integrity: sha512-RPlX0+PHuvxVDZ7xX+EBVAp4RsVxP/TdDSN2mJYdiq1Lc4Hz7EUSjUI7RZrKKlmrIzVhf6Jo2stj7++gVarS0A==} + engines: {node: '>=0.10.0'} + dev: true + + /lowercase-keys@1.0.1: + resolution: {integrity: sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==} + engines: {node: '>=0.10.0'} + dev: true + + /lowercase-keys@2.0.0: + resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} + engines: {node: '>=8'} + dev: true + /lowercase-keys@3.0.0: resolution: {integrity: sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -17848,6 +18663,23 @@ packages: tmpl: 1.0.5 dev: true + /map-age-cleaner@0.1.3: + resolution: {integrity: sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==} + engines: {node: '>=6'} + dependencies: + p-defer: 1.0.0 + dev: true + + /map-obj@1.0.1: + resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} + engines: {node: '>=0.10.0'} + dev: true + + /map-obj@2.0.0: + resolution: {integrity: sha512-TzQSV2DiMYgoF5RycneKVUzIa9bQsj/B3tTgsE3dOGqlzHnGIDaC7XBE7grnA+8kZPnfqSGFe95VHc2oc0VFUQ==} + engines: {node: '>=4'} + dev: true + /map-or-similar@1.5.0: resolution: {integrity: sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg==} dev: true @@ -17877,6 +18709,13 @@ packages: engines: {node: '>= 12'} dev: true + /matcher@3.0.0: + resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==} + engines: {node: '>=10'} + dependencies: + escape-string-regexp: 4.0.0 + dev: true + /mdast-add-list-metadata@1.0.1: resolution: {integrity: sha512-fB/VP4MJ0LaRsog7hGPxgOrSL3gE/2uEdZyDuSEnKCv/8IkYHiDkIQSbChiJoHyxZZXZ9bzckyRk+vNxFzh8rA==} dependencies: @@ -17936,6 +18775,15 @@ packages: vinyl: 2.2.1 vinyl-file: 3.0.0 + /mem@5.1.1: + resolution: {integrity: sha512-qvwipnozMohxLXG1pOqoLiZKNkC4r4qqRucSoDwXowsNGDSULiqFTRUF05vcZWnwJSG22qTsynQhxbaMtnX9gw==} + engines: {node: '>=8'} + dependencies: + map-age-cleaner: 0.1.3 + mimic-fn: 2.1.0 + p-is-promise: 2.1.0 + dev: true + /memfs@3.3.0: resolution: {integrity: sha512-BEE62uMfKOavX3iG7GYX43QJ+hAeeWnwIAuJ/R6q96jaMtiLzhsxHJC8B1L7fK7Pt/vXDRwb3SG/yBpNGDPqzg==} engines: {node: '>= 4.0.0'} @@ -17965,6 +18813,21 @@ packages: engines: {node: '>= 0.10.0'} dev: true + /meow@5.0.0: + resolution: {integrity: sha512-CbTqYU17ABaLefO8vCU153ZZlprKYWDljcndKKDCFcYQITzWCXZAVk4QMFZPgvzrnUQ3uItnIE/LoUOwrT15Ig==} + engines: {node: '>=6'} + dependencies: + camelcase-keys: 4.2.0 + decamelize-keys: 1.1.1 + loud-rejection: 1.6.0 + minimist-options: 3.0.2 + normalize-package-data: 2.5.0 + read-pkg-up: 3.0.0 + redent: 2.0.0 + trim-newlines: 2.0.0 + yargs-parser: 10.1.0 + dev: true + /merge-descriptors@1.0.3: resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} dev: true @@ -18038,6 +18901,11 @@ packages: engines: {node: '>=12'} dev: true + /mimic-response@1.0.1: + resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} + engines: {node: '>=4'} + dev: true + /mimic-response@3.1.0: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} @@ -18092,6 +18960,14 @@ packages: dependencies: brace-expansion: 2.0.1 + /minimist-options@3.0.2: + resolution: {integrity: sha512-FyBrT/d0d4+uiZRbqznPXqw3IpZZG3gl3wKWiX784FycUKVwBt0uLBFkQrtE4tZOrgo78nZp2jnKz3L65T5LdQ==} + engines: {node: '>= 4'} + dependencies: + arrify: 1.0.1 + is-plain-obj: 1.1.0 + dev: true + /minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} @@ -18273,6 +19149,10 @@ packages: arrify: 2.0.1 minimatch: 3.0.5 + /mute-stream@0.0.6: + resolution: {integrity: sha512-m0kBTDLF/0lgzCsPVmJSKM5xkLNX7ZAB0Q+n2DP37JMIRPVC2R4c3BdO6x++bXFKftbhvSfKgwxAexME+BRDRw==} + dev: true + /mute-stream@0.0.8: resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} @@ -18530,6 +19410,25 @@ packages: engines: {node: '>=0.10.0'} dev: true + /normalize-url@2.0.1: + resolution: {integrity: sha512-D6MUW4K/VzoJ4rJ01JFKxDrtY1v9wrgzCX5f2qj/lzH1m/lW6MhUZFKerVsnyjOhOsYzI9Kqqak+10l4LvLpMw==} + engines: {node: '>=4'} + dependencies: + prepend-http: 2.0.0 + query-string: 5.1.1 + sort-keys: 2.0.0 + dev: true + + /normalize-url@4.5.1: + resolution: {integrity: sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==} + engines: {node: '>=8'} + dev: true + + /normalize-url@6.1.0: + resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} + engines: {node: '>=10'} + dev: true + /normalize-url@8.0.0: resolution: {integrity: sha512-uVFpKhj5MheNBJRTiMZ9pE/7hD1QTeEvugSJW/OmLzAp78PB5O6adfMNTvmfKhXBkvCzC+rqifWcVYpGFwTjnw==} engines: {node: '>=14.16'} @@ -18546,6 +19445,14 @@ packages: dependencies: npm-normalize-package-bin: 3.0.1 + /npm-conf@1.1.3: + resolution: {integrity: sha512-Yic4bZHJOt9RCFbRP3GgpqhScOY4HH3V2P8yBj6CeYq118Qr+BLXqT2JvpJ00mryLESpgOxf5XlFv4ZjXxLScw==} + engines: {node: '>=4'} + dependencies: + config-chain: 1.1.13 + pify: 3.0.0 + dev: true + /npm-install-checks@4.0.0: resolution: {integrity: sha512-09OmyDkNLYwqKPOnbI8exiOZU2GVVmQp7tgez2BPi5OZC8M82elDAps7sxC4l//uSUtotWqoEIDwjRvWH4qz8w==} engines: {node: '>=10'} @@ -18558,6 +19465,14 @@ packages: dependencies: semver: 7.6.3 + /npm-keyword@6.1.0: + resolution: {integrity: sha512-ghcShMAA28IPhJAP4d3T+tndUPzHmvqEfaYwLG1whi4WJ06pdhA3vqL8gXF+Jn8wiqbaRuGVfjE5VXjOgVpW4Q==} + engines: {node: '>=8'} + dependencies: + got: 9.6.0 + registry-url: 5.1.0 + dev: true + /npm-normalize-package-bin@1.0.1: resolution: {integrity: sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==} @@ -18717,6 +19632,13 @@ packages: string.prototype.padend: 3.1.6 dev: true + /npm-run-path@2.0.2: + resolution: {integrity: sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==} + engines: {node: '>=4'} + dependencies: + path-key: 2.0.1 + dev: true + /npm-run-path@4.0.1: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} @@ -18730,6 +19652,15 @@ packages: path-key: 4.0.0 dev: true + /npmlog@2.0.4: + resolution: {integrity: sha512-DaL6RTb8Qh4tMe2ttPT1qWccETy2Vi5/8p+htMpLBeXJTr2CAqnF5WQtSP2eFpvaNbhLZ5uilDb98mRm4Q+lZQ==} + deprecated: This package is no longer supported. + dependencies: + ansi: 0.3.1 + are-we-there-yet: 1.1.7 + gauge: 1.2.7 + dev: true + /npmlog@4.1.2: resolution: {integrity: sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==} dependencies: @@ -19002,6 +19933,11 @@ packages: dependencies: fn.name: 1.1.0 + /onetime@1.1.0: + resolution: {integrity: sha512-GZ+g4jayMqzCRMgB2sol7GiCLjKfS1PINkjmx8spcKce1LiVqcbQreXwqs2YAFXC6R03VIG28ZS31t8M866v6A==} + engines: {node: '>=0.10.0'} + dev: true + /onetime@5.1.2: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} @@ -19112,7 +20048,6 @@ packages: /os-homedir@1.0.2: resolution: {integrity: sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==} engines: {node: '>=0.10.0'} - dev: false /os-name@4.0.1: resolution: {integrity: sha512-xl9MAoU97MH1Xt5K9ERft2YfCAoaO6msy1OBA0ozxEC0x0TmIoE6K3QvgJMMZA9yKGLmHXNY/YZoDbiGDj4zYw==} @@ -19122,6 +20057,11 @@ packages: windows-release: 4.0.0 dev: false + /os-shim@0.1.3: + resolution: {integrity: sha512-jd0cvB8qQ5uVt0lvCIexBaROw1KyKm5sbulg2fWOHjETisuCzWyt+eTZKEMs8v6HwzoGs8xik26jg7eCM6pS+A==} + engines: {node: '>= 0.4.0'} + dev: true + /os-tmpdir@1.0.2: resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} engines: {node: '>=0.10.0'} @@ -19130,11 +20070,40 @@ packages: resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} dev: true + /p-any@2.1.0: + resolution: {integrity: sha512-JAERcaMBLYKMq+voYw36+x5Dgh47+/o7yuv2oQYuSSUml4YeqJEFznBrY2UeEkoSHqBua6hz518n/PsowTYLLg==} + engines: {node: '>=8'} + dependencies: + p-cancelable: 2.1.1 + p-some: 4.1.0 + type-fest: 0.3.1 + dev: true + + /p-cancelable@0.4.1: + resolution: {integrity: sha512-HNa1A8LvB1kie7cERyy21VNeHb2CWJJYqyyC2o3klWFfMGlFmWv2Z7sFgZH8ZiaYL95ydToKTFVXgMV/Os0bBQ==} + engines: {node: '>=4'} + dev: true + + /p-cancelable@1.1.0: + resolution: {integrity: sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==} + engines: {node: '>=6'} + dev: true + + /p-cancelable@2.1.1: + resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} + engines: {node: '>=8'} + dev: true + /p-cancelable@3.0.0: resolution: {integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==} engines: {node: '>=12.20'} dev: true + /p-defer@1.0.0: + resolution: {integrity: sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==} + engines: {node: '>=4'} + dev: true + /p-filter@2.1.0: resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} engines: {node: '>=8'} @@ -19146,6 +20115,23 @@ packages: resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} engines: {node: '>=4'} + /p-is-promise@1.1.0: + resolution: {integrity: sha512-zL7VE4JVS2IFSkR2GQKDSPEVxkoH43/p7oEnwpdCndKYJO0HVeRB7fA8TJwuLOTBREtK0ea8eHaxdwcpob5dmg==} + engines: {node: '>=4'} + dev: true + + /p-is-promise@2.1.0: + resolution: {integrity: sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==} + engines: {node: '>=6'} + dev: true + + /p-limit@1.3.0: + resolution: {integrity: sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==} + engines: {node: '>=4'} + dependencies: + p-try: 1.0.0 + dev: true + /p-limit@2.3.0: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} @@ -19165,6 +20151,13 @@ packages: yocto-queue: 1.0.0 dev: true + /p-locate@2.0.0: + resolution: {integrity: sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==} + engines: {node: '>=4'} + dependencies: + p-limit: 1.3.0 + dev: true + /p-locate@4.1.0: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} @@ -19202,6 +20195,21 @@ packages: eventemitter3: 4.0.7 p-timeout: 3.2.0 + /p-some@4.1.0: + resolution: {integrity: sha512-MF/HIbq6GeBqTrTIl5OJubzkGU+qfFhAFi0gnTAK6rgEIJIknEiABHOTtQu4e6JiXjIwuMPMUFQzyHh5QjCl1g==} + engines: {node: '>=8'} + dependencies: + aggregate-error: 3.1.0 + p-cancelable: 2.1.1 + dev: true + + /p-timeout@2.0.1: + resolution: {integrity: sha512-88em58dDVB/KzPEx1X0N3LwFfYZPyDc4B6eF38M1rk9VTZMbxXXgjugz8mmwpS9Ox4BDZ+t6t3QP5+/gazweIA==} + engines: {node: '>=4'} + dependencies: + p-finally: 1.0.0 + dev: true + /p-timeout@3.2.0: resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} engines: {node: '>=8'} @@ -19217,6 +20225,11 @@ packages: transitivePeerDependencies: - supports-color + /p-try@1.0.0: + resolution: {integrity: sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==} + engines: {node: '>=4'} + dev: true + /p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} @@ -19248,6 +20261,36 @@ packages: /package-json-from-dist@1.0.0: resolution: {integrity: sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==} + /package-json@4.0.1: + resolution: {integrity: sha512-q/R5GrMek0vzgoomq6rm9OX+3PQve8sLwTirmK30YB3Cu0Bbt9OX9M/SIUnroN5BGJkzwGsFwDaRGD9EwBOlCA==} + engines: {node: '>=4'} + dependencies: + got: 6.7.1 + registry-auth-token: 3.4.0 + registry-url: 3.1.0 + semver: 5.7.2 + dev: true + + /package-json@6.5.0: + resolution: {integrity: sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==} + engines: {node: '>=8'} + dependencies: + got: 9.6.0 + registry-auth-token: 4.2.2 + registry-url: 5.1.0 + semver: 6.3.1 + dev: true + + /package-json@7.0.0: + resolution: {integrity: sha512-CHJqc94AA8YfSLHGQT3DbvSIuE12NLFekpM4n7LRrAd3dOJtA911+4xe9q6nC3/jcKraq7nNS9VxgtT0KC+diA==} + engines: {node: '>=12'} + dependencies: + got: 11.8.6 + registry-auth-token: 4.2.2 + registry-url: 5.1.0 + semver: 7.6.3 + dev: true + /package-json@8.1.1: resolution: {integrity: sha512-cbH9IAIJHNj9uXi196JVsRlt7cHKak6u/e6AkL/bkRelZ7rlL3X1YKxsZwa36xipOEKAsdtmaG6aAJoM1fx2zA==} engines: {node: '>=14.16'} @@ -19343,6 +20386,10 @@ packages: - supports-color dev: true + /pad-component@0.0.1: + resolution: {integrity: sha512-8EKVBxCRSvLnsX1p2LlSFSH3c2/wuhY9/BXXWu8boL78FbVKqn2L5SpURt1x5iw6Gq8PTqJ7MdPoe5nCtX3I+g==} + dev: true + /param-case@3.0.4: resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} dependencies: @@ -19375,6 +20422,13 @@ packages: is-hexadecimal: 1.0.4 dev: false + /parse-help@1.0.0: + resolution: {integrity: sha512-dlOrbBba6Rrw/nrJ+V7/vkGZdiimWJQzMHZZrYsUq03JE8AV3fAv6kOYX7dP/w2h67lIdmRf8ES8mU44xAgE/Q==} + engines: {node: '>=4'} + dependencies: + execall: 1.0.0 + dev: true + /parse-json@4.0.0: resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==} engines: {node: '>=4'} @@ -19436,10 +20490,22 @@ packages: tslib: 2.6.3 dev: true + /passwd-user@3.0.0: + resolution: {integrity: sha512-Iu90rROks+uDK00ppSewoZyqeCwjGR6W8PcY0Phl8YFWju/lRmIogQb98+vSb5RUeYkONL3IC4ZLBFg4FiE0Hg==} + engines: {node: '>=8'} + dependencies: + execa: 1.0.0 + dev: true + /path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} dev: true + /path-exists@3.0.0: + resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} + engines: {node: '>=4'} + dev: true + /path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -19545,6 +20611,18 @@ packages: resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} engines: {node: '>=6'} + /pinkie-promise@2.0.1: + resolution: {integrity: sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==} + engines: {node: '>=0.10.0'} + dependencies: + pinkie: 2.0.4 + dev: true + + /pinkie@2.0.4: + resolution: {integrity: sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==} + engines: {node: '>=0.10.0'} + dev: true + /pirates@4.0.6: resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} engines: {node: '>= 6'} @@ -19778,6 +20856,16 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + /prepend-http@1.0.4: + resolution: {integrity: sha512-PhmXi5XmoyKw1Un4E+opM2KcsJInDvKyuOumcjjw3waw86ZNjHwVUOOWLc4bCzLdcKNaWBH9e99sbWzDQsVaYg==} + engines: {node: '>=0.10.0'} + dev: true + + /prepend-http@2.0.0: + resolution: {integrity: sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==} + engines: {node: '>=4'} + dev: true + /prettier-linter-helpers@1.0.0: resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} engines: {node: '>=6.0.0'} @@ -19978,6 +21066,13 @@ packages: resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} engines: {node: '>=6'} + /pupa@2.1.1: + resolution: {integrity: sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==} + engines: {node: '>=8'} + dependencies: + escape-goat: 2.1.1 + dev: true + /pupa@3.1.0: resolution: {integrity: sha512-FLpr4flz5xZTSJxSeaheeMKN/EDzMdK7b8PTOC6a5PYFKTucWbdqjgqaEyH0shFiSJrVB1+Qqi4Tk19ccU6Aug==} engines: {node: '>=12.20'} @@ -20029,6 +21124,15 @@ packages: dependencies: side-channel: 1.0.6 + /query-string@5.1.1: + resolution: {integrity: sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==} + engines: {node: '>=0.10.0'} + dependencies: + decode-uri-component: 0.2.2 + object-assign: 4.1.1 + strict-uri-encode: 1.1.0 + dev: true + /querystringify@2.2.0: resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} @@ -20038,6 +21142,11 @@ packages: /queue-tick@1.0.1: resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==} + /quick-lru@1.1.0: + resolution: {integrity: sha512-tRS7sTgyxMXtLum8L65daJnHUhfDUgboRdcWW2bR9vBfrj2+O5HSMbQOJfJJjIVSPFqbBCF37FpwWXGitDc5tA==} + engines: {node: '>=4'} + dev: true + /quick-lru@5.1.1: resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} engines: {node: '>=10'} @@ -20348,6 +21457,14 @@ packages: type-fest: 4.25.0 dev: true + /read-pkg-up@3.0.0: + resolution: {integrity: sha512-YFzFrVvpC6frF1sz8psoHDBGF7fLPc+llq/8NB43oagqWkx8ar5zYtsTORtOjw9W2RHLpWP+zTWwBvf1bCmcSw==} + engines: {node: '>=4'} + dependencies: + find-up: 2.1.0 + read-pkg: 3.0.0 + dev: true + /read-pkg-up@7.0.1: resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} engines: {node: '>=8'} @@ -20478,6 +21595,14 @@ packages: dependencies: resolve: 1.22.8 + /redent@2.0.0: + resolution: {integrity: sha512-XNwrTx77JQCEMXTeb8movBKuK75MgH0RZkujNuDKCezemx/voapl9i2gCSi8WWm8+ox5ycJi1gxF22fR7c0Ciw==} + engines: {node: '>=4'} + dependencies: + indent-string: 3.2.0 + strip-indent: 2.0.0 + dev: true + /redent@3.0.0: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} engines: {node: '>=8'} @@ -20587,6 +21712,20 @@ packages: unicode-match-property-value-ecmascript: 2.1.0 dev: true + /registry-auth-token@3.4.0: + resolution: {integrity: sha512-4LM6Fw8eBQdwMYcES4yTnn2TqIasbXuwDx3um+QRs7S55aMKCBKBxvPXl2RiUjHwuJLTyYfxSpmfSAjQpcuP+A==} + dependencies: + rc: 1.2.8 + safe-buffer: 5.2.1 + dev: true + + /registry-auth-token@4.2.2: + resolution: {integrity: sha512-PC5ZysNb42zpFME6D/XlIgtNGdTl8bBOCw90xQLVMpzuuubJKYDWFAEuUNc+Cn8Z8724tg2SDhDRrkVEsqfDMg==} + engines: {node: '>=6.0.0'} + dependencies: + rc: 1.2.8 + dev: true + /registry-auth-token@5.0.2: resolution: {integrity: sha512-o/3ikDxtXaA59BmZuZrJZDJv8NMDGSj+6j6XaeBmHw8eY1i1qd9+6H+LjVvQXx3HN6aRCGa1cUdJ9RaJZUugnQ==} engines: {node: '>=14'} @@ -20594,6 +21733,20 @@ packages: '@pnpm/npm-conf': 2.2.2 dev: true + /registry-url@3.1.0: + resolution: {integrity: sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA==} + engines: {node: '>=0.10.0'} + dependencies: + rc: 1.2.8 + dev: true + + /registry-url@5.1.0: + resolution: {integrity: sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==} + engines: {node: '>=8'} + dependencies: + rc: 1.2.8 + dev: true + /registry-url@6.0.1: resolution: {integrity: sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q==} engines: {node: '>=12'} @@ -20739,6 +21892,18 @@ packages: supports-preserve-symlinks-flag: 1.0.0 dev: true + /responselike@1.0.2: + resolution: {integrity: sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ==} + dependencies: + lowercase-keys: 1.0.1 + dev: true + + /responselike@2.0.1: + resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==} + dependencies: + lowercase-keys: 2.0.0 + dev: true + /responselike@3.0.0: resolution: {integrity: sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==} engines: {node: '>=14.16'} @@ -20746,6 +21911,14 @@ packages: lowercase-keys: 3.0.0 dev: true + /restore-cursor@1.0.1: + resolution: {integrity: sha512-reSjH4HuiFlxlaBaFCiS6O76ZGG2ygKoSlCsipKdaZuKSPx/+bt9mULkn4l0asVzbEfQQmXRg6Wp6gv6m0wElw==} + engines: {node: '>=0.10.0'} + dependencies: + exit-hook: 1.1.1 + onetime: 1.1.0 + dev: true + /restore-cursor@3.1.0: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} engines: {node: '>=8'} @@ -20789,6 +21962,18 @@ packages: glob: 10.4.5 dev: true + /roarr@2.15.4: + resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==} + engines: {node: '>=8.0'} + dependencies: + boolean: 3.2.0 + detect-node: 2.1.0 + globalthis: 1.0.3 + json-stringify-safe: 5.0.1 + semver-compare: 1.0.0 + sprintf-js: 1.1.3 + dev: true + /rollup-plugin-inject@3.0.2: resolution: {integrity: sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w==} dependencies: @@ -20825,6 +22010,14 @@ packages: fsevents: 2.3.3 dev: true + /root-check@1.0.0: + resolution: {integrity: sha512-lt1ts72QmU7jh1DlOJqFN/le/aiRGAbchSSMhNpLQubDWPEOe0YKCcrhprkgyMxxFAcrEhyfTTUfc+Dj/bo4JA==} + engines: {node: '>=0.10.0'} + dependencies: + downgrade-root: 1.2.2 + sudo-block: 1.2.0 + dev: true + /router@1.3.8: resolution: {integrity: sha512-461UFH44NtSfIlS83PUg2N7OZo86BC/kB3dY77gJdsODsBhhw7+2uE0tzTINxrY9CahCUVk1VhpWCA5i1yoIEg==} engines: {node: '>= 0.8'} @@ -20863,6 +22056,10 @@ packages: dependencies: queue-microtask: 1.2.3 + /rx@4.1.0: + resolution: {integrity: sha512-CiaiuN6gapkdl+cZUr67W6I8jquN4lkak3vtIsIWCl4XIPP8ffsoyN6/+PuGXnQy8Cu8W2y9Xxh31Rq4M6wUug==} + dev: true + /rxjs@6.6.7: resolution: {integrity: sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==} engines: {npm: '>=2.0.0'} @@ -21017,6 +22214,17 @@ packages: resolution: {integrity: sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==} dev: true + /semver-compare@1.0.0: + resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==} + dev: true + + /semver-diff@3.1.1: + resolution: {integrity: sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==} + engines: {node: '>=8'} + dependencies: + semver: 6.3.1 + dev: true + /semver-diff@4.0.0: resolution: {integrity: sha512-0Ju4+6A8iOnpL/Thra7dZsSlOHYAHIeMxfhWQRI1/VLcT3WDBZKKtQt/QkBOsiIN9ZpuvHE6cGZ0x4glCMmfiA==} engines: {node: '>=12'} @@ -21024,6 +22232,18 @@ packages: semver: 7.6.3 dev: true + /semver-regex@2.0.0: + resolution: {integrity: sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw==} + engines: {node: '>=6'} + dev: true + + /semver-truncate@1.1.2: + resolution: {integrity: sha512-V1fGg9i4CL3qesB6U0L6XAm4xOJiHmt4QAacazumuasc03BvtFGIMCduv01JWQ69Nv+JST9TqhSCiJoxoY031w==} + engines: {node: '>=0.10.0'} + dependencies: + semver: 5.7.2 + dev: true + /semver@5.7.2: resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} hasBin: true @@ -21070,6 +22290,13 @@ packages: - supports-color dev: true + /serialize-error@7.0.1: + resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==} + engines: {node: '>=10'} + dependencies: + type-fest: 0.13.1 + dev: true + /serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} dependencies: @@ -21285,12 +22512,27 @@ packages: ip-address: 9.0.5 smart-buffer: 4.2.0 + /sort-keys@2.0.0: + resolution: {integrity: sha512-/dPCrG1s3ePpWm6yBbxZq5Be1dXGLyLn9Z791chDC3NFrpkVbWGzkBwPN1knaciexFXgRJ7hzdnwZ4stHSDmjg==} + engines: {node: '>=4'} + dependencies: + is-plain-obj: 1.1.0 + dev: true + /sort-keys@4.2.0: resolution: {integrity: sha512-aUYIEU/UviqPgc8mHR6IW1EGxkAXpeRETYcrzg8cLAvUPZcpAlleSXHV2mY7G12GphSH6Gzv+4MMVSSkbdteHg==} engines: {node: '>=8'} dependencies: is-plain-obj: 2.1.0 + /sort-on@4.1.1: + resolution: {integrity: sha512-nj8myvTCEErLMMWnye61z1pV5osa7njoosoQNdylD8WyPYHoHCBQx/xn7mGJL6h4oThvGpYSIAxfm8VUr75qTQ==} + engines: {node: '>=8'} + dependencies: + arrify: 2.0.1 + dot-prop: 5.3.0 + dev: true + /source-list-map@2.0.1: resolution: {integrity: sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==} dev: true @@ -21347,6 +22589,14 @@ packages: resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} dev: true + /spawn-sync@1.0.15: + resolution: {integrity: sha512-9DWBgrgYZzNghseho0JOuh+5fg9u6QWhAWa51QC7+U5rCheZ/j1DrEZnyE0RBBRqZ9uEXGPgSSM0nky6burpVw==} + requiresBuild: true + dependencies: + concat-stream: 1.6.2 + os-shim: 0.1.3 + dev: true + /spawnd@10.0.0: resolution: {integrity: sha512-6GKcakMTryb5b1SWCvdubCDHEsR2k+5VZUD5G19umZRarkvj1RyCGyizcqhjewI7cqZo8fTVD8HpnDZbVOLMtg==} engines: {node: '>=16'} @@ -21546,6 +22796,11 @@ packages: optionalDependencies: bare-events: 2.5.0 + /strict-uri-encode@1.1.0: + resolution: {integrity: sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==} + engines: {node: '>=0.10.0'} + dev: true + /string-hash@1.1.3: resolution: {integrity: sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A==} dev: true @@ -21567,6 +22822,14 @@ packages: strip-ansi: 3.0.1 dev: true + /string-width@2.1.1: + resolution: {integrity: sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==} + engines: {node: '>=4'} + dependencies: + is-fullwidth-code-point: 2.0.0 + strip-ansi: 4.0.0 + dev: true + /string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -21688,6 +22951,13 @@ packages: ansi-regex: 2.1.1 dev: true + /strip-ansi@4.0.0: + resolution: {integrity: sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==} + engines: {node: '>=4'} + dependencies: + ansi-regex: 3.0.1 + dev: true + /strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -21728,6 +22998,11 @@ packages: engines: {node: '>=8'} dev: true + /strip-eof@1.0.0: + resolution: {integrity: sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==} + engines: {node: '>=0.10.0'} + dev: true + /strip-final-newline@2.0.0: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} engines: {node: '>=6'} @@ -21737,6 +23012,11 @@ packages: engines: {node: '>=12'} dev: true + /strip-indent@2.0.0: + resolution: {integrity: sha512-RsSNPLpq6YUL7QYy44RnPVTn/lcVZtb48Uof3X5JLbF4zD/Gs7ZFDv2HWol+leoQN2mT86LAzSshGfkTlSOpsA==} + engines: {node: '>=4'} + dev: true + /strip-indent@3.0.0: resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} engines: {node: '>=8'} @@ -21792,6 +23072,15 @@ packages: resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} dev: false + /sudo-block@1.2.0: + resolution: {integrity: sha512-RE3gka+wcmkvAMt7Ht/TORJ6uxIo+MBPCCibLLygj6xec817CtEYDG6IyICFyWwHZwO3c6d61XdWRrgffq7WJQ==} + engines: {node: '>=0.10.0'} + dependencies: + chalk: 1.1.3 + is-docker: 1.1.0 + is-root: 1.0.0 + dev: true + /superagent@8.0.5: resolution: {integrity: sha512-lQVE0Praz7nHiSaJLKBM/cZyi7J0E4io8tWnGSBdBrqAzhzrjQ/F5iGP9Zr29CJC8N5zYdhG2kKaNcB6dKxp7g==} engines: {node: '>=6.4.0 <13 || >=14'} @@ -21820,6 +23109,18 @@ packages: - supports-color dev: true + /supports-color@2.0.0: + resolution: {integrity: sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==} + engines: {node: '>=0.8.0'} + dev: true + + /supports-color@3.2.3: + resolution: {integrity: sha512-Jds2VIYDrlp5ui7t8abHN2bjAu4LV/q4N2KivFPpGH0lrka0BMq/33AmECUXlKPcHigkNaqfXRENFju+rlcy+A==} + engines: {node: '>=0.8.0'} + dependencies: + has-flag: 1.0.0 + dev: true + /supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} @@ -21855,6 +23156,27 @@ packages: resolution: {integrity: sha512-AsS729u2RHUfEra9xJrE39peJcc2stq2+poBXX8bcM08Y6g9j/i/PUzwNQqkaJde7Ntg1TO7bSREbR5sdosQ+g==} dev: true + /tabtab@1.3.2: + resolution: {integrity: sha512-qHWOJ5g7lrpftZMyPv3ZaYZs7PuUTKWEP/TakZHfpq66bSwH25SQXn5616CCh6Hf/1iPcgQJQHGcJkzQuATabQ==} + hasBin: true + dependencies: + debug: 2.6.9 + inquirer: 1.2.3 + minimist: 1.2.8 + mkdirp: 0.5.6 + npmlog: 2.0.4 + object-assign: 4.1.1 + transitivePeerDependencies: + - supports-color + dev: true + + /taketalk@1.0.0: + resolution: {integrity: sha512-kS7E53It6HA8S1FVFBWP7HDwgTiJtkmYk7TsowGlizzVrivR1Mf9mgjXHY1k7rOfozRVMZSfwjB3bevO4QEqpg==} + dependencies: + get-stdin: 4.0.1 + minimist: 1.2.8 + dev: true + /tapable@2.2.1: resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} engines: {node: '>=6'} @@ -21992,15 +23314,32 @@ packages: /through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + /timed-out@4.0.1: + resolution: {integrity: sha512-G7r3AhovYtr5YKOWQkta8RKAPb+J9IsO4uVmzjl8AZwfhs8UcUwTiD6gcJYSgOtzyjvQKrKYn41syHbUWMkafA==} + engines: {node: '>=0.10.0'} + dev: true + /tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} dev: true + /titleize@2.1.0: + resolution: {integrity: sha512-m+apkYlfiQTKLW+sI4vqUkwMEzfgEUEYSqljx1voUE3Wz/z1ZsxyzSxvH2X8uKVrOp7QkByWt0rA6+gvhCKy6g==} + engines: {node: '>=6'} + dev: true + /titleize@3.0.0: resolution: {integrity: sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==} engines: {node: '>=12'} dev: true + /tmp@0.0.29: + resolution: {integrity: sha512-89PTqMWGDva+GqClOqBV9s3SMh7MA3Mq0pJUdAoHuF65YoE7O0LermaZkVfT5/Ngfo18H4eYiyG7zKOtnEbxsw==} + engines: {node: '>=0.4.0'} + dependencies: + os-tmpdir: 1.0.2 + dev: true + /tmp@0.0.33: resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} engines: {node: '>=0.6.0'} @@ -22020,6 +23359,11 @@ packages: resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} engines: {node: '>=4'} + /to-readable-stream@1.0.0: + resolution: {integrity: sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==} + engines: {node: '>=6'} + dev: true + /to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -22060,6 +23404,11 @@ packages: /treeverse@1.0.4: resolution: {integrity: sha512-whw60l7r+8ZU8Tu/Uc2yxtc4ZTZbR/PF3u1IPNKGQ6p8EICLb3Z2lAgoqw9bqYd8IkgnsaOcLzYHFckjqNsf0g==} + /trim-newlines@2.0.0: + resolution: {integrity: sha512-MTBWv3jhVjTU7XR3IQHllbiJs8sc75a80OEhB6or/q7pLTWgQ0bMGQXXYQSrSuXe6WiKWDZ5txXY5P59a/coVA==} + engines: {node: '>=4'} + dev: true + /trim-repeated@1.0.0: resolution: {integrity: sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==} engines: {node: '>=0.10.0'} @@ -22331,6 +23680,22 @@ packages: safe-buffer: 5.2.1 dev: true + /tunnel@0.0.6: + resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==} + engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'} + dev: true + + /twig@1.17.1: + resolution: {integrity: sha512-atxccyr/BHtb1gPMA7Lvki0OuU17XBqHsNH9lzDHt9Rr1293EVZOosSZabEXz/DPVikIW8ZDqSkEddwyJnQN2w==} + engines: {node: '>=10'} + hasBin: true + dependencies: + '@babel/runtime': 7.25.0 + locutus: 2.0.32 + minimatch: 3.0.5 + walk: 2.3.15 + dev: true + /type-check@0.3.2: resolution: {integrity: sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==} engines: {node: '>= 0.8.0'} @@ -22348,6 +23713,11 @@ packages: resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} engines: {node: '>=4'} + /type-fest@0.13.1: + resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} + engines: {node: '>=10'} + dev: true + /type-fest@0.20.2: resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} engines: {node: '>=10'} @@ -22356,6 +23726,11 @@ packages: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} + /type-fest@0.3.1: + resolution: {integrity: sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==} + engines: {node: '>=6'} + dev: true + /type-fest@0.6.0: resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} engines: {node: '>=8'} @@ -22480,6 +23855,10 @@ packages: is-typedarray: 1.0.0 dev: true + /typedarray@0.0.6: + resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + dev: true + /typescript@4.9.5: resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} engines: {node: '>=4.2.0'} @@ -22643,6 +24022,13 @@ packages: dependencies: imurmurhash: 0.1.4 + /unique-string@2.0.0: + resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==} + engines: {node: '>=8'} + dependencies: + crypto-random-string: 2.0.0 + dev: true + /unique-string@3.0.0: resolution: {integrity: sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==} engines: {node: '>=12'} @@ -22707,6 +24093,11 @@ packages: resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==} engines: {node: '>=8'} + /unzip-response@2.0.1: + resolution: {integrity: sha512-N0XH6lqDtFH84JxptQoZYmloF4nzrQqqrAymNj+/gW60AO2AZgOcf4O/nUXJcYfyQkqvMo9lSupBZmmgvuVXlw==} + engines: {node: '>=4'} + dev: true + /unzip-stream@0.3.4: resolution: {integrity: sha512-PyofABPVv+d7fL7GOpusx7eRT9YETY2X04PhwbSipdj6bMxVCFJrr+nm0Mxqbf9hUiTin/UsnuFWBXlDZFy0Cw==} dependencies: @@ -22757,6 +24148,26 @@ packages: picocolors: 1.1.1 dev: true + /update-notifier@5.1.0: + resolution: {integrity: sha512-ItnICHbeMh9GqUy31hFPrD1kcuZ3rpxDZbf4KUDavXwS0bW5m7SLbDQpGX3UYr072cbrF5hFUs3r5tUsPwjfHw==} + engines: {node: '>=10'} + dependencies: + boxen: 5.1.2 + chalk: 4.1.2 + configstore: 5.0.1 + has-yarn: 2.1.0 + import-lazy: 2.1.0 + is-ci: 2.0.0 + is-installed-globally: 0.4.0 + is-npm: 5.0.0 + is-yarn-global: 0.3.0 + latest-version: 5.1.0 + pupa: 2.1.1 + semver: 7.6.3 + semver-diff: 3.1.1 + xdg-basedir: 4.0.0 + dev: true + /update-notifier@6.0.2: resolution: {integrity: sha512-EDxhTEVPZZRLWYcJ4ZXjGFN0oP7qYvbXWzEgRm/Yql4dHX5wDbvh89YHP6PK1lzZJYrMtXUuZZz8XGK+U6U1og==} engines: {node: '>=14.16'} @@ -22800,12 +24211,31 @@ packages: /url-join@4.0.1: resolution: {integrity: sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==} + /url-parse-lax@1.0.0: + resolution: {integrity: sha512-BVA4lR5PIviy2PMseNd2jbFQ+jwSwQGdJejf5ctd1rEXt0Ypd7yanUK9+lYechVlN5VaTJGsu2U/3MDDu6KgBA==} + engines: {node: '>=0.10.0'} + dependencies: + prepend-http: 1.0.4 + dev: true + + /url-parse-lax@3.0.0: + resolution: {integrity: sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ==} + engines: {node: '>=4'} + dependencies: + prepend-http: 2.0.0 + dev: true + /url-parse@1.5.10: resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} dependencies: querystringify: 2.2.0 requires-port: 1.0.0 + /url-to-options@1.0.1: + resolution: {integrity: sha512-0kQLIzG4fdk/G5NONku64rSH/x32NOA39LVQqlK8Le6lvTF6GGRJpqaQFGgU+CLwySIqBSMdwYM0sYcW9f6P4A==} + engines: {node: '>= 4'} + dev: true + /url@0.11.4: resolution: {integrity: sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==} engines: {node: '>= 0.4'} @@ -22830,6 +24260,13 @@ packages: react: 16.14.0 dev: false + /user-home@2.0.0: + resolution: {integrity: sha512-KMWqdlOcjCYdtIJpicDSFBQ8nFwS2i9sslAd6f4+CBGcU4gist2REnr2fxj2YocvJFxSF3ZOHLYLVZnUxv4BZQ==} + engines: {node: '>=0.10.0'} + dependencies: + os-homedir: 1.0.2 + dev: true + /utf8-byte-length@1.0.4: resolution: {integrity: sha512-4+wkEYLBbWxqTahEsWrhxepcoVOJ+1z5PGIjPZxRkytcdSUaNjIjBM7Xn8E+pdSuV7SzvWovBFA54FO0JSoqhA==} dev: false @@ -22999,6 +24436,12 @@ packages: resolution: {integrity: sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA==} dev: true + /walk@2.3.15: + resolution: {integrity: sha512-4eRTBZljBfIISK1Vnt69Gvr2w/wc3U6Vtrw7qiN5iqYJPH7LElcYh/iU4XWhdCy2dZqv1ToMyYlybDylfG/5Vg==} + dependencies: + foreachasync: 3.0.0 + dev: true + /walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} dependencies: @@ -23221,6 +24664,13 @@ packages: dependencies: string-width: 4.2.3 + /widest-line@3.1.0: + resolution: {integrity: sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==} + engines: {node: '>=8'} + dependencies: + string-width: 4.2.3 + dev: true + /widest-line@4.0.1: resolution: {integrity: sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==} engines: {node: '>=12'} @@ -23276,6 +24726,14 @@ packages: resolution: {integrity: sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==} dev: true + /wrap-ansi@2.1.0: + resolution: {integrity: sha512-vAaEaDM946gbNpH5pLVNR+vX2ht6n0Bt3GXwVB1AuAqZosOvHNF3P7wDnh8KLkSqgUh0uh77le7Owgoz+Z9XBw==} + engines: {node: '>=0.10.0'} + dependencies: + string-width: 1.0.2 + strip-ansi: 3.0.1 + dev: true + /wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -23356,6 +24814,11 @@ packages: utf-8-validate: optional: true + /xdg-basedir@4.0.0: + resolution: {integrity: sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==} + engines: {node: '>=8'} + dev: true + /xdg-basedir@5.1.0: resolution: {integrity: sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==} engines: {node: '>=12'} @@ -23408,6 +24871,10 @@ packages: engines: {node: '>=0.6.0'} dev: false + /xregexp@4.0.0: + resolution: {integrity: sha512-PHyM+sQouu7xspQQwELlGwwd05mXUFqwFYfqPO0cC7x4fxyHnnuetmQr6CjJiafIDoH4MogHb9dOoJzR/Y4rFg==} + dev: true + /xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -23447,6 +24914,12 @@ packages: glob: 7.2.0 dev: false + /yargs-parser@10.1.0: + resolution: {integrity: sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==} + dependencies: + camelcase: 4.1.0 + dev: true + /yargs-parser@20.2.9: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} engines: {node: '>=10'} @@ -23489,6 +24962,30 @@ packages: fd-slicer: 1.1.0 dev: true + /yeoman-character@1.1.0: + resolution: {integrity: sha512-oxzeZugaEkVJC+IHwcb+DZDb8IdbZ3f4rHax4+wtJstCx+9BAaMX+Inmp3wmGmTWftJ7n5cPqQRbo1FaV/vNXQ==} + engines: {node: '>=0.10.0'} + hasBin: true + dependencies: + supports-color: 3.2.3 + dev: true + + /yeoman-doctor@5.0.0: + resolution: {integrity: sha512-9Ni+uXWeFix9+1t7s1q40zZdbcpdi/OwgD4N4cVaqI+bppPciOOXQ/RSggannwZu8m8zrSWELn6/93G7308jgg==} + engines: {node: '>=12.10.0'} + hasBin: true + dependencies: + ansi-styles: 3.2.1 + bin-version-check: 4.0.0 + chalk: 2.4.2 + global-agent: 2.2.0 + latest-version: 3.1.0 + log-symbols: 2.2.0 + semver: 5.7.2 + twig: 1.17.1 + user-home: 2.0.0 + dev: true + /yeoman-environment@3.19.3: resolution: {integrity: sha512-/+ODrTUHtlDPRH9qIC0JREH8+7nsRcjDl3Bxn2Xo/rvAaVvixH5275jHwg0C85g4QsF4P6M2ojfScPPAl+pLAg==} engines: {node: '>=12.10.0'} @@ -23593,6 +25090,49 @@ packages: engines: {node: '>=6'} dev: true + /yo@4.3.1(mem-fs@2.1.0): + resolution: {integrity: sha512-KKp5WNPq0KdqfJY4W6HSiDG4DcgvmL4InWfkg5SVG9oYp+DTUUuc5ZmDw9VAvK0Z2J6XeEumDHcWh8NDhzrtOw==} + engines: {node: '>=12.10.0'} + hasBin: true + requiresBuild: true + dependencies: + async: 3.2.4 + chalk: 4.1.2 + cli-list: 0.2.0 + configstore: 5.0.1 + cross-spawn: 7.0.5 + figures: 3.2.0 + fullname: 4.0.1 + global-agent: 3.0.0 + global-tunnel-ng: 2.7.1 + got: 8.3.2 + humanize-string: 2.1.0 + inquirer: 8.2.6 + lodash: 4.17.21 + mem-fs-editor: 9.4.0(mem-fs@2.1.0) + meow: 5.0.0 + npm-keyword: 6.1.0 + open: 8.4.2 + package-json: 7.0.0 + parse-help: 1.0.0 + read-pkg-up: 7.0.1 + root-check: 1.0.0 + sort-on: 4.1.1 + string-length: 4.0.2 + tabtab: 1.3.2 + titleize: 2.1.0 + update-notifier: 5.1.0 + user-home: 2.0.0 + yeoman-character: 1.1.0 + yeoman-doctor: 5.0.0 + yeoman-environment: 3.19.3 + yosay: 2.0.2 + transitivePeerDependencies: + - bluebird + - mem-fs + - supports-color + dev: true + /yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -23602,6 +25142,22 @@ packages: engines: {node: '>=12.20'} dev: true + /yosay@2.0.2: + resolution: {integrity: sha512-avX6nz2esp7IMXGag4gu6OyQBsMh/SEn+ZybGu3yKPlOTE6z9qJrzG/0X5vCq/e0rPFy0CUYCze0G5hL310ibA==} + engines: {node: '>=4'} + hasBin: true + dependencies: + ansi-regex: 2.1.1 + ansi-styles: 3.2.1 + chalk: 1.1.3 + cli-boxes: 1.0.0 + pad-component: 0.0.1 + string-width: 2.1.1 + strip-ansi: 3.0.1 + taketalk: 1.0.0 + wrap-ansi: 2.1.0 + dev: true + /zip-stream@6.0.1: resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} engines: {node: '>= 14'} From 1a329d59afd5ac03fb73d68737d4e4667ed0e9ab Mon Sep 17 00:00:00 2001 From: I743583 Date: Wed, 26 Mar 2025 08:08:36 +0000 Subject: [PATCH 04/41] change folder structure --- .vscode/launch.json | 13 ++++ .../package.json | 2 + .../src/app/index.ts | 52 ++++++++------ .../src/{utils => prompts}/prompt-state.ts | 0 .../src/{app => prompts}/prompts.ts | 8 +-- .../src/utils/app-config.ts | 4 +- .../src/utils/index.ts | 67 ------------------- .../src/utils/utils.ts | 2 +- .../tsconfig.json | 6 ++ pnpm-lock.yaml | 6 ++ 10 files changed, 65 insertions(+), 95 deletions(-) rename packages/bsp-app-download-sub-generator/src/{utils => prompts}/prompt-state.ts (100%) rename packages/bsp-app-download-sub-generator/src/{app => prompts}/prompts.ts (94%) delete mode 100644 packages/bsp-app-download-sub-generator/src/utils/index.ts diff --git a/.vscode/launch.json b/.vscode/launch.json index 103482a9a5..c3625d8f4d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -429,6 +429,19 @@ "program": "${workspaceFolder}/node_modules/jest/bin/jest" }, "cwd": "${workspaceFolder}/packages/system-access" + }, + { + "type": "node", + "request": "launch", + "name": "bsp-app-download-sub-generator: Debug Current Jest File", + "program": "${workspaceFolder}/node_modules/jest/bin/jest.js", + "args": ["${file}", "--config", "jest.config.js", "--coverage=false"], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "windows": { + "program": "${workspaceFolder}/node_modules/jest/bin/jest" + }, + "cwd": "${workspaceFolder}/packages/bsp-app-download-sub-generator" } ] } diff --git a/packages/bsp-app-download-sub-generator/package.json b/packages/bsp-app-download-sub-generator/package.json index 5d77f4a329..d59f622f19 100644 --- a/packages/bsp-app-download-sub-generator/package.json +++ b/packages/bsp-app-download-sub-generator/package.json @@ -47,6 +47,8 @@ "@sap-ux/abap-deploy-config-writer": "workspace:*", "@sap-ux/btp-utils": "workspace:*", "@sap-ux/ui5-info": "workspace:*", + "@sap-ux/axios-extension": "workspace:*", + "@sap-ux/store": "workspace:*", "adm-zip": "0.5.10", "i18next": "23.5.1", "inquirer": "8.2.6", diff --git a/packages/bsp-app-download-sub-generator/src/app/index.ts b/packages/bsp-app-download-sub-generator/src/app/index.ts index b2fcf7ef45..83a6f1427a 100644 --- a/packages/bsp-app-download-sub-generator/src/app/index.ts +++ b/packages/bsp-app-download-sub-generator/src/app/index.ts @@ -5,14 +5,14 @@ import { isInternalFeaturesSettingEnabled } from '@sap-ux/feature-toggle'; import type { Logger } from '@sap-ux/logger'; import { sendTelemetry, TelemetryHelper } from '@sap-ux/fiori-generator-shared'; import { generatorTitle, extractedFilePath, generatorName } from '../utils/constants'; -import { t } from '../utils'; +import { t } from '../utils/i18n'; import { getYUIDetails, downloadApp } from '../utils/utils'; import { EventName } from '../telemetryEvents'; import type { YeomanEnvironment, VSCodeInstance } from '@sap-ux/fiori-generator-shared'; import { getDefaultTargetFolder } from '@sap-ux/fiori-generator-shared'; import type { BspAppDownloadOptions, BspAppDownloadAnswers } from './types'; import { promptNames } from '@sap-ux/odata-service-inquirer'; -import { getQuestions } from './prompts'; +import { getQuestions } from '../prompts/prompts'; import { generate, TemplateType, type FioriElementsApp, type LROPSettings } from '@sap-ux/fiori-elements-writer'; import { getAppConfig } from '../utils/app-config'; import { join } from 'path'; @@ -26,18 +26,25 @@ import { isAppStudio } from '@sap-ux/btp-utils'; import { OdataVersion } from '@sap-ux/odata-service-inquirer'; import { writeApplicationInfoSettings } from '@sap-ux/fiori-tools-settings'; import { generate as generateDeployConfig } from '@sap-ux/abap-deploy-config-writer'; -import { PromptState } from '../utils/prompt-state'; +import { PromptState } from '../prompts/prompt-state'; -/** - * - */ export default class extends Generator { private readonly appWizard: AppWizard; private readonly vscode?: VSCodeInstance; private readonly launchAppDownloaderAsSubGenerator: boolean; private readonly appRootPath: string; private readonly prompts: Prompts; - private answers: BspAppDownloadAnswers; + private answers: BspAppDownloadAnswers = { + selectedApp: { + appId: '', + title: '', + description: '', + repoName: '', + url: '' + }, + targetFolder: '', + [promptNames.systemSelection]: {} + }; public options: BspAppDownloadOptions; private fioriOptions: FioriOptions; // re visit this @@ -100,11 +107,14 @@ export default class extends Generator { const questions = await getQuestions(this.appRootPath); const { selectedApp, targetFolder } = (await this.prompt(questions)) as BspAppDownloadAnswers; if (PromptState.systemSelection.connectedSystem?.serviceProvider && selectedApp?.appId && targetFolder) { - Object.assign(this.answers, { - selectedApp, - targetFolder, - serviceProvider: PromptState.systemSelection.connectedSystem?.serviceProvider - }); + // Object.assign(this.answers, { + // selectedApp, + // targetFolder, + // serviceProvider: PromptState.systemSelection.connectedSystem?.serviceProvider + // }); + this.answers.selectedApp = selectedApp; + this.answers.targetFolder = targetFolder; + //this.answers.serviceProvider = PromptState.systemSelection.connectedSystem?.serviceProvider; this.projectPath = join(targetFolder, selectedApp.appId); this.extractedProjectPath = join(this.projectPath, extractedFilePath); @@ -222,15 +232,15 @@ export default class extends Generator { } async end() { - sendTelemetry( - EventName.GENERATION_SUCCESS, - TelemetryHelper.createTelemetryData({ - appType: 'bsp-app-download-sub-generator', - ...this.options.telemetryData - }) ?? {} - ).catch((error) => { - BspAppDownloadLogger.logger.error(t('error.telemetry', { error })); - }); + // sendTelemetry( + // EventName.GENERATION_SUCCESS, + // TelemetryHelper.createTelemetryData({ + // appType: 'bsp-app-download-sub-generator', + // ...this.options.telemetryData + // }) ?? {} + // ).catch((error) => { + // BspAppDownloadLogger.logger.error(t('error.telemetry', { error })); + // }); debugger; await createLaunchConfig( this.projectPath, diff --git a/packages/bsp-app-download-sub-generator/src/utils/prompt-state.ts b/packages/bsp-app-download-sub-generator/src/prompts/prompt-state.ts similarity index 100% rename from packages/bsp-app-download-sub-generator/src/utils/prompt-state.ts rename to packages/bsp-app-download-sub-generator/src/prompts/prompt-state.ts diff --git a/packages/bsp-app-download-sub-generator/src/app/prompts.ts b/packages/bsp-app-download-sub-generator/src/prompts/prompts.ts similarity index 94% rename from packages/bsp-app-download-sub-generator/src/app/prompts.ts rename to packages/bsp-app-download-sub-generator/src/prompts/prompts.ts index 5a83ae39e9..8373056064 100644 --- a/packages/bsp-app-download-sub-generator/src/app/prompts.ts +++ b/packages/bsp-app-download-sub-generator/src/prompts/prompts.ts @@ -1,7 +1,7 @@ import type { AbapServiceProvider, AppIndex } from '@sap-ux/axios-extension'; import { getSystemSelectionQuestions, promptNames } from '@sap-ux/odata-service-inquirer'; -import type { BspAppDownloadAnswers } from './types'; -import { PromptNames } from './types'; +import type { BspAppDownloadAnswers } from '../app/types'; +import { PromptNames } from '../app/types'; import { Severity } from '@sap-devx/yeoman-ui-types'; import { t } from '../utils/i18n'; import type { FileBrowserQuestion } from '@sap-ux/inquirer-common'; @@ -9,7 +9,7 @@ import type { Logger } from '@sap-ux/logger'; import type { Question } from 'inquirer'; import { getAppList } from '../utils/utils'; import { validateTargetFolderForFioriApp } from '@sap-ux/project-input-validator'; -import { PromptState } from '../utils/prompt-state'; +import { PromptState } from './prompt-state'; /** * Gets the target folder prompt. @@ -43,7 +43,7 @@ const getTargetFolderPrompt = (appRootPath?: string) => { */ export async function getQuestions(appRootPath?: string, log?: Logger): Promise[]> { PromptState.reset(); - const systemQuestions = await getSystemSelectionQuestions({ serviceSelection: { hide: true } }, true); + const systemQuestions = await getSystemSelectionQuestions({ serviceSelection: { hide: true } }, false); // remove this isYUI value let appList: AppIndex = []; let result: Question[] = []; diff --git a/packages/bsp-app-download-sub-generator/src/utils/app-config.ts b/packages/bsp-app-download-sub-generator/src/utils/app-config.ts index 99cf8a1add..a99fbb9d0f 100644 --- a/packages/bsp-app-download-sub-generator/src/utils/app-config.ts +++ b/packages/bsp-app-download-sub-generator/src/utils/app-config.ts @@ -4,13 +4,13 @@ import { OdataVersion } from '@sap-ux/odata-service-inquirer'; import type { AbapServiceProvider, ServiceDocument } from '@sap-ux/axios-extension'; import type { Logger } from '@sap-ux/logger'; import type { Editor } from 'mem-fs-editor'; -import { t } from './i18n'; +import { t } from '../utils/i18n'; import type { BspAppDownloadAnswers } from '../app/types'; import { readManifest } from './utils'; import { getLatestUI5Version } from '@sap-ux/ui5-info'; import { getMinimumUI5Version } from '@sap-ux/project-access'; import { adtSourceTemplateId } from './constants'; -import { PromptState } from '../utils/prompt-state'; +import { PromptState } from '../prompts/prompt-state'; /** * Retrieves metadata for the provided service URL. diff --git a/packages/bsp-app-download-sub-generator/src/utils/index.ts b/packages/bsp-app-download-sub-generator/src/utils/index.ts deleted file mode 100644 index 3135b79c23..0000000000 --- a/packages/bsp-app-download-sub-generator/src/utils/index.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { getHostEnvironment, hostEnvironment } from '@sap-ux/fiori-generator-shared'; -import { t } from './i18n'; -import { MessageType, type AppWizard } from '@sap-devx/yeoman-ui-types'; -import BspAppDownloadLogger from './logger'; - -export enum ERROR_TYPE {} -// ABORT_SIGNAL = 'ABORT_SIGNAL', -// NO_MANIFEST = 'NO_MANIFEST', -// NO_APP_NAME = 'NO_APP_NAME', -// NO_CDS_BIN = 'NO_CDS_BIN', -// NO_MTA_BIN = 'NO_MTA_BIN' - -/** - * Error messages for the deploy configuration generator. - */ -export class ErrorHandler { - /** - * Get the error message for the specified error type. - * - * @param errorType The error type for which the message may be returned - * @returns The error message for the specified error type - */ - public static getErrorMsgFromType(errorType?: ERROR_TYPE): string { - if (errorType) { - return ErrorHandler._errorTypeToMsg[errorType](); - } - return t('errors.unknownError'); - } - - private static readonly _errorTypeToMsg: Record string> = { - // [ERROR_TYPE.ABORT_SIGNAL]: () => t('errors.abortSignal'), - // [ERROR_TYPE.NO_MANIFEST]: () => t('errors.noManifest'), - // [ERROR_TYPE.NO_APP_NAME]: () => t('errors.noAppName') - }; -} - -/** - * Bail out with an error message. - * - * @param errorMessage - Error message to be displayed - */ -export function bail(errorMessage: string): void { - throw new Error(errorMessage); -} - -/** - * Handle error message, display it in the UI or throws an error in CLI. - * - * @param appWizard - AppWizard instance - * @param error - error type or message - * @param error.errorType - error type - * @param error.errorMsg - error message - */ -export function handleErrorMessage( - appWizard: AppWizard, - { errorType, errorMsg }: { errorType?: ERROR_TYPE; errorMsg?: string } -): void { - const error = errorMsg ?? ErrorHandler.getErrorMsgFromType(errorType); - if (getHostEnvironment() === hostEnvironment.cli) { - bail(error); - } else { - BspAppDownloadLogger.logger?.debug(error); - appWizard?.showError(error, MessageType.notification); - } -} - -export { t } from './i18n'; diff --git a/packages/bsp-app-download-sub-generator/src/utils/utils.ts b/packages/bsp-app-download-sub-generator/src/utils/utils.ts index 3144216017..39698d3518 100644 --- a/packages/bsp-app-download-sub-generator/src/utils/utils.ts +++ b/packages/bsp-app-download-sub-generator/src/utils/utils.ts @@ -13,7 +13,7 @@ import type { Editor } from 'mem-fs-editor'; import type { BspAppDownloadAnswers } from '../app/types'; import { FileName, type Manifest } from '@sap-ux/project-access'; import { t } from './i18n'; -import { PromptState } from './prompt-state'; +import { PromptState } from '../prompts/prompt-state'; /** * Returns the details for the YUI prompt. diff --git a/packages/bsp-app-download-sub-generator/tsconfig.json b/packages/bsp-app-download-sub-generator/tsconfig.json index b6597d08a7..e0b353d861 100644 --- a/packages/bsp-app-download-sub-generator/tsconfig.json +++ b/packages/bsp-app-download-sub-generator/tsconfig.json @@ -12,6 +12,9 @@ { "path": "../abap-deploy-config-writer" }, + { + "path": "../axios-extension" + }, { "path": "../btp-utils" }, @@ -51,6 +54,9 @@ { "path": "../project-input-validator" }, + { + "path": "../store" + }, { "path": "../ui5-application-inquirer" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2dc1f14fbd..280f8a9bce 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -909,6 +909,9 @@ importers: '@sap-ux/abap-deploy-config-writer': specifier: workspace:* version: link:../abap-deploy-config-writer + '@sap-ux/axios-extension': + specifier: workspace:* + version: link:../axios-extension '@sap-ux/btp-utils': specifier: workspace:* version: link:../btp-utils @@ -945,6 +948,9 @@ importers: '@sap-ux/project-input-validator': specifier: workspace:* version: link:../project-input-validator + '@sap-ux/store': + specifier: workspace:* + version: link:../store '@sap-ux/ui5-application-inquirer': specifier: workspace:* version: link:../ui5-application-inquirer From a874fec6cd671432259a01e979af77933ba8d796 Mon Sep 17 00:00:00 2001 From: I743583 Date: Thu, 27 Mar 2025 09:10:02 +0000 Subject: [PATCH 05/41] re structure files --- .../package.json | 5 +- .../questions.md | 30 ---- .../src/app/index.ts | 131 ++++++++------ .../src/app/types.ts | 162 ++++++++++++------ .../src/prompts/prompt-state.ts | 2 +- .../src/prompts/prompts.ts | 105 ------------ .../bsp-app-download-sub-generator.i18n.json | 8 +- .../src/utils/app-config.ts | 162 ------------------ .../src/utils/constants.ts | 43 +++-- .../src/utils/eventHook.ts | 25 --- .../src/utils/utils.ts | 127 -------------- pnpm-lock.yaml | 12 +- 12 files changed, 237 insertions(+), 575 deletions(-) delete mode 100644 packages/bsp-app-download-sub-generator/questions.md delete mode 100644 packages/bsp-app-download-sub-generator/src/prompts/prompts.ts delete mode 100644 packages/bsp-app-download-sub-generator/src/utils/app-config.ts delete mode 100644 packages/bsp-app-download-sub-generator/src/utils/eventHook.ts delete mode 100644 packages/bsp-app-download-sub-generator/src/utils/utils.ts diff --git a/packages/bsp-app-download-sub-generator/package.json b/packages/bsp-app-download-sub-generator/package.json index 9322e352e1..08c6a47f84 100644 --- a/packages/bsp-app-download-sub-generator/package.json +++ b/packages/bsp-app-download-sub-generator/package.json @@ -52,7 +52,8 @@ "adm-zip": "0.5.10", "i18next": "23.5.1", "inquirer": "8.2.6", - "yeoman-generator": "5.10.0" + "yeoman-generator": "5.10.0", + "inquirer-autocomplete-prompt": "2.0.1" }, "devDependencies": { "@jest/types": "29.6.3", @@ -61,8 +62,8 @@ "@types/mem-fs-editor": "7.0.1", "@types/yeoman-generator": "5.2.11", "@types/yeoman-environment": "2.10.11", + "@types/inquirer-autocomplete-prompt": "2.0.1", "@types/yeoman-test": "4.0.6", - "@sap-ux/axios-extension": "workspace:*", "@sap-ux/nodejs-utils": "workspace:*", "@sap-ux/store": "workspace:*", "@vscode-logging/logger": "2.0.0", diff --git a/packages/bsp-app-download-sub-generator/questions.md b/packages/bsp-app-download-sub-generator/questions.md deleted file mode 100644 index 516d9cb1ef..0000000000 --- a/packages/bsp-app-download-sub-generator/questions.md +++ /dev/null @@ -1,30 +0,0 @@ - -- for Extracting Downloaded Files - Where would be an appropriate location to extract downloaded files? I’m considering extracting them to a temporary directory defined by: -const tempFilePath = join(homedir(), '.fioritools'); -zip.extractAllTo(tempFilePath, true); -Once writing the app is auccessful, I plan to delete the directory. Does this approach sound reasonable, or do you have a better suggestion for a temporary extraction path? - -- Service Metadata: say we have a service URL like: -https://ldciuia.wdf.sap.corp:44300/sap/opu/odata4/sap/test_service_bindings_07/srvd/sap/test_srvb_01/0001/ from extracted manifest json. -I’m assuming the metadata for this service will always be populated, this is a basic lrop app support correct - so I think its jst gna be an edmx project know? - -- When setting the project path, do we provide an option for the user to specify a name? And if a user selects a name that already exists, how should we handle that scenario? Should we prompt the user for a new name, or overwrite the existing one? - -- sourceTemplates id - -- services.entityConfig.mainEntity - I have double entitties - -- questions about local uri - is it okay to use annotation file manager ? - -- how to get annotations form v2 and v4 ? v2 vs v4 - -- ts or js enabled for tests ? does it matter? - - -// things to do - use zip.entries mem-fs and then then use fe writers and finally use the fs to modify some stuff - -// get edmx https://ldciuia.wdf.sap.corp:44300/sap/opu/odata4/sap/test_service_bindings_07/srvd/sap/test_srvb_01/0001/$metadata - - - -discussion with adp -- diff --git a/packages/bsp-app-download-sub-generator/src/app/index.ts b/packages/bsp-app-download-sub-generator/src/app/index.ts index 83a6f1427a..bfc82b80e7 100644 --- a/packages/bsp-app-download-sub-generator/src/app/index.ts +++ b/packages/bsp-app-download-sub-generator/src/app/index.ts @@ -4,21 +4,20 @@ import { AppWizard, Prompts } from '@sap-devx/yeoman-ui-types'; import { isInternalFeaturesSettingEnabled } from '@sap-ux/feature-toggle'; import type { Logger } from '@sap-ux/logger'; import { sendTelemetry, TelemetryHelper } from '@sap-ux/fiori-generator-shared'; -import { generatorTitle, extractedFilePath, generatorName } from '../utils/constants'; +import { generatorTitle, extractedFilePath, generatorName, defaultAnswers } from '../utils/constants'; import { t } from '../utils/i18n'; -import { getYUIDetails, downloadApp } from '../utils/utils'; +import { getYUIDetails } from '../prompts/prompt-helpers'; +import { downloadApp } from '../utils/download-utils'; import { EventName } from '../telemetryEvents'; import type { YeomanEnvironment, VSCodeInstance } from '@sap-ux/fiori-generator-shared'; import { getDefaultTargetFolder } from '@sap-ux/fiori-generator-shared'; -import type { BspAppDownloadOptions, BspAppDownloadAnswers } from './types'; -import { promptNames } from '@sap-ux/odata-service-inquirer'; -import { getQuestions } from '../prompts/prompts'; +import type { BspAppDownloadOptions, BspAppDownloadAnswers, BspAppDownloadQuestions, AppContentConfig } from './types'; +import { getQuestions } from '../prompts/questions'; import { generate, TemplateType, type FioriElementsApp, type LROPSettings } from '@sap-ux/fiori-elements-writer'; -import { getAppConfig } from '../utils/app-config'; import { join } from 'path'; import { platform } from 'os'; import { generateReadMe, type ReadMe } from '@sap-ux/fiori-generator-shared'; -import { runPostAppGenHook } from '../utils/eventHook'; +import { runPostAppGenHook } from '../utils/event-hook'; import { getDefaultUI5Theme } from '@sap-ux/ui5-info'; import type { DebugOptions, FioriOptions } from '@sap-ux/launch-config'; import { createLaunchConfig } from '@sap-ux/launch-config'; @@ -27,24 +26,21 @@ import { OdataVersion } from '@sap-ux/odata-service-inquirer'; import { writeApplicationInfoSettings } from '@sap-ux/fiori-tools-settings'; import { generate as generateDeployConfig } from '@sap-ux/abap-deploy-config-writer'; import { PromptState } from '../prompts/prompt-state'; +import { PromptNames } from './types'; +import { getAbapDeployConfig, getAppConfig, replaceWebappFiles } from './config'; +import type { AbapDeployConfig } from '@sap-ux/ui5-config'; +/** + * Generator class for downloading a basic app from BSP repository. + * This class handles the process of app selection, downloading the app and generating a fiori app from the downloaded app + */ export default class extends Generator { private readonly appWizard: AppWizard; private readonly vscode?: VSCodeInstance; private readonly launchAppDownloaderAsSubGenerator: boolean; private readonly appRootPath: string; private readonly prompts: Prompts; - private answers: BspAppDownloadAnswers = { - selectedApp: { - appId: '', - title: '', - description: '', - repoName: '', - url: '' - }, - targetFolder: '', - [promptNames.systemSelection]: {} - }; + private answers: BspAppDownloadAnswers = defaultAnswers; public options: BspAppDownloadOptions; private fioriOptions: FioriOptions; // re visit this @@ -61,12 +57,14 @@ export default class extends Generator { constructor(args: string | string[], opts: BspAppDownloadOptions) { super(args, opts); + // Initialize properties from options this.appWizard = opts.appWizard ?? AppWizard.create(opts); this.vscode = opts.vscode; this.launchAppDownloaderAsSubGenerator = opts.launchAppDownloaderAsSubGenerator ?? false; this.appRootPath = opts?.appRootPath ?? getDefaultTargetFolder(this.vscode) ?? this.destinationRoot(); this.options = opts; + // Configure logging BspAppDownloadLogger.configureLogging( this.rootGeneratorName(), this.log, @@ -76,7 +74,7 @@ export default class extends Generator { this.vscode ); - // If launched standalone, set the header, title and description + // Initialize prompts and callbacks if not launched as a subgenerator if (!this.launchAppDownloaderAsSubGenerator) { this.appWizard.setHeaderTitle(generatorTitle); this.prompts = new Prompts(getYUIDetails()); @@ -88,11 +86,15 @@ export default class extends Generator { } } + /** + * Initializes necessary settings and telemetry for the generator. + */ public async initializing(): Promise { if ((this.env as unknown as YeomanEnvironment).conflicter) { (this.env as unknown as YeomanEnvironment).conflicter.force = this.options.force ?? true; } + // Initialize telemetry settings await TelemetryHelper.initTelemetrySettings({ consumerModule: { name: generatorName, @@ -103,24 +105,22 @@ export default class extends Generator { }); } + /** + * Prompts the user for application details and downloads the app. + */ public async prompting(): Promise { - const questions = await getQuestions(this.appRootPath); + const questions: BspAppDownloadQuestions[] = await getQuestions(this.appRootPath); const { selectedApp, targetFolder } = (await this.prompt(questions)) as BspAppDownloadAnswers; if (PromptState.systemSelection.connectedSystem?.serviceProvider && selectedApp?.appId && targetFolder) { - // Object.assign(this.answers, { - // selectedApp, - // targetFolder, - // serviceProvider: PromptState.systemSelection.connectedSystem?.serviceProvider - // }); this.answers.selectedApp = selectedApp; this.answers.targetFolder = targetFolder; - //this.answers.serviceProvider = PromptState.systemSelection.connectedSystem?.serviceProvider; this.projectPath = join(targetFolder, selectedApp.appId); this.extractedProjectPath = join(this.projectPath, extractedFilePath); + // Trigger app download await downloadApp( - this.answers, + this.answers.selectedApp.repoName, this.extractedProjectPath, this.fs, BspAppDownloadLogger.logger as unknown as Logger @@ -128,41 +128,48 @@ export default class extends Generator { } } + /** + * Writes the configuration files for the project, including deployment config, and README. + */ public async writing(): Promise { + const appContentJson = this.fs.readJSON(join(__dirname, 'example-app-content.json')) as unknown as AppContentConfig; //todo: extract from extracted path + + // Generate project files const config = await getAppConfig( - this.answers, + this.answers.selectedApp, this.extractedProjectPath, + appContentJson, this.fs, BspAppDownloadLogger.logger as unknown as Logger ); await generate(this.projectPath, config, this.fs); + + // Generate deploy config + const deployConfig: AbapDeployConfig = getAbapDeployConfig(this.answers.selectedApp, appContentJson); await generateDeployConfig( this.projectPath, - { - // todo: get from json file - target: { - url: `TEST_URL`, - destination: `TEST_DESTINATION` - }, - app: { - name: this.answers.selectedApp.appId, - package: `TEST_PACKAGE`, - description: this.answers.selectedApp.description, - transport: `TEST_TRANSPORT_REQ` - } - }, + deployConfig, undefined, this.fs ); + + // Generate README const readMeConfig = this._getReadMeConfig(config); generateReadMe(this.projectPath, readMeConfig, this.fs); + + // Generate Fiori launch config this.fioriOptions = this._getLaunchConfig(config); writeApplicationInfoSettings(this.projectPath); + + // Replace webapp files with downloaded app files + replaceWebappFiles(this.projectPath, this.extractedProjectPath, this.fs); } /** + * Returns the configuration for the README file. * - * @param config + * @param config - The app configuration object. + * @returns {ReadMe} The configuration for generating the README. */ private _getReadMeConfig(config: FioriElementsApp): ReadMe { const readMeConfig: ReadMe = { @@ -182,8 +189,10 @@ export default class extends Generator { } /** + * Returns the configuration for launching the app with Fiori options. * - * @param config + * @param config - The app configuration object. + * @returns {FioriOptions} The launch configuration options. */ private _getLaunchConfig(config: FioriElementsApp): FioriOptions { const debugOptions: DebugOptions = { @@ -203,6 +212,9 @@ export default class extends Generator { return fioriOptions; } + /** + * Installs npm dependencies for the project. + */ public async install(): Promise { if (!this.options.skipInstall) { try { @@ -214,10 +226,11 @@ export default class extends Generator { BspAppDownloadLogger.logger?.info(t('info.skippedInstallation')); } } + /** * Runs npm install in the specified path. * - * @param path - the path to run npm install + * @param path - The path to run npm install. */ private async _runNpmInstall(path: string): Promise { const npm = platform() === 'win32' ? 'npm.cmd' : 'npm'; @@ -231,25 +244,31 @@ export default class extends Generator { ); } + /** + * Finalizes the generator process by creating launch configurations and running post-generation hooks. + */ async end() { - // sendTelemetry( - // EventName.GENERATION_SUCCESS, - // TelemetryHelper.createTelemetryData({ - // appType: 'bsp-app-download-sub-generator', - // ...this.options.telemetryData - // }) ?? {} - // ).catch((error) => { - // BspAppDownloadLogger.logger.error(t('error.telemetry', { error })); - // }); - debugger; + sendTelemetry( + EventName.GENERATION_SUCCESS, + TelemetryHelper.createTelemetryData({ + appType: 'bsp-app-download-sub-generator', + ...this.options.telemetryData + }) ?? {} + ).catch((error) => { + BspAppDownloadLogger.logger.error(t('error.telemetry', { error })); + }); + + // Create launch configuration await createLaunchConfig( this.projectPath, this.fioriOptions, this.fs, BspAppDownloadLogger.logger as unknown as Logger ); - // delete extracted path before commiting the changes + // Clean up extracted project files // this.fs.delete(this.extractedProjectPath); + + // Run post-generation command hook if available if (this.options.data?.postGenCommands) { await runPostAppGenHook({ path: this.projectPath, @@ -260,5 +279,5 @@ export default class extends Generator { } } -export { promptNames }; +export { PromptNames }; export type { BspAppDownloadOptions }; diff --git a/packages/bsp-app-download-sub-generator/src/app/types.ts b/packages/bsp-app-download-sub-generator/src/app/types.ts index d22b168bf6..33f5678d73 100644 --- a/packages/bsp-app-download-sub-generator/src/app/types.ts +++ b/packages/bsp-app-download-sub-generator/src/app/types.ts @@ -1,76 +1,140 @@ import type Generator from 'yeoman-generator'; import type { AppWizard } from '@sap-devx/yeoman-ui-types'; import type { VSCodeInstance, TelemetryData, LogWrapper } from '@sap-ux/fiori-generator-shared'; -import type { promptNames } from '@sap-ux/odata-service-inquirer'; import type { Destination } from '@sap-ux/btp-utils'; import type { BackendSystem } from '@sap-ux/store'; -import type { AbapServiceProvider } from '@sap-ux/axios-extension'; +import type { AbapServiceProvider, AppIndex } from '@sap-ux/axios-extension'; +import type { YUIQuestion } from '@sap-ux/inquirer-common'; +import type { AutocompleteQuestionOptions } from 'inquirer-autocomplete-prompt'; +/** + * Options for downloading a BSP application. + */ export interface BspAppDownloadOptions extends Generator.GeneratorOptions { - /** - * VSCode instance - */ + /** VSCode instance for interacting with the VSCode environment. */ vscode?: VSCodeInstance; - /** - * AppWizard instance - */ + + /** AppWizard instance for managing the application download flow. */ appWizard?: AppWizard; - /** - * Whether the generator is launched as a subgenerator - */ - launchAppDownloaderAsSubGenerator?: boolean; //todo: check this option - /** - * Path to the application root where the Fiori launchpad configuration will be added. - */ + + /** Indicates if the generator is launched as a subgenerator. */ + launchAppDownloaderAsSubGenerator?: boolean; // TODO: Verify this option. + + /** Path to the application root where the Fiori launchpad configuration will be added. */ appRootPath?: string; - /** - * Telemetry data to be send after deployment configuration has been added - */ + + /** Telemetry data for tracking events post deployment configuration. */ telemetryData?: TelemetryData; - /** - * Logger instance - */ - logWrapper?: LogWrapper; -} -export interface BspAppDownloadAnswers { - [promptNames.systemSelection]: SystemSelectionAnswers; - selectedApp: { - appId: string; - title: string; - description: string; - repoName: string; - url: string; - }; - targetFolder: string; + /** Logger instance for logging operations. */ + logWrapper?: LogWrapper; } -export const PromptNames = { - selectedApp: 'selectedApp', - systemSelection: 'systemSelection', - targetFolder: 'targetFolder' -}; - +/** + * Answers related to system selection in the BSP application download process. + */ export interface SystemSelectionAnswers { /** - * The connected system will allow downstream consumers to access the connected system without creating new connections. - * + * Details of the connected system allowing downstream consumers to access it without creating new connections. */ connectedSystem?: { - /** - * Convienence property to pass the connected system - */ + /** Service provider for the connected ABAP system. */ serviceProvider: AbapServiceProvider; /** - * The persistable backend system representation of the connected service provider - * `newOrUpdated` is set to true if the system was newly created or updated during the connection validation process and should be considered for storage. + * Persistable backend system representation of the connected service provider. + * `newOrUpdated` is true if the system was newly created or updated during connection validation. */ backendSystem?: BackendSystem & { newOrUpdated?: boolean }; - /** - * The destination information for the connected system - */ + /** Destination details of the connected system. */ destination?: Destination; }; } + +/** + * Represents a question in the app download process. + * Extends `YUIQuestion` with optional autocomplete functionality. + */ +export type BspAppDownloadQuestions = YUIQuestion & + Partial>; + +// Extract the type of a single element in the AppIndex array +export type AppItem = AppIndex extends (infer U)[] ? U : never; + +export interface AppInfo { + appId: string; + title: string; + description: string; + repoName: string; + url: string; +} + +/** + * Enum representing the names of prompts used in the BSP application download process. + */ +export enum PromptNames { + selectedApp = 'selectedApp', + systemSelection = 'systemSelection', + targetFolder = 'targetFolder' +} + +/** + * Structure of answers provided by the user for BSP application download prompts. + */ +export interface BspAppDownloadAnswers { + /** Selected backend system connection details. */ + [PromptNames.systemSelection]: SystemSelectionAnswers; + /** Information about the selected application for download. */ + [PromptNames.selectedApp]: AppInfo; + /** Target folder where the BSP application will be generated. */ + [PromptNames.targetFolder]: string; +} + + +interface Metadata { + package: string; + masterLanguage: string; +} + +export interface EntityConfig { + mainEntityName: string; + navigationEntity?: { + EntitySet: string; + Name: string; + }; +} + +interface ServiceBindingDetails extends EntityConfig{ + name: string; + serviceName: string; + serviceVersion: string; +} + +interface ProjectAttribute { + moduleName: string; + applicationTitle: string; + template: string; + minimumUi5Version: string; +} + +interface DeploymentDetails { + repositoryName: string; + repositoryDescription: string; +} + +interface FioriLaunchpadConfiguration { + semanticObject: string; + action: string; + title: string; + subtitle: string; +} + +export interface AppContentConfig { + metadata: Metadata; + serviceBindingDetails: ServiceBindingDetails; + projectAttribute: ProjectAttribute; + deploymentDetails: DeploymentDetails; + fioriLaunchpadConfiguration: FioriLaunchpadConfiguration; +} + \ No newline at end of file diff --git a/packages/bsp-app-download-sub-generator/src/prompts/prompt-state.ts b/packages/bsp-app-download-sub-generator/src/prompts/prompt-state.ts index cdb327feab..665cef12e0 100644 --- a/packages/bsp-app-download-sub-generator/src/prompts/prompt-state.ts +++ b/packages/bsp-app-download-sub-generator/src/prompts/prompt-state.ts @@ -12,7 +12,7 @@ export class PromptState { /** * Returns the current state of the service config. * - * @returns {ServiceConfig} service config + * @returns {SystemSelectionAnswers} service config */ public static get systemSelection(): SystemSelectionAnswers { return this._systemSelection; diff --git a/packages/bsp-app-download-sub-generator/src/prompts/prompts.ts b/packages/bsp-app-download-sub-generator/src/prompts/prompts.ts deleted file mode 100644 index 8373056064..0000000000 --- a/packages/bsp-app-download-sub-generator/src/prompts/prompts.ts +++ /dev/null @@ -1,105 +0,0 @@ -import type { AbapServiceProvider, AppIndex } from '@sap-ux/axios-extension'; -import { getSystemSelectionQuestions, promptNames } from '@sap-ux/odata-service-inquirer'; -import type { BspAppDownloadAnswers } from '../app/types'; -import { PromptNames } from '../app/types'; -import { Severity } from '@sap-devx/yeoman-ui-types'; -import { t } from '../utils/i18n'; -import type { FileBrowserQuestion } from '@sap-ux/inquirer-common'; -import type { Logger } from '@sap-ux/logger'; -import type { Question } from 'inquirer'; -import { getAppList } from '../utils/utils'; -import { validateTargetFolderForFioriApp } from '@sap-ux/project-input-validator'; -import { PromptState } from './prompt-state'; - -/** - * Gets the target folder prompt. - * - * @param appRootPath - The application root path. - * @returns - The target folder prompt. - */ -const getTargetFolderPrompt = (appRootPath?: string) => { - return { - type: 'input', - name: PromptNames.targetFolder, - message: t('prompts.targetPath.targetPathMessage'), - guiType: 'folder-browser', - when: (answers: BspAppDownloadAnswers) => Boolean(answers?.selectedApp?.appId), - guiOptions: { - applyDefaultWhenDirty: true, - mandatory: true, - breadcrumb: t('prompts.targetPath.targetPathBreadcrumb') - }, - validate: async (target, answers: BspAppDownloadAnswers): Promise => { - return await validateTargetFolderForFioriApp(target, answers.selectedApp.appId, true); - }, - default: () => appRootPath - } as FileBrowserQuestion; -}; - -/** - * - * @param appRootPath - * @param log - */ -export async function getQuestions(appRootPath?: string, log?: Logger): Promise[]> { - PromptState.reset(); - const systemQuestions = await getSystemSelectionQuestions({ serviceSelection: { hide: true } }, false); // remove this isYUI value - let appList: AppIndex = []; - let result: Question[] = []; - - const appSelectionPrompt = [ - { - when: async (answers: BspAppDownloadAnswers): Promise => { - if (answers[promptNames.systemSelection] && systemQuestions.answers.connectedSystem?.serviceProvider) { - PromptState.systemSelection = { - connectedSystem: { - serviceProvider: systemQuestions.answers.connectedSystem - .serviceProvider as unknown as AbapServiceProvider - } - }; - appList = await getAppList( - PromptState.systemSelection?.connectedSystem?.serviceProvider as AbapServiceProvider - ); - return !!appList.length; - } - return false; - }, - type: 'list', - name: PromptNames.selectedApp, - guiOptions: { - mandatory: true, - breadcrumb: true - }, - message: t('prompts.appSelection.message'), - choices: async () => - appList.length - ? appList.map((app: any) => ({ - name: `${app['sap.app/id']}`, - value: { - appId: app['sap.app/id'], - title: app['sap.app/title'], - description: app['sap.app/description'], - repoName: app['repoName'], - url: app['url'] - } - })) - : [], - additionalMessages: async () => - appList.length === 0 - ? { - message: t('prompts.appSelection.noAppsDeployed'), - severity: Severity.warning - } - : undefined - } - ]; - - const targetFolderPrompts = getTargetFolderPrompt(appRootPath); - result = [ - ...systemQuestions.prompts, - ...appSelectionPrompt, - targetFolderPrompts - ] as Question[]; - - return result; -} diff --git a/packages/bsp-app-download-sub-generator/src/translations/bsp-app-download-sub-generator.i18n.json b/packages/bsp-app-download-sub-generator/src/translations/bsp-app-download-sub-generator.i18n.json index b96b884c90..4d52c07f37 100644 --- a/packages/bsp-app-download-sub-generator/src/translations/bsp-app-download-sub-generator.i18n.json +++ b/packages/bsp-app-download-sub-generator/src/translations/bsp-app-download-sub-generator.i18n.json @@ -1,7 +1,13 @@ { "error": { "telemetry": "Failed to send telemetry data after downloading app from BSP. {{- error}}", - "noAppsDeployed": "No basic applications deployed to this system can be downloaded. Please see for more details" + "noAppsDeployed": "No basic applications deployed to this system can be downloaded. Please see for more details", + "invalidMetadataPackage": "Invalid or missing package in metadata", + "invalidServiceName": "Invalid or missing serviceName in serviceBindingDetails", + "invalidServiceVersion": "Invalid or missing serviceVersion in serviceBindingDetails", + "invalidMainEntityName": "Invalid or missing mainEntityName in serviceBindingDetails", + "invalidModuleName": "Invalid or missing moduleName in serviceBindingDetails", + "invalidRepositoryName": "Invalid or missing repositoryName in serviceBindingDetails" }, "prompts": { "appSelection": { diff --git a/packages/bsp-app-download-sub-generator/src/utils/app-config.ts b/packages/bsp-app-download-sub-generator/src/utils/app-config.ts deleted file mode 100644 index a99fbb9d0f..0000000000 --- a/packages/bsp-app-download-sub-generator/src/utils/app-config.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { TemplateType, type FioriElementsApp, type LROPSettings } from '@sap-ux/fiori-elements-writer'; -import { type Manifest } from '@sap-ux/project-access'; -import { OdataVersion } from '@sap-ux/odata-service-inquirer'; -import type { AbapServiceProvider, ServiceDocument } from '@sap-ux/axios-extension'; -import type { Logger } from '@sap-ux/logger'; -import type { Editor } from 'mem-fs-editor'; -import { t } from '../utils/i18n'; -import type { BspAppDownloadAnswers } from '../app/types'; -import { readManifest } from './utils'; -import { getLatestUI5Version } from '@sap-ux/ui5-info'; -import { getMinimumUI5Version } from '@sap-ux/project-access'; -import { adtSourceTemplateId } from './constants'; -import { PromptState } from '../prompts/prompt-state'; - -/** - * Retrieves metadata for the provided service URL. - * - * @param {AbapServiceProvider} provider - The ABAP service provider. - * @param {string} serviceUrl - The URL of the service to fetch metadata for. - * @param {Logger} [log] - The logger instance. - * @returns {Promise} - The retrieved metadata. - */ -const fetchMetadata = async (provider: AbapServiceProvider, serviceUrl: string, log?: Logger): Promise => { - try { - return await provider.service(serviceUrl).metadata(); - } catch (err) { - log?.error(`Error fetching metadata: ${err.message}`); - throw err; - } -}; - -// /** -// * Retrieves the main entity name from the manifest routing targets. -// * -// * @param {string} entity - The entity to check for in the manifest. -// * @param {Manifest} manifest - The manifest.json object. -// * @returns {string} - The valid main entity name if found, otherwise throws an error. -// * @throws {Error} - If the routing configuration is invalid or the entity is not found. -// */ -// const getMainEntityName = (entity: string, manifest: Manifest): string => { -// const entityList = `${entity}List`; -// const targetConfig = manifest['sap.ui5']?.routing?.targets?.[entityList]?.options as -// | Record -// | undefined; - -// if (!targetConfig) { -// throw new Error(`Could not find entity name: "${entityList}" in manifest.json!`); -// } -// const { settings } = targetConfig; -// if (!settings) { -// throw new Error(`Invalid or missing 'settings' in navigation source: "${entityList}"`); -// } -// // Extract the contextPath and entitySet from the settings -// const contextPath = settings.contextPath; -// const entitySet = settings.entitySet; -// // Validate the extracted paths -// if (contextPath && contextPath === `/${entity}`) { -// return entity; -// } else if (entitySet && entitySet === entity) { -// return entity; -// } -// throw new Error( -// `Invalid routing configuration for navigation source: "${entityList}". Neither contextPath nor entitySet matches the entity: "${entity}".` -// ); -// }; - -const getEntity = async (provider: AbapServiceProvider, manifest: Manifest): Promise => { - const entities: ServiceDocument = await provider - .service(manifest?.['sap.app']?.dataSources?.mainService.uri ?? '') - .document(); - if (!entities.EntitySets || entities.EntitySets.length === 0) { - throw Error(t('error.entitySetsNotFound')); - } - return entities.EntitySets[0]; -}; - -/** - * Generates the application configuration based on the manifest.json file. - * - * @param {any} answers - The user inputs containing appId. - * @param extractedProjectPath - * @param {Editor} fs - The file system editor. - * @param {Logger} [log] - The logger instance. - * @returns {FioriElementsApp} - The generated application configuration. - */ -export async function getAppConfig( - answers: BspAppDownloadAnswers, - extractedProjectPath: string, - fs: Editor, - log?: Logger -): Promise> { - try { - const { selectedApp } = answers; - const manifest = await readManifest(extractedProjectPath, fs); - const serviceProvider = PromptState.systemSelection?.connectedSystem?.serviceProvider as AbapServiceProvider; - if (!manifest?.['sap.app']?.dataSources) { - throw Error(t('error.dataSourcesNotFound')); - } - - const odataVersion = - manifest?.['sap.app']?.dataSources?.mainService?.settings?.odataVersion === '4.0' - ? OdataVersion.v4 - : OdataVersion.v2; - const metadata = await fetchMetadata(serviceProvider, manifest?.['sap.app']?.dataSources?.mainService.uri, log); - const entity = await getEntity(serviceProvider, manifest); - //const mainEntityName = getMainEntityName(entity, manifest); - const routes = (manifest?.['sap.ui5']?.routing?.routes ?? []) as Array<{ - pattern: string; - name: string; - target: string; - }>; - const mainEntityName = routes.find((route) => route.pattern === ':?query:')?.name; - - const appConfig: FioriElementsApp = { - app: { - id: selectedApp.appId, - title: selectedApp.title, - description: selectedApp.description, - sourceTemplate: { - id: adtSourceTemplateId, - version: manifest?.['sap.app']?.sourceTemplate?.version, - toolsId: manifest?.['sap.app']?.sourceTemplate?.toolsId - }, - projectType: 'EDMXBackend', - flpAppId: `${selectedApp.appId.replace(/[-_.]/g, '')}-tile` // todo: check if flpAppId is correct - }, - package: { - name: selectedApp.appId, - description: selectedApp.description, - devDependencies: {}, - sapuxLayer: 'VENDOR', // todo: add internal feature enabled check, - scripts: {}, - version: manifest?.['sap.app']?.applicationVersion?.version ?? '0.0.1' - }, - template: { - type: TemplateType.ListReportObjectPage, - settings: { - entityConfig: { - mainEntityName: mainEntityName ?? entity ?? '' // todo: add check for v2 service - } - } - }, - service: { - path: manifest?.['sap.app']?.dataSources?.mainService.uri, - version: odataVersion, - metadata, - url: serviceProvider.defaults.baseURL - }, - appOptions: { - addAnnotations: odataVersion === OdataVersion.v4, - addTests: true - }, - ui5: { - version: getMinimumUI5Version(manifest) ?? (await getLatestUI5Version()) - } - }; - return appConfig; - } catch (error) { - log?.error(`Error generating application configuration: ${error.message}`); - throw error; - } -} diff --git a/packages/bsp-app-download-sub-generator/src/utils/constants.ts b/packages/bsp-app-download-sub-generator/src/utils/constants.ts index eaf0812ece..6520bbface 100644 --- a/packages/bsp-app-download-sub-generator/src/utils/constants.ts +++ b/packages/bsp-app-download-sub-generator/src/utils/constants.ts @@ -1,20 +1,41 @@ -export const generatorTitle = 'Fiori App Download from BSP'; -export const generatorDescription = 'Download a Fiori LROP app from a BSP reapository'; -export const extractedFilePath = 'extractedFiles'; +import { PromptNames, type BspAppDownloadAnswers } from '../app/types'; + +// Title and description for the generator +export const generatorTitle = 'Basic App Download from BSP'; +export const generatorDescription = 'Download a basic LROP app from a BSP reapository'; +// Name of the generator used for Fiori app download export const generatorName = '@sap-ux/bsp-app-download-sub-generator'; +// The source template ID used for filtering the apps in the BSP repository export const adtSourceTemplateId = '@sap.adt.sevicebinding.deploy:lrop'; -// filter using source template id +// Default initial answers to use as a fallback. +export const defaultAnswers: BspAppDownloadAnswers = { + [PromptNames.systemSelection]: {}, // Empty or default values for system selection + [PromptNames.selectedApp]: { + appId: '', + title: '', + description: '', + repoName: '', + url: '' + }, + [PromptNames.targetFolder]: '' +}; + +// Path for storing the extracted files from BSP +export const extractedFilePath = 'extractedFiles'; + +// Search parameters to filter applications by the source template ID export const appListSearchParams = { 'sap.app/sourceTemplate/id': adtSourceTemplateId }; +// Fields to retrieve from the app list, useful for displaying app metadata export const appListResultFields = [ - 'sap.app/id', - 'sap.app/title', - 'sap.app/description', - 'sap.app/sourceTemplate/id', - 'repoName', - 'fileType', - 'url' + 'sap.app/id', // ID of the application + 'sap.app/title', // Title of the application + 'sap.app/description', // Description of the application + 'sap.app/sourceTemplate/id', // ID of the source template + 'repoName', // Repository name where the app is located + 'fileType', // Type of file (.zip etc) + 'url' // URL for accessing the app ]; diff --git a/packages/bsp-app-download-sub-generator/src/utils/eventHook.ts b/packages/bsp-app-download-sub-generator/src/utils/eventHook.ts deleted file mode 100644 index fa3699c1d9..0000000000 --- a/packages/bsp-app-download-sub-generator/src/utils/eventHook.ts +++ /dev/null @@ -1,25 +0,0 @@ -import ReuseLibGenLogger from './logger'; -import { t } from './i18n'; -import type { VSCodeInstance } from '@sap-ux/fiori-generator-shared'; - -export interface BspAppGenContext { - path: string; - postGenCommand: string; - vscodeInstance?: VSCodeInstance; -} -export const POST_LIB_GEN_COMMAND = 'sap.ux.library.generated.handler'; - -/** - * Executes post app generation command. - * - * @param context BspAppGenContext - */ -export async function runPostAppGenHook(context: BspAppGenContext): Promise { - try { - await context.vscodeInstance?.commands?.executeCommand?.(context.postGenCommand, { - fsPath: context.path - }); - } catch (e) { - ReuseLibGenLogger.logger.error(t('error.postLibGenHook', { error: e })); - } -} diff --git a/packages/bsp-app-download-sub-generator/src/utils/utils.ts b/packages/bsp-app-download-sub-generator/src/utils/utils.ts deleted file mode 100644 index 39698d3518..0000000000 --- a/packages/bsp-app-download-sub-generator/src/utils/utils.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { - generatorTitle, - generatorDescription, - appListSearchParams, - appListResultFields, - adtSourceTemplateId -} from './constants'; -import type { AbapServiceProvider, AppIndex } from '@sap-ux/axios-extension'; -import AdmZip from 'adm-zip'; -import type { Logger } from '@sap-ux/logger'; -import { join } from 'path'; -import type { Editor } from 'mem-fs-editor'; -import type { BspAppDownloadAnswers } from '../app/types'; -import { FileName, type Manifest } from '@sap-ux/project-access'; -import { t } from './i18n'; -import { PromptState } from '../prompts/prompt-state'; - -/** - * Returns the details for the YUI prompt. - * - * @returns step details - */ -export function getYUIDetails(): { name: string; description: string }[] { - return [ - { - name: generatorTitle, - description: generatorDescription - } - ]; -} - -/** - * Retrieves a list of deployed applications from abap respository. - * - * @param {AbapServiceProvider} provider - The ABAP service provider. - * @param {Logger} log - The logger instance. - * @returns {Promise>} - List of applications filtered by source template. - */ -export async function getAppList(provider: AbapServiceProvider, log?: Logger): Promise { - try { - const appIndexService = provider.getAppIndex(); - return await appIndexService.search(appListSearchParams, appListResultFields); - } catch (error) { - log?.error(`Error fetching application list: ${error.message}`); - return []; - } -} - -/** - * Extracts ZIP archive to a temporary directory. - * - * @param extractedProjectPath - * @param {Buffer} archive - The ZIP archive buffer. - * @param fs - * @param {Logger} log - The logger instance. - */ -async function extractZip(extractedProjectPath: string, archive: Buffer, fs: Editor, log?: Logger): Promise { - try { - const zip = new AdmZip(archive); - //zip.extractAllTo(join(fioriToolsExtractionPath, appId), true); - const zipEntries = zip.getEntries(); // an array of ZipEntry records - zipEntries.forEach(function (zipEntry) { - if (!zipEntry.isDirectory) { - // Extract the file content - const fileContent = zipEntry.getData().toString('utf8'); - // Add the file content to mem-fs at a virtual path - fs.write(join(extractedProjectPath, zipEntry.entryName), fileContent); - } - }); - } catch (error) { - log?.error(`Error extracting zip: ${error.message}`); - } -} - -/** - * Downloads application files from the server. - * - * @param {AbapServiceProvider} provider - The ABAP service provider. - * @param answers - * @param extractedProjectPath - * @param fs - * @param {Logger} log - The logger instance. - */ -export async function downloadApp( - answers: BspAppDownloadAnswers, - extractedProjectPath: string, - fs: Editor, - log?: Logger -): Promise { - try { - const { selectedApp } = answers; - const ui5AbapRepositoryService = ( - PromptState.systemSelection?.connectedSystem?.serviceProvider as AbapServiceProvider - ).getUi5AbapRepository(); - const archive = await ui5AbapRepositoryService.downloadFiles(selectedApp.repoName); - - if (Buffer.isBuffer(archive)) { - await extractZip(extractedProjectPath, archive, fs, log); - } else { - log?.error('Error: The downloaded file is not a Buffer.'); - } - } catch (error) { - throw Error(`Error downloading file: ${error.message}`); - } -} - -/** - * Reads and validates the manifest.json file. - * - * @param {string} extractedProjectPath - The path to the extracted project. - * @param {Editor} fs - The file system editor. - * @returns {Promise} - The manifest object. - */ -export async function readManifest(extractedProjectPath: string, fs: Editor): Promise { - const manifestPath = join(extractedProjectPath, FileName.Manifest); - const manifest = fs.readJSON(manifestPath) as unknown as Manifest; - if (!manifest) { - throw Error(t('error.manifestNotFound')); - } - if (!manifest['sap.app']) { - throw Error(t('error.sapAppNotDefined')); - } - if (manifest['sap.app'].sourceTemplate?.id !== adtSourceTemplateId) { - throw Error(t('error.sourceTemplateNotSupported')); - } - return manifest; -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 31e961180b..f68f8b48bd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -969,6 +969,9 @@ importers: inquirer: specifier: 8.2.6 version: 8.2.6 + inquirer-autocomplete-prompt: + specifier: 2.0.1 + version: 2.0.1(inquirer@8.2.6) yeoman-generator: specifier: 5.10.0 version: 5.10.0(mem-fs@2.1.0)(yeoman-environment@3.19.3) @@ -976,21 +979,18 @@ importers: '@jest/types': specifier: 29.6.3 version: 29.6.3 - '@sap-ux/axios-extension': - specifier: workspace:* - version: link:../axios-extension '@sap-ux/nodejs-utils': specifier: workspace:* version: link:../nodejs-utils - '@sap-ux/store': - specifier: workspace:* - version: link:../store '@types/adm-zip': specifier: 0.5.5 version: 0.5.5 '@types/inquirer': specifier: 8.2.6 version: 8.2.6 + '@types/inquirer-autocomplete-prompt': + specifier: 2.0.1 + version: 2.0.1 '@types/lodash': specifier: 4.14.202 version: 4.14.202 From 5e83c1b8b202b71bc84d9b3100011d5af9e92406 Mon Sep 17 00:00:00 2001 From: I743583 Date: Thu, 27 Mar 2025 15:21:26 +0000 Subject: [PATCH 06/41] fix launch config issue --- .vscode/launch.json | 2 +- .../src/app/index.ts | 45 ++++++++++++------- .../bsp-app-download-sub-generator.i18n.json | 9 +++- packages/launch-config/src/index.ts | 2 +- .../src/launch-config-crud/create.ts | 7 +-- packages/launch-config/src/types/types.ts | 1 + pnpm-lock.yaml | 5 +++ 7 files changed, 50 insertions(+), 21 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 8c7761c1da..dbb54849f6 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -443,6 +443,6 @@ "stopOnEntry": true, "console": "integratedTerminal", "internalConsoleOptions": "neverOpen" - }, + } ] } diff --git a/packages/bsp-app-download-sub-generator/src/app/index.ts b/packages/bsp-app-download-sub-generator/src/app/index.ts index bfc82b80e7..d80e37771b 100644 --- a/packages/bsp-app-download-sub-generator/src/app/index.ts +++ b/packages/bsp-app-download-sub-generator/src/app/index.ts @@ -12,15 +12,15 @@ import { EventName } from '../telemetryEvents'; import type { YeomanEnvironment, VSCodeInstance } from '@sap-ux/fiori-generator-shared'; import { getDefaultTargetFolder } from '@sap-ux/fiori-generator-shared'; import type { BspAppDownloadOptions, BspAppDownloadAnswers, BspAppDownloadQuestions, AppContentConfig } from './types'; -import { getQuestions } from '../prompts/questions'; +import { getPrompts } from '../prompts/prompts'; import { generate, TemplateType, type FioriElementsApp, type LROPSettings } from '@sap-ux/fiori-elements-writer'; -import { join } from 'path'; +import { join, basename, dirname } from 'path'; import { platform } from 'os'; import { generateReadMe, type ReadMe } from '@sap-ux/fiori-generator-shared'; import { runPostAppGenHook } from '../utils/event-hook'; import { getDefaultUI5Theme } from '@sap-ux/ui5-info'; import type { DebugOptions, FioriOptions } from '@sap-ux/launch-config'; -import { createLaunchConfig } from '@sap-ux/launch-config'; +import { createLaunchConfig, updateWorkspaceFoldersIfNeeded } from '@sap-ux/launch-config'; import { isAppStudio } from '@sap-ux/btp-utils'; import { OdataVersion } from '@sap-ux/odata-service-inquirer'; import { writeApplicationInfoSettings } from '@sap-ux/fiori-tools-settings'; @@ -29,6 +29,7 @@ import { PromptState } from '../prompts/prompt-state'; import { PromptNames } from './types'; import { getAbapDeployConfig, getAppConfig, replaceWebappFiles } from './config'; import type { AbapDeployConfig } from '@sap-ux/ui5-config'; +import { sampleAppContentJson } from './example-app-content'; /** * Generator class for downloading a basic app from BSP repository. @@ -36,7 +37,7 @@ import type { AbapDeployConfig } from '@sap-ux/ui5-config'; */ export default class extends Generator { private readonly appWizard: AppWizard; - private readonly vscode?: VSCodeInstance; + private readonly vscode?: any;//VSCodeInstance; //confirm this private readonly launchAppDownloaderAsSubGenerator: boolean; private readonly appRootPath: string; private readonly prompts: Prompts; @@ -109,7 +110,7 @@ export default class extends Generator { * Prompts the user for application details and downloads the app. */ public async prompting(): Promise { - const questions: BspAppDownloadQuestions[] = await getQuestions(this.appRootPath); + const questions: BspAppDownloadQuestions[] = await getPrompts(this.appRootPath); const { selectedApp, targetFolder } = (await this.prompt(questions)) as BspAppDownloadAnswers; if (PromptState.systemSelection.connectedSystem?.serviceProvider && selectedApp?.appId && targetFolder) { this.answers.selectedApp = selectedApp; @@ -132,7 +133,14 @@ export default class extends Generator { * Writes the configuration files for the project, including deployment config, and README. */ public async writing(): Promise { - const appContentJson = this.fs.readJSON(join(__dirname, 'example-app-content.json')) as unknown as AppContentConfig; //todo: extract from extracted path + // const appContentJsonTempPath = join(__dirname, 'example-app-content.json'); + let appContentJson: AppContentConfig = sampleAppContentJson; + // todo: add back once json is available along with downloaded app + // if(!this.fs.exists(appContentJsonTempPath)) { + // appContentJson = this.fs.readJSON(appContentJsonTempPath) as unknown as AppContentConfig; //todo: extract from extracted path + // } else { + // throw new Error(t('error.appContentJsonNotFound', { jsonFileName: 'example-app-content.json' })); + // } // Generate project files const config = await getAppConfig( @@ -162,7 +170,14 @@ export default class extends Generator { writeApplicationInfoSettings(this.projectPath); // Replace webapp files with downloaded app files - replaceWebappFiles(this.projectPath, this.extractedProjectPath, this.fs); + //replaceWebappFiles(this.projectPath, this.extractedProjectPath, this.fs); + // Create launch configuration + await createLaunchConfig( + this.projectPath, + this.fioriOptions, + this.fs, + BspAppDownloadLogger.logger as unknown as Logger + ); } /** @@ -207,6 +222,7 @@ export default class extends Generator { const fioriOptions: FioriOptions = { name: config.app.id, projectRoot: this.projectPath, + skipVsCodeRefresh: true, debugOptions }; return fioriOptions; @@ -257,14 +273,13 @@ export default class extends Generator { ).catch((error) => { BspAppDownloadLogger.logger.error(t('error.telemetry', { error })); }); - - // Create launch configuration - await createLaunchConfig( - this.projectPath, - this.fioriOptions, - this.fs, - BspAppDownloadLogger.logger as unknown as Logger - ); + const test = { + uri: this.vscode?.Uri?.file(join(dirname(this.projectPath))), + projectName: basename(this.projectPath), + vscode: this.vscode + } + debugger; + updateWorkspaceFoldersIfNeeded() // Clean up extracted project files // this.fs.delete(this.extractedProjectPath); diff --git a/packages/bsp-app-download-sub-generator/src/translations/bsp-app-download-sub-generator.i18n.json b/packages/bsp-app-download-sub-generator/src/translations/bsp-app-download-sub-generator.i18n.json index 4d52c07f37..004c4b13e0 100644 --- a/packages/bsp-app-download-sub-generator/src/translations/bsp-app-download-sub-generator.i18n.json +++ b/packages/bsp-app-download-sub-generator/src/translations/bsp-app-download-sub-generator.i18n.json @@ -7,7 +7,14 @@ "invalidServiceVersion": "Invalid or missing serviceVersion in serviceBindingDetails", "invalidMainEntityName": "Invalid or missing mainEntityName in serviceBindingDetails", "invalidModuleName": "Invalid or missing moduleName in serviceBindingDetails", - "invalidRepositoryName": "Invalid or missing repositoryName in serviceBindingDetails" + "invalidRepositoryName": "Invalid or missing repositoryName in serviceBindingDetails", + "appContentJsonNotFound": "{{- jsonFileName }} not found in the downloaded app", + "npmInstall": "Error in install phase: {{- error}}", + "skippedInstallation": "Option `--skipInstall` was specified. Installation of dependencies will be skipped.", + "replaceWebappFilesError": "Error replacing files in the downloaded app: {{- error}}" + }, + "warn": { + "extractedFileNotFound": "Extracted file not found - {{- extractedFilePath}}" }, "prompts": { "appSelection": { diff --git a/packages/launch-config/src/index.ts b/packages/launch-config/src/index.ts index fae4f11272..eced01af32 100644 --- a/packages/launch-config/src/index.ts +++ b/packages/launch-config/src/index.ts @@ -1,5 +1,5 @@ export * from './types'; -export { createLaunchConfig } from './launch-config-crud/create'; +export { createLaunchConfig, updateWorkspaceFoldersIfNeeded } from './launch-config-crud/create'; export { deleteLaunchConfig } from './launch-config-crud/delete'; export { convertOldLaunchConfigToFioriRun } from './launch-config-crud/modify'; export { getLaunchConfigs, getLaunchConfigByName } from './launch-config-crud/read'; diff --git a/packages/launch-config/src/launch-config-crud/create.ts b/packages/launch-config/src/launch-config-crud/create.ts index 93f45c5a1b..29cb87f3e0 100644 --- a/packages/launch-config/src/launch-config-crud/create.ts +++ b/packages/launch-config/src/launch-config-crud/create.ts @@ -106,7 +106,7 @@ async function handleExistingLaunchJson( * * @param {UpdateWorkspaceFolderOptions} updateWorkspaceFolders - The options for updating workspace folders. */ -function updateWorkspaceFoldersIfNeeded(updateWorkspaceFolders?: UpdateWorkspaceFolderOptions): void { +export function updateWorkspaceFoldersIfNeeded(updateWorkspaceFolders?: UpdateWorkspaceFolderOptions): void { if (updateWorkspaceFolders) { const { uri, vscode, projectName } = updateWorkspaceFolders; if (uri && vscode) { @@ -136,6 +136,7 @@ async function handleDebugOptions( rootFolder: string, fs: Editor, debugOptions: DebugOptions, + skipVsCodeRefresh: boolean = false, logger?: Logger ): Promise { const { launchJsonPath, workspaceFolderUri, cwd, appNotInWorkspace } = handleWorkspaceConfig( @@ -171,7 +172,7 @@ async function handleDebugOptions( } as UpdateWorkspaceFolderOptions) : undefined; - updateWorkspaceFoldersIfNeeded(updateWorkspaceFolders); + if (!skipVsCodeRefresh) updateWorkspaceFoldersIfNeeded(updateWorkspaceFolders); return fs; } @@ -198,5 +199,5 @@ export async function createLaunchConfig( if (!debugOptions.vscode) { return fs; } - return await handleDebugOptions(rootFolder, fs, debugOptions, logger); + return await handleDebugOptions(rootFolder, fs, debugOptions, fioriOptions.skipVsCodeRefresh, logger); } diff --git a/packages/launch-config/src/types/types.ts b/packages/launch-config/src/types/types.ts index b0e2ad5587..3d0243a300 100644 --- a/packages/launch-config/src/types/types.ts +++ b/packages/launch-config/src/types/types.ts @@ -21,6 +21,7 @@ export interface FioriOptions { urlParameters?: string; visible?: boolean; debugOptions?: DebugOptions; + skipVsCodeRefresh?: boolean; } export interface LaunchJSON { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ebc824d157..c90be2be86 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13344,6 +13344,11 @@ packages: which: 2.0.2 dev: true + /crypto-random-string@2.0.0: + resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} + engines: {node: '>=8'} + dev: true + /crypto-random-string@4.0.0: resolution: {integrity: sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==} engines: {node: '>=12'} From 508c9cb5b81c912d3e65ba18ceed33f77ac31ae4 Mon Sep 17 00:00:00 2001 From: I743583 Date: Thu, 27 Mar 2025 16:11:57 +0000 Subject: [PATCH 07/41] export updateWorkspaceFoldersIfNeeded from launch config --- packages/launch-config/src/index.ts | 2 +- .../src/launch-config-crud/create.ts | 28 +++++++++++-------- packages/launch-config/src/types/types.ts | 12 ++++++++ 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/packages/launch-config/src/index.ts b/packages/launch-config/src/index.ts index fae4f11272..eced01af32 100644 --- a/packages/launch-config/src/index.ts +++ b/packages/launch-config/src/index.ts @@ -1,5 +1,5 @@ export * from './types'; -export { createLaunchConfig } from './launch-config-crud/create'; +export { createLaunchConfig, updateWorkspaceFoldersIfNeeded } from './launch-config-crud/create'; export { deleteLaunchConfig } from './launch-config-crud/delete'; export { convertOldLaunchConfigToFioriRun } from './launch-config-crud/modify'; export { getLaunchConfigs, getLaunchConfigByName } from './launch-config-crud/read'; diff --git a/packages/launch-config/src/launch-config-crud/create.ts b/packages/launch-config/src/launch-config-crud/create.ts index 4147d6c03f..a0b649e159 100644 --- a/packages/launch-config/src/launch-config-crud/create.ts +++ b/packages/launch-config/src/launch-config-crud/create.ts @@ -106,7 +106,7 @@ async function handleExistingLaunchJson( * * @param {UpdateWorkspaceFolderOptions} updateWorkspaceFolders - The options for updating workspace folders. */ -function updateWorkspaceFoldersIfNeeded(updateWorkspaceFolders?: UpdateWorkspaceFolderOptions): void { +export function updateWorkspaceFoldersIfNeeded(updateWorkspaceFolders?: UpdateWorkspaceFolderOptions): void { if (updateWorkspaceFolders) { const { uri, vscode, projectName } = updateWorkspaceFolders; if (uri && vscode) { @@ -128,6 +128,7 @@ function updateWorkspaceFoldersIfNeeded(updateWorkspaceFolders?: UpdateWorkspace * @param {Editor} fs - The file system editor to read and write the `launch.json` file. * @param {DebugOptions} debugOptions - Debug configuration options that dictate how the `launch.json` * should be generated and what commands should be logged. + * @param {boolean} enableVSCodeReload - A flag indicating whether the workspace should be reloaded in VS Code. * @param {Logger} logger - Logger instance for logging information or warnings. * @returns {Promise} - Returns the file system editor after potentially modifying the workspace * and updating or creating the `launch.json` file. @@ -136,6 +137,7 @@ async function handleDebugOptions( rootFolder: string, fs: Editor, debugOptions: DebugOptions, + enableVSCodeReload: boolean = true, logger?: Logger ): Promise { const { launchJsonPath, workspaceFolderUri, cwd, appNotInWorkspace } = handleWorkspaceConfig( @@ -159,18 +161,20 @@ async function handleDebugOptions( writeLaunchJsonFile(fs, launchJsonWritePath, configurations); } - // The `workspaceFolderUri` is a URI obtained from VS Code that specifies the path to the workspace folder. - // This URI is populated when a reload of the workspace is required. It allows us to identify and update - // the workspace folder correctly within VS Code. - const updateWorkspaceFolders = workspaceFolderUri + // Conditionally update workspace folders based on the enableVSCodeReload flag + if (enableVSCodeReload) { + // The `workspaceFolderUri` is a URI obtained from VS Code that specifies the path to the workspace folder. + // This URI is populated when a reload of the workspace is required. It allows us to identify and update + // the workspace folder correctly within VS Code. + const updateWorkspaceFolders = workspaceFolderUri ? ({ - uri: workspaceFolderUri, - projectName: basename(rootFolder), - vscode: debugOptions.vscode - } as UpdateWorkspaceFolderOptions) + uri: workspaceFolderUri, + projectName: basename(rootFolder), + vscode: debugOptions.vscode + } as UpdateWorkspaceFolderOptions) : undefined; - - updateWorkspaceFoldersIfNeeded(updateWorkspaceFolders); + updateWorkspaceFoldersIfNeeded(updateWorkspaceFolders); + } return fs; } @@ -197,5 +201,5 @@ export async function createLaunchConfig( if (!debugOptions.vscode) { return fs; } - return await handleDebugOptions(rootFolder, fs, debugOptions, logger); + return await handleDebugOptions(rootFolder, fs, debugOptions, fioriOptions.enableVSCodeReload, logger); } diff --git a/packages/launch-config/src/types/types.ts b/packages/launch-config/src/types/types.ts index b0e2ad5587..0608d55907 100644 --- a/packages/launch-config/src/types/types.ts +++ b/packages/launch-config/src/types/types.ts @@ -21,6 +21,18 @@ export interface FioriOptions { urlParameters?: string; visible?: boolean; debugOptions?: DebugOptions; + /** + * Controls whether VS Code should reload when updating workspace folders. + * + * VS Code automatically reloads when workspace folders change and no files are open. + * This flag allows you to override that behavior. + * + * - true (default) - Triggers a VS Code reload when updating workspace folders and no files are open. + * - false - Prevents automatic reloads. + * + * Use this flag to dynamically manage workspace modifications. + */ + enableVSCodeReload?: boolean; } export interface LaunchJSON { From f25c22b8c6a5ab2a57efac5d4077a90abdf965d2 Mon Sep 17 00:00:00 2001 From: I743583 Date: Thu, 27 Mar 2025 16:35:20 +0000 Subject: [PATCH 08/41] vscode trigger fix --- .../src/app/index.ts | 12 +++--- pnpm-lock.yaml | 41 +++++++++++++++++++ 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/packages/bsp-app-download-sub-generator/src/app/index.ts b/packages/bsp-app-download-sub-generator/src/app/index.ts index d80e37771b..4977fa4760 100644 --- a/packages/bsp-app-download-sub-generator/src/app/index.ts +++ b/packages/bsp-app-download-sub-generator/src/app/index.ts @@ -167,8 +167,7 @@ export default class extends Generator { // Generate Fiori launch config this.fioriOptions = this._getLaunchConfig(config); - writeApplicationInfoSettings(this.projectPath); - + debugger; // Replace webapp files with downloaded app files //replaceWebappFiles(this.projectPath, this.extractedProjectPath, this.fs); // Create launch configuration @@ -178,6 +177,7 @@ export default class extends Generator { this.fs, BspAppDownloadLogger.logger as unknown as Logger ); + writeApplicationInfoSettings(this.projectPath, this.fs); } /** @@ -222,7 +222,7 @@ export default class extends Generator { const fioriOptions: FioriOptions = { name: config.app.id, projectRoot: this.projectPath, - skipVsCodeRefresh: true, + enableVSCodeReload: false, debugOptions }; return fioriOptions; @@ -273,13 +273,13 @@ export default class extends Generator { ).catch((error) => { BspAppDownloadLogger.logger.error(t('error.telemetry', { error })); }); - const test = { - uri: this.vscode?.Uri?.file(join(dirname(this.projectPath))), + const updateWorkspaceFolders = { + uri: this.vscode?.Uri?.file(join(this.projectPath)), projectName: basename(this.projectPath), vscode: this.vscode } debugger; - updateWorkspaceFoldersIfNeeded() + updateWorkspaceFoldersIfNeeded(updateWorkspaceFolders) // Clean up extracted project files // this.fs.delete(this.extractedProjectPath); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0404b2c08e..9ef25acde4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15611,6 +15611,13 @@ packages: resolution: {integrity: sha512-epNL4mnl3HUYrwVQtZ8s0nxkE4ogAoSqO1V1fa670Ww1fXp8Yr74zNS9Aib/vLNf0rq0AF/4mboo7ev5XkikXQ==} dev: true + /find-versions@3.2.0: + resolution: {integrity: sha512-P8WRou2S+oe222TOCHitLy8zj+SIsVJh52VP4lvXkaFVnOFFdoWv1H1Jjvel1aI6NCFOAaeAVm8qrI0odiLcww==} + engines: {node: '>=6'} + dependencies: + semver-regex: 2.0.0 + dev: true + /find-yarn-workspace-root2@1.2.16: resolution: {integrity: sha512-hr6hb1w8ePMpPVUK39S4RlwJzi+xPLuVuG8XlwXU3KD5Yn3qgBWVfy3AzNlDhWvE1EORCE65/Qm26rFQt3VLVA==} dependencies: @@ -17344,6 +17351,11 @@ packages: engines: {node: '>=0.10.0'} dev: true + /is-redirect@1.0.0: + resolution: {integrity: sha512-cr/SlUEe5zOGmzvj9bUyC4LVvkNVAXu4GytXLNMr1pny+a65MpQ9IJzFHD5vi7FyJgb4qt27+eS3TuQnqB+RQw==} + engines: {node: '>=0.10.0'} + dev: true + /is-reference@1.2.1: resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} dependencies: @@ -17603,6 +17615,14 @@ packages: istanbul-lib-report: 3.0.1 dev: true + /isurl@1.0.0: + resolution: {integrity: sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w==} + engines: {node: '>= 4'} + dependencies: + has-to-string-tag-x: 1.4.1 + is-object: 1.0.2 + dev: true + /iterate-object@1.3.5: resolution: {integrity: sha512-eL23u8oFooYTq6TtJKjp2RYjZnCkUYQvC0T/6fJfWykXJ3quvdDdzKZ3CEjy8b3JGOvLTjDYMEMIp5243R906A==} dev: true @@ -23431,6 +23451,27 @@ packages: strip-ansi: 6.0.1 dev: true + /tabtab@1.3.2: + resolution: {integrity: sha512-qHWOJ5g7lrpftZMyPv3ZaYZs7PuUTKWEP/TakZHfpq66bSwH25SQXn5616CCh6Hf/1iPcgQJQHGcJkzQuATabQ==} + hasBin: true + dependencies: + debug: 2.6.9 + inquirer: 1.2.3 + minimist: 1.2.8 + mkdirp: 0.5.6 + npmlog: 2.0.4 + object-assign: 4.1.1 + transitivePeerDependencies: + - supports-color + dev: true + + /taketalk@1.0.0: + resolution: {integrity: sha512-kS7E53It6HA8S1FVFBWP7HDwgTiJtkmYk7TsowGlizzVrivR1Mf9mgjXHY1k7rOfozRVMZSfwjB3bevO4QEqpg==} + dependencies: + get-stdin: 4.0.1 + minimist: 1.2.8 + dev: true + /tapable@2.2.1: resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} engines: {node: '>=6'} From 565b5bfe86007e0f097bd4c8a2bf94abec889a2a Mon Sep 17 00:00:00 2001 From: I743583 Date: Fri, 28 Mar 2025 10:14:28 +0000 Subject: [PATCH 09/41] Add enableVSCodeReload flag to control VS Code reload on workspace update --- .changeset/slow-beers-drum.md | 5 ++ .../src/launch-config-crud/create.ts | 22 +++-- packages/launch-config/src/types/types.ts | 6 +- .../test/debug-config/config.test.ts | 80 ++++++++++++++++++- 4 files changed, 101 insertions(+), 12 deletions(-) create mode 100644 .changeset/slow-beers-drum.md diff --git a/.changeset/slow-beers-drum.md b/.changeset/slow-beers-drum.md new file mode 100644 index 0000000000..37023db01c --- /dev/null +++ b/.changeset/slow-beers-drum.md @@ -0,0 +1,5 @@ +--- +'@sap-ux/launch-config': minor +--- + +Add enableVSCodeReload flag to control VS Code reload on workspace update diff --git a/packages/launch-config/src/launch-config-crud/create.ts b/packages/launch-config/src/launch-config-crud/create.ts index a0b649e159..3fa17b4efe 100644 --- a/packages/launch-config/src/launch-config-crud/create.ts +++ b/packages/launch-config/src/launch-config-crud/create.ts @@ -102,9 +102,15 @@ async function handleExistingLaunchJson( } /** - * Updates the workspace folders in VSCode if the update options are provided. + * Updates the workspace folders in VS Code if update options are provided. * - * @param {UpdateWorkspaceFolderOptions} updateWorkspaceFolders - The options for updating workspace folders. + * This function checks if updateWorkspaceFolders is defined and contains valid data. + * If a valid uri and vscode instance are provided, it adds the specified workspace folder to VS Code. + * + * @param {UpdateWorkspaceFolderOptions} [updateWorkspaceFolders] - The options for updating workspace folders. + * @param {Uri} updateWorkspaceFolders.uri - The URI of the workspace folder to be added. + * @param {typeof vscode} updateWorkspaceFolders.vscode - The VS Code instance used for updating the workspace. + * @param {string} updateWorkspaceFolders.projectName - The name of the workspace folder to be added. */ export function updateWorkspaceFoldersIfNeeded(updateWorkspaceFolders?: UpdateWorkspaceFolderOptions): void { if (updateWorkspaceFolders) { @@ -167,12 +173,12 @@ async function handleDebugOptions( // This URI is populated when a reload of the workspace is required. It allows us to identify and update // the workspace folder correctly within VS Code. const updateWorkspaceFolders = workspaceFolderUri - ? ({ - uri: workspaceFolderUri, - projectName: basename(rootFolder), - vscode: debugOptions.vscode - } as UpdateWorkspaceFolderOptions) - : undefined; + ? ({ + uri: workspaceFolderUri, + projectName: basename(rootFolder), + vscode: debugOptions.vscode + } as UpdateWorkspaceFolderOptions) + : undefined; updateWorkspaceFoldersIfNeeded(updateWorkspaceFolders); } return fs; diff --git a/packages/launch-config/src/types/types.ts b/packages/launch-config/src/types/types.ts index 0608d55907..044c22eb7a 100644 --- a/packages/launch-config/src/types/types.ts +++ b/packages/launch-config/src/types/types.ts @@ -23,13 +23,13 @@ export interface FioriOptions { debugOptions?: DebugOptions; /** * Controls whether VS Code should reload when updating workspace folders. - * + * * VS Code automatically reloads when workspace folders change and no files are open. * This flag allows you to override that behavior. - * + * * - true (default) - Triggers a VS Code reload when updating workspace folders and no files are open. * - false - Prevents automatic reloads. - * + * * Use this flag to dynamically manage workspace modifications. */ enableVSCodeReload?: boolean; diff --git a/packages/launch-config/test/debug-config/config.test.ts b/packages/launch-config/test/debug-config/config.test.ts index ba36b6e0d5..8176798d4f 100644 --- a/packages/launch-config/test/debug-config/config.test.ts +++ b/packages/launch-config/test/debug-config/config.test.ts @@ -1,7 +1,25 @@ import { configureLaunchJsonFile } from '../../src/debug-config/config'; -import type { DebugOptions, LaunchConfig, LaunchJSON } from '../../src/types'; +import type { DebugOptions, LaunchConfig, LaunchJSON, FioriOptions } from '../../src/types'; import path from 'path'; import { FIORI_TOOLS_LAUNCH_CONFIG_HANDLER_ID } from '../../src/types'; +import { join } from 'path'; +import { create as createStorage } from 'mem-fs'; +import { create } from 'mem-fs-editor'; +import { FileName } from '@sap-ux/project-access'; +import { TestPaths } from '../test-data/utils'; +import { handleWorkspaceConfig } from '../../src/debug-config/workspaceManager'; +import * as launchConfig from '../../src/launch-config-crud/create'; + +// Mock workspaceManager +jest.mock('../../src/debug-config/workspaceManager', () => ({ + ...jest.requireActual('../../src/debug-config/workspaceManager'), + handleWorkspaceConfig: jest.fn() +})); + +jest.mock('../../src/launch-config-crud/create', () => ({ + ...jest.requireActual('../../src/launch-config-crud/create'), + updateWorkspaceFoldersIfNeeded: jest.fn() +})); const projectName = 'project1'; const cwd = `\${workspaceFolder}`; @@ -184,3 +202,63 @@ describe('debug config tests', () => { expect(localConfigWithRunConfig).toEqual(findConfiguration(launchFile, `Start ${projectName} Local`)); }); }); + +describe('create', () => { + const memFs = create(createStorage()); + const memFilePath = join(TestPaths.tmpDir, 'fe-projects', FileName.Package); + const memFileContent = '{}\n'; + let mockVSCode: any; + let launchJSONPath: string; + let fioriOptions: FioriOptions; + + afterEach(async () => { + memFs.delete(memFilePath); + }); + + beforeEach(() => { + memFs.writeJSON(memFilePath, memFileContent); + mockVSCode = { + workspace: { + workspaceFolders: [], + updateWorkspaceFolders: jest.fn() + } + }; + launchJSONPath = join(TestPaths.tmpDir, '.vscode', 'launch.json'); + (handleWorkspaceConfig as jest.Mock).mockReturnValue({ + launchJsonPath: launchJSONPath, + workspaceFolderUri: launchJSONPath, + cwd: TestPaths.tmpDir, + appNotInWorkspace: true + }); + fioriOptions = { + name: 'launch-config-test', + projectRoot: join(TestPaths.tmpDir, 'fe-projects'), + debugOptions: { + vscode: mockVSCode + } as DebugOptions + }; + }); + + test('should call updateWorkspaceFolders if enableVSCodeReload is true', async () => { + await launchConfig.createLaunchConfig( + TestPaths.tmpDir, + { ...fioriOptions, enableVSCodeReload: true }, // reload enabled + memFs + ); + expect(mockVSCode.workspace.updateWorkspaceFolders).toHaveBeenCalledTimes(1); + }); + + test('should not call updateWorkspaceFolders if enableVSCodeReload is false', async () => { + await launchConfig.createLaunchConfig( + TestPaths.tmpDir, + { ...fioriOptions, enableVSCodeReload: false }, // reload disabled, + memFs + ); + expect(mockVSCode.workspace.updateWorkspaceFolders).not.toHaveBeenCalled(); + }); + + test('should call updateWorkspaceFolders if enableVSCodeReload is not provided (defaults to true)', async () => { + await launchConfig.createLaunchConfig(TestPaths.tmpDir, fioriOptions, memFs); + expect(mockVSCode.workspace.updateWorkspaceFolders).toHaveBeenCalledTimes(1); + }); +}); From e3ae8a4475034399702180520c51214a481a9d73 Mon Sep 17 00:00:00 2001 From: I743583 Date: Fri, 28 Mar 2025 12:18:54 +0000 Subject: [PATCH 10/41] launch config documentation --- .../src/app/config.ts | 207 ++++++++++++++++++ .../src/app/example-app-content.ts | 28 +++ .../src/app/index.ts | 91 +++++--- .../src/prompts/prompt-helpers.ts | 93 ++++++++ .../src/prompts/prompts.ts | 78 +++++++ .../src/utils/download-utils.ts | 59 +++++ .../src/utils/event-hook.ts | 41 ++++ .../src/utils/file-helpers.ts | 28 +++ .../src/utils/validate-app-content-json.ts | 90 ++++++++ .../src/launch-config-crud/create.ts | 1 - 10 files changed, 684 insertions(+), 32 deletions(-) create mode 100644 packages/bsp-app-download-sub-generator/src/app/config.ts create mode 100644 packages/bsp-app-download-sub-generator/src/app/example-app-content.ts create mode 100644 packages/bsp-app-download-sub-generator/src/prompts/prompt-helpers.ts create mode 100644 packages/bsp-app-download-sub-generator/src/prompts/prompts.ts create mode 100644 packages/bsp-app-download-sub-generator/src/utils/download-utils.ts create mode 100644 packages/bsp-app-download-sub-generator/src/utils/event-hook.ts create mode 100644 packages/bsp-app-download-sub-generator/src/utils/file-helpers.ts create mode 100644 packages/bsp-app-download-sub-generator/src/utils/validate-app-content-json.ts diff --git a/packages/bsp-app-download-sub-generator/src/app/config.ts b/packages/bsp-app-download-sub-generator/src/app/config.ts new file mode 100644 index 0000000000..ea29e3dc45 --- /dev/null +++ b/packages/bsp-app-download-sub-generator/src/app/config.ts @@ -0,0 +1,207 @@ +import { TemplateType, type FioriElementsApp, type LROPSettings } from '@sap-ux/fiori-elements-writer'; +import { OdataVersion } from '@sap-ux/odata-service-inquirer'; +import type { AbapServiceProvider } from '@sap-ux/axios-extension'; +import type { Logger } from '@sap-ux/logger'; +import type { Editor } from 'mem-fs-editor'; +import { t } from '../utils/i18n'; +import type { AppInfo, AppContentConfig, EntityConfig } from '../app/types'; +import { readManifest } from '../utils/file-helpers'; +import { getLatestUI5Version } from '@sap-ux/ui5-info'; +import { getMinimumUI5Version } from '@sap-ux/project-access'; +import { adtSourceTemplateId } from '../utils/constants'; +import { PromptState } from '../prompts/prompt-state'; +import { join } from 'path'; +import { validateAppContentJsonFile } from '../utils/validate-app-content-json'; +import type { AbapDeployConfig } from '@sap-ux/ui5-config'; + +/** + * Generates the deployment configuration for an ABAP application. + * + * @param {AppInfo} app - Application info containing `url` and `repoName`. + * @param {AppContentConfig} appContentJson - Application content JSON with deployment details. + * @returns {AbapDeployConfig} The deployment configuration containing `target` and `app` info. + */ +export const getAbapDeployConfig = (app: AppInfo, appContentJson: AppContentConfig): AbapDeployConfig => { + return { + // todo: get from json file + target: { + url: app.url, + destination: app.repoName + }, + app: { + name: appContentJson.deploymentDetails.repositoryName, + package: appContentJson.metadata.package, + description: appContentJson.deploymentDetails?.repositoryDescription, + transport: 'REPLACE_WITH_TRANSPORT' + } + }; +}; + +/** + * Replaces the specified files in the `webapp` directory with the corresponding files from the `extractedPath`. + * + * @param {string} projectPath - The path to the downloaded App. + * @param {string} extractedPath - The path from which files will be copied. + * @param {Editor} fs - The file system editor instance to modify files in memory. + */ +export async function replaceWebappFiles( + projectPath: string, + extractedPath: string, + fs: Editor, + log?: Logger +): Promise { + try { + const webappPath = join(projectPath, 'webapp'); + // Define the paths of the files to be replaced + const filesToReplace = [ + { webappFile: 'manifest.json', extractedFile: 'manifest.json' }, + { webappFile: 'i18n/i18n.properties', extractedFile: 'i18n.properties' }, + { webappFile: 'index.html', extractedFile: 'index.html' } + ]; + + // Loop through each file and perform the replacement + for (const { webappFile, extractedFile } of filesToReplace) { + const webappFilePath = join(webappPath, webappFile); + const extractedFilePath = join(extractedPath, extractedFile); + + // Check if the extracted file exists before replacing + if (fs.exists(extractedFilePath)) { + fs.copy(extractedFilePath, webappFilePath); + } else { + log?.warn(t('warn.extractedFileNotFound', { extractedFilePath })); + } + } + } catch (error) { + log?.error(t('error.replaceWebappFilesError', { error })); + } +} + +/** + * Fetches the metadata of a given service from the provided ABAP service provider. + * + * @param {AbapServiceProvider} provider - The ABAP service provider instance. + * @param {string} serviceUrl - The URL of the service to retrieve metadata for. + * @param {Logger} [log] - An optional logger instance for logging error messages. + * @returns {Promise} - A promise resolving to the service metadata. + * @throws {Error} - Throws an error if the metadata fetch fails. + */ +const fetchServiceMetadata = async (provider: AbapServiceProvider, serviceUrl: string, log?: Logger): Promise => { + try { + return await provider.service(serviceUrl).metadata(); + } catch (err) { + log?.error(`Error fetching metadata: ${err.message}`); + throw err; + } +}; + +/** + * Generates the entity configuration based on the provided application content JSON. + * + * @param {any} appContentJson - The application content JSON containing service binding details. + * @returns {EntityConfig} - The generated entity configuration. + */ +function getEntityConfig(appContentJson: AppContentConfig): EntityConfig { + // Extract main entity name + const mainEntityName = appContentJson.serviceBindingDetails.mainEntityName; + // Initialize entity configuration with main entity name + let entityConfig: EntityConfig = { + mainEntityName: mainEntityName + }; + + // If navigationEntity exists, add it to the entityConfig + if (appContentJson.serviceBindingDetails.navigationEntity) { + entityConfig['navigationEntity'] = { + EntitySet: appContentJson.serviceBindingDetails.navigationEntity.EntitySet, + Name: appContentJson.serviceBindingDetails.navigationEntity.Name + }; + } + return entityConfig; +} + +/** + * Gets the application configuration based on the provided user answers and manifest data. + * This configuration will be used to initialize a new Fiori application. + * + * @param {AppInfo} app - Selected app information. + * @param {string} extractedProjectPath - Path where the app files are extracted. + * @param {Editor} fs - The file system editor to manipulate project files. + * @param {Logger} [log] - An optional logger instance for error logging. + * @returns {Promise>} - A promise resolving to the generated app configuration. + * @throws {Error} - Throws an error if there are issues generating the configuration. + */ +export async function getAppConfig( + app: AppInfo, + extractedProjectPath: string, + appContentJson: AppContentConfig, + fs: Editor, + log?: Logger +): Promise> { + try { + validateAppContentJsonFile(appContentJson, log); + const manifest = await readManifest(extractedProjectPath, fs); + + const serviceProvider = PromptState.systemSelection?.connectedSystem?.serviceProvider as AbapServiceProvider; + + if (!manifest?.['sap.app']?.dataSources) { + throw Error(t('error.dataSourcesNotFound')); + } + + const odataVersion = + manifest?.['sap.app']?.dataSources?.mainService?.settings?.odataVersion === '4.0' + ? OdataVersion.v4 + : OdataVersion.v2; + + // Fetch metadata for the service + const metadata = await fetchServiceMetadata( + serviceProvider, + manifest?.['sap.app']?.dataSources?.mainService.uri, + log + ); + + const appConfig: FioriElementsApp = { + app: { + id: app.appId, + title: app.title, + description: app.description, + sourceTemplate: { + id: adtSourceTemplateId + }, + projectType: 'EDMXBackend', + flpAppId: `${app.appId.replace(/[-_.]/g, '')}-tile` // todo: check if flpAppId is correct + }, + package: { + name: app.appId, + description: app.description, + devDependencies: {}, + scripts: {}, + version: manifest?.['sap.app']?.applicationVersion?.version ?? '0.0.1' + }, + template: { + type: TemplateType.ListReportObjectPage, + settings: { + entityConfig: getEntityConfig(appContentJson) + } + }, + service: { + path: manifest?.['sap.app']?.dataSources?.mainService.uri, + version: odataVersion, + metadata, + url: serviceProvider.defaults.baseURL + }, + appOptions: { + addAnnotations: odataVersion === OdataVersion.v4, + addTests: true + }, + ui5: { + version: + appContentJson.projectAttribute?.minimumUi5Version ?? + getMinimumUI5Version(manifest) ?? + (await getLatestUI5Version()) + } + }; + return appConfig; + } catch (error) { + log?.error(`Error generating application configuration: ${error.message}`); + throw error; + } +} diff --git a/packages/bsp-app-download-sub-generator/src/app/example-app-content.ts b/packages/bsp-app-download-sub-generator/src/app/example-app-content.ts new file mode 100644 index 0000000000..d3996c549b --- /dev/null +++ b/packages/bsp-app-download-sub-generator/src/app/example-app-content.ts @@ -0,0 +1,28 @@ +export const sampleAppContentJson = { + 'metadata': { + 'package': 'Z_I576700', + 'masterLanguage': 'EN' + }, + 'serviceBindingDetails': { + 'name': 'Z_RAHI_SRVB_V2', + 'serviceName': 'Z_RAHI_SRVB_V2', + 'serviceVersion': '0001', + 'mainEntityName': 'I_BusinessPartner' + }, + 'projectAttribute': { + 'moduleName': 'z_module_name', + 'applicationTitle': 'This is a demo app', + 'template': 'List Report', + 'minimumUi5Version': '1.130.5' + }, + 'deploymentDetails': { + 'repositoryName': 'ZTEST_MOD_NAME', + 'repositoryDescription': 'This is demo app' + }, + 'fioriLaunchpadConfiguration': { + 'semanticObject': 'Z_SO', + 'action': 'manage', + 'title': 'Z_Title', + 'subtitle': 'Z_sub_titile' + } +}; diff --git a/packages/bsp-app-download-sub-generator/src/app/index.ts b/packages/bsp-app-download-sub-generator/src/app/index.ts index 4977fa4760..e9fc924c4d 100644 --- a/packages/bsp-app-download-sub-generator/src/app/index.ts +++ b/packages/bsp-app-download-sub-generator/src/app/index.ts @@ -43,7 +43,6 @@ export default class extends Generator { private readonly prompts: Prompts; private answers: BspAppDownloadAnswers = defaultAnswers; public options: BspAppDownloadOptions; - private fioriOptions: FioriOptions; // re visit this private projectPath: string; private extractedProjectPath: string; @@ -165,19 +164,22 @@ export default class extends Generator { const readMeConfig = this._getReadMeConfig(config); generateReadMe(this.projectPath, readMeConfig, this.fs); - // Generate Fiori launch config - this.fioriOptions = this._getLaunchConfig(config); - debugger; + if(this.vscode) { + // Generate Fiori launch config + const fioriOptions = this._getLaunchConfig(config); + // Create launch configuration + await createLaunchConfig( + this.projectPath, + fioriOptions, + this.fs, + BspAppDownloadLogger.logger as unknown as Logger + ); + writeApplicationInfoSettings(this.projectPath, this.fs); + } // Replace webapp files with downloaded app files //replaceWebappFiles(this.projectPath, this.extractedProjectPath, this.fs); - // Create launch configuration - await createLaunchConfig( - this.projectPath, - this.fioriOptions, - this.fs, - BspAppDownloadLogger.logger as unknown as Logger - ); - writeApplicationInfoSettings(this.projectPath, this.fs); + // Clean up extracted project files + // this.fs.delete(this.extractedProjectPath); } /** @@ -222,6 +224,11 @@ export default class extends Generator { const fioriOptions: FioriOptions = { name: config.app.id, projectRoot: this.projectPath, + /** + * The `enableVSCodeReload` property is set to `false` to prevent automatic reloading of the VS Code workspace + * after the app generation process. This is necessary to ensure that the `.vscode/launch-config.json` file is + * written to disk before the workspace reload occurs. See {@link _handlePostAppGeneration} for details. + */ enableVSCodeReload: false, debugOptions }; @@ -261,7 +268,46 @@ export default class extends Generator { } /** - * Finalizes the generator process by creating launch configurations and running post-generation hooks. + * This includes updating workspace folders and running post-generation commands if defined. + */ + private async _handlePostAppGeneration(): Promise { + /** + * `enableVSCodeReload` is set to false when generating launch config. + * This prevents issues where the `.vscode/launch-config.json` file may not be written to disk due to the timing of mem-fs commits. + * + * In Yeoman, a commit occurs between the writing phase and the end phase. If no workspace is open in VS Code and the generated + * app is added to the workspace, VS Code automatically reloads the window. However, by this point in the end phase, the in-memory file system + * (mem-fs) has written all files except for `.vscode/launch-config.json`, because the commit happens before the end phase + * causing it to be missed when the workspace reload occurs. + * + * Workflow: + * 1. **Workspace URI**: The `updateWorkspaceFolders` object is created with the project folder's path, the project name, + * and the VS Code instance to handle workspace folder updates. + * 2. **Update Workspace Folders**: The `updateWorkspaceFoldersIfNeeded` function is called to update the workspace folders, + * if necessary, using the data in `updateWorkspaceFolders` . See {@link updateWorkspaceFoldersIfNeeded} for details. + * 3. **Run Post-Generation Commands**: If defined, post-generation commands from `options.data?.postGenCommands` are executed + * using the `runPostAppGenHook` function. This allows for additional setup or configuration tasks to be performed after + * the app generation process. + */ + if (this.vscode) { + const updateWorkspaceFolders = { + uri: this.vscode?.Uri?.file(join(this.projectPath)), + projectName: basename(this.projectPath), + vscode: this.vscode + }; + updateWorkspaceFoldersIfNeeded(updateWorkspaceFolders); + } + if (this.options.data?.postGenCommands) { + await runPostAppGenHook({ + path: this.projectPath, + vscodeInstance: this.vscode, + postGenCommand: this.options.data?.postGenCommands + }); + } + } + + /** + * Finalises the generator process by creating launch configurations and running post-generation hooks. */ async end() { sendTelemetry( @@ -273,24 +319,7 @@ export default class extends Generator { ).catch((error) => { BspAppDownloadLogger.logger.error(t('error.telemetry', { error })); }); - const updateWorkspaceFolders = { - uri: this.vscode?.Uri?.file(join(this.projectPath)), - projectName: basename(this.projectPath), - vscode: this.vscode - } - debugger; - updateWorkspaceFoldersIfNeeded(updateWorkspaceFolders) - // Clean up extracted project files - // this.fs.delete(this.extractedProjectPath); - - // Run post-generation command hook if available - if (this.options.data?.postGenCommands) { - await runPostAppGenHook({ - path: this.projectPath, - vscodeInstance: this.vscode, - postGenCommand: this.options.data?.postGenCommands - }); - } + this._handlePostAppGeneration(); } } diff --git a/packages/bsp-app-download-sub-generator/src/prompts/prompt-helpers.ts b/packages/bsp-app-download-sub-generator/src/prompts/prompt-helpers.ts new file mode 100644 index 0000000000..b8f5d4892f --- /dev/null +++ b/packages/bsp-app-download-sub-generator/src/prompts/prompt-helpers.ts @@ -0,0 +1,93 @@ +import { generatorTitle, generatorDescription } from '../utils/constants'; +import { appListSearchParams, appListResultFields } from '../utils/constants'; +import type { AbapServiceProvider, AppIndex } from '@sap-ux/axios-extension'; +import type { Logger } from '@sap-ux/logger'; +import type { AppInfo } from '../app/types'; +import { PromptNames } from '../app/types'; +import { PromptState } from './prompt-state'; +import type { BspAppDownloadAnswers, AppItem } from '../app/types'; + +/** + * Returns the details for the YUI prompt. + * + * @returns step details + */ +export function getYUIDetails(): { name: string; description: string }[] { + return [ + { + name: generatorTitle, + description: generatorDescription + } + ]; +} + +/** + * Formats the application list into selectable choices. + * + * @param {AppIndex} appList - List of applications retrieved from the system. + * @returns {Array<{ name: string; value: AppInfo }>} The formatted choices for selection. + * @throws Will throw an error if any required fields are missing. + */ +export const formatAppChoices = (appList: AppIndex): Array<{ name: string; value: AppInfo }> => { + return appList.map((app: AppItem) => { + // Check if any required fields are missing + if ( + !app['sap.app/id'] || + !app['sap.app/title'] || + !app['sap.app/description'] || + !app['repoName'] || + !app['url'] + ) { + throw new Error(`Required fields are missing for app: ${JSON.stringify(app)}`); + } + + return { + name: app['sap.app/id'], + value: { + appId: app['sap.app/id'], + title: app['sap.app/title'], + description: app['sap.app/description'] as string, + repoName: app['repoName'] as string, + url: app['url'] + } + }; + }); +}; + +/** + * Fetches a list of deployed applications from the ABAP repository. + * + * @param {AbapServiceProvider} provider - The ABAP service provider. + * @param {Logger} [log] - The logger instance. + * @returns {Promise} A list of applications filtered by source template. + */ +async function getAppList(provider: AbapServiceProvider, log?: Logger): Promise { + try { + return await provider.getAppIndex().search(appListSearchParams, appListResultFields); + } catch (error) { + log?.error(`Error fetching application list: ${error.message}`); + return []; + } +} + +/** + * Fetches the application list for the selected system. + * + * @param {BspAppDownloadAnswers} answers - The user's answers from the prompts. + * @param {AbapServiceProvider | undefined} serviceProvider - The ABAP service provider. + * @param {Logger} [log] - The logger instance. + * @returns {Promise} A list of applications filtered by source template. + */ +export async function fetchAppListForSelectedSystem( + answers: BspAppDownloadAnswers, + serviceProvider?: AbapServiceProvider, + log?: Logger +): Promise { + if (answers[PromptNames.systemSelection] && serviceProvider) { + PromptState.systemSelection = { + connectedSystem: { serviceProvider } + }; + return await getAppList(serviceProvider, log); + } + return []; +} diff --git a/packages/bsp-app-download-sub-generator/src/prompts/prompts.ts b/packages/bsp-app-download-sub-generator/src/prompts/prompts.ts new file mode 100644 index 0000000000..885b99bfa6 --- /dev/null +++ b/packages/bsp-app-download-sub-generator/src/prompts/prompts.ts @@ -0,0 +1,78 @@ +import type { AppIndex, AbapServiceProvider } from '@sap-ux/axios-extension'; +import { getSystemSelectionQuestions } from '@sap-ux/odata-service-inquirer'; +import type { BspAppDownloadAnswers, BspAppDownloadQuestions } from '../app/types'; +import { PromptNames } from '../app/types'; +import { t } from '../utils/i18n'; +import type { FileBrowserQuestion } from '@sap-ux/inquirer-common'; +import type { Logger } from '@sap-ux/logger'; +import { formatAppChoices } from './prompt-helpers'; +import { validateFioriAppTargetFolder } from '@sap-ux/project-input-validator'; +import { PromptState } from './prompt-state'; +import { fetchAppListForSelectedSystem } from './prompt-helpers'; + +/** + * Gets the target folder selection prompt. + * + * @param {string} [appRootPath] - The application root path. + * @returns {FileBrowserQuestion} The target folder prompt configuration. + */ +const getTargetFolderPrompt = (appRootPath?: string): FileBrowserQuestion => { + return { + type: 'input', + name: PromptNames.targetFolder, + message: t('prompts.targetPath.targetPathMessage'), + guiType: 'folder-browser', + when: (answers: BspAppDownloadAnswers) => Boolean(answers?.selectedApp?.appId), + guiOptions: { + applyDefaultWhenDirty: true, + mandatory: true, + breadcrumb: t('prompts.targetPath.targetPathBreadcrumb') + }, + validate: async (target, answers: BspAppDownloadAnswers): Promise => { + return await validateFioriAppTargetFolder(target, answers.selectedApp.appId, true); + }, + default: () => appRootPath + } as FileBrowserQuestion; +}; + +/** + * Retrieves questions for selecting system, app lists and target path where app will be generated. + * + * @param {string} [appRootPath] - The root path of the application. + * @param {Logger} [log] - Logger instance for debugging. + * @returns {Promise} A list of questions for user interaction. + */ +export async function getPrompts(appRootPath?: string, log?: Logger): Promise { + PromptState.reset(); + const systemQuestions = await getSystemSelectionQuestions({ serviceSelection: { hide: true } }, false); // todo: remove this isYUI value + let appList: AppIndex = []; + let result: BspAppDownloadQuestions[] = []; + + const appSelectionPrompt = [ + { + when: async (answers: BspAppDownloadAnswers): Promise => { + appList = await fetchAppListForSelectedSystem( + answers, + systemQuestions.answers.connectedSystem?.serviceProvider as unknown as AbapServiceProvider, + log + ); + // display app selection prompt only if user has selected a system + return !!systemQuestions.answers.connectedSystem?.serviceProvider; + }, + type: 'list', + name: PromptNames.selectedApp, + guiOptions: { + mandatory: !!appList.length, + breadcrumb: true + }, + message: t('prompts.appSelection.message'), + choices: () => (appList.length ? formatAppChoices(appList) : []), + validate: (): string | boolean => appList.length ? true : t('prompts.appSelection.noAppsDeployed') + } + ]; + + const targetFolderPrompts = getTargetFolderPrompt(appRootPath); + result = [...systemQuestions.prompts, ...appSelectionPrompt, targetFolderPrompts] as BspAppDownloadQuestions[]; + + return result; +} diff --git a/packages/bsp-app-download-sub-generator/src/utils/download-utils.ts b/packages/bsp-app-download-sub-generator/src/utils/download-utils.ts new file mode 100644 index 0000000000..f185aaddf3 --- /dev/null +++ b/packages/bsp-app-download-sub-generator/src/utils/download-utils.ts @@ -0,0 +1,59 @@ +import type { AbapServiceProvider } from '@sap-ux/axios-extension'; +import AdmZip from 'adm-zip'; +import type { Logger } from '@sap-ux/logger'; +import { join } from 'path'; +import type { Editor } from 'mem-fs-editor'; +import { PromptState } from '../prompts/prompt-state'; + +/** + * Extracts a ZIP archive to a temporary directory. + * + * @param {string} extractedProjectPath - The path where the archive should be extracted. + * @param {Buffer} archive - The ZIP archive buffer. + * @param {Editor} fs - The file system editor. + * @param {Logger} [log] - The logger instance. + */ +async function extractZip(extractedProjectPath: string, archive: Buffer, fs: Editor, log?: Logger): Promise { + try { + const zip = new AdmZip(archive); + zip.getEntries().forEach(function (zipEntry) { + if (!zipEntry.isDirectory) { + // Extract the file content + const fileContent = zipEntry.getData().toString('utf8'); + // Add the file content to mem-fs at a virtual path + fs.write(join(extractedProjectPath, zipEntry.entryName), fileContent); + } + }); + } catch (error) { + log?.error(`Error extracting zip: ${error.message}`); + } +} + +/** + * Downloads application files from the ABAP repository. + * + * @param {string} repoName - The repository name of the application. + * @param {string} extractedProjectPath - The path where the application should be extracted. + * @param {Editor} fs - The file system editor. + * @param {Logger} [log] - The logger instance. + * @throws {Error} If the file download fails. + */ +export async function downloadApp( + repoName: string, + extractedProjectPath: string, + fs: Editor, + log?: Logger +): Promise { + try { + const serviceProvider = PromptState.systemSelection?.connectedSystem?.serviceProvider as AbapServiceProvider; + const archive = await serviceProvider.getUi5AbapRepository().downloadFiles(repoName); + + if (Buffer.isBuffer(archive)) { + await extractZip(extractedProjectPath, archive, fs, log); + } else { + log?.error('Error: The downloaded file is not a Buffer.'); + } + } catch (error) { + throw Error(`Error downloading file: ${error.message}`); + } +} diff --git a/packages/bsp-app-download-sub-generator/src/utils/event-hook.ts b/packages/bsp-app-download-sub-generator/src/utils/event-hook.ts new file mode 100644 index 0000000000..2dea3fb72d --- /dev/null +++ b/packages/bsp-app-download-sub-generator/src/utils/event-hook.ts @@ -0,0 +1,41 @@ +import BspAppDownloadLogger from './logger'; +import { t } from './i18n'; +import type { VSCodeInstance } from '@sap-ux/fiori-generator-shared'; + +/** + * Context object for the BSP App generation process. + * Contains the path of the project and the post-generation command. + */ +export interface BspAppGenContext { + // The file path for the generated project + path: string; + // The post-generation command to be executed + postGenCommand: string; + // The VSCode instance to execute the command + vscodeInstance?: VSCodeInstance; +} + +/** + * Executes the post-generation command for the BSP app. + * Runs the specified command in the context of the generated project, typically for tasks like refreshing or reloading the project in the editor. + * + * @param {BspAppGenContext} context - The context containing the project path, post-generation command, and optional VSCode instance. + * @throws {Error} If the VSCode instance or post-generation command is missing from the context. + */ +export async function runPostAppGenHook(context: BspAppGenContext): Promise { + try { + // Ensure that context has necessary values before proceeding + if (!context.vscodeInstance) { + throw new Error('VSCode instance is missing.'); + } + if (!context.postGenCommand) { + throw new Error('Post generation command is missing.'); + } + // Execute the post-generation command + await context.vscodeInstance?.commands?.executeCommand?.(context.postGenCommand, { + fsPath: context.path + }); + } catch (e) { + BspAppDownloadLogger.logger.error(t('error.postGenCommand', { error: e })); + } +} diff --git a/packages/bsp-app-download-sub-generator/src/utils/file-helpers.ts b/packages/bsp-app-download-sub-generator/src/utils/file-helpers.ts new file mode 100644 index 0000000000..e497a6d7eb --- /dev/null +++ b/packages/bsp-app-download-sub-generator/src/utils/file-helpers.ts @@ -0,0 +1,28 @@ +import { adtSourceTemplateId } from './constants'; +import { join } from 'path'; +import type { Editor } from 'mem-fs-editor'; +import { FileName, type Manifest } from '@sap-ux/project-access'; +import { t } from './i18n'; + +/** + * Reads and validates the `manifest.json` file. + * + * @param {string} extractedProjectPath - The path to the extracted project. + * @param {Editor} fs - The file system editor. + * @returns {Promise} The validated manifest object. + * @throws {Error} If the manifest file is missing or invalid. + */ +export async function readManifest(extractedProjectPath: string, fs: Editor): Promise { + const manifestPath = join(extractedProjectPath, FileName.Manifest); + const manifest = fs.readJSON(manifestPath) as unknown as Manifest; + if (!manifest) { + throw Error(t('error.manifestNotFound')); + } + if (!manifest['sap.app']) { + throw Error(t('error.sapAppNotDefined')); + } + if (manifest['sap.app'].sourceTemplate?.id !== adtSourceTemplateId) { + throw Error(t('error.sourceTemplateNotSupported')); + } + return manifest; +} diff --git a/packages/bsp-app-download-sub-generator/src/utils/validate-app-content-json.ts b/packages/bsp-app-download-sub-generator/src/utils/validate-app-content-json.ts new file mode 100644 index 0000000000..f9a053c70b --- /dev/null +++ b/packages/bsp-app-download-sub-generator/src/utils/validate-app-content-json.ts @@ -0,0 +1,90 @@ +import type { Logger } from '@sap-ux/logger'; +import { t } from '../utils/i18n'; +import type { AppContentConfig } from '../app/types'; + +/** + * Validates the metadata section of the app configuration. + * + * @param {AppContentConfig['metadata']} metadata - The metadata object. + * @param {Logger} log - The logger instance. + * @returns {boolean} - Returns true if valid, false otherwise. + */ +const validateMetadata = (metadata: AppContentConfig['metadata'], log?: Logger): boolean => { + if (!metadata.package || typeof metadata.package !== 'string') { + log?.error(t('error.invalidMetadataPackage')); + return false; + } + return true; +}; + +/** + * Validates the service binding details section of the app configuration. + * + * @param {AppContentConfig['serviceBindingDetails']} serviceBinding - The service binding details object. + * @param {Logger} log - The logger instance. + * @returns {boolean} - Returns true if valid, false otherwise. + */ +const validateServiceBindingDetails = ( + serviceBinding: AppContentConfig['serviceBindingDetails'], + log?: Logger +): boolean => { + if (!serviceBinding.serviceName || typeof serviceBinding.serviceName !== 'string') { + log?.error(t('error.invalidServiceName')); + return false; + } + if (!serviceBinding.serviceVersion || typeof serviceBinding.serviceVersion !== 'string') { + log?.error(t('error.invalidServiceVersion')); + return false; + } + if (!serviceBinding.mainEntityName || typeof serviceBinding.mainEntityName !== 'string') { + log?.error(t('error.invalidMainEntityName')); + return false; + } + return true; +}; + +/** + * Validates the project attribute section of the app configuration. + * + * @param {AppContentConfig['projectAttribute']} projectAttribute - The project attribute object. + * @param {Logger} log - The logger instance. + * @returns {boolean} - Returns true if valid, false otherwise. + */ +const validateProjectAttribute = (projectAttribute: AppContentConfig['projectAttribute'], log?: Logger): boolean => { + if (!projectAttribute.moduleName || typeof projectAttribute.moduleName !== 'string') { + log?.error(t('error.invalidModuleName')); + return false; + } + return true; +}; + +/** + * Validates the deployment details section of the app configuration. + * + * @param {AppContentConfig['deploymentDetails']} deploymentDetails - The deployment details object. + * @param {Logger} log - The logger instance. + * @returns {boolean} - Returns true if valid, false otherwise. + */ +const validateDeploymentDetails = (deploymentDetails: AppContentConfig['deploymentDetails'], log?: Logger): boolean => { + if (!deploymentDetails.repositoryName) { + log?.error(t('error.invalidRepositoryName')); + return false; + } + return true; +}; + +/** + * Validates the entire app configuration. + * + * @param {AppContentConfig} config - The app configuration object. + * @param {Logger} log - The logger instance. + * @returns {boolean} - Returns true if the configuration is valid, false otherwise. + */ +export const validateAppContentJsonFile = (config: AppContentConfig, log?: Logger): boolean => { + return ( + validateMetadata(config.metadata, log) && + validateServiceBindingDetails(config.serviceBindingDetails, log) && + validateProjectAttribute(config.projectAttribute, log) && + validateDeploymentDetails(config.deploymentDetails, log) + ); +}; diff --git a/packages/launch-config/src/launch-config-crud/create.ts b/packages/launch-config/src/launch-config-crud/create.ts index 5b75b476dd..3fa17b4efe 100644 --- a/packages/launch-config/src/launch-config-crud/create.ts +++ b/packages/launch-config/src/launch-config-crud/create.ts @@ -166,7 +166,6 @@ async function handleDebugOptions( } else { writeLaunchJsonFile(fs, launchJsonWritePath, configurations); } - debugger; // Conditionally update workspace folders based on the enableVSCodeReload flag if (enableVSCodeReload) { From c155e1d6d57a8668926aea6ea08b5f19f4a79333 Mon Sep 17 00:00:00 2001 From: I743583 Date: Mon, 7 Apr 2025 08:53:29 +0100 Subject: [PATCH 11/41] wip: checkpoint commit --- .../src/app/config.ts | 65 +- .../src/app/index.ts | 85 ++- .../src/app/types.ts | 31 +- .../src/prompts/prompt-helpers.ts | 54 +- .../src/prompts/prompts.ts | 10 +- .../bsp-app-download-sub-generator.i18n.json | 52 +- .../src/utils/constants.ts | 8 +- .../src/utils/download-utils.ts | 25 +- .../src/utils/event-hook.ts | 9 +- .../src/utils/file-helpers.ts | 59 +- .../src/utils/validate-app-content-json.ts | 40 +- .../test/app.test.ts | 144 +++++ .../test/config.test.ts | 0 .../test/fixtures/metadata.xml | 553 ++++++++++++++++++ .../test/prompts/prompt-helpers.test.ts | 143 +++++ .../test/prompts/prompt-state.test.ts | 37 ++ .../test/prompts/prompts.test.ts | 132 +++++ .../test/utils/download-utils.test.ts | 96 +++ .../test/utils/event-hook.test.ts | 58 ++ .../test/utils/file-helpers.test.ts | 68 +++ .../utils/validate-app-content-json.test.ts | 112 ++++ 21 files changed, 1566 insertions(+), 215 deletions(-) create mode 100644 packages/bsp-app-download-sub-generator/test/app.test.ts create mode 100644 packages/bsp-app-download-sub-generator/test/config.test.ts create mode 100644 packages/bsp-app-download-sub-generator/test/fixtures/metadata.xml create mode 100644 packages/bsp-app-download-sub-generator/test/prompts/prompt-helpers.test.ts create mode 100644 packages/bsp-app-download-sub-generator/test/prompts/prompt-state.test.ts create mode 100644 packages/bsp-app-download-sub-generator/test/prompts/prompts.test.ts create mode 100644 packages/bsp-app-download-sub-generator/test/utils/download-utils.test.ts create mode 100644 packages/bsp-app-download-sub-generator/test/utils/event-hook.test.ts create mode 100644 packages/bsp-app-download-sub-generator/test/utils/file-helpers.test.ts create mode 100644 packages/bsp-app-download-sub-generator/test/utils/validate-app-content-json.test.ts diff --git a/packages/bsp-app-download-sub-generator/src/app/config.ts b/packages/bsp-app-download-sub-generator/src/app/config.ts index ea29e3dc45..ddb625a4a5 100644 --- a/packages/bsp-app-download-sub-generator/src/app/config.ts +++ b/packages/bsp-app-download-sub-generator/src/app/config.ts @@ -1,7 +1,6 @@ import { TemplateType, type FioriElementsApp, type LROPSettings } from '@sap-ux/fiori-elements-writer'; import { OdataVersion } from '@sap-ux/odata-service-inquirer'; import type { AbapServiceProvider } from '@sap-ux/axios-extension'; -import type { Logger } from '@sap-ux/logger'; import type { Editor } from 'mem-fs-editor'; import { t } from '../utils/i18n'; import type { AppInfo, AppContentConfig, EntityConfig } from '../app/types'; @@ -13,12 +12,13 @@ import { PromptState } from '../prompts/prompt-state'; import { join } from 'path'; import { validateAppContentJsonFile } from '../utils/validate-app-content-json'; import type { AbapDeployConfig } from '@sap-ux/ui5-config'; +import BspAppDownloadLogger from '../utils/logger'; /** * Generates the deployment configuration for an ABAP application. * * @param {AppInfo} app - Application info containing `url` and `repoName`. - * @param {AppContentConfig} appContentJson - Application content JSON with deployment details. + * @param {AppContentCofig} appContentJson - Application content JSON with deployment details. * @returns {AbapDeployConfig} The deployment configuration containing `target` and `app` info. */ export const getAbapDeployConfig = (app: AppInfo, appContentJson: AppContentConfig): AbapDeployConfig => { @@ -37,60 +37,18 @@ export const getAbapDeployConfig = (app: AppInfo, appContentJson: AppContentConf }; }; -/** - * Replaces the specified files in the `webapp` directory with the corresponding files from the `extractedPath`. - * - * @param {string} projectPath - The path to the downloaded App. - * @param {string} extractedPath - The path from which files will be copied. - * @param {Editor} fs - The file system editor instance to modify files in memory. - */ -export async function replaceWebappFiles( - projectPath: string, - extractedPath: string, - fs: Editor, - log?: Logger -): Promise { - try { - const webappPath = join(projectPath, 'webapp'); - // Define the paths of the files to be replaced - const filesToReplace = [ - { webappFile: 'manifest.json', extractedFile: 'manifest.json' }, - { webappFile: 'i18n/i18n.properties', extractedFile: 'i18n.properties' }, - { webappFile: 'index.html', extractedFile: 'index.html' } - ]; - - // Loop through each file and perform the replacement - for (const { webappFile, extractedFile } of filesToReplace) { - const webappFilePath = join(webappPath, webappFile); - const extractedFilePath = join(extractedPath, extractedFile); - - // Check if the extracted file exists before replacing - if (fs.exists(extractedFilePath)) { - fs.copy(extractedFilePath, webappFilePath); - } else { - log?.warn(t('warn.extractedFileNotFound', { extractedFilePath })); - } - } - } catch (error) { - log?.error(t('error.replaceWebappFilesError', { error })); - } -} - /** * Fetches the metadata of a given service from the provided ABAP service provider. * * @param {AbapServiceProvider} provider - The ABAP service provider instance. * @param {string} serviceUrl - The URL of the service to retrieve metadata for. - * @param {Logger} [log] - An optional logger instance for logging error messages. * @returns {Promise} - A promise resolving to the service metadata. - * @throws {Error} - Throws an error if the metadata fetch fails. */ -const fetchServiceMetadata = async (provider: AbapServiceProvider, serviceUrl: string, log?: Logger): Promise => { +const fetchServiceMetadata = async (provider: AbapServiceProvider, serviceUrl: string): Promise => { try { return await provider.service(serviceUrl).metadata(); } catch (err) { - log?.error(`Error fetching metadata: ${err.message}`); - throw err; + BspAppDownloadLogger.logger?.error(t('error.metadatafetchError', { error: err.message })); } }; @@ -104,7 +62,7 @@ function getEntityConfig(appContentJson: AppContentConfig): EntityConfig { // Extract main entity name const mainEntityName = appContentJson.serviceBindingDetails.mainEntityName; // Initialize entity configuration with main entity name - let entityConfig: EntityConfig = { + const entityConfig: EntityConfig = { mainEntityName: mainEntityName }; @@ -124,6 +82,7 @@ function getEntityConfig(appContentJson: AppContentConfig): EntityConfig { * * @param {AppInfo} app - Selected app information. * @param {string} extractedProjectPath - Path where the app files are extracted. + * @param appContentJson * @param {Editor} fs - The file system editor to manipulate project files. * @param {Logger} [log] - An optional logger instance for error logging. * @returns {Promise>} - A promise resolving to the generated app configuration. @@ -133,12 +92,11 @@ export async function getAppConfig( app: AppInfo, extractedProjectPath: string, appContentJson: AppContentConfig, - fs: Editor, - log?: Logger + fs: Editor ): Promise> { try { - validateAppContentJsonFile(appContentJson, log); - const manifest = await readManifest(extractedProjectPath, fs); + validateAppContentJsonFile(appContentJson); + const manifest = readManifest(extractedProjectPath, fs); const serviceProvider = PromptState.systemSelection?.connectedSystem?.serviceProvider as AbapServiceProvider; @@ -154,8 +112,7 @@ export async function getAppConfig( // Fetch metadata for the service const metadata = await fetchServiceMetadata( serviceProvider, - manifest?.['sap.app']?.dataSources?.mainService.uri, - log + manifest?.['sap.app']?.dataSources?.mainService.uri ); const appConfig: FioriElementsApp = { @@ -201,7 +158,7 @@ export async function getAppConfig( }; return appConfig; } catch (error) { - log?.error(`Error generating application configuration: ${error.message}`); + BspAppDownloadLogger.logger?.error(t('error.appConfigGenError', { error: error.message })); throw error; } } diff --git a/packages/bsp-app-download-sub-generator/src/app/index.ts b/packages/bsp-app-download-sub-generator/src/app/index.ts index e9fc924c4d..f8e6ca0247 100644 --- a/packages/bsp-app-download-sub-generator/src/app/index.ts +++ b/packages/bsp-app-download-sub-generator/src/app/index.ts @@ -1,6 +1,6 @@ import Generator from 'yeoman-generator'; import BspAppDownloadLogger from '../utils/logger'; -import { AppWizard, Prompts } from '@sap-devx/yeoman-ui-types'; +import { AppWizard, Prompts, MessageType } from '@sap-devx/yeoman-ui-types'; import { isInternalFeaturesSettingEnabled } from '@sap-ux/feature-toggle'; import type { Logger } from '@sap-ux/logger'; import { sendTelemetry, TelemetryHelper } from '@sap-ux/fiori-generator-shared'; @@ -9,12 +9,12 @@ import { t } from '../utils/i18n'; import { getYUIDetails } from '../prompts/prompt-helpers'; import { downloadApp } from '../utils/download-utils'; import { EventName } from '../telemetryEvents'; -import type { YeomanEnvironment, VSCodeInstance } from '@sap-ux/fiori-generator-shared'; +import type { YeomanEnvironment } from '@sap-ux/fiori-generator-shared'; import { getDefaultTargetFolder } from '@sap-ux/fiori-generator-shared'; import type { BspAppDownloadOptions, BspAppDownloadAnswers, BspAppDownloadQuestions, AppContentConfig } from './types'; import { getPrompts } from '../prompts/prompts'; import { generate, TemplateType, type FioriElementsApp, type LROPSettings } from '@sap-ux/fiori-elements-writer'; -import { join, basename, dirname } from 'path'; +import { join, basename } from 'path'; import { platform } from 'os'; import { generateReadMe, type ReadMe } from '@sap-ux/fiori-generator-shared'; import { runPostAppGenHook } from '../utils/event-hook'; @@ -27,9 +27,10 @@ import { writeApplicationInfoSettings } from '@sap-ux/fiori-tools-settings'; import { generate as generateDeployConfig } from '@sap-ux/abap-deploy-config-writer'; import { PromptState } from '../prompts/prompt-state'; import { PromptNames } from './types'; -import { getAbapDeployConfig, getAppConfig, replaceWebappFiles } from './config'; +import { getAbapDeployConfig, getAppConfig } from './config'; import type { AbapDeployConfig } from '@sap-ux/ui5-config'; import { sampleAppContentJson } from './example-app-content'; +import { replaceWebappFiles } from '../utils/file-helpers'; /** * Generator class for downloading a basic app from BSP repository. @@ -37,7 +38,7 @@ import { sampleAppContentJson } from './example-app-content'; */ export default class extends Generator { private readonly appWizard: AppWizard; - private readonly vscode?: any;//VSCodeInstance; //confirm this + private readonly vscode?: any; private readonly launchAppDownloaderAsSubGenerator: boolean; private readonly appRootPath: string; private readonly prompts: Prompts; @@ -57,7 +58,7 @@ export default class extends Generator { constructor(args: string | string[], opts: BspAppDownloadOptions) { super(args, opts); - // Initialize properties from options + // Initialise properties from options this.appWizard = opts.appWizard ?? AppWizard.create(opts); this.vscode = opts.vscode; this.launchAppDownloaderAsSubGenerator = opts.launchAppDownloaderAsSubGenerator ?? false; @@ -74,6 +75,7 @@ export default class extends Generator { this.vscode ); + this.prompts = new Prompts([]); // Initialize prompts and callbacks if not launched as a subgenerator if (!this.launchAppDownloaderAsSubGenerator) { this.appWizard.setHeaderTitle(generatorTitle); @@ -110,21 +112,16 @@ export default class extends Generator { */ public async prompting(): Promise { const questions: BspAppDownloadQuestions[] = await getPrompts(this.appRootPath); - const { selectedApp, targetFolder } = (await this.prompt(questions)) as BspAppDownloadAnswers; - if (PromptState.systemSelection.connectedSystem?.serviceProvider && selectedApp?.appId && targetFolder) { - this.answers.selectedApp = selectedApp; - this.answers.targetFolder = targetFolder; + const answers: BspAppDownloadAnswers = await this.prompt(questions); + const { selectedApp, targetFolder } = answers; + if (PromptState.systemSelection.connectedSystem?.serviceProvider && selectedApp?.appId && targetFolder) { + Object.assign(this.answers, answers); this.projectPath = join(targetFolder, selectedApp.appId); this.extractedProjectPath = join(this.projectPath, extractedFilePath); // Trigger app download - await downloadApp( - this.answers.selectedApp.repoName, - this.extractedProjectPath, - this.fs, - BspAppDownloadLogger.logger as unknown as Logger - ); + await downloadApp(this.answers.selectedApp.repoName, this.extractedProjectPath, this.fs); } } @@ -133,38 +130,27 @@ export default class extends Generator { */ public async writing(): Promise { // const appContentJsonTempPath = join(__dirname, 'example-app-content.json'); - let appContentJson: AppContentConfig = sampleAppContentJson; - // todo: add back once json is available along with downloaded app + const appContentJson: AppContentConfig = sampleAppContentJson; + // todo: add back once json is available along with downloaded app // if(!this.fs.exists(appContentJsonTempPath)) { // appContentJson = this.fs.readJSON(appContentJsonTempPath) as unknown as AppContentConfig; //todo: extract from extracted path // } else { - // throw new Error(t('error.appContentJsonNotFound', { jsonFileName: 'example-app-content.json' })); + // BspAppDownloadLogger.logger?.error(t('error.appContentJsonNotFound', { jsonFileName: 'example-app-content.json' })); // } - + // Generate project files - const config = await getAppConfig( - this.answers.selectedApp, - this.extractedProjectPath, - appContentJson, - this.fs, - BspAppDownloadLogger.logger as unknown as Logger - ); + const config = await getAppConfig(this.answers.selectedApp, this.extractedProjectPath, appContentJson, this.fs); await generate(this.projectPath, config, this.fs); - + // Generate deploy config const deployConfig: AbapDeployConfig = getAbapDeployConfig(this.answers.selectedApp, appContentJson); - await generateDeployConfig( - this.projectPath, - deployConfig, - undefined, - this.fs - ); + await generateDeployConfig(this.projectPath, deployConfig, undefined, this.fs); // Generate README const readMeConfig = this._getReadMeConfig(config); generateReadMe(this.projectPath, readMeConfig, this.fs); - if(this.vscode) { + if (this.vscode) { // Generate Fiori launch config const fioriOptions = this._getLaunchConfig(config); // Create launch configuration @@ -177,7 +163,7 @@ export default class extends Generator { writeApplicationInfoSettings(this.projectPath, this.fs); } // Replace webapp files with downloaded app files - //replaceWebappFiles(this.projectPath, this.extractedProjectPath, this.fs); + // replaceWebappFiles(this.projectPath, this.extractedProjectPath, this.fs); // Clean up extracted project files // this.fs.delete(this.extractedProjectPath); } @@ -225,7 +211,7 @@ export default class extends Generator { name: config.app.id, projectRoot: this.projectPath, /** - * The `enableVSCodeReload` property is set to `false` to prevent automatic reloading of the VS Code workspace + * The `enableVSCodeReload` property is set to `false` to prevent automatic reloading of the VS Code workspace * after the app generation process. This is necessary to ensure that the `.vscode/launch-config.json` file is * written to disk before the workspace reload occurs. See {@link _handlePostAppGeneration} for details. */ @@ -243,10 +229,10 @@ export default class extends Generator { try { await this._runNpmInstall(this.projectPath); } catch (error) { - BspAppDownloadLogger.logger?.error(t('error.npmInstall', { error })); + BspAppDownloadLogger.logger?.error(t('error.installationErrors.npmInstall', { error })); } } else { - BspAppDownloadLogger.logger?.info(t('info.skippedInstallation')); + BspAppDownloadLogger.logger?.info(t('info.installationErrors.skippedInstallation')); } } @@ -268,25 +254,25 @@ export default class extends Generator { } /** - * This includes updating workspace folders and running post-generation commands if defined. + * Responsible for updating workspace folders and running post-generation commands if defined. */ private async _handlePostAppGeneration(): Promise { /** - * `enableVSCodeReload` is set to false when generating launch config. + * `enableVSCodeReload` is set to false when generating launch config here {@link _getLaunchConfig}. * This prevents issues where the `.vscode/launch-config.json` file may not be written to disk due to the timing of mem-fs commits. - * + * * In Yeoman, a commit occurs between the writing phase and the end phase. If no workspace is open in VS Code and the generated - * app is added to the workspace, VS Code automatically reloads the window. However, by this point in the end phase, the in-memory file system + * app is added to the workspace, VS Code automatically reloads the window. However, by this point in the end phase, the in-memory file system * (mem-fs) has written all files except for `.vscode/launch-config.json`, because the commit happens before the end phase * causing it to be missed when the workspace reload occurs. * * Workflow: - * 1. **Workspace URI**: The `updateWorkspaceFolders` object is created with the project folder's path, the project name, + * 1. **Workspace URI**: The `updateWorkspaceFolders` object is created with the project folder's path, the project name, * and the VS Code instance to handle workspace folder updates. - * 2. **Update Workspace Folders**: The `updateWorkspaceFoldersIfNeeded` function is called to update the workspace folders, - * if necessary, using the data in `updateWorkspaceFolders` . See {@link updateWorkspaceFoldersIfNeeded} for details. - * 3. **Run Post-Generation Commands**: If defined, post-generation commands from `options.data?.postGenCommands` are executed - * using the `runPostAppGenHook` function. This allows for additional setup or configuration tasks to be performed after + * 2. **Update Workspace Folders**: The `updateWorkspaceFoldersIfNeeded` function is called to update the workspace folders, + * if necessary. See {@link updateWorkspaceFoldersIfNeeded} for details. + * 3. **Run Post-Generation Commands**: If defined, post-generation commands from `options.data?.postGenCommands` are executed + * using the `runPostAppGenHook` function. This allows for additional setup or configuration tasks to be performed after * the app generation process. */ if (this.vscode) { @@ -310,6 +296,7 @@ export default class extends Generator { * Finalises the generator process by creating launch configurations and running post-generation hooks. */ async end() { + this.appWizard.showWarning(t('info.bspAppDownloadCompleteMsg'), MessageType.prompt); sendTelemetry( EventName.GENERATION_SUCCESS, TelemetryHelper.createTelemetryData({ @@ -319,7 +306,7 @@ export default class extends Generator { ).catch((error) => { BspAppDownloadLogger.logger.error(t('error.telemetry', { error })); }); - this._handlePostAppGeneration(); + await this._handlePostAppGeneration(); } } diff --git a/packages/bsp-app-download-sub-generator/src/app/types.ts b/packages/bsp-app-download-sub-generator/src/app/types.ts index 33f5678d73..2dbdf00207 100644 --- a/packages/bsp-app-download-sub-generator/src/app/types.ts +++ b/packages/bsp-app-download-sub-generator/src/app/types.ts @@ -6,6 +6,7 @@ import type { BackendSystem } from '@sap-ux/store'; import type { AbapServiceProvider, AppIndex } from '@sap-ux/axios-extension'; import type { YUIQuestion } from '@sap-ux/inquirer-common'; import type { AutocompleteQuestionOptions } from 'inquirer-autocomplete-prompt'; +import type { SystemSelectionAnswerType } from '@sap-ux/odata-service-inquirer'; /** * Options for downloading a BSP application. @@ -84,17 +85,16 @@ export enum PromptNames { */ export interface BspAppDownloadAnswers { /** Selected backend system connection details. */ - [PromptNames.systemSelection]: SystemSelectionAnswers; + [PromptNames.systemSelection]: SystemSelectionAnswerType; /** Information about the selected application for download. */ [PromptNames.selectedApp]: AppInfo; /** Target folder where the BSP application will be generated. */ [PromptNames.targetFolder]: string; } - interface Metadata { package: string; - masterLanguage: string; + masterLanguage?: string; } export interface EntityConfig { @@ -104,32 +104,32 @@ export interface EntityConfig { Name: string; }; } - -interface ServiceBindingDetails extends EntityConfig{ - name: string; + +interface ServiceBindingDetails extends EntityConfig { + name?: string; serviceName: string; serviceVersion: string; } - + interface ProjectAttribute { moduleName: string; - applicationTitle: string; - template: string; - minimumUi5Version: string; + applicationTitle?: string; + template?: string; + minimumUi5Version?: string; } - + interface DeploymentDetails { repositoryName: string; - repositoryDescription: string; + repositoryDescription?: string; } - + interface FioriLaunchpadConfiguration { semanticObject: string; action: string; title: string; - subtitle: string; + subtitle?: string; } - + export interface AppContentConfig { metadata: Metadata; serviceBindingDetails: ServiceBindingDetails; @@ -137,4 +137,3 @@ export interface AppContentConfig { deploymentDetails: DeploymentDetails; fioriLaunchpadConfiguration: FioriLaunchpadConfiguration; } - \ No newline at end of file diff --git a/packages/bsp-app-download-sub-generator/src/prompts/prompt-helpers.ts b/packages/bsp-app-download-sub-generator/src/prompts/prompt-helpers.ts index b8f5d4892f..b28bf2b09e 100644 --- a/packages/bsp-app-download-sub-generator/src/prompts/prompt-helpers.ts +++ b/packages/bsp-app-download-sub-generator/src/prompts/prompt-helpers.ts @@ -6,6 +6,8 @@ import type { AppInfo } from '../app/types'; import { PromptNames } from '../app/types'; import { PromptState } from './prompt-state'; import type { BspAppDownloadAnswers, AppItem } from '../app/types'; +import { t } from '../utils/i18n'; +import BspAppDownloadLogger from '../utils/logger'; /** * Returns the details for the YUI prompt. @@ -26,32 +28,36 @@ export function getYUIDetails(): { name: string; description: string }[] { * * @param {AppIndex} appList - List of applications retrieved from the system. * @returns {Array<{ name: string; value: AppInfo }>} The formatted choices for selection. - * @throws Will throw an error if any required fields are missing. */ export const formatAppChoices = (appList: AppIndex): Array<{ name: string; value: AppInfo }> => { - return appList.map((app: AppItem) => { - // Check if any required fields are missing - if ( - !app['sap.app/id'] || - !app['sap.app/title'] || - !app['sap.app/description'] || - !app['repoName'] || - !app['url'] - ) { - throw new Error(`Required fields are missing for app: ${JSON.stringify(app)}`); - } - - return { - name: app['sap.app/id'], - value: { - appId: app['sap.app/id'], - title: app['sap.app/title'], - description: app['sap.app/description'] as string, - repoName: app['repoName'] as string, - url: app['url'] + return appList + .filter((app: AppItem) => { + const hasRequiredFields = app['sap.app/id'] && app['sap.app/title'] && app['repoName'] && app['url']; + if (!hasRequiredFields) { + BspAppDownloadLogger.logger?.error(t('error.requiredFieldsMissing', { app: JSON.stringify(app) })); } - }; - }); + return hasRequiredFields; + }) + .map((app) => { + // cast to string because TypeScript doesn't automatically know at the point that these fields are defined + // after filtering out invalid apps. + const id = app['sap.app/id'] as string; + const title = app['sap.app/title'] as string; + const description = (app['sap.app/description'] ?? '') as string; + const repoName = app.repoName as string; + const url = app.url as string; + + return { + name: id, + value: { + appId: id, + title, + description, + repoName, + url + } + }; + }); }; /** @@ -65,7 +71,7 @@ async function getAppList(provider: AbapServiceProvider, log?: Logger): Promise< try { return await provider.getAppIndex().search(appListSearchParams, appListResultFields); } catch (error) { - log?.error(`Error fetching application list: ${error.message}`); + log?.error(t('error.applicationListFetchError', { error: error.message })); return []; } } diff --git a/packages/bsp-app-download-sub-generator/src/prompts/prompts.ts b/packages/bsp-app-download-sub-generator/src/prompts/prompts.ts index 885b99bfa6..d7e25ea7e1 100644 --- a/packages/bsp-app-download-sub-generator/src/prompts/prompts.ts +++ b/packages/bsp-app-download-sub-generator/src/prompts/prompts.ts @@ -20,13 +20,13 @@ const getTargetFolderPrompt = (appRootPath?: string): FileBrowserQuestion Boolean(answers?.selectedApp?.appId), guiOptions: { applyDefaultWhenDirty: true, mandatory: true, - breadcrumb: t('prompts.targetPath.targetPathBreadcrumb') + breadcrumb: t('prompts.targetPath.breadcrumb') }, validate: async (target, answers: BspAppDownloadAnswers): Promise => { return await validateFioriAppTargetFolder(target, answers.selectedApp.appId, true); @@ -63,11 +63,11 @@ export async function getPrompts(appRootPath?: string, log?: Logger): Promise (appList.length ? formatAppChoices(appList) : []), - validate: (): string | boolean => appList.length ? true : t('prompts.appSelection.noAppsDeployed') + validate: (): string | boolean => (appList.length ? true : t('prompts.appSelection.noAppsDeployed')) } ]; @@ -75,4 +75,4 @@ export async function getPrompts(appRootPath?: string, log?: Logger): Promise for more details", - "invalidMetadataPackage": "Invalid or missing package in metadata", - "invalidServiceName": "Invalid or missing serviceName in serviceBindingDetails", - "invalidServiceVersion": "Invalid or missing serviceVersion in serviceBindingDetails", - "invalidMainEntityName": "Invalid or missing mainEntityName in serviceBindingDetails", - "invalidModuleName": "Invalid or missing moduleName in serviceBindingDetails", - "invalidRepositoryName": "Invalid or missing repositoryName in serviceBindingDetails", "appContentJsonNotFound": "{{- jsonFileName }} not found in the downloaded app", - "npmInstall": "Error in install phase: {{- error}}", - "skippedInstallation": "Option `--skipInstall` was specified. Installation of dependencies will be skipped.", - "replaceWebappFilesError": "Error replacing files in the downloaded app: {{- error}}" + "replaceWebappFilesError": "Error replacing files in the downloaded app: {{- error}}", + "requiredFieldsMissing": "Required fields are missing for app: {{- app }}. Check if the app is deployed correctly", + "applicationListFetchError": "Error fetching application list: {{- error}}", + "metadatafetchError": "Error fetching metadata: {{- error}}", + "appConfigGenError": "Error generating application configuration: {{- error}}", + "validationErrors": { + "invalidMetadataPackage": "Invalid or missing package in metadata", + "invalidServiceName": "Invalid or missing serviceName in serviceBindingDetails", + "invalidServiceVersion": "Invalid or missing serviceVersion in serviceBindingDetails", + "invalidMainEntityName": "Invalid or missing mainEntityName in serviceBindingDetails", + "invalidModuleName": "Invalid or missing moduleName in serviceBindingDetails", + "invalidRepositoryName": "Invalid or missing repositoryName in serviceBindingDetails" + }, + "installationErrors": { + "npmInstall": "Error in install phase: {{- error}}", + "skippedInstallation": "Option `--skipInstall` was specified. Installation of dependencies will be skipped." + }, + "appDownloadErrors": { + "downloadedFileNotBufferError": "Error: The downloaded file is not a Buffer.", + "appDownloadFailure": "Error downloading app: {{- error}}", + "zipExtractionError": "Error extracting zip: {{- error}}" + }, + "eventHookErrors": { + "vscodeInstanceMissing": "Error: Missing VSCode instance in event hook", + "postGenCommandMissing": "Error: Missing postGenCommand in event hook", + "commandExecutionFailed": "Error executing postGenCommand in event hook: {{- error}}" + }, + "readManifestErrors": { + "manifestFileNotFound": "Error: Manifest file not found in the downloaded app", + "readManifestFailed": "Error: Failed to read manifest file", + "sapAppNotDefined": "Error: sap.app not defined in the manifest file", + "sourceTemplateNotSupported": "Error: Source template not supported" + } }, "warn": { "extractedFileNotFound": "Extracted file not found - {{- extractedFilePath}}" @@ -19,15 +43,19 @@ "prompts": { "appSelection": { "message": "App", - "hint": "Select the app to download" + "hint": "Select the app to download", + "breadcrumb": "App" }, "targetPath": { - "targetPathMessage": "Project folder path", - "targetPathBreadcrumb": "Project Path" + "message": "Project folder path", + "breadcrumb": "Project Path" } }, "readMe": { "appDescription" : "This application was converted from an ABAP basic app that was deployed from ADT", "launchText": "In order to launch the generated app, simply run the following from the generated app root folder:\n\n```\n npm start\n```" + }, + "success": { + "bspAppDownloadCompleteMsg": "The selected application has been downloaded and updated to support SAP Fiori tools" } } diff --git a/packages/bsp-app-download-sub-generator/src/utils/constants.ts b/packages/bsp-app-download-sub-generator/src/utils/constants.ts index 6520bbface..9cccb106c4 100644 --- a/packages/bsp-app-download-sub-generator/src/utils/constants.ts +++ b/packages/bsp-app-download-sub-generator/src/utils/constants.ts @@ -11,7 +11,13 @@ export const adtSourceTemplateId = '@sap.adt.sevicebinding.deploy:lrop'; // Default initial answers to use as a fallback. export const defaultAnswers: BspAppDownloadAnswers = { - [PromptNames.systemSelection]: {}, // Empty or default values for system selection + [PromptNames.systemSelection]: { + system: { + name: '', + url: '' + }, + type: 'backendSystem' + }, [PromptNames.selectedApp]: { appId: '', title: '', diff --git a/packages/bsp-app-download-sub-generator/src/utils/download-utils.ts b/packages/bsp-app-download-sub-generator/src/utils/download-utils.ts index f185aaddf3..dd74bf4cd2 100644 --- a/packages/bsp-app-download-sub-generator/src/utils/download-utils.ts +++ b/packages/bsp-app-download-sub-generator/src/utils/download-utils.ts @@ -1,9 +1,10 @@ import type { AbapServiceProvider } from '@sap-ux/axios-extension'; import AdmZip from 'adm-zip'; -import type { Logger } from '@sap-ux/logger'; import { join } from 'path'; import type { Editor } from 'mem-fs-editor'; import { PromptState } from '../prompts/prompt-state'; +import { t } from './i18n'; +import BspAppDownloadLogger from '../utils/logger'; /** * Extracts a ZIP archive to a temporary directory. @@ -11,21 +12,20 @@ import { PromptState } from '../prompts/prompt-state'; * @param {string} extractedProjectPath - The path where the archive should be extracted. * @param {Buffer} archive - The ZIP archive buffer. * @param {Editor} fs - The file system editor. - * @param {Logger} [log] - The logger instance. */ -async function extractZip(extractedProjectPath: string, archive: Buffer, fs: Editor, log?: Logger): Promise { +async function extractZip(extractedProjectPath: string, archive: Buffer, fs: Editor): Promise { try { const zip = new AdmZip(archive); zip.getEntries().forEach(function (zipEntry) { if (!zipEntry.isDirectory) { // Extract the file content const fileContent = zipEntry.getData().toString('utf8'); - // Add the file content to mem-fs at a virtual path + // Load the file content into mem-fs for use in the temporary extracted project directory fs.write(join(extractedProjectPath, zipEntry.entryName), fileContent); } }); } catch (error) { - log?.error(`Error extracting zip: ${error.message}`); + BspAppDownloadLogger.logger?.error(t('error.appDownloadErrors.zipExtractionError', { error: error.message })); } } @@ -35,25 +35,18 @@ async function extractZip(extractedProjectPath: string, archive: Buffer, fs: Edi * @param {string} repoName - The repository name of the application. * @param {string} extractedProjectPath - The path where the application should be extracted. * @param {Editor} fs - The file system editor. - * @param {Logger} [log] - The logger instance. - * @throws {Error} If the file download fails. */ -export async function downloadApp( - repoName: string, - extractedProjectPath: string, - fs: Editor, - log?: Logger -): Promise { +export async function downloadApp(repoName: string, extractedProjectPath: string, fs: Editor): Promise { try { const serviceProvider = PromptState.systemSelection?.connectedSystem?.serviceProvider as AbapServiceProvider; const archive = await serviceProvider.getUi5AbapRepository().downloadFiles(repoName); if (Buffer.isBuffer(archive)) { - await extractZip(extractedProjectPath, archive, fs, log); + await extractZip(extractedProjectPath, archive, fs); } else { - log?.error('Error: The downloaded file is not a Buffer.'); + BspAppDownloadLogger.logger?.error(t('error.appDownloadErrors.downloadedFileNotBufferError')); } } catch (error) { - throw Error(`Error downloading file: ${error.message}`); + BspAppDownloadLogger.logger?.error(t('error.appDownloadErrors.appDownloadFailure', { error: error.message })); } } diff --git a/packages/bsp-app-download-sub-generator/src/utils/event-hook.ts b/packages/bsp-app-download-sub-generator/src/utils/event-hook.ts index 2dea3fb72d..d004dd8b56 100644 --- a/packages/bsp-app-download-sub-generator/src/utils/event-hook.ts +++ b/packages/bsp-app-download-sub-generator/src/utils/event-hook.ts @@ -20,22 +20,21 @@ export interface BspAppGenContext { * Runs the specified command in the context of the generated project, typically for tasks like refreshing or reloading the project in the editor. * * @param {BspAppGenContext} context - The context containing the project path, post-generation command, and optional VSCode instance. - * @throws {Error} If the VSCode instance or post-generation command is missing from the context. */ export async function runPostAppGenHook(context: BspAppGenContext): Promise { try { // Ensure that context has necessary values before proceeding if (!context.vscodeInstance) { - throw new Error('VSCode instance is missing.'); + BspAppDownloadLogger.logger?.error(t('error.eventHookErrors.vscodeInstanceMissing')); } - if (!context.postGenCommand) { - throw new Error('Post generation command is missing.'); + if (!context.postGenCommand || context.postGenCommand.trim() === '') { + BspAppDownloadLogger.logger?.error(t('error.eventHookErrors.postGenCommandMissing')); } // Execute the post-generation command await context.vscodeInstance?.commands?.executeCommand?.(context.postGenCommand, { fsPath: context.path }); } catch (e) { - BspAppDownloadLogger.logger.error(t('error.postGenCommand', { error: e })); + BspAppDownloadLogger.logger?.error(t('error.eventHookErrors.commandExecutionFailed', e.message)); } } diff --git a/packages/bsp-app-download-sub-generator/src/utils/file-helpers.ts b/packages/bsp-app-download-sub-generator/src/utils/file-helpers.ts index e497a6d7eb..cac61092f3 100644 --- a/packages/bsp-app-download-sub-generator/src/utils/file-helpers.ts +++ b/packages/bsp-app-download-sub-generator/src/utils/file-helpers.ts @@ -1,28 +1,69 @@ import { adtSourceTemplateId } from './constants'; import { join } from 'path'; import type { Editor } from 'mem-fs-editor'; -import { FileName, type Manifest } from '@sap-ux/project-access'; +import { FileName, DirName, type Manifest } from '@sap-ux/project-access'; import { t } from './i18n'; +import BspAppDownloadLogger from './logger'; /** * Reads and validates the `manifest.json` file. * * @param {string} extractedProjectPath - The path to the extracted project. * @param {Editor} fs - The file system editor. - * @returns {Promise} The validated manifest object. - * @throws {Error} If the manifest file is missing or invalid. + * @returns {Manifest} The validated manifest object. */ -export async function readManifest(extractedProjectPath: string, fs: Editor): Promise { +export function readManifest(extractedProjectPath: string, fs: Editor): Manifest { const manifestPath = join(extractedProjectPath, FileName.Manifest); + if (!fs.exists(manifestPath)) { + BspAppDownloadLogger.logger?.error(t('error.readManifestErrors.manifestFileNotFound')); + } const manifest = fs.readJSON(manifestPath) as unknown as Manifest; if (!manifest) { - throw Error(t('error.manifestNotFound')); + BspAppDownloadLogger.logger?.error(t('error.readManifestErrors.readManifestFailed')); } - if (!manifest['sap.app']) { - throw Error(t('error.sapAppNotDefined')); + if (!manifest?.['sap.app']) { + BspAppDownloadLogger.logger?.error(t('error.readManifestErrors.sapAppNotDefined')); } - if (manifest['sap.app'].sourceTemplate?.id !== adtSourceTemplateId) { - throw Error(t('error.sourceTemplateNotSupported')); + if (manifest?.['sap.app']?.sourceTemplate?.id !== adtSourceTemplateId) { + BspAppDownloadLogger.logger?.error(t('error.readManifestErrors.sourceTemplateNotSupported')); } return manifest; } + +/** + * Replaces the specified files in the `webapp` directory with the corresponding files from the `extractedPath`. + * + * @param {string} projectPath - The path to the downloaded App. + * @param {string} extractedPath - The path from which files will be copied. + * @param {Editor} fs - The file system editor instance to modify files in memory. + */ +export async function replaceWebappFiles( + projectPath: string, + extractedPath: string, + fs: Editor +): Promise { + try { + const webappPath = join(projectPath, DirName.Webapp); + // Define the paths of the files to be replaced + const filesToReplace = [ + { webappFile: FileName.Manifest, extractedFile: FileName.Manifest }, + { webappFile: 'i18n/i18n.properties', extractedFile: 'i18n.properties' }, // replace 'i18n/i18n.properties' in extractedFile + { webappFile: 'index.html', extractedFile: 'index.html' } + ]; + + // Loop through each file and perform the replacement + for (const { webappFile, extractedFile } of filesToReplace) { + const webappFilePath = join(webappPath, webappFile); + const extractedFilePath = join(extractedPath, extractedFile); + + // Check if the extracted file exists before replacing + if (fs.exists(extractedFilePath)) { + fs.copy(extractedFilePath, webappFilePath); + } else { + BspAppDownloadLogger.logger?.warn(t('warn.extractedFileNotFound', { extractedFilePath })); + } + } + } catch (error) { + BspAppDownloadLogger.logger?.error(t('error.replaceWebappFilesError', { error })); + } +} \ No newline at end of file diff --git a/packages/bsp-app-download-sub-generator/src/utils/validate-app-content-json.ts b/packages/bsp-app-download-sub-generator/src/utils/validate-app-content-json.ts index f9a053c70b..e49de62ae8 100644 --- a/packages/bsp-app-download-sub-generator/src/utils/validate-app-content-json.ts +++ b/packages/bsp-app-download-sub-generator/src/utils/validate-app-content-json.ts @@ -1,17 +1,16 @@ -import type { Logger } from '@sap-ux/logger'; import { t } from '../utils/i18n'; import type { AppContentConfig } from '../app/types'; +import BspAppDownloadLogger from '../utils/logger'; /** * Validates the metadata section of the app configuration. * * @param {AppContentConfig['metadata']} metadata - The metadata object. - * @param {Logger} log - The logger instance. * @returns {boolean} - Returns true if valid, false otherwise. */ -const validateMetadata = (metadata: AppContentConfig['metadata'], log?: Logger): boolean => { +const validateMetadata = (metadata: AppContentConfig['metadata']): boolean => { if (!metadata.package || typeof metadata.package !== 'string') { - log?.error(t('error.invalidMetadataPackage')); + BspAppDownloadLogger.logger?.error(t('error.invalidMetadataPackage')); return false; } return true; @@ -21,23 +20,19 @@ const validateMetadata = (metadata: AppContentConfig['metadata'], log?: Logger): * Validates the service binding details section of the app configuration. * * @param {AppContentConfig['serviceBindingDetails']} serviceBinding - The service binding details object. - * @param {Logger} log - The logger instance. * @returns {boolean} - Returns true if valid, false otherwise. */ -const validateServiceBindingDetails = ( - serviceBinding: AppContentConfig['serviceBindingDetails'], - log?: Logger -): boolean => { +const validateServiceBindingDetails = (serviceBinding: AppContentConfig['serviceBindingDetails']): boolean => { if (!serviceBinding.serviceName || typeof serviceBinding.serviceName !== 'string') { - log?.error(t('error.invalidServiceName')); + BspAppDownloadLogger.logger?.error(t('error.invalidServiceName')); return false; } if (!serviceBinding.serviceVersion || typeof serviceBinding.serviceVersion !== 'string') { - log?.error(t('error.invalidServiceVersion')); + BspAppDownloadLogger.logger?.error(t('error.invalidServiceVersion')); return false; } if (!serviceBinding.mainEntityName || typeof serviceBinding.mainEntityName !== 'string') { - log?.error(t('error.invalidMainEntityName')); + BspAppDownloadLogger.logger?.error(t('error.invalidMainEntityName')); return false; } return true; @@ -47,12 +42,11 @@ const validateServiceBindingDetails = ( * Validates the project attribute section of the app configuration. * * @param {AppContentConfig['projectAttribute']} projectAttribute - The project attribute object. - * @param {Logger} log - The logger instance. * @returns {boolean} - Returns true if valid, false otherwise. */ -const validateProjectAttribute = (projectAttribute: AppContentConfig['projectAttribute'], log?: Logger): boolean => { +const validateProjectAttribute = (projectAttribute: AppContentConfig['projectAttribute']): boolean => { if (!projectAttribute.moduleName || typeof projectAttribute.moduleName !== 'string') { - log?.error(t('error.invalidModuleName')); + BspAppDownloadLogger.logger?.error(t('error.invalidModuleName')); return false; } return true; @@ -62,12 +56,11 @@ const validateProjectAttribute = (projectAttribute: AppContentConfig['projectAtt * Validates the deployment details section of the app configuration. * * @param {AppContentConfig['deploymentDetails']} deploymentDetails - The deployment details object. - * @param {Logger} log - The logger instance. * @returns {boolean} - Returns true if valid, false otherwise. */ -const validateDeploymentDetails = (deploymentDetails: AppContentConfig['deploymentDetails'], log?: Logger): boolean => { +const validateDeploymentDetails = (deploymentDetails: AppContentConfig['deploymentDetails']): boolean => { if (!deploymentDetails.repositoryName) { - log?.error(t('error.invalidRepositoryName')); + BspAppDownloadLogger.logger?.error(t('error.invalidRepositoryName')); return false; } return true; @@ -77,14 +70,13 @@ const validateDeploymentDetails = (deploymentDetails: AppContentConfig['deployme * Validates the entire app configuration. * * @param {AppContentConfig} config - The app configuration object. - * @param {Logger} log - The logger instance. * @returns {boolean} - Returns true if the configuration is valid, false otherwise. */ -export const validateAppContentJsonFile = (config: AppContentConfig, log?: Logger): boolean => { +export const validateAppContentJsonFile = (config: AppContentConfig): boolean => { return ( - validateMetadata(config.metadata, log) && - validateServiceBindingDetails(config.serviceBindingDetails, log) && - validateProjectAttribute(config.projectAttribute, log) && - validateDeploymentDetails(config.deploymentDetails, log) + validateMetadata(config.metadata) && + validateServiceBindingDetails(config.serviceBindingDetails) && + validateProjectAttribute(config.projectAttribute) && + validateDeploymentDetails(config.deploymentDetails) ); }; diff --git a/packages/bsp-app-download-sub-generator/test/app.test.ts b/packages/bsp-app-download-sub-generator/test/app.test.ts new file mode 100644 index 0000000000..63100674e6 --- /dev/null +++ b/packages/bsp-app-download-sub-generator/test/app.test.ts @@ -0,0 +1,144 @@ +import yeomanTest from 'yeoman-test'; +import { AppWizard } from '@sap-devx/yeoman-ui-types'; +import { join } from 'path'; +import BspAppDownloadGenerator from '../src/app'; +import * as prompts from '../src/prompts/prompts'; +import type { BspAppDownloadQuestions } from '../src/app/types'; +import { PromptNames } from '../src/app/types'; +import { readManifest } from '../src/utils/file-helpers'; +import fs from 'fs'; +import { getAppConfig } from '../src/app/config'; +import { OdataVersion } from '@sap-ux/odata-service-inquirer'; +import { TemplateType, type FioriElementsApp, type LROPSettings } from '@sap-ux/fiori-elements-writer'; +import { adtSourceTemplateId } from '../src/utils/constants'; + +jest.mock('../src/utils/file-helpers'); +jest.mock('../src/app/config'); + +describe('BSP App Download', () => { + const bspAppDownloadGenPath = join(__dirname, '../src/app/index.ts'); + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + jest.resetModules(); + }) + + beforeEach(() => { + const promptSpy = jest.spyOn(prompts, 'getPrompts').mockResolvedValue([ + { + type: 'list', + name: PromptNames.systemSelection, + message: 'Select a system', + choices: [ + { name: 'system1', value: 'system1' }, + { name: 'system2', value: 'system2' }, + { name: 'system3', value: 'system3' } + ] + }, + { + type: 'list', + name: PromptNames.selectedApp, + message: 'Select an app', + choices: [ + { name: 'App 1', value: { appId: 'app-1', title: 'App 1', description: 'App 1 description', repoName: 'app-1-repo', url: 'url-1' } }, + { name: 'App 2', value: { appId: 'app-2', title: 'App 2', description: 'App 2 description', repoName: 'app-2-repo', url: 'url-2' } } + ] + }, + { + type: 'input', + name: PromptNames.targetFolder, + message: 'Enter the target folder', + default: 'target-folder' + } + ] as any + ); + }); + + test('run bsp app download', async () => { + + const metadata = fs.readFileSync(join(__dirname, 'fixtures', 'metadata.xml'), 'utf8'); + // const appWizard: Partial = { + // setHeaderTitle: jest.fn(), + // showWarning: jest.fn(), + // showError: jest.fn(), + // showInformation: jest.fn() + // }; + const appConfig: FioriElementsApp = { + app: { + id: 'app-1', + title: 'App 1', + description: 'App 1 description', + sourceTemplate: { + id: adtSourceTemplateId + }, + projectType: 'EDMXBackend', + flpAppId: `app-1-tile` + }, + package: { + name: 'app-1', + description: 'App 1 description', + devDependencies: {}, + scripts: {}, + version: '0.0.1' + }, + template: { + type: TemplateType.ListReportObjectPage, + settings: { + entityConfig: { + mainEntityName: 'Booking' + } + } + }, + service: { + path: '/sap/opu/odata4/sap/zsb_travel_draft/srvd/dmo/ui_travel_d_d/0001/', + version: OdataVersion.v4, + metadata: metadata, + url: 'url-1' + }, + appOptions: { + addAnnotations: true, + addTests: true + }, + ui5: { + version: '1.88.0' + } + }; + + + // (readManifest as jest.Mock).mockReturnValue({ + // 'sap.app': { + // sourceTemplate: { + // id: 'id' + // }, + // dataSources: { + // mainService: { + // uri: "/sap/opu/odata4/sap/zsb_travel_draft/srvd/dmo/ui_travel_d_d/0001/", + // type: "OData", + // settings: { + // odataVersion: "4.0" + // } + // } + // } + // } + // }) + (getAppConfig as jest.Mock).mockResolvedValue(appConfig); + await yeomanTest + .run(BspAppDownloadGenerator, { + resolved: bspAppDownloadGenPath + }) + .cd('.') + .withPrompts({ + systemSelection: 'system3', + selectedApp: { + appId: 'app-1', + title: 'App 1', + description: 'App 1 description', + repoName: 'app-1-repo', + url: 'url-1' + }, + targetFolder: 'target-folder' + }) + }); +}); + +//index.ts | 58.02 | 32.25 | 33.33 | 58.02 | 120-125,155-320 \ No newline at end of file diff --git a/packages/bsp-app-download-sub-generator/test/config.test.ts b/packages/bsp-app-download-sub-generator/test/config.test.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/bsp-app-download-sub-generator/test/fixtures/metadata.xml b/packages/bsp-app-download-sub-generator/test/fixtures/metadata.xml new file mode 100644 index 0000000000..29fb8b020e --- /dev/null +++ b/packages/bsp-app-download-sub-generator/test/fixtures/metadata.xml @@ -0,0 +1,553 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CpWfHandle + RetentionTime + CpWfDefId + PaWfDefId + Consumer + LastChangeOn + Context + CallbackClass + + + + + + + + + + + + + + + __OperationControl + + + + + + + + + __OperationControl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eq + ne + gt + ge + lt + le + and + or + contains + startswith + endswith + any + all + + + + + application/json + application/pdf + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/bsp-app-download-sub-generator/test/prompts/prompt-helpers.test.ts b/packages/bsp-app-download-sub-generator/test/prompts/prompt-helpers.test.ts new file mode 100644 index 0000000000..da15c3a49e --- /dev/null +++ b/packages/bsp-app-download-sub-generator/test/prompts/prompt-helpers.test.ts @@ -0,0 +1,143 @@ +import { fetchAppListForSelectedSystem, formatAppChoices, getYUIDetails } from '../../src/prompts/prompt-helpers'; +import { PromptNames, BspAppDownloadAnswers, AppItem } from '../../src/app/types'; +import { PromptState } from '../../src/prompts/prompt-state'; +import type { Logger } from '@sap-ux/logger'; +import type { AbapServiceProvider, AppIndex } from '@sap-ux/axios-extension'; +import { generatorTitle, generatorDescription } from '../../src/utils/constants'; +import { t } from '../../src/utils/i18n'; +import BspAppDownloadLogger from '../../src/utils/logger'; + +jest.mock('../../src/utils/logger', () => ({ + logger: { + error: jest.fn() + } +})); + +describe('fetchAppListForSelectedSystem', () => { + const mockServiceProvider = { + getAppIndex: jest.fn().mockReturnValue({ + search: jest.fn().mockResolvedValue([{ id: 'app1' }, { id: 'app2' }]) + }) + } as unknown as AbapServiceProvider; + + const mockLogger = { + error: jest.fn() + } as unknown as Logger; + + const mockAnswers: BspAppDownloadAnswers = { + [PromptNames.systemSelection]: { + system: { + name: 'mockSystemName', + url: 'mockUrl', + client: 'mockClient', + userDisplayName: 'mockUserDisplayName', + authenticationType: 'basic' + }, + type: 'backendSystem' + }, + [PromptNames.selectedApp]: { + appId: 'mockAppId', + title: 'mockTitle', + description: 'mockDescription', + repoName: 'mockRepoName', + url: 'mockUrl' + }, + [PromptNames.targetFolder]: 'mockTargetFolder' + }; + + it('should fetch the application list when systemSelection and serviceProvider are provided', async () => { + const result = await fetchAppListForSelectedSystem(mockAnswers, mockServiceProvider, mockLogger); + + expect(mockServiceProvider.getAppIndex().search).toHaveBeenCalledWith( + expect.anything(), + expect.anything() + ); + expect(result).toEqual([{ id: 'app1' }, { id: 'app2' }]); + expect(PromptState.systemSelection).toEqual({ + connectedSystem: { serviceProvider: mockServiceProvider } + }); + }); + + it('should return an empty array when systemSelection is not provided', async () => { + const result = await fetchAppListForSelectedSystem({} as BspAppDownloadAnswers, mockServiceProvider, mockLogger); + expect(result).toEqual([]); + }); + + it('should return an empty array when serviceProvider is not provided', async () => { + const result = await fetchAppListForSelectedSystem(mockAnswers, undefined, mockLogger); + expect(result).toEqual([]); + }); + + it('should log an error if getAppList throws an error', async () => { + const error = new Error('Mock error'); + mockServiceProvider.getAppIndex().search = jest.fn().mockRejectedValue(error); + const result = await fetchAppListForSelectedSystem(mockAnswers, mockServiceProvider, mockLogger); + expect(mockLogger.error).toHaveBeenCalledWith(t('error.applicationListFetchError', { error: error.message })); + expect(result).toEqual([]); + }); +}); + +describe('formatAppChoices', () => { + const validApp: AppItem = { + 'sap.app/id': 'app1', + 'sap.app/title': 'App 1', + 'sap.app/description': 'Description for App 1', + 'repoName': 'repo1', + 'url': 'http://mock-url.com/app1' + }; + + const invalidApp: AppItem = { + 'sap.app/id': 'app2', + 'sap.app/title': 'App 2', + 'repoName': '', // no repo name + 'url': 'http://mock-url.com/app2' + }; + + it('should format valid app list correctly', () => { + const appList: AppIndex = [validApp]; + const result = formatAppChoices(appList); + + expect(result).toEqual([ + { + name: 'app1', + value: { + appId: 'app1', + title: 'App 1', + description: 'Description for App 1', + repoName: 'repo1', + url: 'http://mock-url.com/app1' + } + } + ]); + }); + + it('should log error if required fields are missing', () => { + const appList: AppIndex = [invalidApp]; + const result = formatAppChoices(appList); + expect(BspAppDownloadLogger.logger.error).toBeCalledWith( t('error.requiredFieldsMissing', { app: JSON.stringify(appList) }) ); + }); + + it('should handle a mix of valid and invalid apps by throwing an error', () => { + const appList: AppIndex = [validApp, invalidApp]; + const result = formatAppChoices(appList); + expect(BspAppDownloadLogger.logger.error).toBeCalledWith( t('error.requiredFieldsMissing', { app: JSON.stringify(appList) }) ); + }); + + it('should return an empty array if the app list is empty', () => { + const appList: AppIndex = []; + const result = formatAppChoices(appList); + expect(result).toEqual([]); + }); +}); + +describe('getYUIDetails', () => { + it('should return an array with the correct name and description', () => { + const result = getYUIDetails(); + expect(result).toEqual([ + { + name: generatorTitle, + description: generatorDescription + } + ]); + }); +}); \ No newline at end of file diff --git a/packages/bsp-app-download-sub-generator/test/prompts/prompt-state.test.ts b/packages/bsp-app-download-sub-generator/test/prompts/prompt-state.test.ts new file mode 100644 index 0000000000..ed1f3e7fe8 --- /dev/null +++ b/packages/bsp-app-download-sub-generator/test/prompts/prompt-state.test.ts @@ -0,0 +1,37 @@ +import { PromptState } from '../../src/prompts/prompt-state'; +import type { SystemSelectionAnswers } from '../../src/app/types'; +import type { AbapServiceProvider } from '@sap-ux/axios-extension'; + +describe('PromptState', () => { + + const mockServiceProvider = { + getAppIndex: jest.fn().mockReturnValue({ + search: jest.fn().mockResolvedValue([{ id: 'app1' }, { id: 'app2' }]) + }) + } as unknown as AbapServiceProvider; + let mockSystemSelection: SystemSelectionAnswers; + beforeEach(() => { + mockSystemSelection = { + connectedSystem: { + serviceProvider: mockServiceProvider + } + } + }); + + afterEach(() => { + PromptState.reset(); + }); + + it('should set the state of systemSelection', () => { + PromptState.systemSelection = mockSystemSelection; + expect(PromptState.systemSelection).toEqual(mockSystemSelection); + }); + + it('should reset systemSelection to an empty object', () => { + PromptState.systemSelection = mockSystemSelection; + expect(PromptState.systemSelection).toEqual(mockSystemSelection); + + PromptState.reset(); + expect(PromptState.systemSelection).toEqual({}); + }); +}); \ No newline at end of file diff --git a/packages/bsp-app-download-sub-generator/test/prompts/prompts.test.ts b/packages/bsp-app-download-sub-generator/test/prompts/prompts.test.ts new file mode 100644 index 0000000000..ba547e2067 --- /dev/null +++ b/packages/bsp-app-download-sub-generator/test/prompts/prompts.test.ts @@ -0,0 +1,132 @@ +import { getPrompts } from '../../src/prompts/prompts'; +import { getSystemSelectionQuestions } from '@sap-ux/odata-service-inquirer'; +import { fetchAppListForSelectedSystem, formatAppChoices } from '../../src/prompts/prompt-helpers'; +import { PromptNames } from '../../src/app/types'; +import type { BspAppDownloadAnswers, BspAppDownloadQuestions } from '../../src/app/types'; +import type { Logger } from '@sap-ux/logger'; +import { join } from 'path'; +import { t } from '../../src/utils/i18n'; +import { validateFioriAppTargetFolder } from '@sap-ux/project-input-validator'; + +jest.mock('@sap-ux/odata-service-inquirer', () => ({ + getSystemSelectionQuestions: jest.fn() +})); + +jest.mock('@sap-ux/project-input-validator', () => ({ + validateFioriAppTargetFolder: jest.fn() +})); + +jest.mock('../../src/prompts/prompt-helpers', () => ({ + fetchAppListForSelectedSystem: jest.fn(), + formatAppChoices: jest.fn() +})); + +describe('getPrompts', () => { + let mockLogger: Logger; + const appRootPath = join('/mock/path'); + const mockAnswers = { + selectedApp: { appId: 'app1' } + } as unknown as BspAppDownloadAnswers; + const mockAppList = [{ appId: 'app1', name: 'Test App' }]; + + beforeEach(() => { + mockLogger = { error: jest.fn(), info: jest.fn(), warn: jest.fn() } as unknown as Logger; + (getSystemSelectionQuestions as jest.Mock).mockResolvedValue({ + prompts: [{ type: 'input', name: 'system' }], + answers: { connectedSystem: { serviceProvider: {} } } + }); + (fetchAppListForSelectedSystem as jest.Mock).mockResolvedValue([{ appId: 'app1', name: 'Test App' }]); + (formatAppChoices as jest.Mock).mockReturnValue(mockAppList); + }); + + it('should return system questions, app selection, and target folder prompts', async () => { + const prompts = await getPrompts(appRootPath, mockLogger); + expect(prompts.length).toBeGreaterThanOrEqual(2); + + // system prompts + const systemPrompt = prompts.find(p => p.name === 'system'); + expect(systemPrompt).toBeDefined(); + expect(systemPrompt?.type).toBe('input'); + expect(systemPrompt?.name).toBe('system'); + + // app selection prompts + const appSelectionPrompt = prompts.find(p => p.name === PromptNames.selectedApp) as BspAppDownloadQuestions; + expect(appSelectionPrompt).toBeDefined(); + if (typeof appSelectionPrompt?.when === 'function') { + await expect(appSelectionPrompt.when({ system: 'test' } as unknown as BspAppDownloadAnswers)).resolves.toBe(true); + }; + if (appSelectionPrompt?.type === 'list') { + const listPrompt = appSelectionPrompt as unknown as { choices: () => { name: string; value: string }[] }; + expect(listPrompt.choices()).toEqual(mockAppList); + }; + expect(appSelectionPrompt && appSelectionPrompt.validate && appSelectionPrompt.validate(mockAppList)).toBe(true); + expect(appSelectionPrompt?.guiOptions?.breadcrumb).toBe(t('prompts.appSelection.breadcrumb')); + + // target folder prompt + const targetFolderPrompt = prompts.find(p => p.name === PromptNames.targetFolder); + expect(targetFolderPrompt).toBeDefined(); + }); + + it('should handle no apps available scenario', async () => { + (fetchAppListForSelectedSystem as jest.Mock).mockResolvedValue([]); + + const prompts = await getPrompts(appRootPath, mockLogger); + + const appSelectionPrompt = prompts.find(p => p.name === PromptNames.selectedApp); + expect(appSelectionPrompt).toBeDefined(); + // no apps deployed message should be displayed + expect(appSelectionPrompt && appSelectionPrompt.validate && appSelectionPrompt.validate('')).toBe(t('prompts.appSelection.noAppsDeployed')); + if (appSelectionPrompt?.type === 'list') { + const listPrompt = appSelectionPrompt as unknown as { choices: () => { name: string; value: string }[] }; + expect(listPrompt.choices()).toEqual([]); + }; + + // target folder prompt should not be displayed + const targetFolderPrompt = prompts.find(p => p.name === PromptNames.targetFolder); + expect(targetFolderPrompt).toBeDefined(); + if (typeof targetFolderPrompt?.when === 'function') { + expect(targetFolderPrompt.when( {} as unknown as BspAppDownloadAnswers)).toBe(false); + }; + }); + + it('should validate the target folder path when it is valid', async () => { + // Mock validateFioriAppTargetFolder to return true (valid path) + (validateFioriAppTargetFolder as jest.Mock).mockResolvedValue(true); + const prompts = await getPrompts(appRootPath); + + // target folder prompt + const targetFolderPrompt = prompts.find(p => p.name === PromptNames.targetFolder); + expect(targetFolderPrompt).toBeDefined(); + const result = targetFolderPrompt !== undefined && targetFolderPrompt.validate ? await targetFolderPrompt.validate(appRootPath, mockAnswers) : undefined; + + // Assert that validation returns true + expect(result).toBe(true); + expect(validateFioriAppTargetFolder).toHaveBeenCalledWith(appRootPath, 'app1', true); + }); + + it('should return error message when the target folder path is invalid', async () => { + const errorMessage = `The project folder path already contains an SAP Fiori application in the folder: ${appRootPath}. Please choose a different folder and try again.`; + (validateFioriAppTargetFolder as jest.Mock).mockResolvedValue(errorMessage); + const prompts = await getPrompts(appRootPath); + + // target folder prompt + const targetFolderPrompt = prompts.find(p => p.name === PromptNames.targetFolder); + expect(targetFolderPrompt).toBeDefined(); + const result = targetFolderPrompt !== undefined && targetFolderPrompt.validate ? await targetFolderPrompt.validate(appRootPath, mockAnswers) : undefined; + + // Assert that validation returns the error message + expect(result).toBe(errorMessage); + expect(validateFioriAppTargetFolder).toHaveBeenCalledWith(appRootPath, 'app1', true); + }); + + it('should return default path when no target folder is provided', async () => { + const prompts = await getPrompts(appRootPath); + + // target folder prompt + const targetFolderPrompt = prompts.find(p => p.name === PromptNames.targetFolder); + expect(targetFolderPrompt).toBeDefined(); + const result = await targetFolderPrompt?.default(); + expect(result).toBe(appRootPath); + }); + +}); diff --git a/packages/bsp-app-download-sub-generator/test/utils/download-utils.test.ts b/packages/bsp-app-download-sub-generator/test/utils/download-utils.test.ts new file mode 100644 index 0000000000..ca2c445a20 --- /dev/null +++ b/packages/bsp-app-download-sub-generator/test/utils/download-utils.test.ts @@ -0,0 +1,96 @@ +import { downloadApp } from '../../src/utils/download-utils'; +import AdmZip from 'adm-zip'; +import { PromptState } from '../../src/prompts/prompt-state'; +import type { Logger } from '@sap-ux/logger'; +import type { Editor } from 'mem-fs-editor'; +import type { AbapServiceProvider } from '@sap-ux/axios-extension'; +import { join } from 'path'; +import { t } from '../../src/utils/i18n'; +import BspAppDownloadLogger from '../../src/utils/logger'; + +jest.mock('adm-zip'); +jest.mock('../../src/utils/logger', () => ({ + logger: { + error: jest.fn() + } +})); + +describe('download-utils', () => { + let mockFs: Editor; + let mockLog: Logger; + let mockServiceProvider: AbapServiceProvider; + + beforeEach(() => { + mockFs = { + write: jest.fn(), + } as unknown as Editor; + + mockLog = { + error: jest.fn(), + } as unknown as Logger; + + mockServiceProvider = { + getUi5AbapRepository: jest.fn().mockReturnValue({ + downloadFiles: jest.fn(), + }), + } as unknown as AbapServiceProvider; + + (PromptState.systemSelection as any) = { + connectedSystem: { + serviceProvider: mockServiceProvider, + }, + }; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + const extractedPath = join('path/to/extract'); + it('should download and extract the application files', async () => { + await downloadApp('repoName', extractedPath, mockFs); + expect(mockServiceProvider.getUi5AbapRepository().downloadFiles).toHaveBeenCalledWith('repoName'); + }); + + it('should log an error if the downloaded file is not a Buffer', async () => { + jest.spyOn(mockServiceProvider.getUi5AbapRepository(), 'downloadFiles').mockResolvedValue('not-a-buffer' as any); + await downloadApp('repoName', extractedPath, mockFs); + expect(BspAppDownloadLogger.logger.error).toBeCalledWith(t('error.appDownloadErrors.downloadedFileNotBufferError')); + }); + + it('should log an error if the download fails', async () => { + const errorMessage = 'Mock download error'; + jest.spyOn(mockServiceProvider.getUi5AbapRepository(), 'downloadFiles').mockRejectedValue(new Error(errorMessage)); + await downloadApp('repoName', extractedPath, mockFs) + expect(BspAppDownloadLogger.logger.error).toBeCalledWith(t('error.appDownloadErrors.appDownloadFailure', { error: errorMessage })); + }); + + it('should extract files from a ZIP archive and write them to the file system', async () => { + const appContents = 'app contents', appName = 'app-name'; + const mockZipEntry = { + isDirectory: false, + entryName: appName, + getData: jest.fn().mockReturnValue(Buffer.from(appContents)) + }; + const mockZip = { + getEntries: jest.fn().mockReturnValue([mockZipEntry]), + }; + (AdmZip as jest.Mock).mockImplementation(() => mockZip); + + jest.spyOn(mockServiceProvider.getUi5AbapRepository(), 'downloadFiles').mockResolvedValue(Buffer.from(appContents)); + await downloadApp('repoName', extractedPath, mockFs); + + expect(mockZip.getEntries).toHaveBeenCalled(); + expect(mockFs.write).toHaveBeenCalledWith(`${extractedPath}/${appName}`, appContents); + }); + + it('should log an error if extraction fails', async () => { + const errorMessage = 'Mock extraction error'; + (AdmZip as jest.Mock).mockImplementation(() => { + throw new Error(errorMessage); + }); + jest.spyOn(mockServiceProvider.getUi5AbapRepository(), 'downloadFiles').mockResolvedValue(Buffer.from('app contents')); + await downloadApp('repoName', extractedPath, mockFs); + expect(BspAppDownloadLogger.logger.error).toBeCalledWith(t('error.appDownloadErrors.zipExtractionError', { error: errorMessage })); + }); +}); \ No newline at end of file diff --git a/packages/bsp-app-download-sub-generator/test/utils/event-hook.test.ts b/packages/bsp-app-download-sub-generator/test/utils/event-hook.test.ts new file mode 100644 index 0000000000..a7da8c5f4b --- /dev/null +++ b/packages/bsp-app-download-sub-generator/test/utils/event-hook.test.ts @@ -0,0 +1,58 @@ +import { runPostAppGenHook, type BspAppGenContext } from '../../src/utils/event-hook'; +import type { VSCodeInstance } from '@sap-ux/fiori-generator-shared'; +import { t } from '../../src/utils/i18n'; +import BspAppDownloadLogger from '../../src/utils/logger'; + +jest.mock('../../src/utils/logger', () => ({ + logger: { + error: jest.fn() + } +})); + +describe('runPostAppGenHook', () => { + let mockContext: BspAppGenContext; + + beforeEach(() => { + mockContext = { + vscodeInstance: { + commands: { + executeCommand: jest.fn() + } + } as unknown as VSCodeInstance, + postGenCommand: 'mockCommand', + path: '/mock/path' + }; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should log an error if vscodeInstance is missing', async () => { + mockContext.vscodeInstance = undefined; + await runPostAppGenHook(mockContext); + expect(BspAppDownloadLogger.logger.error).toBeCalledWith(t('error.eventHookErrors.vscodeInstanceMissing')); + }); + + it('should log an error if postGenCommand is missing', async () => { + mockContext.postGenCommand = ''; + await runPostAppGenHook(mockContext); + expect(BspAppDownloadLogger.logger.error).toBeCalledWith(t('error.eventHookErrors.postGenCommandMissing')); + }); + + it('should execute the post-generation command successfully', async () => { + await runPostAppGenHook(mockContext); + expect(mockContext.vscodeInstance?.commands?.executeCommand).toHaveBeenCalledWith('mockCommand', { + fsPath: '/mock/path' + }); + }); + + it('should log an error if executeCommand throws an error', async () => { + const mockError = new Error('Command execution failed'); + if (mockContext.vscodeInstance) { + mockContext.vscodeInstance.commands.executeCommand = jest.fn().mockRejectedValue(mockError); + } + await runPostAppGenHook(mockContext); + expect(BspAppDownloadLogger.logger.error).toBeCalledWith(t('error.eventHookErrors.commandExecutionFailed')); + }); +}); \ No newline at end of file diff --git a/packages/bsp-app-download-sub-generator/test/utils/file-helpers.test.ts b/packages/bsp-app-download-sub-generator/test/utils/file-helpers.test.ts new file mode 100644 index 0000000000..3fec4a0990 --- /dev/null +++ b/packages/bsp-app-download-sub-generator/test/utils/file-helpers.test.ts @@ -0,0 +1,68 @@ +import { readManifest } from '../../src/utils/file-helpers'; +import type { Editor } from 'mem-fs-editor'; +import { t } from '../../src/utils/i18n'; +import { adtSourceTemplateId } from '../../src/utils/constants'; +import BspAppDownloadLogger from '../../src/utils/logger'; + +jest.mock('../../src/utils/logger', () => ({ + logger: { + error: jest.fn() + } +})); + +describe('readManifest', () => { + const mockReadJSON = jest.fn(); + const mockFs = { readJSON: mockReadJSON, exists: jest.fn() } as unknown as Editor; + const extractedProjectPath = 'project-path'; + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('should return manifest when valid manifest is read', async () => { + const validManifest = { + 'sap.app': { + id: 'test-app', + sourceTemplate: { + id: adtSourceTemplateId + } + } + }; + mockReadJSON.mockReturnValue(validManifest); + const result = readManifest(extractedProjectPath, mockFs); + expect(result).toBe(validManifest); + expect(mockFs.readJSON).toHaveBeenCalledWith('project-path/manifest.json'); + }); + + it('should throw an error if manifest is not found', async () => { + mockReadJSON.mockReturnValue(null); + readManifest(extractedProjectPath, mockFs) + expect(BspAppDownloadLogger.logger.error).toBeCalledWith(t('error.readManifestErrors.readManifestFailed')); + }); + + it('should throw an error if "sap.app" is not defined in the manifest', async () => { + const invalidManifestNoSapApp = { + // No 'sap.app' field + }; + // Mock fs readJSON function to return a manifest without 'sap.app' + mockReadJSON.mockReturnValue(invalidManifestNoSapApp); + readManifest(extractedProjectPath, mockFs) + expect(BspAppDownloadLogger.logger.error).toBeCalledWith(t('error.readManifestErrors.sapAppNotDefined')); + }); + + it('should throw an error if the sourceTemplate.id is not supported', async () => { + const invalidManifestWrongTemplate = { + 'sap.app': { + id: 'test-app', + sourceTemplate: { + id: 'wrong-template-id' + } + } + }; + // Mock fs readJSON function to return a manifest with an unsupported sourceTemplate.id + mockReadJSON.mockReturnValue(invalidManifestWrongTemplate); + readManifest(extractedProjectPath, mockFs) + expect(BspAppDownloadLogger.logger.error).toBeCalledWith(t('error.readManifestErrors.sourceTemplateNotSupported')); + }); +}); diff --git a/packages/bsp-app-download-sub-generator/test/utils/validate-app-content-json.test.ts b/packages/bsp-app-download-sub-generator/test/utils/validate-app-content-json.test.ts new file mode 100644 index 0000000000..599552608f --- /dev/null +++ b/packages/bsp-app-download-sub-generator/test/utils/validate-app-content-json.test.ts @@ -0,0 +1,112 @@ +import { validateAppContentJsonFile } from '../../src/utils/validate-app-content-json'; +import { AppContentConfig } from '../../src/app/types'; +import { t } from '../../src/utils/i18n'; +import BspAppDownloadLogger from '../../src/utils/logger'; + +jest.mock('../../src/utils/logger', () => ({ + logger: { + error: jest.fn() + } +})); + +describe('validateAppContentJsonFile', () => { + const validConfig: AppContentConfig = { + metadata: { package: 'valid-package' }, + serviceBindingDetails: { + serviceName: 'validService', + serviceVersion: '1.0.0', + mainEntityName: 'validEntity', + }, + projectAttribute: { moduleName: 'validModule' }, + deploymentDetails: { repositoryName: 'validRepository' }, + fioriLaunchpadConfiguration: { + semanticObject: 'semanticObject', + action: 'action', + title: 'title' + }, + }; + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should return true when all validation functions pass', () => { + const result = validateAppContentJsonFile(validConfig); + expect(result).toBe(true); + }); + + it('should return false and log an error when metadata validation fails', () => { + const invalidMetadataConfig = { + ...validConfig, + metadata: { package: '' } // Invalid package + } as unknown as AppContentConfig; + + const result = validateAppContentJsonFile(invalidMetadataConfig); + expect(result).toBe(false); + expect(BspAppDownloadLogger.logger.error).toBeCalledWith(t('error.invalidMetadataPackage')); + }); + + it('should return false and log an error when service binding details validation fails', () => { + const invalidServiceBindingConfig = { + ...validConfig, + serviceBindingDetails: { + ...validConfig.serviceBindingDetails, + serviceName: '', // Invalid service name + } + } as unknown as AppContentConfig; + + const result = validateAppContentJsonFile(invalidServiceBindingConfig); + expect(result).toBe(false); + expect(BspAppDownloadLogger.logger.error).toBeCalledWith(t('error.invalidServiceName')); + }); + + it('should return false and log an error when service binding version is not provided', () => { + const invalidServiceBindingConfig = { + ...validConfig, + serviceBindingDetails: { + ...validConfig.serviceBindingDetails, + serviceVersion: '' // Invalid service version + } + } as unknown as AppContentConfig; + + const result = validateAppContentJsonFile(invalidServiceBindingConfig); + expect(result).toBe(false); + expect(BspAppDownloadLogger.logger.error).toBeCalledWith(t('error.invalidServiceVersion')); + }); + + it('should return false and log an error when main entity name is missing', () => { + const invalidServiceBindingConfig = { + ...validConfig, + serviceBindingDetails: { + ...validConfig.serviceBindingDetails, + mainEntityName: '' // Invalid main entity name + } + } as unknown as AppContentConfig; + + const result = validateAppContentJsonFile(invalidServiceBindingConfig); + expect(result).toBe(false); + expect(BspAppDownloadLogger.logger.error).toBeCalledWith(t('error.invalidMainEntityName')); + }); + + it('should return false and log an error when project attribute validation fails', () => { + const invalidProjectAttributeConfig = { + ...validConfig, + projectAttribute: { moduleName: '' } // Invalid module name + } as unknown as AppContentConfig; + + const result = validateAppContentJsonFile(invalidProjectAttributeConfig); + expect(result).toBe(false); + expect(BspAppDownloadLogger.logger.error).toBeCalledWith(t('error.invalidModuleName')); + }); + + it('should return false and log an error when deployment details validation fails', () => { + const invalidDeploymentDetailsConfig = { + ...validConfig, + deploymentDetails: { repositoryName: '' } // Invalid repository name + } as unknown as AppContentConfig; + + const result = validateAppContentJsonFile(invalidDeploymentDetailsConfig); + expect(result).toBe(false); + expect(BspAppDownloadLogger.logger.error).toBeCalledWith(t('error.invalidRepositoryName')); + }); +}); From 2054cdc7137b1ffca707a04a32ad32d5c46e4edc Mon Sep 17 00:00:00 2001 From: I743583 Date: Tue, 8 Apr 2025 19:11:55 +0100 Subject: [PATCH 12/41] wip: save --- .../src/abap/ui5-abap-repository-service.ts | 19 +- .../package.json | 2 + .../src/app/config.ts | 16 +- .../src/app/index.ts | 41 ++- .../src/app/types.ts | 18 +- .../src/prompts/prompt-helpers.ts | 74 ++--- .../src/prompts/prompts.ts | 46 +-- .../bsp-app-download-sub-generator.i18n.json | 9 +- .../src/utils/constants.ts | 10 +- .../src/utils/file-helpers.ts | 6 +- .../src/utils/validate-app-content-json.ts | 82 ----- .../test/app.test.ts | 13 +- .../test/config.test.ts | 291 ++++++++++++++++++ .../test/prompts/prompt-helpers.test.ts | 29 +- .../test/prompts/prompts.test.ts | 21 +- .../utils/validate-app-content-json.test.ts | 112 ------- pnpm-lock.yaml | 6 + 17 files changed, 481 insertions(+), 314 deletions(-) delete mode 100644 packages/bsp-app-download-sub-generator/src/utils/validate-app-content-json.ts delete mode 100644 packages/bsp-app-download-sub-generator/test/utils/validate-app-content-json.test.ts diff --git a/packages/axios-extension/src/abap/ui5-abap-repository-service.ts b/packages/axios-extension/src/abap/ui5-abap-repository-service.ts index 034d0114ca..0495fe92ff 100644 --- a/packages/axios-extension/src/abap/ui5-abap-repository-service.ts +++ b/packages/axios-extension/src/abap/ui5-abap-repository-service.ts @@ -141,6 +141,21 @@ export class Ui5AbapRepositoryService extends ODataService { } } + /** + * + * @param str string to check + * @returns true if the string is base64 encoded, false otherwise + */ + private isBase64Encoded(str: string): boolean { + try { + // Decode the string and re-encode it to verify integrity + return Buffer.from(str, 'base64').toString('base64') === str.trim(); + } catch (e) { + // If decoding fails, it's not valid Base64 + return false; + } + } + /** * Get the application files as zip archive. This will only work on ABAP systems 2308 or newer. * @@ -156,7 +171,9 @@ export class Ui5AbapRepositoryService extends ODataService { } }); const data = response.odata(); - return data.ZipArchive ? Buffer.from(data.ZipArchive, 'base64') : undefined; + return this.isBase64Encoded(data.ZipArchive) + ? Buffer.from(data.ZipArchive, 'base64') + : Buffer.from(data.ZipArchive); } catch (error) { this.log.debug(`Retrieving application ${app}, ${error}`); if (isAxiosError(error) && error.response?.status === 404) { diff --git a/packages/bsp-app-download-sub-generator/package.json b/packages/bsp-app-download-sub-generator/package.json index 08c6a47f84..b04a5fbea5 100644 --- a/packages/bsp-app-download-sub-generator/package.json +++ b/packages/bsp-app-download-sub-generator/package.json @@ -65,6 +65,8 @@ "@types/inquirer-autocomplete-prompt": "2.0.1", "@types/yeoman-test": "4.0.6", "@sap-ux/nodejs-utils": "workspace:*", + "@types/fs-extra": "9.0.13", + "fs-extra": "10.0.0", "@sap-ux/store": "workspace:*", "@vscode-logging/logger": "2.0.0", "@types/adm-zip": "0.5.5", diff --git a/packages/bsp-app-download-sub-generator/src/app/config.ts b/packages/bsp-app-download-sub-generator/src/app/config.ts index ddb625a4a5..49ff389494 100644 --- a/packages/bsp-app-download-sub-generator/src/app/config.ts +++ b/packages/bsp-app-download-sub-generator/src/app/config.ts @@ -9,8 +9,6 @@ import { getLatestUI5Version } from '@sap-ux/ui5-info'; import { getMinimumUI5Version } from '@sap-ux/project-access'; import { adtSourceTemplateId } from '../utils/constants'; import { PromptState } from '../prompts/prompt-state'; -import { join } from 'path'; -import { validateAppContentJsonFile } from '../utils/validate-app-content-json'; import type { AbapDeployConfig } from '@sap-ux/ui5-config'; import BspAppDownloadLogger from '../utils/logger'; @@ -48,7 +46,7 @@ const fetchServiceMetadata = async (provider: AbapServiceProvider, serviceUrl: s try { return await provider.service(serviceUrl).metadata(); } catch (err) { - BspAppDownloadLogger.logger?.error(t('error.metadatafetchError', { error: err.message })); + BspAppDownloadLogger.logger?.error(t('error.metadataFetchError', { error: err.message })); } }; @@ -84,7 +82,6 @@ function getEntityConfig(appContentJson: AppContentConfig): EntityConfig { * @param {string} extractedProjectPath - Path where the app files are extracted. * @param appContentJson * @param {Editor} fs - The file system editor to manipulate project files. - * @param {Logger} [log] - An optional logger instance for error logging. * @returns {Promise>} - A promise resolving to the generated app configuration. * @throws {Error} - Throws an error if there are issues generating the configuration. */ @@ -95,13 +92,10 @@ export async function getAppConfig( fs: Editor ): Promise> { try { - validateAppContentJsonFile(appContentJson); const manifest = readManifest(extractedProjectPath, fs); - const serviceProvider = PromptState.systemSelection?.connectedSystem?.serviceProvider as AbapServiceProvider; - if (!manifest?.['sap.app']?.dataSources) { - throw Error(t('error.dataSourcesNotFound')); + BspAppDownloadLogger.logger?.error(t('error.dataSourcesNotFound')); } const odataVersion = @@ -112,9 +106,8 @@ export async function getAppConfig( // Fetch metadata for the service const metadata = await fetchServiceMetadata( serviceProvider, - manifest?.['sap.app']?.dataSources?.mainService.uri + manifest?.['sap.app']?.dataSources?.mainService.uri ?? '' ); - const appConfig: FioriElementsApp = { app: { id: app.appId, @@ -123,8 +116,7 @@ export async function getAppConfig( sourceTemplate: { id: adtSourceTemplateId }, - projectType: 'EDMXBackend', - flpAppId: `${app.appId.replace(/[-_.]/g, '')}-tile` // todo: check if flpAppId is correct + projectType: 'EDMXBackend' }, package: { name: app.appId, diff --git a/packages/bsp-app-download-sub-generator/src/app/index.ts b/packages/bsp-app-download-sub-generator/src/app/index.ts index f8e6ca0247..ec3d3ac8fc 100644 --- a/packages/bsp-app-download-sub-generator/src/app/index.ts +++ b/packages/bsp-app-download-sub-generator/src/app/index.ts @@ -11,7 +11,7 @@ import { downloadApp } from '../utils/download-utils'; import { EventName } from '../telemetryEvents'; import type { YeomanEnvironment } from '@sap-ux/fiori-generator-shared'; import { getDefaultTargetFolder } from '@sap-ux/fiori-generator-shared'; -import type { BspAppDownloadOptions, BspAppDownloadAnswers, BspAppDownloadQuestions, AppContentConfig } from './types'; +import type { BspAppDownloadOptions, BspAppDownloadAnswers, BspAppDownloadQuestions, AppContentConfig, QuickDeployedAppConfig } from './types'; import { getPrompts } from '../prompts/prompts'; import { generate, TemplateType, type FioriElementsApp, type LROPSettings } from '@sap-ux/fiori-elements-writer'; import { join, basename } from 'path'; @@ -31,6 +31,8 @@ import { getAbapDeployConfig, getAppConfig } from './config'; import type { AbapDeployConfig } from '@sap-ux/ui5-config'; import { sampleAppContentJson } from './example-app-content'; import { replaceWebappFiles } from '../utils/file-helpers'; +import { fetchAppListForSelectedSystem, extractAppData } from '../prompts/prompt-helpers'; +import { isValidPromptState, validateAppContentJsonFile } from '../utils/validators'; /** * Generator class for downloading a basic app from BSP repository. @@ -76,7 +78,7 @@ export default class extends Generator { ); this.prompts = new Prompts([]); - // Initialize prompts and callbacks if not launched as a subgenerator + // Initialise prompts and callbacks if not launched as a subgenerator if (!this.launchAppDownloaderAsSubGenerator) { this.appWizard.setHeaderTitle(generatorTitle); this.prompts = new Prompts(getYUIDetails()); @@ -111,20 +113,40 @@ export default class extends Generator { * Prompts the user for application details and downloads the app. */ public async prompting(): Promise { - const questions: BspAppDownloadQuestions[] = await getPrompts(this.appRootPath); + const quickDeployedAppConfig = this.options?.data?.quickDeployedAppConfig; + const questions: BspAppDownloadQuestions[] = await getPrompts(this.appRootPath, quickDeployedAppConfig); const answers: BspAppDownloadAnswers = await this.prompt(questions); - const { selectedApp, targetFolder } = answers; - - if (PromptState.systemSelection.connectedSystem?.serviceProvider && selectedApp?.appId && targetFolder) { + const { targetFolder } = answers; + if (quickDeployedAppConfig?.appId) { + // Handle quick deployed app download where prompts for system selection and app selection are not shown + // Only target folder ptompt is shown + await this._handleQuickDeployedAppDownload(quickDeployedAppConfig, targetFolder); + } else { + // Handle normal app download where prompts for system selection and app selection are shown Object.assign(this.answers, answers); - this.projectPath = join(targetFolder, selectedApp.appId); + } + if (isValidPromptState(targetFolder, this.answers.selectedApp.appId)) { + this.projectPath = join(targetFolder, this.answers.selectedApp.appId); this.extractedProjectPath = join(this.projectPath, extractedFilePath); - // Trigger app download await downloadApp(this.answers.selectedApp.repoName, this.extractedProjectPath, this.fs); } } + private async _handleQuickDeployedAppDownload (quickDeployedAppConfig: QuickDeployedAppConfig, targetFolder: string): Promise { + debugger; + const appList = await fetchAppListForSelectedSystem( + quickDeployedAppConfig.serviceProvider, + quickDeployedAppConfig.appId + ); + if(!appList.length) { + BspAppDownloadLogger.logger?.error(t('error.quickDeployedAppDownloadErrors.noAppsFound', { appId: quickDeployedAppConfig.appId })); + } + this.answers.selectedApp = extractAppData(appList[0]).value; + this.answers.targetFolder = targetFolder; + this.answers.systemSelection = PromptState.systemSelection; + } + /** * Writes the configuration files for the project, including deployment config, and README. */ @@ -139,6 +161,7 @@ export default class extends Generator { // } // Generate project files + validateAppContentJsonFile(appContentJson); const config = await getAppConfig(this.answers.selectedApp, this.extractedProjectPath, appContentJson, this.fs); await generate(this.projectPath, config, this.fs); @@ -163,7 +186,7 @@ export default class extends Generator { writeApplicationInfoSettings(this.projectPath, this.fs); } // Replace webapp files with downloaded app files - // replaceWebappFiles(this.projectPath, this.extractedProjectPath, this.fs); + replaceWebappFiles(this.projectPath, this.extractedProjectPath, this.fs); // Clean up extracted project files // this.fs.delete(this.extractedProjectPath); } diff --git a/packages/bsp-app-download-sub-generator/src/app/types.ts b/packages/bsp-app-download-sub-generator/src/app/types.ts index 2dbdf00207..09bc9d206e 100644 --- a/packages/bsp-app-download-sub-generator/src/app/types.ts +++ b/packages/bsp-app-download-sub-generator/src/app/types.ts @@ -6,8 +6,19 @@ import type { BackendSystem } from '@sap-ux/store'; import type { AbapServiceProvider, AppIndex } from '@sap-ux/axios-extension'; import type { YUIQuestion } from '@sap-ux/inquirer-common'; import type { AutocompleteQuestionOptions } from 'inquirer-autocomplete-prompt'; -import type { SystemSelectionAnswerType } from '@sap-ux/odata-service-inquirer'; +/** + * Quick deploy app config are applicable only in scenarios where an application + * deployed via ADT Quick Deploy is being downloaded from a BSP repository. + */ +export interface QuickDeployedAppConfig { + /** application Id to be downloaded. */ + appId: string; + /** appUrl is the URL pointing to the application */ + appUrl?: string; + /** service provider is used to identify the system from which the app is downloaded from. */ + serviceProvider: AbapServiceProvider; +} /** * Options for downloading a BSP application. */ @@ -15,6 +26,9 @@ export interface BspAppDownloadOptions extends Generator.GeneratorOptions { /** VSCode instance for interacting with the VSCode environment. */ vscode?: VSCodeInstance; + /** The quick deploy config is provided only when an ADT quick deployed app is being downloaded */ + quickDeployedAppConfig?: QuickDeployedAppConfig; + /** AppWizard instance for managing the application download flow. */ appWizard?: AppWizard; @@ -85,7 +99,7 @@ export enum PromptNames { */ export interface BspAppDownloadAnswers { /** Selected backend system connection details. */ - [PromptNames.systemSelection]: SystemSelectionAnswerType; + [PromptNames.systemSelection]: SystemSelectionAnswers; /** Information about the selected application for download. */ [PromptNames.selectedApp]: AppInfo; /** Target folder where the BSP application will be generated. */ diff --git a/packages/bsp-app-download-sub-generator/src/prompts/prompt-helpers.ts b/packages/bsp-app-download-sub-generator/src/prompts/prompt-helpers.ts index b28bf2b09e..039781fc06 100644 --- a/packages/bsp-app-download-sub-generator/src/prompts/prompt-helpers.ts +++ b/packages/bsp-app-download-sub-generator/src/prompts/prompt-helpers.ts @@ -1,7 +1,6 @@ import { generatorTitle, generatorDescription } from '../utils/constants'; import { appListSearchParams, appListResultFields } from '../utils/constants'; import type { AbapServiceProvider, AppIndex } from '@sap-ux/axios-extension'; -import type { Logger } from '@sap-ux/logger'; import type { AppInfo } from '../app/types'; import { PromptNames } from '../app/types'; import { PromptState } from './prompt-state'; @@ -23,6 +22,33 @@ export function getYUIDetails(): { name: string; description: string }[] { ]; } +/** + * Returns the prompt details for the selected application. + * + * @param {AppItem} app - The application item to extract details from. + * @returns { name: string; value: AppInfo } The extracted details including name and value. + */ +export const extractAppData = (app: AppItem): { name: string; value: AppInfo } => { + // cast to string because TypeScript doesn't automatically know at the point that these fields are defined + // after filtering out invalid apps. + const id = app['sap.app/id'] as string; + const title = app['sap.app/title'] as string; + const description = (app['sap.app/description'] ?? '') as string; + const repoName = app.repoName as string; + const url = app.url as string; + + return { + name: id, + value: { + appId: id, + title, + description, + repoName, + url + } + }; +}; + /** * Formats the application list into selectable choices. * @@ -38,40 +64,24 @@ export const formatAppChoices = (appList: AppIndex): Array<{ name: string; value } return hasRequiredFields; }) - .map((app) => { - // cast to string because TypeScript doesn't automatically know at the point that these fields are defined - // after filtering out invalid apps. - const id = app['sap.app/id'] as string; - const title = app['sap.app/title'] as string; - const description = (app['sap.app/description'] ?? '') as string; - const repoName = app.repoName as string; - const url = app.url as string; - - return { - name: id, - value: { - appId: id, - title, - description, - repoName, - url - } - }; - }); + .map((app) => extractAppData(app)); }; /** * Fetches a list of deployed applications from the ABAP repository. * * @param {AbapServiceProvider} provider - The ABAP service provider. - * @param {Logger} [log] - The logger instance. * @returns {Promise} A list of applications filtered by source template. */ -async function getAppList(provider: AbapServiceProvider, log?: Logger): Promise { +async function getAppList(provider: AbapServiceProvider, appId?: string): Promise { try { - return await provider.getAppIndex().search(appListSearchParams, appListResultFields); + const searchParams = appId ? { + ...appListSearchParams, + 'sap.app/id': appId + } : appListSearchParams + return await provider.getAppIndex().search(searchParams, appListResultFields); } catch (error) { - log?.error(t('error.applicationListFetchError', { error: error.message })); + BspAppDownloadLogger.logger?.error(t('error.applicationListFetchError', { error: error.message })); return []; } } @@ -79,21 +89,15 @@ async function getAppList(provider: AbapServiceProvider, log?: Logger): Promise< /** * Fetches the application list for the selected system. * - * @param {BspAppDownloadAnswers} answers - The user's answers from the prompts. - * @param {AbapServiceProvider | undefined} serviceProvider - The ABAP service provider. - * @param {Logger} [log] - The logger instance. + * @param {AbapServiceProvider} serviceProvider - The ABAP service provider. * @returns {Promise} A list of applications filtered by source template. */ -export async function fetchAppListForSelectedSystem( - answers: BspAppDownloadAnswers, - serviceProvider?: AbapServiceProvider, - log?: Logger -): Promise { - if (answers[PromptNames.systemSelection] && serviceProvider) { +export async function fetchAppListForSelectedSystem(serviceProvider: AbapServiceProvider, appId?: string): Promise { + if (serviceProvider) { PromptState.systemSelection = { connectedSystem: { serviceProvider } }; - return await getAppList(serviceProvider, log); + return await getAppList(serviceProvider, appId); } return []; } diff --git a/packages/bsp-app-download-sub-generator/src/prompts/prompts.ts b/packages/bsp-app-download-sub-generator/src/prompts/prompts.ts index d7e25ea7e1..9005a0f8a2 100644 --- a/packages/bsp-app-download-sub-generator/src/prompts/prompts.ts +++ b/packages/bsp-app-download-sub-generator/src/prompts/prompts.ts @@ -1,10 +1,9 @@ import type { AppIndex, AbapServiceProvider } from '@sap-ux/axios-extension'; import { getSystemSelectionQuestions } from '@sap-ux/odata-service-inquirer'; -import type { BspAppDownloadAnswers, BspAppDownloadQuestions } from '../app/types'; +import type { BspAppDownloadAnswers, BspAppDownloadQuestions, QuickDeployedAppConfig } from '../app/types'; import { PromptNames } from '../app/types'; import { t } from '../utils/i18n'; import type { FileBrowserQuestion } from '@sap-ux/inquirer-common'; -import type { Logger } from '@sap-ux/logger'; import { formatAppChoices } from './prompt-helpers'; import { validateFioriAppTargetFolder } from '@sap-ux/project-input-validator'; import { PromptState } from './prompt-state'; @@ -16,20 +15,30 @@ import { fetchAppListForSelectedSystem } from './prompt-helpers'; * @param {string} [appRootPath] - The application root path. * @returns {FileBrowserQuestion} The target folder prompt configuration. */ -const getTargetFolderPrompt = (appRootPath?: string): FileBrowserQuestion => { +const getTargetFolderPrompt = (appRootPath?: string, appId?: string): FileBrowserQuestion => { return { type: 'input', name: PromptNames.targetFolder, message: t('prompts.targetPath.message'), guiType: 'folder-browser', - when: (answers: BspAppDownloadAnswers) => Boolean(answers?.selectedApp?.appId), + when: (answers: BspAppDownloadAnswers) => { + // Display the prompt if appId is provided. This occurs when the generator is invoked + // as part of the quick deployment process from ADT. + if (appId) { + return true; + } + // If appId is not provided, check if the user has selected an app. + // If an app is selected, display the prompt accordingly. + return Boolean(answers?.selectedApp?.appId) + }, guiOptions: { applyDefaultWhenDirty: true, mandatory: true, breadcrumb: t('prompts.targetPath.breadcrumb') }, validate: async (target, answers: BspAppDownloadAnswers): Promise => { - return await validateFioriAppTargetFolder(target, answers.selectedApp.appId, true); + const selectedAppId = answers.selectedApp?.appId ?? appId; + return await validateFioriAppTargetFolder(target, selectedAppId, true); }, default: () => appRootPath } as FileBrowserQuestion; @@ -39,23 +48,26 @@ const getTargetFolderPrompt = (appRootPath?: string): FileBrowserQuestion} A list of questions for user interaction. */ -export async function getPrompts(appRootPath?: string, log?: Logger): Promise { +export async function getPrompts(appRootPath?: string, quickDeployedAppConfig?: QuickDeployedAppConfig): Promise { PromptState.reset(); + // If quickDeployedAppConfig is provided, return only the target folder prompt + if (quickDeployedAppConfig?.appId) { + return [getTargetFolderPrompt(appRootPath, quickDeployedAppConfig.appId)] as BspAppDownloadQuestions[]; + } + const systemQuestions = await getSystemSelectionQuestions({ serviceSelection: { hide: true } }, false); // todo: remove this isYUI value let appList: AppIndex = []; - let result: BspAppDownloadQuestions[] = []; - const appSelectionPrompt = [ { when: async (answers: BspAppDownloadAnswers): Promise => { - appList = await fetchAppListForSelectedSystem( - answers, - systemQuestions.answers.connectedSystem?.serviceProvider as unknown as AbapServiceProvider, - log - ); + if(answers[PromptNames.systemSelection]) { + appList = await fetchAppListForSelectedSystem( + systemQuestions.answers.connectedSystem?.serviceProvider as AbapServiceProvider + ); + } // display app selection prompt only if user has selected a system return !!systemQuestions.answers.connectedSystem?.serviceProvider; }, @@ -71,8 +83,6 @@ export async function getPrompts(appRootPath?: string, log?: Logger): Promise for more details", "appContentJsonNotFound": "{{- jsonFileName }} not found in the downloaded app", "replaceWebappFilesError": "Error replacing files in the downloaded app: {{- error}}", "requiredFieldsMissing": "Required fields are missing for app: {{- app }}. Check if the app is deployed correctly", "applicationListFetchError": "Error fetching application list: {{- error}}", - "metadatafetchError": "Error fetching metadata: {{- error}}", + "metadataFetchError": "Error fetching metadata: {{- error}}", "appConfigGenError": "Error generating application configuration: {{- error}}", "validationErrors": { "invalidMetadataPackage": "Invalid or missing package in metadata", @@ -35,6 +34,9 @@ "readManifestFailed": "Error: Failed to read manifest file", "sapAppNotDefined": "Error: sap.app not defined in the manifest file", "sourceTemplateNotSupported": "Error: Source template not supported" + }, + "quickDeployedAppDownloadErrors": { + "noAppsFound": "No application with if {{ appId }} found in the system. Please check if the application is deployed correctly" } }, "warn": { @@ -44,7 +46,8 @@ "appSelection": { "message": "App", "hint": "Select the app to download", - "breadcrumb": "App" + "breadcrumb": "App", + "noAppsDeployed": "No basic applications deployed to this system can be downloaded. Please see for more details" }, "targetPath": { "message": "Project folder path", diff --git a/packages/bsp-app-download-sub-generator/src/utils/constants.ts b/packages/bsp-app-download-sub-generator/src/utils/constants.ts index 9cccb106c4..b1960a2c75 100644 --- a/packages/bsp-app-download-sub-generator/src/utils/constants.ts +++ b/packages/bsp-app-download-sub-generator/src/utils/constants.ts @@ -2,7 +2,7 @@ import { PromptNames, type BspAppDownloadAnswers } from '../app/types'; // Title and description for the generator export const generatorTitle = 'Basic App Download from BSP'; -export const generatorDescription = 'Download a basic LROP app from a BSP reapository'; +export const generatorDescription = 'Download a basic LROP app from a BSP repository'; // Name of the generator used for Fiori app download export const generatorName = '@sap-ux/bsp-app-download-sub-generator'; @@ -11,13 +11,7 @@ export const adtSourceTemplateId = '@sap.adt.sevicebinding.deploy:lrop'; // Default initial answers to use as a fallback. export const defaultAnswers: BspAppDownloadAnswers = { - [PromptNames.systemSelection]: { - system: { - name: '', - url: '' - }, - type: 'backendSystem' - }, + [PromptNames.systemSelection]: {}, [PromptNames.selectedApp]: { appId: '', title: '', diff --git a/packages/bsp-app-download-sub-generator/src/utils/file-helpers.ts b/packages/bsp-app-download-sub-generator/src/utils/file-helpers.ts index cac61092f3..dce4c9568a 100644 --- a/packages/bsp-app-download-sub-generator/src/utils/file-helpers.ts +++ b/packages/bsp-app-download-sub-generator/src/utils/file-helpers.ts @@ -30,6 +30,7 @@ export function readManifest(extractedProjectPath: string, fs: Editor): Manifest return manifest; } + /** * Replaces the specified files in the `webapp` directory with the corresponding files from the `extractedPath`. * @@ -66,4 +67,7 @@ export async function replaceWebappFiles( } catch (error) { BspAppDownloadLogger.logger?.error(t('error.replaceWebappFilesError', { error })); } -} \ No newline at end of file +} + + + diff --git a/packages/bsp-app-download-sub-generator/src/utils/validate-app-content-json.ts b/packages/bsp-app-download-sub-generator/src/utils/validate-app-content-json.ts deleted file mode 100644 index e49de62ae8..0000000000 --- a/packages/bsp-app-download-sub-generator/src/utils/validate-app-content-json.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { t } from '../utils/i18n'; -import type { AppContentConfig } from '../app/types'; -import BspAppDownloadLogger from '../utils/logger'; - -/** - * Validates the metadata section of the app configuration. - * - * @param {AppContentConfig['metadata']} metadata - The metadata object. - * @returns {boolean} - Returns true if valid, false otherwise. - */ -const validateMetadata = (metadata: AppContentConfig['metadata']): boolean => { - if (!metadata.package || typeof metadata.package !== 'string') { - BspAppDownloadLogger.logger?.error(t('error.invalidMetadataPackage')); - return false; - } - return true; -}; - -/** - * Validates the service binding details section of the app configuration. - * - * @param {AppContentConfig['serviceBindingDetails']} serviceBinding - The service binding details object. - * @returns {boolean} - Returns true if valid, false otherwise. - */ -const validateServiceBindingDetails = (serviceBinding: AppContentConfig['serviceBindingDetails']): boolean => { - if (!serviceBinding.serviceName || typeof serviceBinding.serviceName !== 'string') { - BspAppDownloadLogger.logger?.error(t('error.invalidServiceName')); - return false; - } - if (!serviceBinding.serviceVersion || typeof serviceBinding.serviceVersion !== 'string') { - BspAppDownloadLogger.logger?.error(t('error.invalidServiceVersion')); - return false; - } - if (!serviceBinding.mainEntityName || typeof serviceBinding.mainEntityName !== 'string') { - BspAppDownloadLogger.logger?.error(t('error.invalidMainEntityName')); - return false; - } - return true; -}; - -/** - * Validates the project attribute section of the app configuration. - * - * @param {AppContentConfig['projectAttribute']} projectAttribute - The project attribute object. - * @returns {boolean} - Returns true if valid, false otherwise. - */ -const validateProjectAttribute = (projectAttribute: AppContentConfig['projectAttribute']): boolean => { - if (!projectAttribute.moduleName || typeof projectAttribute.moduleName !== 'string') { - BspAppDownloadLogger.logger?.error(t('error.invalidModuleName')); - return false; - } - return true; -}; - -/** - * Validates the deployment details section of the app configuration. - * - * @param {AppContentConfig['deploymentDetails']} deploymentDetails - The deployment details object. - * @returns {boolean} - Returns true if valid, false otherwise. - */ -const validateDeploymentDetails = (deploymentDetails: AppContentConfig['deploymentDetails']): boolean => { - if (!deploymentDetails.repositoryName) { - BspAppDownloadLogger.logger?.error(t('error.invalidRepositoryName')); - return false; - } - return true; -}; - -/** - * Validates the entire app configuration. - * - * @param {AppContentConfig} config - The app configuration object. - * @returns {boolean} - Returns true if the configuration is valid, false otherwise. - */ -export const validateAppContentJsonFile = (config: AppContentConfig): boolean => { - return ( - validateMetadata(config.metadata) && - validateServiceBindingDetails(config.serviceBindingDetails) && - validateProjectAttribute(config.projectAttribute) && - validateDeploymentDetails(config.deploymentDetails) - ); -}; diff --git a/packages/bsp-app-download-sub-generator/test/app.test.ts b/packages/bsp-app-download-sub-generator/test/app.test.ts index 63100674e6..5cdc6878a4 100644 --- a/packages/bsp-app-download-sub-generator/test/app.test.ts +++ b/packages/bsp-app-download-sub-generator/test/app.test.ts @@ -7,16 +7,21 @@ import type { BspAppDownloadQuestions } from '../src/app/types'; import { PromptNames } from '../src/app/types'; import { readManifest } from '../src/utils/file-helpers'; import fs from 'fs'; +import { TestFixture } from './fixtures'; import { getAppConfig } from '../src/app/config'; import { OdataVersion } from '@sap-ux/odata-service-inquirer'; import { TemplateType, type FioriElementsApp, type LROPSettings } from '@sap-ux/fiori-elements-writer'; import { adtSourceTemplateId } from '../src/utils/constants'; +import { removeSync } from 'fs-extra'; jest.mock('../src/utils/file-helpers'); jest.mock('../src/app/config'); describe('BSP App Download', () => { + const testFixture = new TestFixture(); const bspAppDownloadGenPath = join(__dirname, '../src/app/index.ts'); + const testOutputDir = join(__dirname, 'test-output'); + const curTestOutPath = join(testOutputDir, 'app-1'); afterEach(() => { jest.clearAllMocks(); jest.restoreAllMocks(); @@ -54,6 +59,10 @@ describe('BSP App Download', () => { ); }); + beforeAll(() => { + removeSync(curTestOutPath); // even for in memory + }); + test('run bsp app download', async () => { const metadata = fs.readFileSync(join(__dirname, 'fixtures', 'metadata.xml'), 'utf8'); @@ -63,6 +72,8 @@ describe('BSP App Download', () => { // showError: jest.fn(), // showInformation: jest.fn() // }; + // const testPath = join(curTestOutPath, name); + // const fs = await generate(testPath, config); const appConfig: FioriElementsApp = { app: { id: 'app-1', @@ -140,5 +151,3 @@ describe('BSP App Download', () => { }) }); }); - -//index.ts | 58.02 | 32.25 | 33.33 | 58.02 | 120-125,155-320 \ No newline at end of file diff --git a/packages/bsp-app-download-sub-generator/test/config.test.ts b/packages/bsp-app-download-sub-generator/test/config.test.ts index e69de29bb2..08c095def3 100644 --- a/packages/bsp-app-download-sub-generator/test/config.test.ts +++ b/packages/bsp-app-download-sub-generator/test/config.test.ts @@ -0,0 +1,291 @@ +import { getAppConfig, getAbapDeployConfig } from '../src/app/config'; +import type { AbapServiceProvider } from '@sap-ux/axios-extension'; +import type { Editor } from 'mem-fs-editor'; +import { getLatestUI5Version } from '@sap-ux/ui5-info'; +import { getMinimumUI5Version } from '@sap-ux/project-access'; +import { PromptState } from '../src/prompts/prompt-state'; +import type { AppInfo, AppContentConfig } from '../src/app/types'; +import { readManifest } from '../src/utils/file-helpers'; +import { sampleAppContentTestData } from './fixtures/example-app-content'; +import { t } from '../src/utils/i18n'; +import { adtSourceTemplateId } from '../src/utils/constants'; +import BspAppDownloadLogger from '../src/utils/logger'; + +jest.mock('../src/utils/logger', () => ({ + logger: { + error: jest.fn() + } +})); + +jest.mock('../src/utils/file-helpers', () => ({ + ...jest.requireActual('../src/utils/file-helpers'), + readManifest: jest.fn() +})); + +jest.mock('@sap-ux/ui5-info', () => ({ + ...jest.requireActual('@sap-ux/ui5-info'), + getLatestUI5Version: jest.fn() +})); + +jest.mock('@sap-ux/project-access', () => ({ + ...jest.requireActual('@sap-ux/project-access'), + getMinimumUI5Version: jest.fn() +})); + +describe('getAppConfig', () => { + const mockApp: AppInfo = { + appId: 'testAppId', + title: 'Test App', + description: 'Test Description', + repoName: 'testRepoName', + url: 'https://example.com/testApp' + }; + const mockAppContentJson: AppContentConfig = sampleAppContentTestData; + const mockFs = {} as Editor; + const expectedAppConfig = { + app: { + id: mockApp.appId, + title: mockApp.title, + description: mockApp.description, + sourceTemplate: { id: adtSourceTemplateId }, + projectType: 'EDMXBackend' + }, + package: { + name: mockApp.appId, + description: mockApp.description, + devDependencies: {}, + scripts: {}, + version: '1.0.0' + }, + template: { + type: expect.any(String), + settings: { + entityConfig: { + mainEntityName: sampleAppContentTestData.serviceBindingDetails.mainEntityName + } + } + }, + service: { + path: '/odata/service', + version: expect.any(String), + metadata: undefined, + url: 'https://test-url.com' + }, + appOptions: { + addAnnotations: true, + addTests: true + }, + ui5: { + version: sampleAppContentTestData.projectAttribute.minimumUi5Version + } + }; + + beforeEach(() => { + jest.clearAllMocks(); + jest.resetAllMocks(); + }); + + it('should generate app configuration successfully', async () => { + const mockManifest = { + 'sap.app': { + dataSources: { + mainService: { + uri: '/odata/service', + settings: { odataVersion: '4.0' } + } + }, + applicationVersion: { version: '1.0.0' } + } + }; + + const mockServiceProvider = { + defaults: { baseURL: 'https://test-url.com' } + } as unknown as AbapServiceProvider; + + PromptState.systemSelection = { + connectedSystem: { serviceProvider: mockServiceProvider } + }; + + (readManifest as jest.Mock).mockReturnValue(mockManifest); + (getLatestUI5Version as jest.Mock).mockResolvedValue('1.100.0'); + (getMinimumUI5Version as jest.Mock).mockReturnValue('1.90.0'); + + const result = await getAppConfig(mockApp, '/path/to/project', mockAppContentJson, mockFs); + expect(result).toEqual(expectedAppConfig); + }); + + it('should generate app configuration successfully when navigation entity is provided', async () => { + const mockManifest = { + 'sap.app': { + dataSources: { + mainService: { + uri: '/odata/service', + settings: { odataVersion: '4.0' } + } + }, + applicationVersion: { version: '1.0.0' } + } + }; + + const mockServiceProvider = { + defaults: { baseURL: 'https://test-url.com' } + } as unknown as AbapServiceProvider; + + PromptState.systemSelection = { + connectedSystem: { serviceProvider: mockServiceProvider } + }; + + (readManifest as jest.Mock).mockReturnValue(mockManifest); + (getLatestUI5Version as jest.Mock).mockResolvedValue('1.100.0'); + (getMinimumUI5Version as jest.Mock).mockReturnValue('1.90.0'); + + const mockAppContentJsonWithNavEntity = { + ...mockAppContentJson, + serviceBindingDetails: { + ...mockAppContentJson.serviceBindingDetails, + mainEntityName: mockAppContentJson.serviceBindingDetails.mainEntityName, + navigationEntity: { + EntitySet: 'EnitySet', + Name: 'SomeNavigationProperty' + } + } + } + const result = await getAppConfig(mockApp, '/path/to/project', mockAppContentJsonWithNavEntity, mockFs); + const expectedAppConfigWithNavEntity = { + ...expectedAppConfig, + template: { + ...expectedAppConfig.template, + settings: { + entityConfig: { + mainEntityName: mockAppContentJson.serviceBindingDetails.mainEntityName, + navigationEntity: { + EntitySet: mockAppContentJsonWithNavEntity.serviceBindingDetails.navigationEntity.EntitySet, + Name: mockAppContentJsonWithNavEntity.serviceBindingDetails.navigationEntity.Name + } + } + } + } + }; + expect(result).toEqual(expectedAppConfigWithNavEntity); + }); + + it('should throw an error if manifest data sources are missing', async () => { + const mockManifest = { + 'sap.app': {} + }; + + (readManifest as jest.Mock).mockReturnValue(mockManifest); + const result = await getAppConfig(mockApp, '/path/to/project', mockAppContentJson, mockFs); + expect(BspAppDownloadLogger.logger.error).toBeCalledWith(t('error.dataSourcesNotFound')); + }); + + it('should log an error if fetchServiceMetadata throws an error', async () => { + const mockProvider = { + service: jest.fn().mockReturnValue({ + metadata: jest.fn().mockRejectedValue(new Error('Metadata fetch failed')) + }) + } as unknown as AbapServiceProvider; + const mockManifest = { + 'sap.app': { + dataSources: { + mainService: { + uri: '/odata/service', + settings: { odataVersion: '4.0' } + } + } + } + }; + + const errorMsg = 'Metadata fetch failed'; + const mockServiceProvider = { + defaults: { baseURL: 'https://test-url.com' }, + service: jest.fn().mockReturnValue({ + metadata: jest.fn().mockRejectedValue(new Error(errorMsg)) + }) + } as unknown as AbapServiceProvider; + + PromptState.systemSelection = { + connectedSystem: { serviceProvider: mockServiceProvider } + }; + + (readManifest as jest.Mock).mockReturnValue(mockManifest); + + await getAppConfig(mockApp, '/path/to/project', mockAppContentJson, mockFs); + expect(BspAppDownloadLogger.logger?.error).toHaveBeenCalledWith(t('error.metadataFetchError', { error: errorMsg })); + }); + + it('should generate app config when minUi5Version is not provided in manifest', async () => { + const mockManifest = { + 'sap.app': { + dataSources: { + mainService: { + uri: '/odata/service', + settings: { odataVersion: '4.0' } + } + }, + applicationVersion: { version: '1.0.0' } + } + }; + + const mockServiceProvider = { + defaults: { baseURL: 'https://test-url.com' }, + service: jest.fn().mockReturnValue({ + metadata: jest.fn().mockResolvedValue({ + dataServices: { + schema: [] + } + }) + }) + } as unknown as AbapServiceProvider; + + PromptState.systemSelection = { + connectedSystem: { serviceProvider: mockServiceProvider } + }; + (readManifest as jest.Mock).mockReturnValue(mockManifest); + (getLatestUI5Version as jest.Mock).mockResolvedValue('1.100.0'); + (getMinimumUI5Version as jest.Mock).mockReturnValue('1.90.0'); + + const mockAppContentJsonWithoutUi5Version = { + ...sampleAppContentTestData, + projectAttribute: { + ...sampleAppContentTestData.projectAttribute, + minimumUi5Version: null + } + } as unknown as AppContentConfig; + await getAppConfig(mockApp, '/path/to/project', mockAppContentJsonWithoutUi5Version, mockFs); + expect(BspAppDownloadLogger.logger?.error).not.toHaveBeenCalled(); + }); + +}); + +describe('getAbapDeployConfig', () => { + it('should generate the correct deployment configuration', () => { + const app: AppInfo = { + url: 'https://target-url.com', + repoName: 'TEST_REPO', + appId: 'TEST_APP_ID', + title: 'Test App', + description: 'Test Description' + }; + + const expectedConfig = { + target: { + url: 'https://target-url.com', + destination: 'TEST_REPO' + }, + app: { + name: 'TEST_REPO_NAME', + package: 'TEST_PACKAGE', + description: 'This is a test repository', + transport: 'REPLACE_WITH_TRANSPORT' + } + }; + + // Call the function + const result = getAbapDeployConfig(app, sampleAppContentTestData); + + // Assertions + expect(result).toEqual(expectedConfig); + }); +}); + \ No newline at end of file diff --git a/packages/bsp-app-download-sub-generator/test/prompts/prompt-helpers.test.ts b/packages/bsp-app-download-sub-generator/test/prompts/prompt-helpers.test.ts index da15c3a49e..93e9911cef 100644 --- a/packages/bsp-app-download-sub-generator/test/prompts/prompt-helpers.test.ts +++ b/packages/bsp-app-download-sub-generator/test/prompts/prompt-helpers.test.ts @@ -1,7 +1,6 @@ import { fetchAppListForSelectedSystem, formatAppChoices, getYUIDetails } from '../../src/prompts/prompt-helpers'; import { PromptNames, BspAppDownloadAnswers, AppItem } from '../../src/app/types'; import { PromptState } from '../../src/prompts/prompt-state'; -import type { Logger } from '@sap-ux/logger'; import type { AbapServiceProvider, AppIndex } from '@sap-ux/axios-extension'; import { generatorTitle, generatorDescription } from '../../src/utils/constants'; import { t } from '../../src/utils/i18n'; @@ -20,20 +19,11 @@ describe('fetchAppListForSelectedSystem', () => { }) } as unknown as AbapServiceProvider; - const mockLogger = { - error: jest.fn() - } as unknown as Logger; - const mockAnswers: BspAppDownloadAnswers = { [PromptNames.systemSelection]: { - system: { - name: 'mockSystemName', - url: 'mockUrl', - client: 'mockClient', - userDisplayName: 'mockUserDisplayName', - authenticationType: 'basic' - }, - type: 'backendSystem' + connectedSystem: { + serviceProvider: mockServiceProvider + } }, [PromptNames.selectedApp]: { appId: 'mockAppId', @@ -46,7 +36,7 @@ describe('fetchAppListForSelectedSystem', () => { }; it('should fetch the application list when systemSelection and serviceProvider are provided', async () => { - const result = await fetchAppListForSelectedSystem(mockAnswers, mockServiceProvider, mockLogger); + const result = await fetchAppListForSelectedSystem(mockServiceProvider, mockAnswers[PromptNames.selectedApp].appId); expect(mockServiceProvider.getAppIndex().search).toHaveBeenCalledWith( expect.anything(), @@ -58,21 +48,16 @@ describe('fetchAppListForSelectedSystem', () => { }); }); - it('should return an empty array when systemSelection is not provided', async () => { - const result = await fetchAppListForSelectedSystem({} as BspAppDownloadAnswers, mockServiceProvider, mockLogger); - expect(result).toEqual([]); - }); - it('should return an empty array when serviceProvider is not provided', async () => { - const result = await fetchAppListForSelectedSystem(mockAnswers, undefined, mockLogger); + const result = await fetchAppListForSelectedSystem(undefined as unknown as AbapServiceProvider); expect(result).toEqual([]); }); it('should log an error if getAppList throws an error', async () => { const error = new Error('Mock error'); mockServiceProvider.getAppIndex().search = jest.fn().mockRejectedValue(error); - const result = await fetchAppListForSelectedSystem(mockAnswers, mockServiceProvider, mockLogger); - expect(mockLogger.error).toHaveBeenCalledWith(t('error.applicationListFetchError', { error: error.message })); + const result = await fetchAppListForSelectedSystem(mockServiceProvider, mockAnswers[PromptNames.selectedApp].appId); + expect(BspAppDownloadLogger.logger.error).toBeCalledWith(t('error.applicationListFetchError', { error: error.message })); expect(result).toEqual([]); }); }); diff --git a/packages/bsp-app-download-sub-generator/test/prompts/prompts.test.ts b/packages/bsp-app-download-sub-generator/test/prompts/prompts.test.ts index ba547e2067..e32e6672c3 100644 --- a/packages/bsp-app-download-sub-generator/test/prompts/prompts.test.ts +++ b/packages/bsp-app-download-sub-generator/test/prompts/prompts.test.ts @@ -3,9 +3,9 @@ import { getSystemSelectionQuestions } from '@sap-ux/odata-service-inquirer'; import { fetchAppListForSelectedSystem, formatAppChoices } from '../../src/prompts/prompt-helpers'; import { PromptNames } from '../../src/app/types'; import type { BspAppDownloadAnswers, BspAppDownloadQuestions } from '../../src/app/types'; -import type { Logger } from '@sap-ux/logger'; import { join } from 'path'; import { t } from '../../src/utils/i18n'; +import type { AbapServiceProvider } from '@sap-ux/axios-extension'; import { validateFioriAppTargetFolder } from '@sap-ux/project-input-validator'; jest.mock('@sap-ux/odata-service-inquirer', () => ({ @@ -22,25 +22,30 @@ jest.mock('../../src/prompts/prompt-helpers', () => ({ })); describe('getPrompts', () => { - let mockLogger: Logger; const appRootPath = join('/mock/path'); + const mockServiceProvider = { + getAppIndex: jest.fn().mockReturnValue({ + search: jest.fn().mockResolvedValue([{ id: 'app1' }, { id: 'app2' }]) + }) + } as unknown as AbapServiceProvider; const mockAnswers = { selectedApp: { appId: 'app1' } } as unknown as BspAppDownloadAnswers; const mockAppList = [{ appId: 'app1', name: 'Test App' }]; beforeEach(() => { - mockLogger = { error: jest.fn(), info: jest.fn(), warn: jest.fn() } as unknown as Logger; (getSystemSelectionQuestions as jest.Mock).mockResolvedValue({ prompts: [{ type: 'input', name: 'system' }], - answers: { connectedSystem: { serviceProvider: {} } } + answers: { + connectedSystem: { serviceProvider: mockServiceProvider } + } }); (fetchAppListForSelectedSystem as jest.Mock).mockResolvedValue([{ appId: 'app1', name: 'Test App' }]); (formatAppChoices as jest.Mock).mockReturnValue(mockAppList); }); it('should return system questions, app selection, and target folder prompts', async () => { - const prompts = await getPrompts(appRootPath, mockLogger); + const prompts = await getPrompts(appRootPath); expect(prompts.length).toBeGreaterThanOrEqual(2); // system prompts @@ -53,7 +58,9 @@ describe('getPrompts', () => { const appSelectionPrompt = prompts.find(p => p.name === PromptNames.selectedApp) as BspAppDownloadQuestions; expect(appSelectionPrompt).toBeDefined(); if (typeof appSelectionPrompt?.when === 'function') { - await expect(appSelectionPrompt.when({ system: 'test' } as unknown as BspAppDownloadAnswers)).resolves.toBe(true); + await expect(appSelectionPrompt.when({ [PromptNames.systemSelection]: { + connectedSystem: { serviceProvider: mockServiceProvider } + } } as unknown as BspAppDownloadAnswers)).resolves.toBe(true); }; if (appSelectionPrompt?.type === 'list') { const listPrompt = appSelectionPrompt as unknown as { choices: () => { name: string; value: string }[] }; @@ -70,7 +77,7 @@ describe('getPrompts', () => { it('should handle no apps available scenario', async () => { (fetchAppListForSelectedSystem as jest.Mock).mockResolvedValue([]); - const prompts = await getPrompts(appRootPath, mockLogger); + const prompts = await getPrompts(appRootPath); const appSelectionPrompt = prompts.find(p => p.name === PromptNames.selectedApp); expect(appSelectionPrompt).toBeDefined(); diff --git a/packages/bsp-app-download-sub-generator/test/utils/validate-app-content-json.test.ts b/packages/bsp-app-download-sub-generator/test/utils/validate-app-content-json.test.ts deleted file mode 100644 index 599552608f..0000000000 --- a/packages/bsp-app-download-sub-generator/test/utils/validate-app-content-json.test.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { validateAppContentJsonFile } from '../../src/utils/validate-app-content-json'; -import { AppContentConfig } from '../../src/app/types'; -import { t } from '../../src/utils/i18n'; -import BspAppDownloadLogger from '../../src/utils/logger'; - -jest.mock('../../src/utils/logger', () => ({ - logger: { - error: jest.fn() - } -})); - -describe('validateAppContentJsonFile', () => { - const validConfig: AppContentConfig = { - metadata: { package: 'valid-package' }, - serviceBindingDetails: { - serviceName: 'validService', - serviceVersion: '1.0.0', - mainEntityName: 'validEntity', - }, - projectAttribute: { moduleName: 'validModule' }, - deploymentDetails: { repositoryName: 'validRepository' }, - fioriLaunchpadConfiguration: { - semanticObject: 'semanticObject', - action: 'action', - title: 'title' - }, - }; - - afterEach(() => { - jest.clearAllMocks(); - }); - - it('should return true when all validation functions pass', () => { - const result = validateAppContentJsonFile(validConfig); - expect(result).toBe(true); - }); - - it('should return false and log an error when metadata validation fails', () => { - const invalidMetadataConfig = { - ...validConfig, - metadata: { package: '' } // Invalid package - } as unknown as AppContentConfig; - - const result = validateAppContentJsonFile(invalidMetadataConfig); - expect(result).toBe(false); - expect(BspAppDownloadLogger.logger.error).toBeCalledWith(t('error.invalidMetadataPackage')); - }); - - it('should return false and log an error when service binding details validation fails', () => { - const invalidServiceBindingConfig = { - ...validConfig, - serviceBindingDetails: { - ...validConfig.serviceBindingDetails, - serviceName: '', // Invalid service name - } - } as unknown as AppContentConfig; - - const result = validateAppContentJsonFile(invalidServiceBindingConfig); - expect(result).toBe(false); - expect(BspAppDownloadLogger.logger.error).toBeCalledWith(t('error.invalidServiceName')); - }); - - it('should return false and log an error when service binding version is not provided', () => { - const invalidServiceBindingConfig = { - ...validConfig, - serviceBindingDetails: { - ...validConfig.serviceBindingDetails, - serviceVersion: '' // Invalid service version - } - } as unknown as AppContentConfig; - - const result = validateAppContentJsonFile(invalidServiceBindingConfig); - expect(result).toBe(false); - expect(BspAppDownloadLogger.logger.error).toBeCalledWith(t('error.invalidServiceVersion')); - }); - - it('should return false and log an error when main entity name is missing', () => { - const invalidServiceBindingConfig = { - ...validConfig, - serviceBindingDetails: { - ...validConfig.serviceBindingDetails, - mainEntityName: '' // Invalid main entity name - } - } as unknown as AppContentConfig; - - const result = validateAppContentJsonFile(invalidServiceBindingConfig); - expect(result).toBe(false); - expect(BspAppDownloadLogger.logger.error).toBeCalledWith(t('error.invalidMainEntityName')); - }); - - it('should return false and log an error when project attribute validation fails', () => { - const invalidProjectAttributeConfig = { - ...validConfig, - projectAttribute: { moduleName: '' } // Invalid module name - } as unknown as AppContentConfig; - - const result = validateAppContentJsonFile(invalidProjectAttributeConfig); - expect(result).toBe(false); - expect(BspAppDownloadLogger.logger.error).toBeCalledWith(t('error.invalidModuleName')); - }); - - it('should return false and log an error when deployment details validation fails', () => { - const invalidDeploymentDetailsConfig = { - ...validConfig, - deploymentDetails: { repositoryName: '' } // Invalid repository name - } as unknown as AppContentConfig; - - const result = validateAppContentJsonFile(invalidDeploymentDetailsConfig); - expect(result).toBe(false); - expect(BspAppDownloadLogger.logger.error).toBeCalledWith(t('error.invalidRepositoryName')); - }); -}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9ef25acde4..4cfaef65c3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -991,6 +991,9 @@ importers: '@types/adm-zip': specifier: 0.5.5 version: 0.5.5 + '@types/fs-extra': + specifier: 9.0.13 + version: 9.0.13 '@types/inquirer': specifier: 8.2.6 version: 8.2.6 @@ -1018,6 +1021,9 @@ importers: '@vscode-logging/logger': specifier: 2.0.0 version: 2.0.0 + fs-extra: + specifier: 10.0.0 + version: 10.0.0 lodash: specifier: 4.17.21 version: 4.17.21 From fe690195a1e55d39778bf82b94aa2bec98ee8637 Mon Sep 17 00:00:00 2001 From: I743583 Date: Tue, 8 Apr 2025 19:12:54 +0100 Subject: [PATCH 13/41] wip: add files --- .../src/utils/validators.ts | 93 +++++++++ .../test/fixtures/example-app-content.ts | 28 +++ .../test/fixtures/index.ts | 19 ++ .../test/fixtures/project/package.json | 3 + .../project/webapp/i18n/i18n.properties | 18 ++ .../fixtures/project/webapp/manifest.json | 180 ++++++++++++++++++ .../test/utils/validators.test.ts | 112 +++++++++++ 7 files changed, 453 insertions(+) create mode 100644 packages/bsp-app-download-sub-generator/src/utils/validators.ts create mode 100644 packages/bsp-app-download-sub-generator/test/fixtures/example-app-content.ts create mode 100644 packages/bsp-app-download-sub-generator/test/fixtures/index.ts create mode 100644 packages/bsp-app-download-sub-generator/test/fixtures/project/package.json create mode 100644 packages/bsp-app-download-sub-generator/test/fixtures/project/webapp/i18n/i18n.properties create mode 100755 packages/bsp-app-download-sub-generator/test/fixtures/project/webapp/manifest.json create mode 100644 packages/bsp-app-download-sub-generator/test/utils/validators.test.ts diff --git a/packages/bsp-app-download-sub-generator/src/utils/validators.ts b/packages/bsp-app-download-sub-generator/src/utils/validators.ts new file mode 100644 index 0000000000..ad7f8914a1 --- /dev/null +++ b/packages/bsp-app-download-sub-generator/src/utils/validators.ts @@ -0,0 +1,93 @@ +import { t } from '../utils/i18n'; +import type { AppContentConfig, AppInfo } from '../app/types'; +import BspAppDownloadLogger from '../utils/logger'; +import { PromptState } from '../prompts/prompt-state'; + +/** + * Validates the metadata section of the app configuration. + * + * @param {AppContentConfig['metadata']} metadata - The metadata object. + * @returns {boolean} - Returns true if valid, false otherwise. + */ +const validateMetadata = (metadata: AppContentConfig['metadata']): boolean => { + if (!metadata.package || typeof metadata.package !== 'string') { + BspAppDownloadLogger.logger?.error(t('error.invalidMetadataPackage')); + return false; + } + return true; +}; + +/** + * Validates the service binding details section of the app configuration. + * + * @param {AppContentConfig['serviceBindingDetails']} serviceBinding - The service binding details object. + * @returns {boolean} - Returns true if valid, false otherwise. + */ +const validateServiceBindingDetails = (serviceBinding: AppContentConfig['serviceBindingDetails']): boolean => { + if (!serviceBinding.serviceName || typeof serviceBinding.serviceName !== 'string') { + BspAppDownloadLogger.logger?.error(t('error.invalidServiceName')); + return false; + } + if (!serviceBinding.serviceVersion || typeof serviceBinding.serviceVersion !== 'string') { + BspAppDownloadLogger.logger?.error(t('error.invalidServiceVersion')); + return false; + } + if (!serviceBinding.mainEntityName || typeof serviceBinding.mainEntityName !== 'string') { + BspAppDownloadLogger.logger?.error(t('error.invalidMainEntityName')); + return false; + } + return true; +}; + +/** + * Validates the project attribute section of the app configuration. + * + * @param {AppContentConfig['projectAttribute']} projectAttribute - The project attribute object. + * @returns {boolean} - Returns true if valid, false otherwise. + */ +const validateProjectAttribute = (projectAttribute: AppContentConfig['projectAttribute']): boolean => { + if (!projectAttribute.moduleName || typeof projectAttribute.moduleName !== 'string') { + BspAppDownloadLogger.logger?.error(t('error.invalidModuleName')); + return false; + } + return true; +}; + +/** + * Validates the deployment details section of the app configuration. + * + * @param {AppContentConfig['deploymentDetails']} deploymentDetails - The deployment details object. + * @returns {boolean} - Returns true if valid, false otherwise. + */ +const validateDeploymentDetails = (deploymentDetails: AppContentConfig['deploymentDetails']): boolean => { + if (!deploymentDetails.repositoryName) { + BspAppDownloadLogger.logger?.error(t('error.invalidRepositoryName')); + return false; + } + return true; +}; + +/** + * Validates the entire app configuration. + * + * @param {AppContentConfig} config - The app configuration object. + * @returns {boolean} - Returns true if the configuration is valid, false otherwise. + */ +export const validateAppContentJsonFile = (config: AppContentConfig): boolean => { + return ( + validateMetadata(config.metadata) && + validateServiceBindingDetails(config.serviceBindingDetails) && + validateProjectAttribute(config.projectAttribute) && + validateDeploymentDetails(config.deploymentDetails) + ); +}; + +/** Validates the prompt state for the app download process. + * + * @param {string} targetFolder - The target folder for the app download. + * @param {string} [appId] - The selected app id. + * @returns {boolean} - Returns true if the prompt state is valid, false otherwise. + */ +export const isValidPromptState = (targetFolder: string, appId?: string): boolean => { + return !!(PromptState.systemSelection.connectedSystem?.serviceProvider && appId && targetFolder); +}; diff --git a/packages/bsp-app-download-sub-generator/test/fixtures/example-app-content.ts b/packages/bsp-app-download-sub-generator/test/fixtures/example-app-content.ts new file mode 100644 index 0000000000..771c87cfe3 --- /dev/null +++ b/packages/bsp-app-download-sub-generator/test/fixtures/example-app-content.ts @@ -0,0 +1,28 @@ +export const sampleAppContentTestData = { + metadata: { + package: 'TEST_PACKAGE', + masterLanguage: 'EN' + }, + serviceBindingDetails: { + name: 'TEST_SERVICE_BINDING', + serviceName: 'TEST_SERVICE_NAME', + serviceVersion: '0001', + mainEntityName: 'TEST_ENTITY' + }, + projectAttribute: { + moduleName: 'test_module_name', + applicationTitle: 'Test Application Title', + template: 'Test Template', + minimumUi5Version: '1.100.0' + }, + deploymentDetails: { + repositoryName: 'TEST_REPO_NAME', + repositoryDescription: 'This is a test repository' + }, + fioriLaunchpadConfiguration: { + semanticObject: 'TEST_SEMANTIC_OBJECT', + action: 'testAction', + title: 'Test Title', + subtitle: 'Test Subtitle' + } +}; \ No newline at end of file diff --git a/packages/bsp-app-download-sub-generator/test/fixtures/index.ts b/packages/bsp-app-download-sub-generator/test/fixtures/index.ts new file mode 100644 index 0000000000..9c92c9f32f --- /dev/null +++ b/packages/bsp-app-download-sub-generator/test/fixtures/index.ts @@ -0,0 +1,19 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +/** + * A simple caching store for test fixtures + * + * @export + * @class TestFixture + */ +export class TestFixture { + private fileContents: { [filename: string]: string } = {}; + + getContents(relativePath: string): string { + if (!this.fileContents[relativePath]) { + this.fileContents[relativePath] = fs.readFileSync(path.join(__dirname, relativePath)).toString(); + } + return this.fileContents[relativePath]; + } +} diff --git a/packages/bsp-app-download-sub-generator/test/fixtures/project/package.json b/packages/bsp-app-download-sub-generator/test/fixtures/project/package.json new file mode 100644 index 0000000000..5f2b205122 --- /dev/null +++ b/packages/bsp-app-download-sub-generator/test/fixtures/project/package.json @@ -0,0 +1,3 @@ +{ + "scripts": {} +} \ No newline at end of file diff --git a/packages/bsp-app-download-sub-generator/test/fixtures/project/webapp/i18n/i18n.properties b/packages/bsp-app-download-sub-generator/test/fixtures/project/webapp/i18n/i18n.properties new file mode 100644 index 0000000000..9e5857c34d --- /dev/null +++ b/packages/bsp-app-download-sub-generator/test/fixtures/project/webapp/i18n/i18n.properties @@ -0,0 +1,18 @@ +# This is the resource bundle for project13356 + +#Texts for manifest.json + +#XTIT: Application name +appTitle=App Gen App Title + +#YDES: Application description +appDescription=App Gen Desc + +flpTitle=FLP Title +flpSubtitle=FLP Subtitle + +# User app specific + +click=Click +reload=Reload +error=Please check your network connection \ No newline at end of file diff --git a/packages/bsp-app-download-sub-generator/test/fixtures/project/webapp/manifest.json b/packages/bsp-app-download-sub-generator/test/fixtures/project/webapp/manifest.json new file mode 100755 index 0000000000..3508a595d5 --- /dev/null +++ b/packages/bsp-app-download-sub-generator/test/fixtures/project/webapp/manifest.json @@ -0,0 +1,180 @@ +{ + "_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/MOCK_TRAVEL/", + "type": "OData", + "settings": { + "annotations": ["MOCK_TRAVEL_VAN", "annotation"], + "localUri": "localService/metadata.xml" + } + }, + "MOCK_TRAVEL_VAN": { + "uri": "/sap/opu/odata/IWFND/CATALOGSERVICE;v=2/Annotations(TechnicalName='MOCK_TRAVEL_VAN',Version='0001')/$value/", + "type": "ODataAnnotation", + "settings": { + "localUri": "localService/MOCK_TRAVEL_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/bsp-app-download-sub-generator/test/utils/validators.test.ts b/packages/bsp-app-download-sub-generator/test/utils/validators.test.ts new file mode 100644 index 0000000000..3445fb7d76 --- /dev/null +++ b/packages/bsp-app-download-sub-generator/test/utils/validators.test.ts @@ -0,0 +1,112 @@ +import { validateAppContentJsonFile } from '../../src/utils/validators'; +import { AppContentConfig } from '../../src/app/types'; +import { t } from '../../src/utils/i18n'; +import BspAppDownloadLogger from '../../src/utils/logger'; + +jest.mock('../../src/utils/logger', () => ({ + logger: { + error: jest.fn() + } +})); + +describe('validateAppContentJsonFile', () => { + const validConfig: AppContentConfig = { + metadata: { package: 'valid-package' }, + serviceBindingDetails: { + serviceName: 'validService', + serviceVersion: '1.0.0', + mainEntityName: 'validEntity', + }, + projectAttribute: { moduleName: 'validModule' }, + deploymentDetails: { repositoryName: 'validRepository' }, + fioriLaunchpadConfiguration: { + semanticObject: 'semanticObject', + action: 'action', + title: 'title' + }, + }; + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should return true when all validation functions pass', () => { + const result = validateAppContentJsonFile(validConfig); + expect(result).toBe(true); + }); + + it('should return false and log an error when metadata validation fails', () => { + const invalidMetadataConfig = { + ...validConfig, + metadata: { package: '' } // Invalid package + } as unknown as AppContentConfig; + + const result = validateAppContentJsonFile(invalidMetadataConfig); + expect(result).toBe(false); + expect(BspAppDownloadLogger.logger.error).toBeCalledWith(t('error.invalidMetadataPackage')); + }); + + it('should return false and log an error when service binding details validation fails', () => { + const invalidServiceBindingConfig = { + ...validConfig, + serviceBindingDetails: { + ...validConfig.serviceBindingDetails, + serviceName: '', // Invalid service name + } + } as unknown as AppContentConfig; + + const result = validateAppContentJsonFile(invalidServiceBindingConfig); + expect(result).toBe(false); + expect(BspAppDownloadLogger.logger.error).toBeCalledWith(t('error.invalidServiceName')); + }); + + it('should return false and log an error when service binding version is not provided', () => { + const invalidServiceBindingConfig = { + ...validConfig, + serviceBindingDetails: { + ...validConfig.serviceBindingDetails, + serviceVersion: '' // Invalid service version + } + } as unknown as AppContentConfig; + + const result = validateAppContentJsonFile(invalidServiceBindingConfig); + expect(result).toBe(false); + expect(BspAppDownloadLogger.logger.error).toBeCalledWith(t('error.invalidServiceVersion')); + }); + + it('should return false and log an error when main entity name is missing', () => { + const invalidServiceBindingConfig = { + ...validConfig, + serviceBindingDetails: { + ...validConfig.serviceBindingDetails, + mainEntityName: '' // Invalid main entity name + } + } as unknown as AppContentConfig; + + const result = validateAppContentJsonFile(invalidServiceBindingConfig); + expect(result).toBe(false); + expect(BspAppDownloadLogger.logger.error).toBeCalledWith(t('error.invalidMainEntityName')); + }); + + it('should return false and log an error when project attribute validation fails', () => { + const invalidProjectAttributeConfig = { + ...validConfig, + projectAttribute: { moduleName: '' } // Invalid module name + } as unknown as AppContentConfig; + + const result = validateAppContentJsonFile(invalidProjectAttributeConfig); + expect(result).toBe(false); + expect(BspAppDownloadLogger.logger.error).toBeCalledWith(t('error.invalidModuleName')); + }); + + it('should return false and log an error when deployment details validation fails', () => { + const invalidDeploymentDetailsConfig = { + ...validConfig, + deploymentDetails: { repositoryName: '' } // Invalid repository name + } as unknown as AppContentConfig; + + const result = validateAppContentJsonFile(invalidDeploymentDetailsConfig); + expect(result).toBe(false); + expect(BspAppDownloadLogger.logger.error).toBeCalledWith(t('error.invalidRepositoryName')); + }); +}); From eaf9be10a86b0a4edbf045cb04f591119f42be79 Mon Sep 17 00:00:00 2001 From: I743583 Date: Wed, 9 Apr 2025 14:54:34 +0100 Subject: [PATCH 14/41] add tests --- .../src/app/example-app-content.ts | 44 +- .../src/app/index.ts | 85 +-- .../src/app/types.ts | 2 +- .../src/prompts/prompt-helpers.ts | 20 +- .../src/prompts/prompts.ts | 14 +- .../bsp-app-download-sub-generator.i18n.json | 5 +- .../src/utils/download-utils.ts | 2 +- .../src/utils/file-helpers.ts | 11 +- .../src/utils/validators.ts | 7 +- .../test/app.test.ts | 536 ++++++++++++++---- .../test/config.test.ts | 2 +- .../test/fixtures/downloaded-app/cdm.json | 60 ++ .../test/fixtures/downloaded-app/component.js | 11 + .../example-app-content.ts | 0 .../test/fixtures/downloaded-app/flp.html | 101 ++++ .../fixtures/downloaded-app/i18n.properties | 5 + .../test/fixtures/downloaded-app/index.html | 33 ++ .../fixtures/downloaded-app/manifest.json | 149 +++++ .../test/fixtures/project/package.json | 3 - .../project/webapp/i18n/i18n.properties | 18 - .../fixtures/project/webapp/manifest.json | 180 ------ .../test/utils/download-utils.test.ts | 5 - .../test/utils/logger.test.ts | 47 ++ 23 files changed, 929 insertions(+), 411 deletions(-) create mode 100644 packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/cdm.json create mode 100644 packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/component.js rename packages/bsp-app-download-sub-generator/test/fixtures/{ => downloaded-app}/example-app-content.ts (100%) create mode 100644 packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/flp.html create mode 100644 packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/i18n.properties create mode 100644 packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/index.html create mode 100644 packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/manifest.json delete mode 100644 packages/bsp-app-download-sub-generator/test/fixtures/project/package.json delete mode 100644 packages/bsp-app-download-sub-generator/test/fixtures/project/webapp/i18n/i18n.properties delete mode 100755 packages/bsp-app-download-sub-generator/test/fixtures/project/webapp/manifest.json create mode 100644 packages/bsp-app-download-sub-generator/test/utils/logger.test.ts diff --git a/packages/bsp-app-download-sub-generator/src/app/example-app-content.ts b/packages/bsp-app-download-sub-generator/src/app/example-app-content.ts index d3996c549b..af44b5f04b 100644 --- a/packages/bsp-app-download-sub-generator/src/app/example-app-content.ts +++ b/packages/bsp-app-download-sub-generator/src/app/example-app-content.ts @@ -1,28 +1,28 @@ -export const sampleAppContentJson = { - 'metadata': { - 'package': 'Z_I576700', - 'masterLanguage': 'EN' +export const sampleAppContentTestData = { + metadata: { + package: 'TEST_PACKAGE', + masterLanguage: 'EN' }, - 'serviceBindingDetails': { - 'name': 'Z_RAHI_SRVB_V2', - 'serviceName': 'Z_RAHI_SRVB_V2', - 'serviceVersion': '0001', - 'mainEntityName': 'I_BusinessPartner' + serviceBindingDetails: { + name: 'TEST_SERVICE_BINDING', + serviceName: 'TEST_SERVICE_NAME', + serviceVersion: '0001', + mainEntityName: 'TEST_ENTITY' }, - 'projectAttribute': { - 'moduleName': 'z_module_name', - 'applicationTitle': 'This is a demo app', - 'template': 'List Report', - 'minimumUi5Version': '1.130.5' + projectAttribute: { + moduleName: 'test_module_name', + applicationTitle: 'Test Application Title', + template: 'Test Template', + minimumUi5Version: '1.100.0' }, - 'deploymentDetails': { - 'repositoryName': 'ZTEST_MOD_NAME', - 'repositoryDescription': 'This is demo app' + deploymentDetails: { + repositoryName: 'TEST_REPO_NAME', + repositoryDescription: 'This is a test repository' }, - 'fioriLaunchpadConfiguration': { - 'semanticObject': 'Z_SO', - 'action': 'manage', - 'title': 'Z_Title', - 'subtitle': 'Z_sub_titile' + fioriLaunchpadConfiguration: { + semanticObject: 'TEST_SEMANTIC_OBJECT', + action: 'testAction', + title: 'Test Title', + subtitle: 'Test Subtitle' } }; diff --git a/packages/bsp-app-download-sub-generator/src/app/index.ts b/packages/bsp-app-download-sub-generator/src/app/index.ts index ec3d3ac8fc..02a53dd81c 100644 --- a/packages/bsp-app-download-sub-generator/src/app/index.ts +++ b/packages/bsp-app-download-sub-generator/src/app/index.ts @@ -11,7 +11,13 @@ import { downloadApp } from '../utils/download-utils'; import { EventName } from '../telemetryEvents'; import type { YeomanEnvironment } from '@sap-ux/fiori-generator-shared'; import { getDefaultTargetFolder } from '@sap-ux/fiori-generator-shared'; -import type { BspAppDownloadOptions, BspAppDownloadAnswers, BspAppDownloadQuestions, AppContentConfig, QuickDeployedAppConfig } from './types'; +import type { + BspAppDownloadOptions, + BspAppDownloadAnswers, + BspAppDownloadQuestions, + AppContentConfig, + QuickDeployedAppConfig +} from './types'; import { getPrompts } from '../prompts/prompts'; import { generate, TemplateType, type FioriElementsApp, type LROPSettings } from '@sap-ux/fiori-elements-writer'; import { join, basename } from 'path'; @@ -29,7 +35,7 @@ import { PromptState } from '../prompts/prompt-state'; import { PromptNames } from './types'; import { getAbapDeployConfig, getAppConfig } from './config'; import type { AbapDeployConfig } from '@sap-ux/ui5-config'; -import { sampleAppContentJson } from './example-app-content'; +import { sampleAppContentTestData } from './example-app-content'; import { replaceWebappFiles } from '../utils/file-helpers'; import { fetchAppListForSelectedSystem, extractAppData } from '../prompts/prompt-helpers'; import { isValidPromptState, validateAppContentJsonFile } from '../utils/validators'; @@ -46,7 +52,6 @@ export default class extends Generator { private readonly prompts: Prompts; private answers: BspAppDownloadAnswers = defaultAnswers; public options: BspAppDownloadOptions; - // re visit this private projectPath: string; private extractedProjectPath: string; setPromptsCallback: (fn: object) => void; @@ -91,7 +96,7 @@ export default class extends Generator { } /** - * Initializes necessary settings and telemetry for the generator. + * Initialises necessary settings and telemetry for the generator. */ public async initializing(): Promise { if ((this.env as unknown as YeomanEnvironment).conflicter) { @@ -118,41 +123,53 @@ export default class extends Generator { const answers: BspAppDownloadAnswers = await this.prompt(questions); const { targetFolder } = answers; if (quickDeployedAppConfig?.appId) { - // Handle quick deployed app download where prompts for system selection and app selection are not shown - // Only target folder ptompt is shown + // Handle quick deployed app download where prompts for system selection and app selection are not displayed + // Only target folder prompt is shown await this._handleQuickDeployedAppDownload(quickDeployedAppConfig, targetFolder); } else { - // Handle normal app download where prompts for system selection and app selection are shown + // Handle app download where prompts for system selection and app selection are shown Object.assign(this.answers, answers); } - if (isValidPromptState(targetFolder, this.answers.selectedApp.appId)) { - this.projectPath = join(targetFolder, this.answers.selectedApp.appId); - this.extractedProjectPath = join(this.projectPath, extractedFilePath); - // Trigger app download - await downloadApp(this.answers.selectedApp.repoName, this.extractedProjectPath, this.fs); - } } - private async _handleQuickDeployedAppDownload (quickDeployedAppConfig: QuickDeployedAppConfig, targetFolder: string): Promise { + /** + * + * @param quickDeployedAppConfig + * @param targetFolder + */ + private async _handleQuickDeployedAppDownload( + quickDeployedAppConfig: QuickDeployedAppConfig, + targetFolder: string + ): Promise { debugger; const appList = await fetchAppListForSelectedSystem( quickDeployedAppConfig.serviceProvider, quickDeployedAppConfig.appId ); - if(!appList.length) { - BspAppDownloadLogger.logger?.error(t('error.quickDeployedAppDownloadErrors.noAppsFound', { appId: quickDeployedAppConfig.appId })); + if (!appList.length) { + BspAppDownloadLogger.logger?.error( + t('error.quickDeployedAppDownloadErrors.noAppsFound', { appId: quickDeployedAppConfig.appId }) + ); + throw new Error(); } this.answers.selectedApp = extractAppData(appList[0]).value; this.answers.targetFolder = targetFolder; - this.answers.systemSelection = PromptState.systemSelection; + this.answers.systemSelection = PromptState.systemSelection; } /** * Writes the configuration files for the project, including deployment config, and README. */ public async writing(): Promise { + if (isValidPromptState(this.answers.targetFolder, this.answers.selectedApp.appId)) { + this.projectPath = join(this.answers.targetFolder, this.answers.selectedApp.appId); + this.extractedProjectPath = join(this.projectPath, extractedFilePath); + // Trigger app download + await downloadApp(this.answers.selectedApp.repoName, this.extractedProjectPath, this.fs); + } + // const appContentJsonTempPath = join(__dirname, 'example-app-content.json'); - const appContentJson: AppContentConfig = sampleAppContentJson; + const appContentJson: AppContentConfig = sampleAppContentTestData; // todo: add back once json is available along with downloaded app // if(!this.fs.exists(appContentJsonTempPath)) { // appContentJson = this.fs.readJSON(appContentJsonTempPath) as unknown as AppContentConfig; //todo: extract from extracted path @@ -186,7 +203,7 @@ export default class extends Generator { writeApplicationInfoSettings(this.projectPath, this.fs); } // Replace webapp files with downloaded app files - replaceWebappFiles(this.projectPath, this.extractedProjectPath, this.fs); + await replaceWebappFiles(this.projectPath, this.extractedProjectPath, this.fs); // Clean up extracted project files // this.fs.delete(this.extractedProjectPath); } @@ -306,11 +323,11 @@ export default class extends Generator { }; updateWorkspaceFoldersIfNeeded(updateWorkspaceFolders); } - if (this.options.data?.postGenCommands) { + if (this.options.data?.postGenCommand) { await runPostAppGenHook({ path: this.projectPath, vscodeInstance: this.vscode, - postGenCommand: this.options.data?.postGenCommands + postGenCommand: this.options.data?.postGenCommand }); } } @@ -319,17 +336,21 @@ export default class extends Generator { * Finalises the generator process by creating launch configurations and running post-generation hooks. */ async end() { - this.appWizard.showWarning(t('info.bspAppDownloadCompleteMsg'), MessageType.prompt); - sendTelemetry( - EventName.GENERATION_SUCCESS, - TelemetryHelper.createTelemetryData({ - appType: 'bsp-app-download-sub-generator', - ...this.options.telemetryData - }) ?? {} - ).catch((error) => { - BspAppDownloadLogger.logger.error(t('error.telemetry', { error })); - }); - await this._handlePostAppGeneration(); + try { + this.appWizard.showWarning(t('info.bspAppDownloadCompleteMsg'), MessageType.notification); + await sendTelemetry( + EventName.GENERATION_SUCCESS, + TelemetryHelper.createTelemetryData({ + appType: 'bsp-app-download-sub-generator', + ...this.options.telemetryData + }) ?? {} + ).catch((error) => { + BspAppDownloadLogger.logger?.error(t('error.telemetry', { error: error.message })); + }); + await this._handlePostAppGeneration(); + } catch (error) { + BspAppDownloadLogger.logger?.error(t('error.endPhase', { error: error.message })); + } } } diff --git a/packages/bsp-app-download-sub-generator/src/app/types.ts b/packages/bsp-app-download-sub-generator/src/app/types.ts index 09bc9d206e..41f0bbbf88 100644 --- a/packages/bsp-app-download-sub-generator/src/app/types.ts +++ b/packages/bsp-app-download-sub-generator/src/app/types.ts @@ -8,7 +8,7 @@ import type { YUIQuestion } from '@sap-ux/inquirer-common'; import type { AutocompleteQuestionOptions } from 'inquirer-autocomplete-prompt'; /** - * Quick deploy app config are applicable only in scenarios where an application + * Quick deploy app config are applicable only in scenarios where an application * deployed via ADT Quick Deploy is being downloaded from a BSP repository. */ export interface QuickDeployedAppConfig { diff --git a/packages/bsp-app-download-sub-generator/src/prompts/prompt-helpers.ts b/packages/bsp-app-download-sub-generator/src/prompts/prompt-helpers.ts index 039781fc06..a9721da13a 100644 --- a/packages/bsp-app-download-sub-generator/src/prompts/prompt-helpers.ts +++ b/packages/bsp-app-download-sub-generator/src/prompts/prompt-helpers.ts @@ -2,9 +2,8 @@ import { generatorTitle, generatorDescription } from '../utils/constants'; import { appListSearchParams, appListResultFields } from '../utils/constants'; import type { AbapServiceProvider, AppIndex } from '@sap-ux/axios-extension'; import type { AppInfo } from '../app/types'; -import { PromptNames } from '../app/types'; import { PromptState } from './prompt-state'; -import type { BspAppDownloadAnswers, AppItem } from '../app/types'; +import type { AppItem } from '../app/types'; import { t } from '../utils/i18n'; import BspAppDownloadLogger from '../utils/logger'; @@ -71,14 +70,17 @@ export const formatAppChoices = (appList: AppIndex): Array<{ name: string; value * Fetches a list of deployed applications from the ABAP repository. * * @param {AbapServiceProvider} provider - The ABAP service provider. + * @param appId * @returns {Promise} A list of applications filtered by source template. */ async function getAppList(provider: AbapServiceProvider, appId?: string): Promise { try { - const searchParams = appId ? { - ...appListSearchParams, - 'sap.app/id': appId - } : appListSearchParams + const searchParams = appId + ? { + ...appListSearchParams, + 'sap.app/id': appId + } + : appListSearchParams; return await provider.getAppIndex().search(searchParams, appListResultFields); } catch (error) { BspAppDownloadLogger.logger?.error(t('error.applicationListFetchError', { error: error.message })); @@ -90,9 +92,13 @@ async function getAppList(provider: AbapServiceProvider, appId?: string): Promis * Fetches the application list for the selected system. * * @param {AbapServiceProvider} serviceProvider - The ABAP service provider. + * @param appId * @returns {Promise} A list of applications filtered by source template. */ -export async function fetchAppListForSelectedSystem(serviceProvider: AbapServiceProvider, appId?: string): Promise { +export async function fetchAppListForSelectedSystem( + serviceProvider: AbapServiceProvider, + appId?: string +): Promise { if (serviceProvider) { PromptState.systemSelection = { connectedSystem: { serviceProvider } diff --git a/packages/bsp-app-download-sub-generator/src/prompts/prompts.ts b/packages/bsp-app-download-sub-generator/src/prompts/prompts.ts index 9005a0f8a2..078130e7f1 100644 --- a/packages/bsp-app-download-sub-generator/src/prompts/prompts.ts +++ b/packages/bsp-app-download-sub-generator/src/prompts/prompts.ts @@ -13,6 +13,7 @@ import { fetchAppListForSelectedSystem } from './prompt-helpers'; * Gets the target folder selection prompt. * * @param {string} [appRootPath] - The application root path. + * @param appId * @returns {FileBrowserQuestion} The target folder prompt configuration. */ const getTargetFolderPrompt = (appRootPath?: string, appId?: string): FileBrowserQuestion => { @@ -22,14 +23,14 @@ const getTargetFolderPrompt = (appRootPath?: string, appId?: string): FileBrowse message: t('prompts.targetPath.message'), guiType: 'folder-browser', when: (answers: BspAppDownloadAnswers) => { - // Display the prompt if appId is provided. This occurs when the generator is invoked + // Display the prompt if appId is provided. This occurs when the generator is invoked // as part of the quick deployment process from ADT. if (appId) { return true; } // If appId is not provided, check if the user has selected an app. // If an app is selected, display the prompt accordingly. - return Boolean(answers?.selectedApp?.appId) + return Boolean(answers?.selectedApp?.appId); }, guiOptions: { applyDefaultWhenDirty: true, @@ -51,7 +52,10 @@ const getTargetFolderPrompt = (appRootPath?: string, appId?: string): FileBrowse * @param {QuickDeployConfig} [quickDeployedAppConfig] - quick deploy config. * @returns {Promise} A list of questions for user interaction. */ -export async function getPrompts(appRootPath?: string, quickDeployedAppConfig?: QuickDeployedAppConfig): Promise { +export async function getPrompts( + appRootPath?: string, + quickDeployedAppConfig?: QuickDeployedAppConfig +): Promise { PromptState.reset(); // If quickDeployedAppConfig is provided, return only the target folder prompt if (quickDeployedAppConfig?.appId) { @@ -63,7 +67,7 @@ export async function getPrompts(appRootPath?: string, quickDeployedAppConfig?: const appSelectionPrompt = [ { when: async (answers: BspAppDownloadAnswers): Promise => { - if(answers[PromptNames.systemSelection]) { + if (answers[PromptNames.systemSelection]) { appList = await fetchAppListForSelectedSystem( systemQuestions.answers.connectedSystem?.serviceProvider as AbapServiceProvider ); @@ -85,4 +89,4 @@ export async function getPrompts(appRootPath?: string, quickDeployedAppConfig?: const targetFolderPrompts = getTargetFolderPrompt(appRootPath, quickDeployedAppConfig?.appId); return [...systemQuestions.prompts, ...appSelectionPrompt, targetFolderPrompts] as BspAppDownloadQuestions[]; -} \ No newline at end of file +} diff --git a/packages/bsp-app-download-sub-generator/src/translations/bsp-app-download-sub-generator.i18n.json b/packages/bsp-app-download-sub-generator/src/translations/bsp-app-download-sub-generator.i18n.json index 0028208f9d..524c023e02 100644 --- a/packages/bsp-app-download-sub-generator/src/translations/bsp-app-download-sub-generator.i18n.json +++ b/packages/bsp-app-download-sub-generator/src/translations/bsp-app-download-sub-generator.i18n.json @@ -7,6 +7,7 @@ "applicationListFetchError": "Error fetching application list: {{- error}}", "metadataFetchError": "Error fetching metadata: {{- error}}", "appConfigGenError": "Error generating application configuration: {{- error}}", + "endPhase": "Error in end phase: {{- error}}", "validationErrors": { "invalidMetadataPackage": "Invalid or missing package in metadata", "invalidServiceName": "Invalid or missing serviceName in serviceBindingDetails", @@ -36,7 +37,7 @@ "sourceTemplateNotSupported": "Error: Source template not supported" }, "quickDeployedAppDownloadErrors": { - "noAppsFound": "No application with if {{ appId }} found in the system. Please check if the application is deployed correctly" + "noAppsFound": "No application with id {{ appId }} found in the system. Please check if the application is deployed correctly" } }, "warn": { @@ -58,7 +59,7 @@ "appDescription" : "This application was converted from an ABAP basic app that was deployed from ADT", "launchText": "In order to launch the generated app, simply run the following from the generated app root folder:\n\n```\n npm start\n```" }, - "success": { + "info": { "bspAppDownloadCompleteMsg": "The selected application has been downloaded and updated to support SAP Fiori tools" } } diff --git a/packages/bsp-app-download-sub-generator/src/utils/download-utils.ts b/packages/bsp-app-download-sub-generator/src/utils/download-utils.ts index dd74bf4cd2..f9562350ed 100644 --- a/packages/bsp-app-download-sub-generator/src/utils/download-utils.ts +++ b/packages/bsp-app-download-sub-generator/src/utils/download-utils.ts @@ -39,8 +39,8 @@ async function extractZip(extractedProjectPath: string, archive: Buffer, fs: Edi export async function downloadApp(repoName: string, extractedProjectPath: string, fs: Editor): Promise { try { const serviceProvider = PromptState.systemSelection?.connectedSystem?.serviceProvider as AbapServiceProvider; + debugger; const archive = await serviceProvider.getUi5AbapRepository().downloadFiles(repoName); - if (Buffer.isBuffer(archive)) { await extractZip(extractedProjectPath, archive, fs); } else { diff --git a/packages/bsp-app-download-sub-generator/src/utils/file-helpers.ts b/packages/bsp-app-download-sub-generator/src/utils/file-helpers.ts index dce4c9568a..b1115df993 100644 --- a/packages/bsp-app-download-sub-generator/src/utils/file-helpers.ts +++ b/packages/bsp-app-download-sub-generator/src/utils/file-helpers.ts @@ -30,7 +30,6 @@ export function readManifest(extractedProjectPath: string, fs: Editor): Manifest return manifest; } - /** * Replaces the specified files in the `webapp` directory with the corresponding files from the `extractedPath`. * @@ -38,11 +37,7 @@ export function readManifest(extractedProjectPath: string, fs: Editor): Manifest * @param {string} extractedPath - The path from which files will be copied. * @param {Editor} fs - The file system editor instance to modify files in memory. */ -export async function replaceWebappFiles( - projectPath: string, - extractedPath: string, - fs: Editor -): Promise { +export async function replaceWebappFiles(projectPath: string, extractedPath: string, fs: Editor): Promise { try { const webappPath = join(projectPath, DirName.Webapp); // Define the paths of the files to be replaced @@ -51,7 +46,6 @@ export async function replaceWebappFiles( { webappFile: 'i18n/i18n.properties', extractedFile: 'i18n.properties' }, // replace 'i18n/i18n.properties' in extractedFile { webappFile: 'index.html', extractedFile: 'index.html' } ]; - // Loop through each file and perform the replacement for (const { webappFile, extractedFile } of filesToReplace) { const webappFilePath = join(webappPath, webappFile); @@ -68,6 +62,3 @@ export async function replaceWebappFiles( BspAppDownloadLogger.logger?.error(t('error.replaceWebappFilesError', { error })); } } - - - diff --git a/packages/bsp-app-download-sub-generator/src/utils/validators.ts b/packages/bsp-app-download-sub-generator/src/utils/validators.ts index ad7f8914a1..0fc3e6c7d6 100644 --- a/packages/bsp-app-download-sub-generator/src/utils/validators.ts +++ b/packages/bsp-app-download-sub-generator/src/utils/validators.ts @@ -1,5 +1,5 @@ import { t } from '../utils/i18n'; -import type { AppContentConfig, AppInfo } from '../app/types'; +import type { AppContentConfig } from '../app/types'; import BspAppDownloadLogger from '../utils/logger'; import { PromptState } from '../prompts/prompt-state'; @@ -82,8 +82,9 @@ export const validateAppContentJsonFile = (config: AppContentConfig): boolean => ); }; -/** Validates the prompt state for the app download process. - * +/** + * Validates the prompt state for the app download process. + * * @param {string} targetFolder - The target folder for the app download. * @param {string} [appId] - The selected app id. * @returns {boolean} - Returns true if the prompt state is valid, false otherwise. diff --git a/packages/bsp-app-download-sub-generator/test/app.test.ts b/packages/bsp-app-download-sub-generator/test/app.test.ts index 5cdc6878a4..a7e5d83487 100644 --- a/packages/bsp-app-download-sub-generator/test/app.test.ts +++ b/packages/bsp-app-download-sub-generator/test/app.test.ts @@ -1,153 +1,447 @@ import yeomanTest from 'yeoman-test'; -import { AppWizard } from '@sap-devx/yeoman-ui-types'; +import { AppWizard, MessageType } from '@sap-devx/yeoman-ui-types'; import { join } from 'path'; import BspAppDownloadGenerator from '../src/app'; import * as prompts from '../src/prompts/prompts'; -import type { BspAppDownloadQuestions } from '../src/app/types'; import { PromptNames } from '../src/app/types'; -import { readManifest } from '../src/utils/file-helpers'; import fs from 'fs'; import { TestFixture } from './fixtures'; import { getAppConfig } from '../src/app/config'; import { OdataVersion } from '@sap-ux/odata-service-inquirer'; import { TemplateType, type FioriElementsApp, type LROPSettings } from '@sap-ux/fiori-elements-writer'; -import { adtSourceTemplateId } from '../src/utils/constants'; +import { adtSourceTemplateId, extractedFilePath } from '../src/utils/constants'; import { removeSync } from 'fs-extra'; +import { isValidPromptState } from '../src/utils/validators'; +import { hostEnvironment, sendTelemetry } from '@sap-ux/fiori-generator-shared'; +import * as memFs from 'mem-fs'; +import * as editor from 'mem-fs-editor'; +import { FileName, DirName } from '@sap-ux/project-access'; +import BspAppDownloadLogger from '../src/utils/logger'; +import { t } from '../src/utils/i18n'; +import { type AbapServiceProvider } from '@sap-ux/axios-extension'; +import { fetchAppListForSelectedSystem } from '../src/prompts/prompt-helpers'; -jest.mock('../src/utils/file-helpers'); -jest.mock('../src/app/config'); +jest.setTimeout(60000); +jest.mock('../src/prompts/prompt-helpers', () => ({ + ...jest.requireActual('../src/prompts/prompt-helpers'), + fetchAppListForSelectedSystem: jest.fn() +})); + +jest.mock('../src/utils/logger', () => ({ + ...jest.requireActual('../src/utils/logger'), + logger: { + error: jest.fn(), + info: jest.fn(), + warn: jest.fn() + }, + configureLogging: jest.fn() +})); + +jest.mock('../src/utils/file-helpers', () => ({ + ...jest.requireActual('../src/utils/file-helpers'), + readManifest: jest.fn() +})); +jest.mock('../src/utils/download-utils'); +jest.mock('../src/app/config', () => ({ + ...jest.requireActual('../src/app/config'), + getAppConfig: jest.fn() +})); +jest.mock('../src/utils/validators'); +jest.mock('@sap-ux/fiori-generator-shared', () => { + return { + ...(jest.requireActual('@sap-ux/fiori-generator-shared') as {}), + TelemetryHelper: { + initTelemetrySettings: jest.fn(), + createTelemetryData: jest.fn().mockReturnValue({ + OperatingSystem: 'CLI', + Platform: 'darwin' + }) + }, + sendTelemetry: jest.fn(), + isExtensionInstalled: jest.fn(), + getHostEnvironment: () => { + return hostEnvironment.cli; + } + }; +}); +const mockSendTelemetry = sendTelemetry as jest.Mock; + +function createAppConfig(appId: string, metadata: string): FioriElementsApp { + return { + app: { + id: appId, + title: 'App 1', + description: 'App 1 description', + sourceTemplate: { id: adtSourceTemplateId }, + projectType: 'EDMXBackend', + flpAppId: `app-1-tile` + }, + package: { + name: appId, + description: 'App 1 description', + devDependencies: {}, + scripts: {}, + version: '0.0.1' + }, + template: { + type: TemplateType.ListReportObjectPage, + settings: { + entityConfig: { mainEntityName: 'Booking' } + } + }, + service: { + path: '/sap/opu/odata4/sap/zsb_travel_draft/srvd/dmo/ui_travel_d_d/0001/', + version: OdataVersion.v4, + metadata: metadata, + url: 'url-1' + }, + appOptions: { + addAnnotations: true, + addTests: true + }, + ui5: { + version: '1.100.0' + } + }; +} + +function mockPrompts(testOutputDir: string): void { + jest.spyOn(prompts, 'getPrompts').mockResolvedValue([ + { + type: 'list', + name: PromptNames.systemSelection, + message: 'Select a system', + choices: [ + { name: 'system1', value: 'system1' }, + { name: 'system2', value: 'system2' }, + { name: 'system3', value: 'system3' } + ] + }, + { + type: 'list', + name: PromptNames.selectedApp, + message: 'Select an app', + choices: [ + { name: 'App 1', value: { appId: 'app-1', title: 'App 1', description: 'App 1 description', repoName: 'app-1-repo', url: 'url-1' } }, + { name: 'App 2', value: { appId: 'app-2', title: 'App 2', description: 'App 2 description', repoName: 'app-2-repo', url: 'url-2' } } + ] + }, + { + type: 'input', + name: PromptNames.targetFolder, + message: 'Enter the target folder', + default: testOutputDir + } + ] as any); +} + +function copyFilesToExtractedProjectPath( + testFixtureDir: string, + extractedProjectPath: string +): void { + if (!fs.existsSync(extractedProjectPath)) { + fs.mkdirSync(extractedProjectPath, { recursive: true }); + } + + // List all files in the test fixture directory + const files = fs.readdirSync(testFixtureDir); + // Copy each file to the extracted project path + files.forEach((file) => { + const sourceFilePath = join(testFixtureDir, file); + const destinationFilePath = join(extractedProjectPath, file); + // Copy the file + fs.copyFileSync(sourceFilePath, destinationFilePath); + }); +} + + +function verifyGeneratedFiles(testOutputDir: string, appId: string, extractedProjectPath: string): void { + const projectPath = join(`${testOutputDir}/${appId}`); + expect(fs.existsSync(projectPath)).toBe(true); + + const expectedFiles = [ + FileName.Ui5Yaml, + FileName.Ui5LocalYaml, + FileName.Ui5MockYaml, + FileName.UI5DeployYaml, + FileName.Package, + 'README.md', + DirName.Webapp, + join(DirName.Webapp, FileName.Manifest), + join(DirName.Webapp, 'index.html'), + join(DirName.Webapp, 'Component.js'), + join(DirName.Webapp, 'test'), + join(DirName.Webapp, DirName.LocalService, 'mainService', 'metadata.xml'), + join(DirName.Webapp, 'i18n', 'i18n.properties'), + join(DirName.Webapp, DirName.Annotations, 'annotation.xml') + ]; + + expectedFiles.forEach((file) => { + const filePath = join(projectPath, file); + expect(fs.existsSync(filePath)).toBe(true); + }); + + expect(fs.readFileSync(join(projectPath, DirName.Webapp, FileName.Manifest), 'utf-8')).toBe( + fs.readFileSync(join(extractedProjectPath, FileName.Manifest), 'utf-8') + ); + expect(fs.readFileSync(join(projectPath, DirName.Webapp, 'i18n', 'i18n.properties'), 'utf-8')).toBe( + fs.readFileSync(join(extractedProjectPath, 'i18n.properties'), 'utf-8') + ); + expect(fs.readFileSync(join(projectPath, DirName.Webapp, 'index.html'), 'utf-8')).toBe( + fs.readFileSync(join(extractedProjectPath, 'index.html'), 'utf-8') + ); +} describe('BSP App Download', () => { const testFixture = new TestFixture(); const bspAppDownloadGenPath = join(__dirname, '../src/app/index.ts'); const testOutputDir = join(__dirname, 'test-output'); - const curTestOutPath = join(testOutputDir, 'app-1'); + const metadata = testFixture.getContents('metadata.xml'); + let appConfig: FioriElementsApp; + let mockVSCode: any; + const mockAppWizard: Partial = { + setHeaderTitle: jest.fn(), + showWarning: jest.fn(), + showError: jest.fn(), + showInformation: jest.fn() + }; + const store = memFs.create(); + const fsEditor = editor.create(store); + const appId = 'app-1', repoName = 'app-1-repo'; + const extractedProjectPath = join(testOutputDir, appId, extractedFilePath); + const testFixtureDir = join(__dirname, 'fixtures', 'downloaded-app'); + copyFilesToExtractedProjectPath(testFixtureDir, extractedProjectPath); + afterEach(() => { jest.clearAllMocks(); jest.restoreAllMocks(); jest.resetModules(); + removeSync(testOutputDir); }) beforeEach(() => { - const promptSpy = jest.spyOn(prompts, 'getPrompts').mockResolvedValue([ - { - type: 'list', - name: PromptNames.systemSelection, - message: 'Select a system', - choices: [ - { name: 'system1', value: 'system1' }, - { name: 'system2', value: 'system2' }, - { name: 'system3', value: 'system3' } - ] - }, - { - type: 'list', - name: PromptNames.selectedApp, - message: 'Select an app', - choices: [ - { name: 'App 1', value: { appId: 'app-1', title: 'App 1', description: 'App 1 description', repoName: 'app-1-repo', url: 'url-1' } }, - { name: 'App 2', value: { appId: 'app-2', title: 'App 2', description: 'App 2 description', repoName: 'app-2-repo', url: 'url-2' } } - ] - }, - { - type: 'input', - name: PromptNames.targetFolder, - message: 'Enter the target folder', - default: 'target-folder' - } - ] as any - ); + copyFilesToExtractedProjectPath(testFixtureDir, extractedProjectPath); + appConfig = createAppConfig(appId, metadata); + mockPrompts(testOutputDir); + mockVSCode = { + workspace: { + workspaceFolders: [], + updateWorkspaceFolders: jest.fn() + } + }; }); - beforeAll(() => { - removeSync(curTestOutPath); // even for in memory + it('Should successfully run BSP app download', async () => { + (isValidPromptState as jest.Mock).mockReturnValue(true); + (getAppConfig as jest.Mock).mockResolvedValue(appConfig); + await expect( + yeomanTest + .run(BspAppDownloadGenerator, { + resolved: bspAppDownloadGenPath + }) + .cd('.') + .withOptions({ + appRootPath: testOutputDir, + appWizard: mockAppWizard, + vscode: mockVSCode, + skipInstall: true + }) + .withPrompts({ + systemSelection: 'system3', + selectedApp: { + appId: appConfig.app.id, + title: appConfig.app.title, + description: appConfig.app.description, + repoName: 'app-1-repo', + url: 'url-1' + }, + targetFolder: testOutputDir + }) + ) + .resolves.not.toThrow(); + verifyGeneratedFiles(testOutputDir, appId, extractedProjectPath); + expect(mockAppWizard.showWarning).toHaveBeenCalledWith(t('info.bspAppDownloadCompleteMsg'), MessageType.notification); + expect(BspAppDownloadLogger.logger.info).toHaveBeenCalledWith(t('info.installationErrors.skippedInstallation')); + + + }); + + it('Should not throw error in end phase if telemetry fails', async () => { + const errorMsg = 'Telemetry error'; + mockSendTelemetry.mockRejectedValue(new Error(errorMsg)); + (isValidPromptState as jest.Mock).mockReturnValue(true); + (getAppConfig as jest.Mock).mockResolvedValue(appConfig); + + await expect( + yeomanTest + .run(BspAppDownloadGenerator, { + resolved: bspAppDownloadGenPath + }) + .cd('.') + .withOptions({ + appRootPath: testOutputDir, + appWizard: mockAppWizard, + vscode: mockVSCode + }) + .withPrompts({ + systemSelection: 'system3', + selectedApp: { + appId: appConfig.app.id, + title: appConfig.app.title, + description: appConfig.app.description, + repoName: 'app-1-repo', + url: 'url-1' + }, + targetFolder: testOutputDir + }) + ) + .resolves.not.toThrow(); + expect(BspAppDownloadLogger.logger.error).toHaveBeenCalledWith(t('error.telemetry', { error: errorMsg })); + verifyGeneratedFiles(testOutputDir, appId, extractedProjectPath); }); - test('run bsp app download', async () => { - - const metadata = fs.readFileSync(join(__dirname, 'fixtures', 'metadata.xml'), 'utf8'); - // const appWizard: Partial = { - // setHeaderTitle: jest.fn(), - // showWarning: jest.fn(), - // showError: jest.fn(), - // showInformation: jest.fn() - // }; - // const testPath = join(curTestOutPath, name); - // const fs = await generate(testPath, config); - const appConfig: FioriElementsApp = { - app: { - id: 'app-1', - title: 'App 1', - description: 'App 1 description', - sourceTemplate: { - id: adtSourceTemplateId - }, - projectType: 'EDMXBackend', - flpAppId: `app-1-tile` - }, - package: { - name: 'app-1', - description: 'App 1 description', - devDependencies: {}, - scripts: {}, - version: '0.0.1' - }, - template: { - type: TemplateType.ListReportObjectPage, - settings: { - entityConfig: { - mainEntityName: 'Booking' + it('Should execute post app gen hook event when postGenCommand is provided', async () => { + (isValidPromptState as jest.Mock).mockReturnValue(true); + (getAppConfig as jest.Mock).mockResolvedValue(appConfig); + + await expect( + yeomanTest + .run(BspAppDownloadGenerator, { + resolved: bspAppDownloadGenPath + }) + .cd('.') + .withOptions({ + appRootPath: testOutputDir, + appWizard: mockAppWizard, + vscode: mockVSCode, + data: { + postGenCommand: 'test-post-gen-command' } - } - }, - service: { - path: '/sap/opu/odata4/sap/zsb_travel_draft/srvd/dmo/ui_travel_d_d/0001/', - version: OdataVersion.v4, - metadata: metadata, - url: 'url-1' - }, - appOptions: { - addAnnotations: true, - addTests: true - }, - ui5: { - version: '1.88.0' - } - }; - - - // (readManifest as jest.Mock).mockReturnValue({ - // 'sap.app': { - // sourceTemplate: { - // id: 'id' - // }, - // dataSources: { - // mainService: { - // uri: "/sap/opu/odata4/sap/zsb_travel_draft/srvd/dmo/ui_travel_d_d/0001/", - // type: "OData", - // settings: { - // odataVersion: "4.0" - // } - // } - // } - // } - // }) + }) + .withPrompts({ + systemSelection: 'system3', + selectedApp: { + appId: appConfig.app.id, + title: appConfig.app.title, + description: appConfig.app.description, + repoName: 'app-1-repo', + url: 'url-1' + }, + targetFolder: testOutputDir + }) + ) + .resolves.not.toThrow(); + verifyGeneratedFiles(testOutputDir, appId, extractedProjectPath); + }); + + it('Should successfully download a quick deployed app from BSP', async () => { + (isValidPromptState as jest.Mock).mockReturnValue(true); (getAppConfig as jest.Mock).mockResolvedValue(appConfig); - await yeomanTest - .run(BspAppDownloadGenerator, { - resolved: bspAppDownloadGenPath - }) - .cd('.') - .withPrompts({ - systemSelection: 'system3', - selectedApp: { - appId: 'app-1', - title: 'App 1', - description: 'App 1 description', - repoName: 'app-1-repo', + (fetchAppListForSelectedSystem as jest.Mock).mockResolvedValue([ + { + 'sap.app/id': appConfig.app.id, + 'sap.app/title': appConfig.app.title, + repoName: repoName, url: 'url-1' - }, - targetFolder: 'target-folder' - }) + } + ]); + const mockServiceProvider = { + defaults: { baseURL: 'https://test-url.com' }, + service: jest.fn().mockReturnValue({ + metadata: jest.fn().mockResolvedValue({ + dataServices: { + schema: [] + } + }) + }) + } as unknown as AbapServiceProvider; + + await expect( + yeomanTest + .run(BspAppDownloadGenerator, { + resolved: bspAppDownloadGenPath + }) + .cd('.') + .withOptions({ + appRootPath: testOutputDir, + appWizard: mockAppWizard, + vscode: mockVSCode, + skipInstall: false, + data: { + postGenCommand: 'test-post-gen-command', + quickDeployedAppConfig: { + appId: appConfig.app.id, + appUrl: 'https://app-url.com/app', + serviceProvider: mockServiceProvider + } + } + }) + .withPrompts({ + systemSelection: 'system3', + selectedApp: { + appId: appConfig.app.id, + title: appConfig.app.title, + description: appConfig.app.description, + repoName: repoName, + url: 'url-1' + }, + targetFolder: testOutputDir + }) + ) + .resolves.not.toThrow(); + expect(fetchAppListForSelectedSystem).toHaveBeenCalledWith(mockServiceProvider, appConfig.app.id); + verifyGeneratedFiles(testOutputDir, appId, extractedProjectPath); }); -}); + + it('Should throw error when fetchAppListForSelectedSystem fetches no app', async () => { + (isValidPromptState as jest.Mock).mockReturnValue(true); + (getAppConfig as jest.Mock).mockResolvedValue(appConfig); + (fetchAppListForSelectedSystem as jest.Mock).mockResolvedValue([]); + const mockServiceProvider = { + defaults: { baseURL: 'https://test-url.com' }, + service: jest.fn().mockReturnValue({ + metadata: jest.fn().mockResolvedValue({ + dataServices: { + schema: [] + } + }) + }) + } as unknown as AbapServiceProvider; + + await expect( + yeomanTest + .run(BspAppDownloadGenerator, { + resolved: bspAppDownloadGenPath + }) + .cd('.') + .withOptions({ + appRootPath: testOutputDir, + appWizard: mockAppWizard, + vscode: mockVSCode, + data: { + postGenCommand: 'test-post-gen-command', + quickDeployedAppConfig: { + appId: appConfig.app.id, + appUrl: 'https://app-url.com/app', + serviceProvider: mockServiceProvider + } + } + }) + .withPrompts({ + systemSelection: 'system3', + selectedApp: { + appId: appConfig.app.id, + title: appConfig.app.title, + description: appConfig.app.description, + repoName: repoName, + url: 'url-1' + }, + targetFolder: testOutputDir + }) + ) + .rejects.toThrowError(); + expect(fetchAppListForSelectedSystem).toHaveBeenCalledWith(mockServiceProvider, appConfig.app.id); + expect(BspAppDownloadLogger.logger.error).toHaveBeenCalledWith(t('error.quickDeployedAppDownloadErrors.noAppsFound', { appId: appConfig.app.id })); + expect(fs.existsSync(join(`${testOutputDir}/${appId}/${DirName.Webapp}`))).toBe(false); + }); +}); \ No newline at end of file diff --git a/packages/bsp-app-download-sub-generator/test/config.test.ts b/packages/bsp-app-download-sub-generator/test/config.test.ts index 08c095def3..8050d29f2a 100644 --- a/packages/bsp-app-download-sub-generator/test/config.test.ts +++ b/packages/bsp-app-download-sub-generator/test/config.test.ts @@ -6,10 +6,10 @@ import { getMinimumUI5Version } from '@sap-ux/project-access'; import { PromptState } from '../src/prompts/prompt-state'; import type { AppInfo, AppContentConfig } from '../src/app/types'; import { readManifest } from '../src/utils/file-helpers'; -import { sampleAppContentTestData } from './fixtures/example-app-content'; import { t } from '../src/utils/i18n'; import { adtSourceTemplateId } from '../src/utils/constants'; import BspAppDownloadLogger from '../src/utils/logger'; +import { sampleAppContentTestData } from './fixtures/downloaded-app/example-app-content'; jest.mock('../src/utils/logger', () => ({ logger: { diff --git a/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/cdm.json b/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/cdm.json new file mode 100644 index 0000000000..b184cb0f6f --- /dev/null +++ b/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/cdm.json @@ -0,0 +1,60 @@ +{ + "_version": "3.1.0", + "site": { + "identification": { + "namespace": "", + "title": "Fiori Elements Application Preview", + "description": "Fiori Elements Application Preview" + }, + "payload": {} + }, + "groups": {}, + "catalogs": {}, + "applications": { + "travel.approver": { + "sap.app": { + "id": "travel.approver", + "title": "Fiori Application Preview", + "crossNavigation": { + "inbounds": { + "app-preview": { + "semanticObject": "app", + "action": "preview", + "title": "Fiori Application Preview", + "subTitle": "", + "info": "", + "signature": { + "parameters": { }, + "additionalParameters": "allowed" + } + } + } + } + }, + "sap.ui5": { + "componentName": "travel.approver" + }, + "sap.flp": { + "type": "application" + }, + "sap.ui": { + "technology": "UI5", + "deviceTypes": { + "desktop": true, + "tablet": true, + "phone": true + } + }, + "sap.platform.runtime": { + "componentProperties": { + "url": "./", + "asyncHints": {} + } + } + } + }, + "visualizations": {}, + "vizTypes": {}, + "pages": {}, + "menus": {} +} diff --git a/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/component.js b/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/component.js new file mode 100644 index 0000000000..403733011d --- /dev/null +++ b/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/component.js @@ -0,0 +1,11 @@ +sap.ui.define( + ["sap/suite/ui/generic/template/lib/AppComponent"], + function (Component){ + "use strict"; + return Component.extend("travel.approver.Component",{ + metadata:{ + manifest: "json" + } + }); + } +); diff --git a/packages/bsp-app-download-sub-generator/test/fixtures/example-app-content.ts b/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/example-app-content.ts similarity index 100% rename from packages/bsp-app-download-sub-generator/test/fixtures/example-app-content.ts rename to packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/example-app-content.ts diff --git a/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/flp.html b/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/flp.html new file mode 100644 index 0000000000..976b37bb29 --- /dev/null +++ b/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/flp.html @@ -0,0 +1,101 @@ + + + + + + + Fiori Elements Application Preview + + + + +
+
+
+ + diff --git a/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/i18n.properties b/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/i18n.properties new file mode 100644 index 0000000000..639ffcc9de --- /dev/null +++ b/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/i18n.properties @@ -0,0 +1,5 @@ +#XTIT: Application title +appTitle=Travel Approver + +#XTIT: Application description +appDescription=Travel Approver diff --git a/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/index.html b/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/index.html new file mode 100644 index 0000000000..d97b6ab192 --- /dev/null +++ b/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/index.html @@ -0,0 +1,33 @@ + + + + + + + Fiori Application Preview + + + + + +
+
+ + diff --git a/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/manifest.json b/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/manifest.json new file mode 100644 index 0000000000..782b1a3061 --- /dev/null +++ b/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/manifest.json @@ -0,0 +1,149 @@ +{ + "sap.app": { + "id": "travel.approver", + "type": "application", + "i18n": "i18n.properties", + "applicationVersion":{ + "version": "0.0.1" + }, + "title": "{{appTitle}}", + "description": "{{appDescription}}", + "resources": "resources.json", + "sourceTemplate": { + "id": "@sap.adt.sevicebinding.deploy:lrop", + "version": "1.0.0", + "toolsId": "15AB9F96A8DF1FE081C6CD7B64A2046B" + }, + "dataSources": { + "mainService": { + "uri": "/sap/opu/odata/sap/ZSB_TRAVEL_APPROVER", + "type": "OData", + "settings": { + "odataVersion": "2.0", + "annotations": [ + "mainAnnotations" + ] + } + }, + "mainAnnotations": { + "uri": "/sap/opu/odata/IWFND/CATALOGSERVICE;v=2/Annotations(TechnicalName='ZSB_TRAVEL_APPROVER_VAN',Version='0001')/$value/", + "type": "ODataAnnotation" + } + } + + }, + "sap.ui": { + "technology": "UI5" + }, + "sap.ui5": { + "resources": { + "js": [], + "css": [] + }, + "dependencies": { + "libs": { + "sap.ui.generic.app": {}, + "sap.suite.ui.generic.template": {}, + "sap.suite.ui.commons": {} +} +, + "components": {} + }, + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "settings": { + "bundleName": "travel.approver.i18n" + } + }, + "@i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "uri": "i18n/i18n.properties" + }, + "": { + "dataSource": "mainService", + "preload": true, + "settings": { + "defaultBindingMode": "TwoWay", + "refreshAfterChange": false, + "metadataUrlParams": { + "sap-value-list": "none" + } + } + + } + }, + "routing": {} + + }, + "sap.ui.generic.app": { + "_version": "1.3.0", + "settings": { + "forceGlobalRefresh": false, + "objectPageHeaderType": "Dynamic", + "considerAnalyticalParameters": true, + "showDraftToggle": false + }, + "pages": { + "ListReport|Travel": { + "entitySet": "Travel", + "component": { + "name": "sap.suite.ui.generic.template.ListReport", + "list": true, + "settings": { + "condensedTableLayout": true, + "smartVariantManagement": true, + "enableTableFilterInPageVariant": true, + "filterSettings": { + "dateSettings": { + "useDateRange": true + } + } + } + }, + "pages": { + "ObjectPage|Travel": { + "entitySet": "Travel", + "defaultLayoutTypeIfExternalNavigation": "MidColumnFullScreen", + "component": { + "name": "sap.suite.ui.generic.template.ObjectPage" + }, + "pages": { "ObjectPage|Booking": { + "navigationProperty": "to_Booking", + "entitySet": "Booking", + "defaultLayoutTypeIfExternalNavigation": "MidColumnFullScreen", + "component": { + "name": "sap.suite.ui.generic.template.ObjectPage" + }, + "pages": { "ObjectPage|BookingStatus": { + "navigationProperty": "to_BookingStatus", + "entitySet": "BookingStatus", + "defaultLayoutTypeIfExternalNavigation": "MidColumnFullScreen", + "component": { + "name": "sap.suite.ui.generic.template.ObjectPage" + }, + "pages": {} + } + } + } + , + "ObjectPage|OverallStatus": { + "navigationProperty": "to_OverallStatus", + "entitySet": "OverallStatus", + "defaultLayoutTypeIfExternalNavigation": "MidColumnFullScreen", + "component": { + "name": "sap.suite.ui.generic.template.ObjectPage" + }, + "pages": {} + } + } + } + } + } + } + }, + + "sap.fiori": { + "archeType": "transactional" + } +} diff --git a/packages/bsp-app-download-sub-generator/test/fixtures/project/package.json b/packages/bsp-app-download-sub-generator/test/fixtures/project/package.json deleted file mode 100644 index 5f2b205122..0000000000 --- a/packages/bsp-app-download-sub-generator/test/fixtures/project/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "scripts": {} -} \ No newline at end of file diff --git a/packages/bsp-app-download-sub-generator/test/fixtures/project/webapp/i18n/i18n.properties b/packages/bsp-app-download-sub-generator/test/fixtures/project/webapp/i18n/i18n.properties deleted file mode 100644 index 9e5857c34d..0000000000 --- a/packages/bsp-app-download-sub-generator/test/fixtures/project/webapp/i18n/i18n.properties +++ /dev/null @@ -1,18 +0,0 @@ -# This is the resource bundle for project13356 - -#Texts for manifest.json - -#XTIT: Application name -appTitle=App Gen App Title - -#YDES: Application description -appDescription=App Gen Desc - -flpTitle=FLP Title -flpSubtitle=FLP Subtitle - -# User app specific - -click=Click -reload=Reload -error=Please check your network connection \ No newline at end of file diff --git a/packages/bsp-app-download-sub-generator/test/fixtures/project/webapp/manifest.json b/packages/bsp-app-download-sub-generator/test/fixtures/project/webapp/manifest.json deleted file mode 100755 index 3508a595d5..0000000000 --- a/packages/bsp-app-download-sub-generator/test/fixtures/project/webapp/manifest.json +++ /dev/null @@ -1,180 +0,0 @@ -{ - "_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/MOCK_TRAVEL/", - "type": "OData", - "settings": { - "annotations": ["MOCK_TRAVEL_VAN", "annotation"], - "localUri": "localService/metadata.xml" - } - }, - "MOCK_TRAVEL_VAN": { - "uri": "/sap/opu/odata/IWFND/CATALOGSERVICE;v=2/Annotations(TechnicalName='MOCK_TRAVEL_VAN',Version='0001')/$value/", - "type": "ODataAnnotation", - "settings": { - "localUri": "localService/MOCK_TRAVEL_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/bsp-app-download-sub-generator/test/utils/download-utils.test.ts b/packages/bsp-app-download-sub-generator/test/utils/download-utils.test.ts index ca2c445a20..8fc95a8cc1 100644 --- a/packages/bsp-app-download-sub-generator/test/utils/download-utils.test.ts +++ b/packages/bsp-app-download-sub-generator/test/utils/download-utils.test.ts @@ -17,7 +17,6 @@ jest.mock('../../src/utils/logger', () => ({ describe('download-utils', () => { let mockFs: Editor; - let mockLog: Logger; let mockServiceProvider: AbapServiceProvider; beforeEach(() => { @@ -25,10 +24,6 @@ describe('download-utils', () => { write: jest.fn(), } as unknown as Editor; - mockLog = { - error: jest.fn(), - } as unknown as Logger; - mockServiceProvider = { getUi5AbapRepository: jest.fn().mockReturnValue({ downloadFiles: jest.fn(), diff --git a/packages/bsp-app-download-sub-generator/test/utils/logger.test.ts b/packages/bsp-app-download-sub-generator/test/utils/logger.test.ts new file mode 100644 index 0000000000..aa3ba49a89 --- /dev/null +++ b/packages/bsp-app-download-sub-generator/test/utils/logger.test.ts @@ -0,0 +1,47 @@ +import BspAppDownloadLogger from '../../src/utils/logger'; +import { DefaultLogger, LogWrapper } from '@sap-ux/fiori-generator-shared'; +import type { Logger } from 'yeoman-environment'; +import type { IVSCodeExtLogger, LogLevel } from '@vscode-logging/logger'; + +describe('BspAppDownloadLogger', () => { + const testLoggerName = 'testLogger'; + afterEach(() => { + // Reset the logger to the default after each test + BspAppDownloadLogger.logger = DefaultLogger; + }); + + it('should return the default logger initially', () => { + expect(BspAppDownloadLogger.logger).toBe(DefaultLogger); + }); + + it('should allow setting a custom logger', () => { + const mockLogger = { log: jest.fn() } as unknown as LogWrapper; + BspAppDownloadLogger.logger = mockLogger; + expect(BspAppDownloadLogger.logger).toBe(mockLogger); + }); + + it('should configure the logger with provided parameters', () => { + const mockYoLogger = { log: jest.fn() } as unknown as Logger; + const mockLogWrapper = { log: jest.fn() } as unknown as LogWrapper; + + BspAppDownloadLogger.configureLogging(testLoggerName, mockYoLogger, mockLogWrapper); + + expect(BspAppDownloadLogger.logger).toBe(mockLogWrapper); + }); + + it('should create a new LogWrapper if none is provided', () => { + const mockYoLogger = { log: jest.fn() } as unknown as Logger; + const mockLogLevel: LogLevel = 'info'; + const mockVscLogger = { + log: jest.fn(), + debug: jest.fn(), + getChildLogger: jest.fn().mockReturnValue({ + log: jest.fn(), + debug: jest.fn() + }) + } as unknown as IVSCodeExtLogger; + + BspAppDownloadLogger.configureLogging(testLoggerName, mockYoLogger, undefined, mockLogLevel, mockVscLogger); + expect(BspAppDownloadLogger.logger).toBeInstanceOf(LogWrapper); + }); +}); \ No newline at end of file From 8c6b1e7c337cf18f6300d7bdb76f8ca5a3f12004 Mon Sep 17 00:00:00 2001 From: I743583 Date: Thu, 10 Apr 2025 14:29:34 +0100 Subject: [PATCH 15/41] use qfa json from downloaded app --- .changeset/slow-beers-drum.md | 5 - .../src/abap/ui5-abap-repository-service.ts | 7 +- .../abap/ui5-abap-repository-service.test.ts | 2 +- .../src/app/{config.ts => app-config.ts} | 63 ++--- .../src/app/example-app-content.ts | 28 --- .../src/app/index.ts | 89 ++++--- .../src/app/types.ts | 75 +++--- .../src/prompts/prompt-helpers.ts | 6 +- .../src/prompts/prompts.ts | 10 +- .../bsp-app-download-sub-generator.i18n.json | 3 +- .../src/utils/constants.ts | 2 + .../src/utils/download-utils.ts | 1 - .../src/utils/file-helpers.ts | 35 ++- .../src/utils/validators.ts | 38 +-- .../{config.test.ts => app-config.test.ts} | 82 ++++--- .../test/app.test.ts | 56 +++-- .../test/fixtures/downloaded-app/cdm.json | 60 ----- .../test/fixtures/downloaded-app/component.js | 4 +- .../downloaded-app/example-app-content.ts | 28 --- .../test/fixtures/downloaded-app/flp.html | 101 -------- .../fixtures/downloaded-app/i18n.properties | 5 - .../downloaded-app/i18n/i18n.properties | 5 + .../test/fixtures/downloaded-app/index.html | 12 +- .../fixtures/downloaded-app/manifest.json | 220 +++++++++++------- .../test/fixtures/downloaded-app/qfa.json | 27 +++ .../test/utils/validators.test.ts | 72 +++--- 26 files changed, 448 insertions(+), 588 deletions(-) delete mode 100644 .changeset/slow-beers-drum.md rename packages/bsp-app-download-sub-generator/src/app/{config.ts => app-config.ts} (71%) delete mode 100644 packages/bsp-app-download-sub-generator/src/app/example-app-content.ts rename packages/bsp-app-download-sub-generator/test/{config.test.ts => app-config.test.ts} (77%) delete mode 100644 packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/cdm.json delete mode 100644 packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/example-app-content.ts delete mode 100644 packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/flp.html delete mode 100644 packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/i18n.properties create mode 100644 packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/i18n/i18n.properties create mode 100644 packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/qfa.json diff --git a/.changeset/slow-beers-drum.md b/.changeset/slow-beers-drum.md deleted file mode 100644 index 37023db01c..0000000000 --- a/.changeset/slow-beers-drum.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@sap-ux/launch-config': minor ---- - -Add enableVSCodeReload flag to control VS Code reload on workspace update diff --git a/packages/axios-extension/src/abap/ui5-abap-repository-service.ts b/packages/axios-extension/src/abap/ui5-abap-repository-service.ts index 0495fe92ff..b1f3631be6 100644 --- a/packages/axios-extension/src/abap/ui5-abap-repository-service.ts +++ b/packages/axios-extension/src/abap/ui5-abap-repository-service.ts @@ -171,9 +171,10 @@ export class Ui5AbapRepositoryService extends ODataService { } }); const data = response.odata(); - return this.isBase64Encoded(data.ZipArchive) - ? Buffer.from(data.ZipArchive, 'base64') - : Buffer.from(data.ZipArchive); + + if (!data.ZipArchive) return undefined; + const isBase64 = this.isBase64Encoded(data.ZipArchive); + return Buffer.from(data.ZipArchive, isBase64 ? 'base64' : undefined); } catch (error) { this.log.debug(`Retrieving application ${app}, ${error}`); if (isAxiosError(error) && error.response?.status === 404) { diff --git a/packages/axios-extension/test/abap/ui5-abap-repository-service.test.ts b/packages/axios-extension/test/abap/ui5-abap-repository-service.test.ts index d531d98c2d..64d10c8c77 100644 --- a/packages/axios-extension/test/abap/ui5-abap-repository-service.test.ts +++ b/packages/axios-extension/test/abap/ui5-abap-repository-service.test.ts @@ -26,7 +26,7 @@ describe('Ui5AbapRepositoryService', () => { const validAppInfo: AppInfo = { Name: validApp, Package: 'my_package', - ZipArchive: 'EncodeZippedDataHere' + ZipArchive: 'EncodeZippedDataHere@!#' }; const updateParams = `CodePage='UTF8'&CondenseMessagesInHttpResponseHeader=X&format=json`; const sapMessageHeader = JSON.stringify({ diff --git a/packages/bsp-app-download-sub-generator/src/app/config.ts b/packages/bsp-app-download-sub-generator/src/app/app-config.ts similarity index 71% rename from packages/bsp-app-download-sub-generator/src/app/config.ts rename to packages/bsp-app-download-sub-generator/src/app/app-config.ts index 49ff389494..6a01243db3 100644 --- a/packages/bsp-app-download-sub-generator/src/app/config.ts +++ b/packages/bsp-app-download-sub-generator/src/app/app-config.ts @@ -3,7 +3,7 @@ import { OdataVersion } from '@sap-ux/odata-service-inquirer'; import type { AbapServiceProvider } from '@sap-ux/axios-extension'; import type { Editor } from 'mem-fs-editor'; import { t } from '../utils/i18n'; -import type { AppInfo, AppContentConfig, EntityConfig } from '../app/types'; +import type { AppInfo, QfaJsonConfig } from '../app/types'; import { readManifest } from '../utils/file-helpers'; import { getLatestUI5Version } from '@sap-ux/ui5-info'; import { getMinimumUI5Version } from '@sap-ux/project-access'; @@ -11,25 +11,25 @@ import { adtSourceTemplateId } from '../utils/constants'; import { PromptState } from '../prompts/prompt-state'; import type { AbapDeployConfig } from '@sap-ux/ui5-config'; import BspAppDownloadLogger from '../utils/logger'; +import { supportedUi5VersionFallbacks } from '@sap-ux/ui5-info'; /** * Generates the deployment configuration for an ABAP application. * * @param {AppInfo} app - Application info containing `url` and `repoName`. - * @param {AppContentCofig} appContentJson - Application content JSON with deployment details. + * @param {QfaJsonConfig} qfaJson - The QFA JSON configuration containing app details. * @returns {AbapDeployConfig} The deployment configuration containing `target` and `app` info. */ -export const getAbapDeployConfig = (app: AppInfo, appContentJson: AppContentConfig): AbapDeployConfig => { +export const getAbapDeployConfig = (app: AppInfo, qfaJson: QfaJsonConfig): AbapDeployConfig => { return { - // todo: get from json file target: { url: app.url, destination: app.repoName }, app: { - name: appContentJson.deploymentDetails.repositoryName, - package: appContentJson.metadata.package, - description: appContentJson.deploymentDetails?.repositoryDescription, + name: qfaJson.deployment_details.repository_name, + package: qfaJson.metadata.package, + description: qfaJson.deployment_details.repository_description, transport: 'REPLACE_WITH_TRANSPORT' } }; @@ -42,7 +42,7 @@ export const getAbapDeployConfig = (app: AppInfo, appContentJson: AppContentConf * @param {string} serviceUrl - The URL of the service to retrieve metadata for. * @returns {Promise} - A promise resolving to the service metadata. */ -const fetchServiceMetadata = async (provider: AbapServiceProvider, serviceUrl: string): Promise => { +const fetchServiceMetadata = async (provider: AbapServiceProvider, serviceUrl: string): Promise => { try { return await provider.service(serviceUrl).metadata(); } catch (err) { @@ -50,37 +50,13 @@ const fetchServiceMetadata = async (provider: AbapServiceProvider, serviceUrl: s } }; -/** - * Generates the entity configuration based on the provided application content JSON. - * - * @param {any} appContentJson - The application content JSON containing service binding details. - * @returns {EntityConfig} - The generated entity configuration. - */ -function getEntityConfig(appContentJson: AppContentConfig): EntityConfig { - // Extract main entity name - const mainEntityName = appContentJson.serviceBindingDetails.mainEntityName; - // Initialize entity configuration with main entity name - const entityConfig: EntityConfig = { - mainEntityName: mainEntityName - }; - - // If navigationEntity exists, add it to the entityConfig - if (appContentJson.serviceBindingDetails.navigationEntity) { - entityConfig['navigationEntity'] = { - EntitySet: appContentJson.serviceBindingDetails.navigationEntity.EntitySet, - Name: appContentJson.serviceBindingDetails.navigationEntity.Name - }; - } - return entityConfig; -} - /** * Gets the application configuration based on the provided user answers and manifest data. * This configuration will be used to initialize a new Fiori application. * * @param {AppInfo} app - Selected app information. * @param {string} extractedProjectPath - Path where the app files are extracted. - * @param appContentJson + * @param {QfaJsonConfig} qfaJson - The QFA JSON configuration containing app details. * @param {Editor} fs - The file system editor to manipulate project files. * @returns {Promise>} - A promise resolving to the generated app configuration. * @throws {Error} - Throws an error if there are issues generating the configuration. @@ -88,7 +64,7 @@ function getEntityConfig(appContentJson: AppContentConfig): EntityConfig { export async function getAppConfig( app: AppInfo, extractedProjectPath: string, - appContentJson: AppContentConfig, + qfaJson: QfaJsonConfig, fs: Editor ): Promise> { try { @@ -116,7 +92,8 @@ export async function getAppConfig( sourceTemplate: { id: adtSourceTemplateId }, - projectType: 'EDMXBackend' + projectType: 'EDMXBackend', + flpAppId: `${app.appId.replace(/[-_.#]/g, '')}-tile`, }, package: { name: app.appId, @@ -128,13 +105,15 @@ export async function getAppConfig( template: { type: TemplateType.ListReportObjectPage, settings: { - entityConfig: getEntityConfig(appContentJson) + entityConfig: { + mainEntityName: qfaJson.service_binding_details.main_entity_name + } } }, service: { path: manifest?.['sap.app']?.dataSources?.mainService.uri, version: odataVersion, - metadata, + metadata: metadata, url: serviceProvider.defaults.baseURL }, appOptions: { @@ -143,11 +122,19 @@ export async function getAppConfig( }, ui5: { version: - appContentJson.projectAttribute?.minimumUi5Version ?? + qfaJson.project_attribute?.minimum_ui5_version ?? + //supportedUi5VersionFallbacks[0].version ?? getMinimumUI5Version(manifest) ?? (await getLatestUI5Version()) } }; + //todo: confirm this + if(qfaJson.service_binding_details.navigation_entity) { + appConfig.template.settings.entityConfig.navigationEntity = { + EntitySet: qfaJson.service_binding_details.navigation_entity, + Name: qfaJson.service_binding_details.navigation_entity + }; + } return appConfig; } catch (error) { BspAppDownloadLogger.logger?.error(t('error.appConfigGenError', { error: error.message })); diff --git a/packages/bsp-app-download-sub-generator/src/app/example-app-content.ts b/packages/bsp-app-download-sub-generator/src/app/example-app-content.ts deleted file mode 100644 index af44b5f04b..0000000000 --- a/packages/bsp-app-download-sub-generator/src/app/example-app-content.ts +++ /dev/null @@ -1,28 +0,0 @@ -export const sampleAppContentTestData = { - metadata: { - package: 'TEST_PACKAGE', - masterLanguage: 'EN' - }, - serviceBindingDetails: { - name: 'TEST_SERVICE_BINDING', - serviceName: 'TEST_SERVICE_NAME', - serviceVersion: '0001', - mainEntityName: 'TEST_ENTITY' - }, - projectAttribute: { - moduleName: 'test_module_name', - applicationTitle: 'Test Application Title', - template: 'Test Template', - minimumUi5Version: '1.100.0' - }, - deploymentDetails: { - repositoryName: 'TEST_REPO_NAME', - repositoryDescription: 'This is a test repository' - }, - fioriLaunchpadConfiguration: { - semanticObject: 'TEST_SEMANTIC_OBJECT', - action: 'testAction', - title: 'Test Title', - subtitle: 'Test Subtitle' - } -}; diff --git a/packages/bsp-app-download-sub-generator/src/app/index.ts b/packages/bsp-app-download-sub-generator/src/app/index.ts index 02a53dd81c..3502bb2488 100644 --- a/packages/bsp-app-download-sub-generator/src/app/index.ts +++ b/packages/bsp-app-download-sub-generator/src/app/index.ts @@ -4,7 +4,7 @@ import { AppWizard, Prompts, MessageType } from '@sap-devx/yeoman-ui-types'; import { isInternalFeaturesSettingEnabled } from '@sap-ux/feature-toggle'; import type { Logger } from '@sap-ux/logger'; import { sendTelemetry, TelemetryHelper } from '@sap-ux/fiori-generator-shared'; -import { generatorTitle, extractedFilePath, generatorName, defaultAnswers } from '../utils/constants'; +import { generatorTitle, extractedFilePath, generatorName, defaultAnswers, qfaJsonFileName } from '../utils/constants'; import { t } from '../utils/i18n'; import { getYUIDetails } from '../prompts/prompt-helpers'; import { downloadApp } from '../utils/download-utils'; @@ -15,7 +15,7 @@ import type { BspAppDownloadOptions, BspAppDownloadAnswers, BspAppDownloadQuestions, - AppContentConfig, + QfaJsonConfig, QuickDeployedAppConfig } from './types'; import { getPrompts } from '../prompts/prompts'; @@ -33,12 +33,11 @@ import { writeApplicationInfoSettings } from '@sap-ux/fiori-tools-settings'; import { generate as generateDeployConfig } from '@sap-ux/abap-deploy-config-writer'; import { PromptState } from '../prompts/prompt-state'; import { PromptNames } from './types'; -import { getAbapDeployConfig, getAppConfig } from './config'; +import { getAbapDeployConfig, getAppConfig } from './app-config'; import type { AbapDeployConfig } from '@sap-ux/ui5-config'; -import { sampleAppContentTestData } from './example-app-content'; -import { replaceWebappFiles } from '../utils/file-helpers'; +import { replaceWebappFiles, makeValidJson } from '../utils/file-helpers'; import { fetchAppListForSelectedSystem, extractAppData } from '../prompts/prompt-helpers'; -import { isValidPromptState, validateAppContentJsonFile } from '../utils/validators'; +import { isValidPromptState, validateQfaJsonFile } from '../utils/validators'; /** * Generator class for downloading a basic app from BSP repository. @@ -134,14 +133,13 @@ export default class extends Generator { /** * - * @param quickDeployedAppConfig - * @param targetFolder + * @param quickDeployedAppConfig - The configuration for the quick deployed app. + * @param targetFolder - The target folder where the app will be downloaded. */ private async _handleQuickDeployedAppDownload( quickDeployedAppConfig: QuickDeployedAppConfig, targetFolder: string ): Promise { - debugger; const appList = await fetchAppListForSelectedSystem( quickDeployedAppConfig.serviceProvider, quickDeployedAppConfig.appId @@ -150,7 +148,7 @@ export default class extends Generator { BspAppDownloadLogger.logger?.error( t('error.quickDeployedAppDownloadErrors.noAppsFound', { appId: quickDeployedAppConfig.appId }) ); - throw new Error(); + throw new Error(); } this.answers.selectedApp = extractAppData(appList[0]).value; this.answers.targetFolder = targetFolder; @@ -168,44 +166,43 @@ export default class extends Generator { await downloadApp(this.answers.selectedApp.repoName, this.extractedProjectPath, this.fs); } - // const appContentJsonTempPath = join(__dirname, 'example-app-content.json'); - const appContentJson: AppContentConfig = sampleAppContentTestData; - // todo: add back once json is available along with downloaded app - // if(!this.fs.exists(appContentJsonTempPath)) { - // appContentJson = this.fs.readJSON(appContentJsonTempPath) as unknown as AppContentConfig; //todo: extract from extracted path - // } else { - // BspAppDownloadLogger.logger?.error(t('error.appContentJsonNotFound', { jsonFileName: 'example-app-content.json' })); - // } + const qfaJsonFilePath = join(this.extractedProjectPath, qfaJsonFileName); + if (this.fs.exists(qfaJsonFilePath)) { + const qfaJson: QfaJsonConfig = makeValidJson(qfaJsonFilePath, this.fs); + // Generate project files + validateQfaJsonFile(qfaJson); - // Generate project files - validateAppContentJsonFile(appContentJson); - const config = await getAppConfig(this.answers.selectedApp, this.extractedProjectPath, appContentJson, this.fs); - await generate(this.projectPath, config, this.fs); + // Generate app config + const config = await getAppConfig(this.answers.selectedApp, this.extractedProjectPath, qfaJson, this.fs); + await generate(this.projectPath, config, this.fs); - // Generate deploy config - const deployConfig: AbapDeployConfig = getAbapDeployConfig(this.answers.selectedApp, appContentJson); - await generateDeployConfig(this.projectPath, deployConfig, undefined, this.fs); + // Generate deploy config + const deployConfig: AbapDeployConfig = getAbapDeployConfig(this.answers.selectedApp, qfaJson); + await generateDeployConfig(this.projectPath, deployConfig, undefined, this.fs); - // Generate README - const readMeConfig = this._getReadMeConfig(config); - generateReadMe(this.projectPath, readMeConfig, this.fs); + // Generate README + const readMeConfig = this._getReadMeConfig(config); + generateReadMe(this.projectPath, readMeConfig, this.fs); - if (this.vscode) { - // Generate Fiori launch config - const fioriOptions = this._getLaunchConfig(config); - // Create launch configuration - await createLaunchConfig( - this.projectPath, - fioriOptions, - this.fs, - BspAppDownloadLogger.logger as unknown as Logger - ); - writeApplicationInfoSettings(this.projectPath, this.fs); + if (this.vscode) { + // Generate Fiori launch config + const fioriOptions = this._getLaunchConfig(config); + // Create launch configuration + await createLaunchConfig( + this.projectPath, + fioriOptions, + this.fs, + BspAppDownloadLogger.logger as unknown as Logger + ); + writeApplicationInfoSettings(this.projectPath, this.fs); + } + // Replace webapp files with downloaded app files + await replaceWebappFiles(this.projectPath, this.extractedProjectPath, this.fs); + // Clean up extracted project files + this.fs.delete(this.extractedProjectPath); + } else { + BspAppDownloadLogger.logger?.error(t('error.qfaJsonNotFound', { jsonFileName: qfaJsonFileName })); } - // Replace webapp files with downloaded app files - await replaceWebappFiles(this.projectPath, this.extractedProjectPath, this.fs); - // Clean up extracted project files - // this.fs.delete(this.extractedProjectPath); } /** @@ -221,7 +218,7 @@ export default class extends Generator { appNamespace: '', // todo: cant find namespace in manifest json - default? appDescription: t('readMe.appDescription'), ui5Theme: getDefaultUI5Theme(config.ui5?.version), - generatorName: generatorName, // todo: check if this is correct + generatorName: generatorName, // todo: check if this name is okay ? generatorVersion: this.rootGeneratorVersion(), ui5Version: config.ui5?.version ?? '', template: TemplateType.ListReportObjectPage, @@ -335,9 +332,9 @@ export default class extends Generator { /** * Finalises the generator process by creating launch configurations and running post-generation hooks. */ - async end() { + async end(): Promise { try { - this.appWizard.showWarning(t('info.bspAppDownloadCompleteMsg'), MessageType.notification); + this.appWizard.showInformation(t('info.bspAppDownloadCompleteMsg'), MessageType.notification); await sendTelemetry( EventName.GENERATION_SUCCESS, TelemetryHelper.createTelemetryData({ diff --git a/packages/bsp-app-download-sub-generator/src/app/types.ts b/packages/bsp-app-download-sub-generator/src/app/types.ts index 41f0bbbf88..3678a4e126 100644 --- a/packages/bsp-app-download-sub-generator/src/app/types.ts +++ b/packages/bsp-app-download-sub-generator/src/app/types.ts @@ -106,48 +106,37 @@ export interface BspAppDownloadAnswers { [PromptNames.targetFolder]: string; } -interface Metadata { - package: string; - masterLanguage?: string; -} - -export interface EntityConfig { - mainEntityName: string; - navigationEntity?: { - EntitySet: string; - Name: string; +/** + * Interface representing the configuration of a QFA JSON file. + * This QFA JSON file is used for configuring the application download process + * and contains user inputs. + */ +export interface QfaJsonConfig { + metadata: { + package: string; + master_language?: string; + }; + service_binding_details: { + name?: string; + service_name: string; + service_version: string; + main_entity_name: string; + navigation_entity?: string; + }; + project_attribute: { + module_name: string; + application_title?: string; + minimum_ui5_version?: string; + template?: string; + }; + deployment_details: { + repository_name: string; + repository_description?: string; + }; + fiori_launchpad_configuration: { + semantic_object: string; + action: string; + title: string; + subtitle?: string; }; -} - -interface ServiceBindingDetails extends EntityConfig { - name?: string; - serviceName: string; - serviceVersion: string; -} - -interface ProjectAttribute { - moduleName: string; - applicationTitle?: string; - template?: string; - minimumUi5Version?: string; -} - -interface DeploymentDetails { - repositoryName: string; - repositoryDescription?: string; -} - -interface FioriLaunchpadConfiguration { - semanticObject: string; - action: string; - title: string; - subtitle?: string; -} - -export interface AppContentConfig { - metadata: Metadata; - serviceBindingDetails: ServiceBindingDetails; - projectAttribute: ProjectAttribute; - deploymentDetails: DeploymentDetails; - fioriLaunchpadConfiguration: FioriLaunchpadConfiguration; } diff --git a/packages/bsp-app-download-sub-generator/src/prompts/prompt-helpers.ts b/packages/bsp-app-download-sub-generator/src/prompts/prompt-helpers.ts index a9721da13a..577e3553b0 100644 --- a/packages/bsp-app-download-sub-generator/src/prompts/prompt-helpers.ts +++ b/packages/bsp-app-download-sub-generator/src/prompts/prompt-helpers.ts @@ -25,7 +25,7 @@ export function getYUIDetails(): { name: string; description: string }[] { * Returns the prompt details for the selected application. * * @param {AppItem} app - The application item to extract details from. - * @returns { name: string; value: AppInfo } The extracted details including name and value. + * @returns {{ name: string; value: AppInfo }} The extracted details including name and value. */ export const extractAppData = (app: AppItem): { name: string; value: AppInfo } => { // cast to string because TypeScript doesn't automatically know at the point that these fields are defined @@ -70,7 +70,7 @@ export const formatAppChoices = (appList: AppIndex): Array<{ name: string; value * Fetches a list of deployed applications from the ABAP repository. * * @param {AbapServiceProvider} provider - The ABAP service provider. - * @param appId + * @param {string} appId - Application ID to filter the list. * @returns {Promise} A list of applications filtered by source template. */ async function getAppList(provider: AbapServiceProvider, appId?: string): Promise { @@ -92,7 +92,7 @@ async function getAppList(provider: AbapServiceProvider, appId?: string): Promis * Fetches the application list for the selected system. * * @param {AbapServiceProvider} serviceProvider - The ABAP service provider. - * @param appId + * @param {string} appId - Application ID to be downloaded. * @returns {Promise} A list of applications filtered by source template. */ export async function fetchAppListForSelectedSystem( diff --git a/packages/bsp-app-download-sub-generator/src/prompts/prompts.ts b/packages/bsp-app-download-sub-generator/src/prompts/prompts.ts index 078130e7f1..06d1e2b67f 100644 --- a/packages/bsp-app-download-sub-generator/src/prompts/prompts.ts +++ b/packages/bsp-app-download-sub-generator/src/prompts/prompts.ts @@ -1,6 +1,6 @@ import type { AppIndex, AbapServiceProvider } from '@sap-ux/axios-extension'; import { getSystemSelectionQuestions } from '@sap-ux/odata-service-inquirer'; -import type { BspAppDownloadAnswers, BspAppDownloadQuestions, QuickDeployedAppConfig } from '../app/types'; +import type { BspAppDownloadAnswers, BspAppDownloadQuestions, QuickDeployedAppConfig, AppInfo } from '../app/types'; import { PromptNames } from '../app/types'; import { t } from '../utils/i18n'; import type { FileBrowserQuestion } from '@sap-ux/inquirer-common'; @@ -13,7 +13,7 @@ import { fetchAppListForSelectedSystem } from './prompt-helpers'; * Gets the target folder selection prompt. * * @param {string} [appRootPath] - The application root path. - * @param appId + * @param {string} appId - The application ID. * @returns {FileBrowserQuestion} The target folder prompt configuration. */ const getTargetFolderPrompt = (appRootPath?: string, appId?: string): FileBrowserQuestion => { @@ -49,7 +49,7 @@ const getTargetFolderPrompt = (appRootPath?: string, appId?: string): FileBrowse * Retrieves questions for selecting system, app lists and target path where app will be generated. * * @param {string} [appRootPath] - The root path of the application. - * @param {QuickDeployConfig} [quickDeployedAppConfig] - quick deploy config. + * @param {QuickDeployedAppConfig} [quickDeployedAppConfig] - quick deployed app config. * @returns {Promise} A list of questions for user interaction. */ export async function getPrompts( @@ -62,7 +62,7 @@ export async function getPrompts( return [getTargetFolderPrompt(appRootPath, quickDeployedAppConfig.appId)] as BspAppDownloadQuestions[]; } - const systemQuestions = await getSystemSelectionQuestions({ serviceSelection: { hide: true } }, false); // todo: remove this isYUI value + const systemQuestions = await getSystemSelectionQuestions({ serviceSelection: { hide: true } }, false); let appList: AppIndex = []; const appSelectionPrompt = [ { @@ -82,7 +82,7 @@ export async function getPrompts( breadcrumb: t('prompts.appSelection.breadcrumb') }, message: t('prompts.appSelection.message'), - choices: () => (appList.length ? formatAppChoices(appList) : []), + choices: (): { name: string; value: AppInfo }[] => (appList.length ? formatAppChoices(appList) : []), validate: (): string | boolean => (appList.length ? true : t('prompts.appSelection.noAppsDeployed')) } ]; diff --git a/packages/bsp-app-download-sub-generator/src/translations/bsp-app-download-sub-generator.i18n.json b/packages/bsp-app-download-sub-generator/src/translations/bsp-app-download-sub-generator.i18n.json index 524c023e02..33a81473a4 100644 --- a/packages/bsp-app-download-sub-generator/src/translations/bsp-app-download-sub-generator.i18n.json +++ b/packages/bsp-app-download-sub-generator/src/translations/bsp-app-download-sub-generator.i18n.json @@ -1,13 +1,14 @@ { "error": { "telemetry": "Failed to send telemetry data after downloading app from BSP. {{- error}}", - "appContentJsonNotFound": "{{- jsonFileName }} not found in the downloaded app", + "qfaJsonNotFound": "{{- jsonFileName }} not found in the downloaded app", "replaceWebappFilesError": "Error replacing files in the downloaded app: {{- error}}", "requiredFieldsMissing": "Required fields are missing for app: {{- app }}. Check if the app is deployed correctly", "applicationListFetchError": "Error fetching application list: {{- error}}", "metadataFetchError": "Error fetching metadata: {{- error}}", "appConfigGenError": "Error generating application configuration: {{- error}}", "endPhase": "Error in end phase: {{- error}}", + "errorProcessingJsonFile": "Error processing JSON file: {{- error}}", "validationErrors": { "invalidMetadataPackage": "Invalid or missing package in metadata", "invalidServiceName": "Invalid or missing serviceName in serviceBindingDetails", diff --git a/packages/bsp-app-download-sub-generator/src/utils/constants.ts b/packages/bsp-app-download-sub-generator/src/utils/constants.ts index b1960a2c75..5008836d24 100644 --- a/packages/bsp-app-download-sub-generator/src/utils/constants.ts +++ b/packages/bsp-app-download-sub-generator/src/utils/constants.ts @@ -8,6 +8,8 @@ export const generatorDescription = 'Download a basic LROP app from a BSP reposi export const generatorName = '@sap-ux/bsp-app-download-sub-generator'; // The source template ID used for filtering the apps in the BSP repository export const adtSourceTemplateId = '@sap.adt.sevicebinding.deploy:lrop'; +// The name of the QFA JSON file provided with the downloaded app, containing all user inputs. +export const qfaJsonFileName = 'qfa.json'; // Default initial answers to use as a fallback. export const defaultAnswers: BspAppDownloadAnswers = { diff --git a/packages/bsp-app-download-sub-generator/src/utils/download-utils.ts b/packages/bsp-app-download-sub-generator/src/utils/download-utils.ts index f9562350ed..de7ea0b9aa 100644 --- a/packages/bsp-app-download-sub-generator/src/utils/download-utils.ts +++ b/packages/bsp-app-download-sub-generator/src/utils/download-utils.ts @@ -39,7 +39,6 @@ async function extractZip(extractedProjectPath: string, archive: Buffer, fs: Edi export async function downloadApp(repoName: string, extractedProjectPath: string, fs: Editor): Promise { try { const serviceProvider = PromptState.systemSelection?.connectedSystem?.serviceProvider as AbapServiceProvider; - debugger; const archive = await serviceProvider.getUi5AbapRepository().downloadFiles(repoName); if (Buffer.isBuffer(archive)) { await extractZip(extractedProjectPath, archive, fs); diff --git a/packages/bsp-app-download-sub-generator/src/utils/file-helpers.ts b/packages/bsp-app-download-sub-generator/src/utils/file-helpers.ts index b1115df993..b59741d377 100644 --- a/packages/bsp-app-download-sub-generator/src/utils/file-helpers.ts +++ b/packages/bsp-app-download-sub-generator/src/utils/file-helpers.ts @@ -4,6 +4,7 @@ import type { Editor } from 'mem-fs-editor'; import { FileName, DirName, type Manifest } from '@sap-ux/project-access'; import { t } from './i18n'; import BspAppDownloadLogger from './logger'; +import type { QfaJsonConfig } from '../app/types'; /** * Reads and validates the `manifest.json` file. @@ -43,8 +44,9 @@ export async function replaceWebappFiles(projectPath: string, extractedPath: str // Define the paths of the files to be replaced const filesToReplace = [ { webappFile: FileName.Manifest, extractedFile: FileName.Manifest }, - { webappFile: 'i18n/i18n.properties', extractedFile: 'i18n.properties' }, // replace 'i18n/i18n.properties' in extractedFile - { webappFile: 'index.html', extractedFile: 'index.html' } + { webappFile: join('i18n', 'i18n.properties'), extractedFile: join('i18n', 'i18n.properties') }, + { webappFile: 'index.html', extractedFile: 'index.html' }, + { webappFile: 'Component.js', extractedFile: 'component.js' } ]; // Loop through each file and perform the replacement for (const { webappFile, extractedFile } of filesToReplace) { @@ -53,7 +55,14 @@ export async function replaceWebappFiles(projectPath: string, extractedPath: str // Check if the extracted file exists before replacing if (fs.exists(extractedFilePath)) { - fs.copy(extractedFilePath, webappFilePath); + if (webappFile === FileName.Manifest) { + // Pretify manifest.json file + const manifestContent = fs.read(extractedFilePath); + const prettifiedContent = JSON.stringify(JSON.parse(manifestContent), null, 2); + fs.write(webappFilePath, prettifiedContent); + } else { + fs.copy(extractedFilePath, webappFilePath); + } } else { BspAppDownloadLogger.logger?.warn(t('warn.extractedFileNotFound', { extractedFilePath })); } @@ -62,3 +71,23 @@ export async function replaceWebappFiles(projectPath: string, extractedPath: str BspAppDownloadLogger.logger?.error(t('error.replaceWebappFilesError', { error })); } } + +/** + * + * @param filePath - Path to the JSON file + * @param fs - File system editor instance + * @returns - Parsed JSON object + */ +export function makeValidJson(filePath: string, fs: Editor): QfaJsonConfig { + try { + // Read the file contents + const fileContents = fs.read(filePath); + // Replace property names without quotes with quoted property names + const validJsonString = fileContents.replace(/(\w+):/g, '"$1":'); + // Parse to ensure it's valid JSON + const validJson: QfaJsonConfig = JSON.parse(validJsonString); + return validJson; + } catch (error) { + throw new Error(t('error.errorProcessingJsonFile', { error })); + } +} diff --git a/packages/bsp-app-download-sub-generator/src/utils/validators.ts b/packages/bsp-app-download-sub-generator/src/utils/validators.ts index 0fc3e6c7d6..e4cdec2c0a 100644 --- a/packages/bsp-app-download-sub-generator/src/utils/validators.ts +++ b/packages/bsp-app-download-sub-generator/src/utils/validators.ts @@ -1,15 +1,15 @@ import { t } from '../utils/i18n'; -import type { AppContentConfig } from '../app/types'; +import type { QfaJsonConfig } from '../app/types'; import BspAppDownloadLogger from '../utils/logger'; import { PromptState } from '../prompts/prompt-state'; /** * Validates the metadata section of the app configuration. * - * @param {AppContentConfig['metadata']} metadata - The metadata object. + * @param {QfaJsonConfig['metadata']} metadata - The metadata object. * @returns {boolean} - Returns true if valid, false otherwise. */ -const validateMetadata = (metadata: AppContentConfig['metadata']): boolean => { +const validateMetadata = (metadata: QfaJsonConfig['metadata']): boolean => { if (!metadata.package || typeof metadata.package !== 'string') { BspAppDownloadLogger.logger?.error(t('error.invalidMetadataPackage')); return false; @@ -20,19 +20,19 @@ const validateMetadata = (metadata: AppContentConfig['metadata']): boolean => { /** * Validates the service binding details section of the app configuration. * - * @param {AppContentConfig['serviceBindingDetails']} serviceBinding - The service binding details object. + * @param {QfaJsonConfig['serviceBindingDetails']} serviceBinding - The service binding details object. * @returns {boolean} - Returns true if valid, false otherwise. */ -const validateServiceBindingDetails = (serviceBinding: AppContentConfig['serviceBindingDetails']): boolean => { - if (!serviceBinding.serviceName || typeof serviceBinding.serviceName !== 'string') { +const validateServiceBindingDetails = (serviceBinding: QfaJsonConfig['service_binding_details']): boolean => { + if (!serviceBinding.service_name || typeof serviceBinding.service_name !== 'string') { BspAppDownloadLogger.logger?.error(t('error.invalidServiceName')); return false; } - if (!serviceBinding.serviceVersion || typeof serviceBinding.serviceVersion !== 'string') { + if (!serviceBinding.service_version || typeof serviceBinding.service_version !== 'string') { BspAppDownloadLogger.logger?.error(t('error.invalidServiceVersion')); return false; } - if (!serviceBinding.mainEntityName || typeof serviceBinding.mainEntityName !== 'string') { + if (!serviceBinding.main_entity_name || typeof serviceBinding.main_entity_name !== 'string') { BspAppDownloadLogger.logger?.error(t('error.invalidMainEntityName')); return false; } @@ -42,11 +42,11 @@ const validateServiceBindingDetails = (serviceBinding: AppContentConfig['service /** * Validates the project attribute section of the app configuration. * - * @param {AppContentConfig['projectAttribute']} projectAttribute - The project attribute object. + * @param {QfaJsonConfig['projectAttribute']} projectAttribute - The project attribute object. * @returns {boolean} - Returns true if valid, false otherwise. */ -const validateProjectAttribute = (projectAttribute: AppContentConfig['projectAttribute']): boolean => { - if (!projectAttribute.moduleName || typeof projectAttribute.moduleName !== 'string') { +const validateProjectAttribute = (projectAttribute: QfaJsonConfig['project_attribute']): boolean => { + if (!projectAttribute.module_name || typeof projectAttribute.module_name !== 'string') { BspAppDownloadLogger.logger?.error(t('error.invalidModuleName')); return false; } @@ -56,11 +56,11 @@ const validateProjectAttribute = (projectAttribute: AppContentConfig['projectAtt /** * Validates the deployment details section of the app configuration. * - * @param {AppContentConfig['deploymentDetails']} deploymentDetails - The deployment details object. + * @param {QfaJsonConfig['deploymentDetails']} deploymentDetails - The deployment details object. * @returns {boolean} - Returns true if valid, false otherwise. */ -const validateDeploymentDetails = (deploymentDetails: AppContentConfig['deploymentDetails']): boolean => { - if (!deploymentDetails.repositoryName) { +const validateDeploymentDetails = (deploymentDetails: QfaJsonConfig['deployment_details']): boolean => { + if (!deploymentDetails.repository_name) { BspAppDownloadLogger.logger?.error(t('error.invalidRepositoryName')); return false; } @@ -70,15 +70,15 @@ const validateDeploymentDetails = (deploymentDetails: AppContentConfig['deployme /** * Validates the entire app configuration. * - * @param {AppContentConfig} config - The app configuration object. + * @param {QfaJsonConfig} config - The QFA JSON configuration containing app details. * @returns {boolean} - Returns true if the configuration is valid, false otherwise. */ -export const validateAppContentJsonFile = (config: AppContentConfig): boolean => { +export const validateQfaJsonFile = (config: QfaJsonConfig): boolean => { return ( validateMetadata(config.metadata) && - validateServiceBindingDetails(config.serviceBindingDetails) && - validateProjectAttribute(config.projectAttribute) && - validateDeploymentDetails(config.deploymentDetails) + validateServiceBindingDetails(config.service_binding_details) && + validateProjectAttribute(config.project_attribute) && + validateDeploymentDetails(config.deployment_details) ); }; diff --git a/packages/bsp-app-download-sub-generator/test/config.test.ts b/packages/bsp-app-download-sub-generator/test/app-config.test.ts similarity index 77% rename from packages/bsp-app-download-sub-generator/test/config.test.ts rename to packages/bsp-app-download-sub-generator/test/app-config.test.ts index 8050d29f2a..b9e2452356 100644 --- a/packages/bsp-app-download-sub-generator/test/config.test.ts +++ b/packages/bsp-app-download-sub-generator/test/app-config.test.ts @@ -1,15 +1,17 @@ -import { getAppConfig, getAbapDeployConfig } from '../src/app/config'; +import { getAppConfig, getAbapDeployConfig } from '../src/app/app-config'; import type { AbapServiceProvider } from '@sap-ux/axios-extension'; import type { Editor } from 'mem-fs-editor'; import { getLatestUI5Version } from '@sap-ux/ui5-info'; import { getMinimumUI5Version } from '@sap-ux/project-access'; import { PromptState } from '../src/prompts/prompt-state'; -import type { AppInfo, AppContentConfig } from '../src/app/types'; +import type { AppInfo, QfaJsonConfig } from '../src/app/types'; import { readManifest } from '../src/utils/file-helpers'; import { t } from '../src/utils/i18n'; import { adtSourceTemplateId } from '../src/utils/constants'; import BspAppDownloadLogger from '../src/utils/logger'; -import { sampleAppContentTestData } from './fixtures/downloaded-app/example-app-content'; +import { TestFixture } from './fixtures'; +import { join } from 'path'; +import { qfaJsonFileName } from '../src/utils/constants'; jest.mock('../src/utils/logger', () => ({ logger: { @@ -32,6 +34,9 @@ jest.mock('@sap-ux/project-access', () => ({ getMinimumUI5Version: jest.fn() })); +const testFixture = new TestFixture(); +const mockQfaJson: QfaJsonConfig = JSON.parse(testFixture.getContents(join('downloaded-app', qfaJsonFileName))); + describe('getAppConfig', () => { const mockApp: AppInfo = { appId: 'testAppId', @@ -40,13 +45,13 @@ describe('getAppConfig', () => { repoName: 'testRepoName', url: 'https://example.com/testApp' }; - const mockAppContentJson: AppContentConfig = sampleAppContentTestData; const mockFs = {} as Editor; const expectedAppConfig = { app: { id: mockApp.appId, title: mockApp.title, description: mockApp.description, + flpAppId: `${mockApp.appId}-tile`, sourceTemplate: { id: adtSourceTemplateId }, projectType: 'EDMXBackend' }, @@ -61,7 +66,7 @@ describe('getAppConfig', () => { type: expect.any(String), settings: { entityConfig: { - mainEntityName: sampleAppContentTestData.serviceBindingDetails.mainEntityName + mainEntityName: mockQfaJson.service_binding_details.main_entity_name } } }, @@ -76,7 +81,7 @@ describe('getAppConfig', () => { addTests: true }, ui5: { - version: sampleAppContentTestData.projectAttribute.minimumUi5Version + version: mockQfaJson.project_attribute.minimum_ui5_version ?? '1.90.0' } }; @@ -109,8 +114,16 @@ describe('getAppConfig', () => { (readManifest as jest.Mock).mockReturnValue(mockManifest); (getLatestUI5Version as jest.Mock).mockResolvedValue('1.100.0'); (getMinimumUI5Version as jest.Mock).mockReturnValue('1.90.0'); - - const result = await getAppConfig(mockApp, '/path/to/project', mockAppContentJson, mockFs); + const mockQfaJsonWithoutNavEntity = { + ...mockQfaJson, + service_binding_details: { + name: mockQfaJson.service_binding_details.name, + service_name: mockQfaJson.service_binding_details.service_name, + service_version: mockQfaJson.service_binding_details.service_version, + main_entity_name: mockQfaJson.service_binding_details.main_entity_name, + } + } + const result = await getAppConfig(mockApp, '/path/to/project', mockQfaJsonWithoutNavEntity, mockFs); expect(result).toEqual(expectedAppConfig); }); @@ -139,28 +152,25 @@ describe('getAppConfig', () => { (getLatestUI5Version as jest.Mock).mockResolvedValue('1.100.0'); (getMinimumUI5Version as jest.Mock).mockReturnValue('1.90.0'); - const mockAppContentJsonWithNavEntity = { - ...mockAppContentJson, - serviceBindingDetails: { - ...mockAppContentJson.serviceBindingDetails, - mainEntityName: mockAppContentJson.serviceBindingDetails.mainEntityName, - navigationEntity: { - EntitySet: 'EnitySet', - Name: 'SomeNavigationProperty' - } + const mockQfaJsonJsonWithNavEntity = { + ...mockQfaJson, + service_binding_details: { + ...mockQfaJson.service_binding_details, + main_entity_name: mockQfaJson.service_binding_details.main_entity_name, + navigation_entity: mockQfaJson.service_binding_details.navigation_entity } } - const result = await getAppConfig(mockApp, '/path/to/project', mockAppContentJsonWithNavEntity, mockFs); + const result = await getAppConfig(mockApp, '/path/to/project', mockQfaJsonJsonWithNavEntity, mockFs); const expectedAppConfigWithNavEntity = { ...expectedAppConfig, template: { ...expectedAppConfig.template, settings: { entityConfig: { - mainEntityName: mockAppContentJson.serviceBindingDetails.mainEntityName, + mainEntityName: mockQfaJson.service_binding_details.main_entity_name, navigationEntity: { - EntitySet: mockAppContentJsonWithNavEntity.serviceBindingDetails.navigationEntity.EntitySet, - Name: mockAppContentJsonWithNavEntity.serviceBindingDetails.navigationEntity.Name + EntitySet: mockQfaJsonJsonWithNavEntity.service_binding_details.navigation_entity, + Name: mockQfaJsonJsonWithNavEntity.service_binding_details.navigation_entity } } } @@ -175,7 +185,7 @@ describe('getAppConfig', () => { }; (readManifest as jest.Mock).mockReturnValue(mockManifest); - const result = await getAppConfig(mockApp, '/path/to/project', mockAppContentJson, mockFs); + const result = await getAppConfig(mockApp, '/path/to/project', mockQfaJson, mockFs); expect(BspAppDownloadLogger.logger.error).toBeCalledWith(t('error.dataSourcesNotFound')); }); @@ -210,7 +220,7 @@ describe('getAppConfig', () => { (readManifest as jest.Mock).mockReturnValue(mockManifest); - await getAppConfig(mockApp, '/path/to/project', mockAppContentJson, mockFs); + await getAppConfig(mockApp, '/path/to/project', mockQfaJson, mockFs); expect(BspAppDownloadLogger.logger?.error).toHaveBeenCalledWith(t('error.metadataFetchError', { error: errorMsg })); }); @@ -245,14 +255,14 @@ describe('getAppConfig', () => { (getLatestUI5Version as jest.Mock).mockResolvedValue('1.100.0'); (getMinimumUI5Version as jest.Mock).mockReturnValue('1.90.0'); - const mockAppContentJsonWithoutUi5Version = { - ...sampleAppContentTestData, - projectAttribute: { - ...sampleAppContentTestData.projectAttribute, - minimumUi5Version: null + const mockQfaJsonJsonWithoutUi5Version = { + ...mockQfaJson, + project_attribute: { + ...mockQfaJson.project_attribute, + minimum_ui5_version: null } - } as unknown as AppContentConfig; - await getAppConfig(mockApp, '/path/to/project', mockAppContentJsonWithoutUi5Version, mockFs); + } as unknown as QfaJsonConfig; + await getAppConfig(mockApp, '/path/to/project', mockQfaJsonJsonWithoutUi5Version, mockFs); expect(BspAppDownloadLogger.logger?.error).not.toHaveBeenCalled(); }); @@ -274,17 +284,13 @@ describe('getAbapDeployConfig', () => { destination: 'TEST_REPO' }, app: { - name: 'TEST_REPO_NAME', - package: 'TEST_PACKAGE', - description: 'This is a test repository', + name: 'ZSB_TRVL_APR2', + package: '$TMP', + description: 'Travel Approver 2.0', transport: 'REPLACE_WITH_TRANSPORT' } }; - - // Call the function - const result = getAbapDeployConfig(app, sampleAppContentTestData); - - // Assertions + const result = getAbapDeployConfig(app, mockQfaJson); expect(result).toEqual(expectedConfig); }); }); diff --git a/packages/bsp-app-download-sub-generator/test/app.test.ts b/packages/bsp-app-download-sub-generator/test/app.test.ts index a7e5d83487..749a147ba1 100644 --- a/packages/bsp-app-download-sub-generator/test/app.test.ts +++ b/packages/bsp-app-download-sub-generator/test/app.test.ts @@ -6,15 +6,13 @@ import * as prompts from '../src/prompts/prompts'; import { PromptNames } from '../src/app/types'; import fs from 'fs'; import { TestFixture } from './fixtures'; -import { getAppConfig } from '../src/app/config'; +import { getAppConfig } from '../src/app/app-config'; import { OdataVersion } from '@sap-ux/odata-service-inquirer'; import { TemplateType, type FioriElementsApp, type LROPSettings } from '@sap-ux/fiori-elements-writer'; import { adtSourceTemplateId, extractedFilePath } from '../src/utils/constants'; import { removeSync } from 'fs-extra'; import { isValidPromptState } from '../src/utils/validators'; import { hostEnvironment, sendTelemetry } from '@sap-ux/fiori-generator-shared'; -import * as memFs from 'mem-fs'; -import * as editor from 'mem-fs-editor'; import { FileName, DirName } from '@sap-ux/project-access'; import BspAppDownloadLogger from '../src/utils/logger'; import { t } from '../src/utils/i18n'; @@ -42,8 +40,8 @@ jest.mock('../src/utils/file-helpers', () => ({ readManifest: jest.fn() })); jest.mock('../src/utils/download-utils'); -jest.mock('../src/app/config', () => ({ - ...jest.requireActual('../src/app/config'), +jest.mock('../src/app/app-config', () => ({ + ...jest.requireActual('../src/app/app-config'), getAppConfig: jest.fn() })); jest.mock('../src/utils/validators'); @@ -142,20 +140,28 @@ function copyFilesToExtractedProjectPath( if (!fs.existsSync(extractedProjectPath)) { fs.mkdirSync(extractedProjectPath, { recursive: true }); } - // List all files in the test fixture directory const files = fs.readdirSync(testFixtureDir); // Copy each file to the extracted project path files.forEach((file) => { - const sourceFilePath = join(testFixtureDir, file); - const destinationFilePath = join(extractedProjectPath, file); - // Copy the file - fs.copyFileSync(sourceFilePath, destinationFilePath); + const sourceFilePath = join(testFixtureDir, file); + const destinationFilePath = join(extractedProjectPath, file); + if(file === 'i18n') { + // Create the directory if it doesn't exist + if (!fs.existsSync(destinationFilePath)) { + fs.mkdirSync(destinationFilePath, { recursive: true }); + } + // Copy the i18n.properties file + fs.copyFileSync(join(sourceFilePath, 'i18n.properties'), join(destinationFilePath, 'i18n.properties')); + } else { + // Copy the file + fs.copyFileSync(sourceFilePath, destinationFilePath); + } }); } -function verifyGeneratedFiles(testOutputDir: string, appId: string, extractedProjectPath: string): void { +function verifyGeneratedFiles(testOutputDir: string, appId: string, testFixtureDir: string): void { const projectPath = join(`${testOutputDir}/${appId}`); expect(fs.existsSync(projectPath)).toBe(true); @@ -181,14 +187,18 @@ function verifyGeneratedFiles(testOutputDir: string, appId: string, extractedPro expect(fs.existsSync(filePath)).toBe(true); }); - expect(fs.readFileSync(join(projectPath, DirName.Webapp, FileName.Manifest), 'utf-8')).toBe( - fs.readFileSync(join(extractedProjectPath, FileName.Manifest), 'utf-8') - ); + const projectManifest = JSON.stringify( + JSON.parse(fs.readFileSync(join(projectPath, DirName.Webapp, FileName.Manifest), 'utf-8')) + ); + const extractedManifest = JSON.stringify( + JSON.parse(fs.readFileSync(join(testFixtureDir, FileName.Manifest), 'utf-8')) + ); + expect(projectManifest).toEqual(extractedManifest); expect(fs.readFileSync(join(projectPath, DirName.Webapp, 'i18n', 'i18n.properties'), 'utf-8')).toBe( - fs.readFileSync(join(extractedProjectPath, 'i18n.properties'), 'utf-8') + fs.readFileSync(join(testFixtureDir, 'i18n', 'i18n.properties'), 'utf-8') ); expect(fs.readFileSync(join(projectPath, DirName.Webapp, 'index.html'), 'utf-8')).toBe( - fs.readFileSync(join(extractedProjectPath, 'index.html'), 'utf-8') + fs.readFileSync(join(testFixtureDir, 'index.html'), 'utf-8') ); } @@ -205,8 +215,6 @@ describe('BSP App Download', () => { showError: jest.fn(), showInformation: jest.fn() }; - const store = memFs.create(); - const fsEditor = editor.create(store); const appId = 'app-1', repoName = 'app-1-repo'; const extractedProjectPath = join(testOutputDir, appId, extractedFilePath); const testFixtureDir = join(__dirname, 'fixtures', 'downloaded-app'); @@ -259,11 +267,9 @@ describe('BSP App Download', () => { }) ) .resolves.not.toThrow(); - verifyGeneratedFiles(testOutputDir, appId, extractedProjectPath); - expect(mockAppWizard.showWarning).toHaveBeenCalledWith(t('info.bspAppDownloadCompleteMsg'), MessageType.notification); + verifyGeneratedFiles(testOutputDir, appId, testFixtureDir); + expect(mockAppWizard.showInformation).toHaveBeenCalledWith(t('info.bspAppDownloadCompleteMsg'), MessageType.notification); expect(BspAppDownloadLogger.logger.info).toHaveBeenCalledWith(t('info.installationErrors.skippedInstallation')); - - }); it('Should not throw error in end phase if telemetry fails', async () => { @@ -297,7 +303,7 @@ describe('BSP App Download', () => { ) .resolves.not.toThrow(); expect(BspAppDownloadLogger.logger.error).toHaveBeenCalledWith(t('error.telemetry', { error: errorMsg })); - verifyGeneratedFiles(testOutputDir, appId, extractedProjectPath); + verifyGeneratedFiles(testOutputDir, appId, testFixtureDir); }); it('Should execute post app gen hook event when postGenCommand is provided', async () => { @@ -331,7 +337,7 @@ describe('BSP App Download', () => { }) ) .resolves.not.toThrow(); - verifyGeneratedFiles(testOutputDir, appId, extractedProjectPath); + verifyGeneratedFiles(testOutputDir, appId, testFixtureDir); }); it('Should successfully download a quick deployed app from BSP', async () => { @@ -390,7 +396,7 @@ describe('BSP App Download', () => { ) .resolves.not.toThrow(); expect(fetchAppListForSelectedSystem).toHaveBeenCalledWith(mockServiceProvider, appConfig.app.id); - verifyGeneratedFiles(testOutputDir, appId, extractedProjectPath); + verifyGeneratedFiles(testOutputDir, appId, testFixtureDir); }); it('Should throw error when fetchAppListForSelectedSystem fetches no app', async () => { diff --git a/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/cdm.json b/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/cdm.json deleted file mode 100644 index b184cb0f6f..0000000000 --- a/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/cdm.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "_version": "3.1.0", - "site": { - "identification": { - "namespace": "", - "title": "Fiori Elements Application Preview", - "description": "Fiori Elements Application Preview" - }, - "payload": {} - }, - "groups": {}, - "catalogs": {}, - "applications": { - "travel.approver": { - "sap.app": { - "id": "travel.approver", - "title": "Fiori Application Preview", - "crossNavigation": { - "inbounds": { - "app-preview": { - "semanticObject": "app", - "action": "preview", - "title": "Fiori Application Preview", - "subTitle": "", - "info": "", - "signature": { - "parameters": { }, - "additionalParameters": "allowed" - } - } - } - } - }, - "sap.ui5": { - "componentName": "travel.approver" - }, - "sap.flp": { - "type": "application" - }, - "sap.ui": { - "technology": "UI5", - "deviceTypes": { - "desktop": true, - "tablet": true, - "phone": true - } - }, - "sap.platform.runtime": { - "componentProperties": { - "url": "./", - "asyncHints": {} - } - } - } - }, - "visualizations": {}, - "vizTypes": {}, - "pages": {}, - "menus": {} -} diff --git a/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/component.js b/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/component.js index 403733011d..d3d5735986 100644 --- a/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/component.js +++ b/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/component.js @@ -1,8 +1,8 @@ sap.ui.define( - ["sap/suite/ui/generic/template/lib/AppComponent"], + ["sap/fe/core/AppComponent"], function (Component){ "use strict"; - return Component.extend("travel.approver.Component",{ + return Component.extend("travel.approver.2.Component",{ metadata:{ manifest: "json" } diff --git a/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/example-app-content.ts b/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/example-app-content.ts deleted file mode 100644 index 771c87cfe3..0000000000 --- a/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/example-app-content.ts +++ /dev/null @@ -1,28 +0,0 @@ -export const sampleAppContentTestData = { - metadata: { - package: 'TEST_PACKAGE', - masterLanguage: 'EN' - }, - serviceBindingDetails: { - name: 'TEST_SERVICE_BINDING', - serviceName: 'TEST_SERVICE_NAME', - serviceVersion: '0001', - mainEntityName: 'TEST_ENTITY' - }, - projectAttribute: { - moduleName: 'test_module_name', - applicationTitle: 'Test Application Title', - template: 'Test Template', - minimumUi5Version: '1.100.0' - }, - deploymentDetails: { - repositoryName: 'TEST_REPO_NAME', - repositoryDescription: 'This is a test repository' - }, - fioriLaunchpadConfiguration: { - semanticObject: 'TEST_SEMANTIC_OBJECT', - action: 'testAction', - title: 'Test Title', - subtitle: 'Test Subtitle' - } -}; \ No newline at end of file diff --git a/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/flp.html b/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/flp.html deleted file mode 100644 index 976b37bb29..0000000000 --- a/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/flp.html +++ /dev/null @@ -1,101 +0,0 @@ - - - - - - - Fiori Elements Application Preview - - - - -
-
-
- - diff --git a/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/i18n.properties b/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/i18n.properties deleted file mode 100644 index 639ffcc9de..0000000000 --- a/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/i18n.properties +++ /dev/null @@ -1,5 +0,0 @@ -#XTIT: Application title -appTitle=Travel Approver - -#XTIT: Application description -appDescription=Travel Approver diff --git a/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/i18n/i18n.properties b/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/i18n/i18n.properties new file mode 100644 index 0000000000..24e8a22c51 --- /dev/null +++ b/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/i18n/i18n.properties @@ -0,0 +1,5 @@ +#XTIT: Application title +appTitle=Travel Approver 2.0 + +#XTIT: Application description +appDescription=Travel Approver 2.0 diff --git a/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/index.html b/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/index.html index d97b6ab192..9e5c0174cf 100644 --- a/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/index.html +++ b/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/index.html @@ -7,26 +7,24 @@ Fiori Application Preview -
diff --git a/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/manifest.json b/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/manifest.json index 782b1a3061..1975a5235c 100644 --- a/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/manifest.json +++ b/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/manifest.json @@ -1,9 +1,10 @@ { + "_version": "1.65.0", "sap.app": { - "id": "travel.approver", + "id": "travel.approver.2", "type": "application", - "i18n": "i18n.properties", - "applicationVersion":{ + "i18n": "i18n/i18n.properties", + "applicationVersion": { "version": "0.0.1" }, "title": "{{appTitle}}", @@ -12,48 +13,39 @@ "sourceTemplate": { "id": "@sap.adt.sevicebinding.deploy:lrop", "version": "1.0.0", - "toolsId": "15AB9F96A8DF1FE081C6CD7B64A2046B" + "toolsId": "15AB9F96A8DF1FE085AC7E6BBC288DEE" }, "dataSources": { "mainService": { - "uri": "/sap/opu/odata/sap/ZSB_TRAVEL_APPROVER", + "uri": "/sap/opu/odata4/sap/zsb_travel_draft_2/srvd/dmo/ui_travel_d_d/0001/", "type": "OData", "settings": { - "odataVersion": "2.0", - "annotations": [ - "mainAnnotations" - ] + "odataVersion": "4.0" } - }, - "mainAnnotations": { - "uri": "/sap/opu/odata/IWFND/CATALOGSERVICE;v=2/Annotations(TechnicalName='ZSB_TRAVEL_APPROVER_VAN',Version='0001')/$value/", - "type": "ODataAnnotation" } } - }, "sap.ui": { "technology": "UI5" }, "sap.ui5": { + "flexEnabled": true, "resources": { "js": [], "css": [] }, "dependencies": { + "minUI5Version": "1.136.0", "libs": { - "sap.ui.generic.app": {}, - "sap.suite.ui.generic.template": {}, - "sap.suite.ui.commons": {} -} -, + "sap.fe.templates": {} + }, "components": {} }, "models": { "i18n": { "type": "sap.ui.model.resource.ResourceModel", "settings": { - "bundleName": "travel.approver.i18n" + "bundleName": "travel.approver.2.i18n.i18n" } }, "@i18n": { @@ -64,86 +56,134 @@ "dataSource": "mainService", "preload": true, "settings": { - "defaultBindingMode": "TwoWay", - "refreshAfterChange": false, - "metadataUrlParams": { - "sap-value-list": "none" - } + "operationMode": "Server", + "autoExpandSelect": true, + "earlyRequests": true } - } }, - "routing": {} - - }, - "sap.ui.generic.app": { - "_version": "1.3.0", - "settings": { - "forceGlobalRefresh": false, - "objectPageHeaderType": "Dynamic", - "considerAnalyticalParameters": true, - "showDraftToggle": false - }, - "pages": { - "ListReport|Travel": { - "entitySet": "Travel", - "component": { - "name": "sap.suite.ui.generic.template.ListReport", - "list": true, - "settings": { - "condensedTableLayout": true, - "smartVariantManagement": true, - "enableTableFilterInPageVariant": true, - "filterSettings": { - "dateSettings": { - "useDateRange": true - } + "routing": { + "config": {}, + "routes": [ + { + "pattern": ":?query:", + "name": "TravelList", + "target": "TravelList" + }, + { + "pattern": "Travel({TravelKey}):?query:", + "name": "TravelObjectPage", + "target": "TravelObjectPage" + }, + { + "pattern": "Travel({TravelKey})/_Booking({BookingKey}):?query:", + "name": "BookingObjectPage", + "target": "BookingObjectPage" + }, + { + "pattern": "Travel({TravelKey})/_Booking({BookingKey})/_BookingStatus({BookingStatusKey}):?query:", + "name": "BookingStatusObjectPage", + "target": "BookingStatusObjectPage" + }, + { + "pattern": "Travel({TravelKey})/_Booking({BookingKey})/_BookingSupplement({BookingSupplementKey}):?query:", + "name": "BookingSupplementObjectPage", + "target": "BookingSupplementObjectPage" + } + ], + "targets": { + "TravelList": { + "type": "Component", + "id": "TravelList", + "name": "sap.fe.templates.ListReport", + "options": { + "settings": { + "contextPath": "/Travel", + "variantManagement": "Page", + "navigation": { + "Travel": { + "detail": { + "route": "TravelObjectPage" + } + } + }, + "controlConfiguration": {} } } }, - "pages": { - "ObjectPage|Travel": { - "entitySet": "Travel", - "defaultLayoutTypeIfExternalNavigation": "MidColumnFullScreen", - "component": { - "name": "sap.suite.ui.generic.template.ObjectPage" - }, - "pages": { "ObjectPage|Booking": { - "navigationProperty": "to_Booking", - "entitySet": "Booking", - "defaultLayoutTypeIfExternalNavigation": "MidColumnFullScreen", - "component": { - "name": "sap.suite.ui.generic.template.ObjectPage" - }, - "pages": { "ObjectPage|BookingStatus": { - "navigationProperty": "to_BookingStatus", - "entitySet": "BookingStatus", - "defaultLayoutTypeIfExternalNavigation": "MidColumnFullScreen", - "component": { - "name": "sap.suite.ui.generic.template.ObjectPage" - }, - "pages": {} - } - } - } - , - "ObjectPage|OverallStatus": { - "navigationProperty": "to_OverallStatus", - "entitySet": "OverallStatus", - "defaultLayoutTypeIfExternalNavigation": "MidColumnFullScreen", - "component": { - "name": "sap.suite.ui.generic.template.ObjectPage" - }, - "pages": {} - } - } + "TravelObjectPage": { + "type": "Component", + "id": "TravelObjectPage", + "name": "sap.fe.templates.ObjectPage", + "options": { + "settings": { + "contextPath": "/Travel", + "editableHeaderContent": false, + "navigation": { + "_Booking": { + "detail": { + "route": "BookingObjectPage" + } + } + }, + "controlConfiguration": {} + } } - } + }, + "BookingObjectPage": { + "type": "Component", + "id": "BookingObjectPage", + "name": "sap.fe.templates.ObjectPage", + "options": { + "settings": { + "contextPath": "/Travel/_Booking", + "editableHeaderContent": false, + "navigation": { + "_BookingStatus": { + "detail": { + "route": "BookingStatusObjectPage" + } + }, + "_BookingSupplement": { + "detail": { + "route": "BookingSupplementObjectPage" + } + } + }, + "controlConfiguration": {} + } + } + }, + "BookingStatusObjectPage": { + "type": "Component", + "id": "BookingStatusObjectPage", + "name": "sap.fe.templates.ObjectPage", + "options": { + "settings": { + "contextPath": "/Travel/_Booking/_BookingStatus", + "editableHeaderContent": false, + "navigation": {}, + "controlConfiguration": {} + } + } + }, + "BookingSupplementObjectPage": { + "type": "Component", + "id": "BookingSupplementObjectPage", + "name": "sap.fe.templates.ObjectPage", + "options": { + "settings": { + "contextPath": "/Travel/_Booking/_BookingSupplement", + "editableHeaderContent": false, + "navigation": {}, + "controlConfiguration": {} + } + } + } } } }, - "sap.fiori": { "archeType": "transactional" } -} +} \ No newline at end of file diff --git a/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/qfa.json b/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/qfa.json new file mode 100644 index 0000000000..2f493595e4 --- /dev/null +++ b/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/qfa.json @@ -0,0 +1,27 @@ +{ + "metadata": { + "package": "$TMP", + "master_language": "EN" + }, + "service_binding_details": { + "name": "ZSB_TRAVEL_DRAFT_2", + "service_name": "/DMO/UI_TRAVEL_D_D", + "service_version": "0001", + "main_entity_name": "Travel", + "navigation_entity": "_Booking" + }, + "project_attribute": { + "module_name": "travel.approver.2", + "application_title": "Travel Approver 2.0" + }, + "deployment_details": { + "repository_name": "ZSB_TRVL_APR2", + "repository_description": "Travel Approver 2.0" + }, + "fiori_launchpad_configuration": { + "semantic_object": "", + "action": "", + "title": "", + "subtitle": "" + } +} diff --git a/packages/bsp-app-download-sub-generator/test/utils/validators.test.ts b/packages/bsp-app-download-sub-generator/test/utils/validators.test.ts index 3445fb7d76..8e6b49d055 100644 --- a/packages/bsp-app-download-sub-generator/test/utils/validators.test.ts +++ b/packages/bsp-app-download-sub-generator/test/utils/validators.test.ts @@ -1,5 +1,5 @@ -import { validateAppContentJsonFile } from '../../src/utils/validators'; -import { AppContentConfig } from '../../src/app/types'; +import { validateQfaJsonFile } from '../../src/utils/validators'; +import { QfaJsonConfig } from '../../src/app/types'; import { t } from '../../src/utils/i18n'; import BspAppDownloadLogger from '../../src/utils/logger'; @@ -9,18 +9,18 @@ jest.mock('../../src/utils/logger', () => ({ } })); -describe('validateAppContentJsonFile', () => { - const validConfig: AppContentConfig = { +describe('validateQfaJsonFile', () => { + const validConfig: QfaJsonConfig = { metadata: { package: 'valid-package' }, - serviceBindingDetails: { - serviceName: 'validService', - serviceVersion: '1.0.0', - mainEntityName: 'validEntity', + service_binding_details: { + service_name: 'validService', + service_version: '1.0.0', + main_entity_name: 'validEntity', }, - projectAttribute: { moduleName: 'validModule' }, - deploymentDetails: { repositoryName: 'validRepository' }, - fioriLaunchpadConfiguration: { - semanticObject: 'semanticObject', + project_attribute: { module_name: 'validModule' }, + deployment_details: { repository_name: 'validRepository' }, + fiori_launchpad_configuration: { + semantic_object: 'semanticObject', action: 'action', title: 'title' }, @@ -31,7 +31,7 @@ describe('validateAppContentJsonFile', () => { }); it('should return true when all validation functions pass', () => { - const result = validateAppContentJsonFile(validConfig); + const result = validateQfaJsonFile(validConfig); expect(result).toBe(true); }); @@ -39,9 +39,9 @@ describe('validateAppContentJsonFile', () => { const invalidMetadataConfig = { ...validConfig, metadata: { package: '' } // Invalid package - } as unknown as AppContentConfig; + } as unknown as QfaJsonConfig; - const result = validateAppContentJsonFile(invalidMetadataConfig); + const result = validateQfaJsonFile(invalidMetadataConfig); expect(result).toBe(false); expect(BspAppDownloadLogger.logger.error).toBeCalledWith(t('error.invalidMetadataPackage')); }); @@ -49,13 +49,13 @@ describe('validateAppContentJsonFile', () => { it('should return false and log an error when service binding details validation fails', () => { const invalidServiceBindingConfig = { ...validConfig, - serviceBindingDetails: { - ...validConfig.serviceBindingDetails, - serviceName: '', // Invalid service name + service_binding_details: { + ...validConfig.service_binding_details, + service_name: '', // Invalid service name } - } as unknown as AppContentConfig; + } as unknown as QfaJsonConfig; - const result = validateAppContentJsonFile(invalidServiceBindingConfig); + const result = validateQfaJsonFile(invalidServiceBindingConfig); expect(result).toBe(false); expect(BspAppDownloadLogger.logger.error).toBeCalledWith(t('error.invalidServiceName')); }); @@ -63,13 +63,13 @@ describe('validateAppContentJsonFile', () => { it('should return false and log an error when service binding version is not provided', () => { const invalidServiceBindingConfig = { ...validConfig, - serviceBindingDetails: { - ...validConfig.serviceBindingDetails, - serviceVersion: '' // Invalid service version + service_binding_details: { + ...validConfig.service_binding_details, + service_version: '' // Invalid service version } - } as unknown as AppContentConfig; + } as unknown as QfaJsonConfig; - const result = validateAppContentJsonFile(invalidServiceBindingConfig); + const result = validateQfaJsonFile(invalidServiceBindingConfig); expect(result).toBe(false); expect(BspAppDownloadLogger.logger.error).toBeCalledWith(t('error.invalidServiceVersion')); }); @@ -77,13 +77,13 @@ describe('validateAppContentJsonFile', () => { it('should return false and log an error when main entity name is missing', () => { const invalidServiceBindingConfig = { ...validConfig, - serviceBindingDetails: { - ...validConfig.serviceBindingDetails, - mainEntityName: '' // Invalid main entity name + service_binding_details: { + ...validConfig.service_binding_details, + main_entity_name: '' // Invalid main entity name } - } as unknown as AppContentConfig; + } as unknown as QfaJsonConfig; - const result = validateAppContentJsonFile(invalidServiceBindingConfig); + const result = validateQfaJsonFile(invalidServiceBindingConfig); expect(result).toBe(false); expect(BspAppDownloadLogger.logger.error).toBeCalledWith(t('error.invalidMainEntityName')); }); @@ -91,10 +91,10 @@ describe('validateAppContentJsonFile', () => { it('should return false and log an error when project attribute validation fails', () => { const invalidProjectAttributeConfig = { ...validConfig, - projectAttribute: { moduleName: '' } // Invalid module name - } as unknown as AppContentConfig; + project_attribute: { module_name: '' } // Invalid module name + } as unknown as QfaJsonConfig; - const result = validateAppContentJsonFile(invalidProjectAttributeConfig); + const result = validateQfaJsonFile(invalidProjectAttributeConfig); expect(result).toBe(false); expect(BspAppDownloadLogger.logger.error).toBeCalledWith(t('error.invalidModuleName')); }); @@ -102,10 +102,10 @@ describe('validateAppContentJsonFile', () => { it('should return false and log an error when deployment details validation fails', () => { const invalidDeploymentDetailsConfig = { ...validConfig, - deploymentDetails: { repositoryName: '' } // Invalid repository name - } as unknown as AppContentConfig; + deployment_details: { repository_name: '' } // Invalid repository name + } as unknown as QfaJsonConfig; - const result = validateAppContentJsonFile(invalidDeploymentDetailsConfig); + const result = validateQfaJsonFile(invalidDeploymentDetailsConfig); expect(result).toBe(false); expect(BspAppDownloadLogger.logger.error).toBeCalledWith(t('error.invalidRepositoryName')); }); From 47415f5190d2f7222e39e36e5ba2d1dcb7b67cc4 Mon Sep 17 00:00:00 2001 From: I743583 Date: Thu, 10 Apr 2025 14:43:22 +0100 Subject: [PATCH 16/41] fix lint issues --- .../src/abap/ui5-abap-repository-service.ts | 6 ++++-- packages/bsp-app-download-sub-generator/package.json | 2 +- .../src/app/app-config.ts | 10 ++++------ .../bsp-app-download-sub-generator/src/app/index.ts | 2 +- .../bsp-app-download-sub-generator/src/app/types.ts | 1 + 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/axios-extension/src/abap/ui5-abap-repository-service.ts b/packages/axios-extension/src/abap/ui5-abap-repository-service.ts index b1f3631be6..e54ea34a28 100644 --- a/packages/axios-extension/src/abap/ui5-abap-repository-service.ts +++ b/packages/axios-extension/src/abap/ui5-abap-repository-service.ts @@ -142,7 +142,7 @@ export class Ui5AbapRepositoryService extends ODataService { } /** - * + * * @param str string to check * @returns true if the string is base64 encoded, false otherwise */ @@ -172,7 +172,9 @@ export class Ui5AbapRepositoryService extends ODataService { }); const data = response.odata(); - if (!data.ZipArchive) return undefined; + if (!data.ZipArchive) { + return undefined; + } const isBase64 = this.isBase64Encoded(data.ZipArchive); return Buffer.from(data.ZipArchive, isBase64 ? 'base64' : undefined); } catch (error) { diff --git a/packages/bsp-app-download-sub-generator/package.json b/packages/bsp-app-download-sub-generator/package.json index b04a5fbea5..4401637a69 100644 --- a/packages/bsp-app-download-sub-generator/package.json +++ b/packages/bsp-app-download-sub-generator/package.json @@ -1,6 +1,6 @@ { "name": "@sap-ux/bsp-app-download-sub-generator", - "description": "Generator for downloading Fiori LROP Apps from BSP", + "description": "Generator to support downloading ABAP Deployed Fiori App from BSP Repository", "version": "0.1.40", "repository": { "type": "git", diff --git a/packages/bsp-app-download-sub-generator/src/app/app-config.ts b/packages/bsp-app-download-sub-generator/src/app/app-config.ts index 6a01243db3..a32d119cf1 100644 --- a/packages/bsp-app-download-sub-generator/src/app/app-config.ts +++ b/packages/bsp-app-download-sub-generator/src/app/app-config.ts @@ -11,7 +11,6 @@ import { adtSourceTemplateId } from '../utils/constants'; import { PromptState } from '../prompts/prompt-state'; import type { AbapDeployConfig } from '@sap-ux/ui5-config'; import BspAppDownloadLogger from '../utils/logger'; -import { supportedUi5VersionFallbacks } from '@sap-ux/ui5-info'; /** * Generates the deployment configuration for an ABAP application. @@ -30,7 +29,7 @@ export const getAbapDeployConfig = (app: AppInfo, qfaJson: QfaJsonConfig): AbapD name: qfaJson.deployment_details.repository_name, package: qfaJson.metadata.package, description: qfaJson.deployment_details.repository_description, - transport: 'REPLACE_WITH_TRANSPORT' + transport: qfaJson.deployment_details.transport_request ?? 'REPLACE_WITH_TRANSPORT' } }; }; @@ -92,8 +91,8 @@ export async function getAppConfig( sourceTemplate: { id: adtSourceTemplateId }, - projectType: 'EDMXBackend', - flpAppId: `${app.appId.replace(/[-_.#]/g, '')}-tile`, + projectType: 'EDMXBackend', + flpAppId: `${app.appId.replace(/[-_.#]/g, '')}-tile` }, package: { name: app.appId, @@ -123,13 +122,12 @@ export async function getAppConfig( ui5: { version: qfaJson.project_attribute?.minimum_ui5_version ?? - //supportedUi5VersionFallbacks[0].version ?? getMinimumUI5Version(manifest) ?? (await getLatestUI5Version()) } }; //todo: confirm this - if(qfaJson.service_binding_details.navigation_entity) { + if (qfaJson.service_binding_details.navigation_entity) { appConfig.template.settings.entityConfig.navigationEntity = { EntitySet: qfaJson.service_binding_details.navigation_entity, Name: qfaJson.service_binding_details.navigation_entity diff --git a/packages/bsp-app-download-sub-generator/src/app/index.ts b/packages/bsp-app-download-sub-generator/src/app/index.ts index 3502bb2488..624772bbd3 100644 --- a/packages/bsp-app-download-sub-generator/src/app/index.ts +++ b/packages/bsp-app-download-sub-generator/src/app/index.ts @@ -102,7 +102,7 @@ export default class extends Generator { (this.env as unknown as YeomanEnvironment).conflicter.force = this.options.force ?? true; } - // Initialize telemetry settings + // Initialise telemetry settings await TelemetryHelper.initTelemetrySettings({ consumerModule: { name: generatorName, diff --git a/packages/bsp-app-download-sub-generator/src/app/types.ts b/packages/bsp-app-download-sub-generator/src/app/types.ts index 3678a4e126..c277cb7f8d 100644 --- a/packages/bsp-app-download-sub-generator/src/app/types.ts +++ b/packages/bsp-app-download-sub-generator/src/app/types.ts @@ -132,6 +132,7 @@ export interface QfaJsonConfig { deployment_details: { repository_name: string; repository_description?: string; + transport_request?: string; }; fiori_launchpad_configuration: { semantic_object: string; From ec0e29303676a2aed4d6153275c3c9027889ec75 Mon Sep 17 00:00:00 2001 From: I743583 Date: Thu, 10 Apr 2025 15:09:20 +0100 Subject: [PATCH 17/41] add missing dev dependency --- packages/bsp-app-download-sub-generator/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/bsp-app-download-sub-generator/package.json b/packages/bsp-app-download-sub-generator/package.json index 4401637a69..b68c29dd80 100644 --- a/packages/bsp-app-download-sub-generator/package.json +++ b/packages/bsp-app-download-sub-generator/package.json @@ -68,6 +68,7 @@ "@types/fs-extra": "9.0.13", "fs-extra": "10.0.0", "@sap-ux/store": "workspace:*", + "@sap-ux/ui5-config": "workspace:*", "@vscode-logging/logger": "2.0.0", "@types/adm-zip": "0.5.5", "memfs": "3.4.13", From 2e1f240019bc4f29f3c7612a1d57ea74815e9818 Mon Sep 17 00:00:00 2001 From: I743583 Date: Thu, 10 Apr 2025 15:21:27 +0100 Subject: [PATCH 18/41] pnpm revursive install --- .../src/app/index.ts | 18 +++++++++--------- .../tsconfig.json | 3 +++ pnpm-lock.yaml | 3 +++ 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/packages/bsp-app-download-sub-generator/src/app/index.ts b/packages/bsp-app-download-sub-generator/src/app/index.ts index 624772bbd3..6909399441 100644 --- a/packages/bsp-app-download-sub-generator/src/app/index.ts +++ b/packages/bsp-app-download-sub-generator/src/app/index.ts @@ -83,15 +83,13 @@ export default class extends Generator { this.prompts = new Prompts([]); // Initialise prompts and callbacks if not launched as a subgenerator - if (!this.launchAppDownloaderAsSubGenerator) { - this.appWizard.setHeaderTitle(generatorTitle); - this.prompts = new Prompts(getYUIDetails()); - this.setPromptsCallback = (fn): void => { - if (this.prompts) { - this.prompts.setCallback(fn); - } - }; - } + this.appWizard.setHeaderTitle(generatorTitle); + this.prompts = new Prompts(getYUIDetails()); + this.setPromptsCallback = (fn): void => { + if (this.prompts) { + this.prompts.setCallback(fn); + } + }; } /** @@ -140,6 +138,7 @@ export default class extends Generator { quickDeployedAppConfig: QuickDeployedAppConfig, targetFolder: string ): Promise { + console.log("quickDeployedAppConfig---", quickDeployedAppConfig, "--"); const appList = await fetchAppListForSelectedSystem( quickDeployedAppConfig.serviceProvider, quickDeployedAppConfig.appId @@ -150,6 +149,7 @@ export default class extends Generator { ); throw new Error(); } + console.log("appList---", appList, "--"); this.answers.selectedApp = extractAppData(appList[0]).value; this.answers.targetFolder = targetFolder; this.answers.systemSelection = PromptState.systemSelection; diff --git a/packages/bsp-app-download-sub-generator/tsconfig.json b/packages/bsp-app-download-sub-generator/tsconfig.json index e0b353d861..17d4a860b2 100644 --- a/packages/bsp-app-download-sub-generator/tsconfig.json +++ b/packages/bsp-app-download-sub-generator/tsconfig.json @@ -63,6 +63,9 @@ { "path": "../ui5-application-writer" }, + { + "path": "../ui5-config" + }, { "path": "../ui5-info" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4cfaef65c3..88a377af7e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -988,6 +988,9 @@ importers: '@sap-ux/nodejs-utils': specifier: workspace:* version: link:../nodejs-utils + '@sap-ux/ui5-config': + specifier: workspace:* + version: link:../ui5-config '@types/adm-zip': specifier: 0.5.5 version: 0.5.5 From f1647385fdbba4477aba706e48687bee710ac7fb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 10 Apr 2025 14:28:29 +0000 Subject: [PATCH 19/41] Linting auto fix commit --- packages/bsp-app-download-sub-generator/src/app/index.ts | 4 ++-- packages/fiori-tools-settings/src/applicationInfoHandler.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/bsp-app-download-sub-generator/src/app/index.ts b/packages/bsp-app-download-sub-generator/src/app/index.ts index 6909399441..20d730e964 100644 --- a/packages/bsp-app-download-sub-generator/src/app/index.ts +++ b/packages/bsp-app-download-sub-generator/src/app/index.ts @@ -138,7 +138,7 @@ export default class extends Generator { quickDeployedAppConfig: QuickDeployedAppConfig, targetFolder: string ): Promise { - console.log("quickDeployedAppConfig---", quickDeployedAppConfig, "--"); + console.log('quickDeployedAppConfig---', quickDeployedAppConfig, '--'); const appList = await fetchAppListForSelectedSystem( quickDeployedAppConfig.serviceProvider, quickDeployedAppConfig.appId @@ -149,7 +149,7 @@ export default class extends Generator { ); throw new Error(); } - console.log("appList---", appList, "--"); + console.log('appList---', appList, '--'); this.answers.selectedApp = extractAppData(appList[0]).value; this.answers.targetFolder = targetFolder; this.answers.systemSelection = PromptState.systemSelection; diff --git a/packages/fiori-tools-settings/src/applicationInfoHandler.ts b/packages/fiori-tools-settings/src/applicationInfoHandler.ts index 66fb60c7da..fb4399573c 100644 --- a/packages/fiori-tools-settings/src/applicationInfoHandler.ts +++ b/packages/fiori-tools-settings/src/applicationInfoHandler.ts @@ -50,7 +50,7 @@ export function writeApplicationInfoSettings(path: string, fs?: Editor) { appInfoContents.latestGeneratedFiles.push(path); fs.write(appInfoFilePath, JSON.stringify(appInfoContents, null, 2)); fs.commit((err) => { - if(err){ + if (err) { console.log('Error in writting to AppInfo.json file', err); } }); @@ -70,7 +70,7 @@ export function deleteAppInfoSettings(fs?: Editor) { try { fs.delete(appInfoFilePath); fs.commit((err) => { - if(err){ + if (err) { console.log('Failed to commit the deletion of the AppInfo.json file: ', err); } }); From 53e9abbd47c94b9b630b43c70ad293a90edf9566 Mon Sep 17 00:00:00 2001 From: I743583 Date: Thu, 10 Apr 2025 19:30:05 +0100 Subject: [PATCH 20/41] removed todos --- .../src/app/app-config.ts | 13 ++++++----- .../src/app/index.ts | 10 +++----- .../src/app/types.ts | 3 --- .../src/prompts/prompt-state.ts | 18 +++++++++++++++ .../test/app-config.test.ts | 23 +++++++++++++++---- 5 files changed, 46 insertions(+), 21 deletions(-) diff --git a/packages/bsp-app-download-sub-generator/src/app/app-config.ts b/packages/bsp-app-download-sub-generator/src/app/app-config.ts index a32d119cf1..3b9742fdca 100644 --- a/packages/bsp-app-download-sub-generator/src/app/app-config.ts +++ b/packages/bsp-app-download-sub-generator/src/app/app-config.ts @@ -5,7 +5,7 @@ import type { Editor } from 'mem-fs-editor'; import { t } from '../utils/i18n'; import type { AppInfo, QfaJsonConfig } from '../app/types'; import { readManifest } from '../utils/file-helpers'; -import { getLatestUI5Version } from '@sap-ux/ui5-info'; +import { supportedUi5VersionFallbacks, getUI5Versions } from '@sap-ux/ui5-info'; import { getMinimumUI5Version } from '@sap-ux/project-access'; import { adtSourceTemplateId } from '../utils/constants'; import { PromptState } from '../prompts/prompt-state'; @@ -22,7 +22,8 @@ import BspAppDownloadLogger from '../utils/logger'; export const getAbapDeployConfig = (app: AppInfo, qfaJson: QfaJsonConfig): AbapDeployConfig => { return { target: { - url: app.url, + url: PromptState.baseURL, + client: PromptState.sapClient, destination: app.repoName }, app: { @@ -83,6 +84,9 @@ export async function getAppConfig( serviceProvider, manifest?.['sap.app']?.dataSources?.mainService.uri ?? '' ); + const availableUI5Versions = await getUI5Versions({ includeMaintained: true }); + const manifestUi5Version = getMinimumUI5Version(manifest); + const ui5Version = availableUI5Versions.find((version) => version.version === manifestUi5Version); const appConfig: FioriElementsApp = { app: { id: app.appId, @@ -120,10 +124,7 @@ export async function getAppConfig( addTests: true }, ui5: { - version: - qfaJson.project_attribute?.minimum_ui5_version ?? - getMinimumUI5Version(manifest) ?? - (await getLatestUI5Version()) + version: ui5Version ? manifestUi5Version : supportedUi5VersionFallbacks[0].version } }; //todo: confirm this diff --git a/packages/bsp-app-download-sub-generator/src/app/index.ts b/packages/bsp-app-download-sub-generator/src/app/index.ts index 6909399441..78f858c14a 100644 --- a/packages/bsp-app-download-sub-generator/src/app/index.ts +++ b/packages/bsp-app-download-sub-generator/src/app/index.ts @@ -46,7 +46,6 @@ import { isValidPromptState, validateQfaJsonFile } from '../utils/validators'; export default class extends Generator { private readonly appWizard: AppWizard; private readonly vscode?: any; - private readonly launchAppDownloaderAsSubGenerator: boolean; private readonly appRootPath: string; private readonly prompts: Prompts; private answers: BspAppDownloadAnswers = defaultAnswers; @@ -67,7 +66,6 @@ export default class extends Generator { // Initialise properties from options this.appWizard = opts.appWizard ?? AppWizard.create(opts); this.vscode = opts.vscode; - this.launchAppDownloaderAsSubGenerator = opts.launchAppDownloaderAsSubGenerator ?? false; this.appRootPath = opts?.appRootPath ?? getDefaultTargetFolder(this.vscode) ?? this.destinationRoot(); this.options = opts; @@ -81,7 +79,7 @@ export default class extends Generator { this.vscode ); - this.prompts = new Prompts([]); + //this.prompts = new Prompts([]); // Initialise prompts and callbacks if not launched as a subgenerator this.appWizard.setHeaderTitle(generatorTitle); this.prompts = new Prompts(getYUIDetails()); @@ -138,7 +136,6 @@ export default class extends Generator { quickDeployedAppConfig: QuickDeployedAppConfig, targetFolder: string ): Promise { - console.log("quickDeployedAppConfig---", quickDeployedAppConfig, "--"); const appList = await fetchAppListForSelectedSystem( quickDeployedAppConfig.serviceProvider, quickDeployedAppConfig.appId @@ -149,7 +146,6 @@ export default class extends Generator { ); throw new Error(); } - console.log("appList---", appList, "--"); this.answers.selectedApp = extractAppData(appList[0]).value; this.answers.targetFolder = targetFolder; this.answers.systemSelection = PromptState.systemSelection; @@ -218,7 +214,7 @@ export default class extends Generator { appNamespace: '', // todo: cant find namespace in manifest json - default? appDescription: t('readMe.appDescription'), ui5Theme: getDefaultUI5Theme(config.ui5?.version), - generatorName: generatorName, // todo: check if this name is okay ? + generatorName: generatorName, generatorVersion: this.rootGeneratorVersion(), ui5Version: config.ui5?.version ?? '', template: TemplateType.ListReportObjectPage, @@ -238,7 +234,7 @@ export default class extends Generator { const debugOptions: DebugOptions = { vscode: this.vscode, addStartCmd: true, - sapClientParam: '', // todo: check if sap-client info is available + sapClientParam: PromptState.sapClient, flpAppId: config.app.flpAppId ?? config.app.id, flpSandboxAvailable: true, isAppStudio: isAppStudio(), diff --git a/packages/bsp-app-download-sub-generator/src/app/types.ts b/packages/bsp-app-download-sub-generator/src/app/types.ts index c277cb7f8d..ac6bf216f6 100644 --- a/packages/bsp-app-download-sub-generator/src/app/types.ts +++ b/packages/bsp-app-download-sub-generator/src/app/types.ts @@ -32,9 +32,6 @@ export interface BspAppDownloadOptions extends Generator.GeneratorOptions { /** AppWizard instance for managing the application download flow. */ appWizard?: AppWizard; - /** Indicates if the generator is launched as a subgenerator. */ - launchAppDownloaderAsSubGenerator?: boolean; // TODO: Verify this option. - /** Path to the application root where the Fiori launchpad configuration will be added. */ appRootPath?: string; diff --git a/packages/bsp-app-download-sub-generator/src/prompts/prompt-state.ts b/packages/bsp-app-download-sub-generator/src/prompts/prompt-state.ts index 665cef12e0..7a971b271c 100644 --- a/packages/bsp-app-download-sub-generator/src/prompts/prompt-state.ts +++ b/packages/bsp-app-download-sub-generator/src/prompts/prompt-state.ts @@ -27,6 +27,24 @@ export class PromptState { this._systemSelection = value; } + /** + * Get the baseURL from the connected system's service provider defaults. + * + * @returns {string | undefined} baseURL + */ + public static get baseURL(): string | undefined { + return this._systemSelection.connectedSystem?.serviceProvider?.defaults?.baseURL; + } + + /** + * Get the sap-client parameter from the connected system's service provider defaults. + * + * @returns {string | undefined} sap-client + */ + public static get sapClient(): string | undefined { + return this._systemSelection.connectedSystem?.serviceProvider?.defaults?.params?.['sap-client']; + } + static reset(): void { PromptState.systemSelection = {}; } diff --git a/packages/bsp-app-download-sub-generator/test/app-config.test.ts b/packages/bsp-app-download-sub-generator/test/app-config.test.ts index b9e2452356..a6f12935ea 100644 --- a/packages/bsp-app-download-sub-generator/test/app-config.test.ts +++ b/packages/bsp-app-download-sub-generator/test/app-config.test.ts @@ -104,7 +104,10 @@ describe('getAppConfig', () => { }; const mockServiceProvider = { - defaults: { baseURL: 'https://test-url.com' } + defaults: { + baseURL: 'https://test-url.com', + params: { 'sap-client': '100' } + } } as unknown as AbapServiceProvider; PromptState.systemSelection = { @@ -141,7 +144,10 @@ describe('getAppConfig', () => { }; const mockServiceProvider = { - defaults: { baseURL: 'https://test-url.com' } + defaults: { + baseURL: 'https://test-url.com', + params: { 'sap-client': '100' } + } } as unknown as AbapServiceProvider; PromptState.systemSelection = { @@ -208,7 +214,10 @@ describe('getAppConfig', () => { const errorMsg = 'Metadata fetch failed'; const mockServiceProvider = { - defaults: { baseURL: 'https://test-url.com' }, + defaults: { + baseURL: 'https://test-url.com', + params: { 'sap-client': '100' } + }, service: jest.fn().mockReturnValue({ metadata: jest.fn().mockRejectedValue(new Error(errorMsg)) }) @@ -238,7 +247,10 @@ describe('getAppConfig', () => { }; const mockServiceProvider = { - defaults: { baseURL: 'https://test-url.com' }, + defaults: { + baseURL: 'https://test-url.com', + params: { 'sap-client': '100' } + }, service: jest.fn().mockReturnValue({ metadata: jest.fn().mockResolvedValue({ dataServices: { @@ -280,7 +292,8 @@ describe('getAbapDeployConfig', () => { const expectedConfig = { target: { - url: 'https://target-url.com', + url: 'https://test-url.com', + client: '100', destination: 'TEST_REPO' }, app: { From ae66a9fa31423d55c17e58c905c7ab56cf5b80fc Mon Sep 17 00:00:00 2001 From: I743583 Date: Fri, 11 Apr 2025 10:29:21 +0100 Subject: [PATCH 21/41] update todo comments --- .../bsp-app-download-sub-generator/src/app/app-config.ts | 1 + packages/bsp-app-download-sub-generator/src/app/index.ts | 6 ------ .../bsp-app-download-sub-generator/test/app-config.test.ts | 2 +- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/bsp-app-download-sub-generator/src/app/app-config.ts b/packages/bsp-app-download-sub-generator/src/app/app-config.ts index 3b9742fdca..1911727206 100644 --- a/packages/bsp-app-download-sub-generator/src/app/app-config.ts +++ b/packages/bsp-app-download-sub-generator/src/app/app-config.ts @@ -124,6 +124,7 @@ export async function getAppConfig( addTests: true }, ui5: { + //todo: confirm this and also add to the manifest json being replaced version: ui5Version ? manifestUi5Version : supportedUi5VersionFallbacks[0].version } }; diff --git a/packages/bsp-app-download-sub-generator/src/app/index.ts b/packages/bsp-app-download-sub-generator/src/app/index.ts index 78f858c14a..d042a0f754 100644 --- a/packages/bsp-app-download-sub-generator/src/app/index.ts +++ b/packages/bsp-app-download-sub-generator/src/app/index.ts @@ -9,7 +9,6 @@ import { t } from '../utils/i18n'; import { getYUIDetails } from '../prompts/prompt-helpers'; import { downloadApp } from '../utils/download-utils'; import { EventName } from '../telemetryEvents'; -import type { YeomanEnvironment } from '@sap-ux/fiori-generator-shared'; import { getDefaultTargetFolder } from '@sap-ux/fiori-generator-shared'; import type { BspAppDownloadOptions, @@ -79,7 +78,6 @@ export default class extends Generator { this.vscode ); - //this.prompts = new Prompts([]); // Initialise prompts and callbacks if not launched as a subgenerator this.appWizard.setHeaderTitle(generatorTitle); this.prompts = new Prompts(getYUIDetails()); @@ -94,10 +92,6 @@ export default class extends Generator { * Initialises necessary settings and telemetry for the generator. */ public async initializing(): Promise { - if ((this.env as unknown as YeomanEnvironment).conflicter) { - (this.env as unknown as YeomanEnvironment).conflicter.force = this.options.force ?? true; - } - // Initialise telemetry settings await TelemetryHelper.initTelemetrySettings({ consumerModule: { diff --git a/packages/bsp-app-download-sub-generator/test/app-config.test.ts b/packages/bsp-app-download-sub-generator/test/app-config.test.ts index a6f12935ea..31ff360042 100644 --- a/packages/bsp-app-download-sub-generator/test/app-config.test.ts +++ b/packages/bsp-app-download-sub-generator/test/app-config.test.ts @@ -81,7 +81,7 @@ describe('getAppConfig', () => { addTests: true }, ui5: { - version: mockQfaJson.project_attribute.minimum_ui5_version ?? '1.90.0' + version: mockQfaJson.project_attribute.minimum_ui5_version ?? '1.129.0' } }; From 909c28cca0e82e6d0d9dbfc5368556bf7149116a Mon Sep 17 00:00:00 2001 From: I743583 Date: Fri, 11 Apr 2025 10:53:00 +0100 Subject: [PATCH 22/41] moce download app to prompting phase --- .../bsp-app-download-sub-generator/src/app/index.ts | 13 ++++++------- .../src/utils/download-utils.ts | 2 +- .../test/utils/download-utils.test.ts | 8 ++++---- .../test/utils/file-helpers.test.ts | 3 ++- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/bsp-app-download-sub-generator/src/app/index.ts b/packages/bsp-app-download-sub-generator/src/app/index.ts index d042a0f754..2323b802e6 100644 --- a/packages/bsp-app-download-sub-generator/src/app/index.ts +++ b/packages/bsp-app-download-sub-generator/src/app/index.ts @@ -119,6 +119,12 @@ export default class extends Generator { // Handle app download where prompts for system selection and app selection are shown Object.assign(this.answers, answers); } + if (isValidPromptState(this.answers.targetFolder, this.answers.selectedApp.appId)) { + this.projectPath = join(this.answers.targetFolder, this.answers.selectedApp.appId); + this.extractedProjectPath = join(this.projectPath, extractedFilePath); + // Trigger app download + await downloadApp(this.answers.selectedApp.repoName, this.extractedProjectPath, this.fs); + } } /** @@ -149,13 +155,6 @@ export default class extends Generator { * Writes the configuration files for the project, including deployment config, and README. */ public async writing(): Promise { - if (isValidPromptState(this.answers.targetFolder, this.answers.selectedApp.appId)) { - this.projectPath = join(this.answers.targetFolder, this.answers.selectedApp.appId); - this.extractedProjectPath = join(this.projectPath, extractedFilePath); - // Trigger app download - await downloadApp(this.answers.selectedApp.repoName, this.extractedProjectPath, this.fs); - } - const qfaJsonFilePath = join(this.extractedProjectPath, qfaJsonFileName); if (this.fs.exists(qfaJsonFilePath)) { const qfaJson: QfaJsonConfig = makeValidJson(qfaJsonFilePath, this.fs); diff --git a/packages/bsp-app-download-sub-generator/src/utils/download-utils.ts b/packages/bsp-app-download-sub-generator/src/utils/download-utils.ts index de7ea0b9aa..7207fe695e 100644 --- a/packages/bsp-app-download-sub-generator/src/utils/download-utils.ts +++ b/packages/bsp-app-download-sub-generator/src/utils/download-utils.ts @@ -46,6 +46,6 @@ export async function downloadApp(repoName: string, extractedProjectPath: string BspAppDownloadLogger.logger?.error(t('error.appDownloadErrors.downloadedFileNotBufferError')); } } catch (error) { - BspAppDownloadLogger.logger?.error(t('error.appDownloadErrors.appDownloadFailure', { error: error.message })); + throw new Error(t('error.appDownloadErrors.appDownloadFailure', { error: error.message })); } } diff --git a/packages/bsp-app-download-sub-generator/test/utils/download-utils.test.ts b/packages/bsp-app-download-sub-generator/test/utils/download-utils.test.ts index 8fc95a8cc1..14742f46e1 100644 --- a/packages/bsp-app-download-sub-generator/test/utils/download-utils.test.ts +++ b/packages/bsp-app-download-sub-generator/test/utils/download-utils.test.ts @@ -53,11 +53,11 @@ describe('download-utils', () => { expect(BspAppDownloadLogger.logger.error).toBeCalledWith(t('error.appDownloadErrors.downloadedFileNotBufferError')); }); - it('should log an error if the download fails', async () => { + it('should throw an error if the download fails', async () => { const errorMessage = 'Mock download error'; jest.spyOn(mockServiceProvider.getUi5AbapRepository(), 'downloadFiles').mockRejectedValue(new Error(errorMessage)); - await downloadApp('repoName', extractedPath, mockFs) - expect(BspAppDownloadLogger.logger.error).toBeCalledWith(t('error.appDownloadErrors.appDownloadFailure', { error: errorMessage })); + await expect(downloadApp('repoName', extractedPath, mockFs)).rejects.toThrowError( + t('error.appDownloadErrors.appDownloadFailure', { error: errorMessage })); }); it('should extract files from a ZIP archive and write them to the file system', async () => { @@ -76,7 +76,7 @@ describe('download-utils', () => { await downloadApp('repoName', extractedPath, mockFs); expect(mockZip.getEntries).toHaveBeenCalled(); - expect(mockFs.write).toHaveBeenCalledWith(`${extractedPath}/${appName}`, appContents); + expect(mockFs.write).toHaveBeenCalledWith(join(`${extractedPath}/${appName}`), appContents); }); it('should log an error if extraction fails', async () => { diff --git a/packages/bsp-app-download-sub-generator/test/utils/file-helpers.test.ts b/packages/bsp-app-download-sub-generator/test/utils/file-helpers.test.ts index 3fec4a0990..5d697eee48 100644 --- a/packages/bsp-app-download-sub-generator/test/utils/file-helpers.test.ts +++ b/packages/bsp-app-download-sub-generator/test/utils/file-helpers.test.ts @@ -3,6 +3,7 @@ import type { Editor } from 'mem-fs-editor'; import { t } from '../../src/utils/i18n'; import { adtSourceTemplateId } from '../../src/utils/constants'; import BspAppDownloadLogger from '../../src/utils/logger'; +import { join } from 'path'; jest.mock('../../src/utils/logger', () => ({ logger: { @@ -32,7 +33,7 @@ describe('readManifest', () => { mockReadJSON.mockReturnValue(validManifest); const result = readManifest(extractedProjectPath, mockFs); expect(result).toBe(validManifest); - expect(mockFs.readJSON).toHaveBeenCalledWith('project-path/manifest.json'); + expect(mockFs.readJSON).toHaveBeenCalledWith(join('project-path','manifest.json')); }); it('should throw an error if manifest is not found', async () => { From ff40680a191a3494fd389f6d38962e8476dfd2d5 Mon Sep 17 00:00:00 2001 From: I743583 Date: Sun, 13 Apr 2025 12:00:27 +0100 Subject: [PATCH 23/41] chore: Rename generator and test files for repo-app-download-sub-generator --- .vscode/launch.json | 6 +- .../src/utils/file-helpers.ts | 93 ------ .../test/fixtures/downloaded-app/qfa.json | 27 -- .../.eslintignore | 0 .../.eslintrc.js | 0 .../LICENSE | 0 .../README.md | 6 +- .../jest.config.js | 0 .../package.json | 8 +- .../src/app/app-config.ts | 36 +-- .../src/app/index.ts | 75 ++--- .../src/app/types.ts | 49 ++- .../src/prompts/prompt-helpers.ts | 6 +- .../src/prompts/prompt-state.ts | 2 +- .../src/prompts/prompts.ts | 22 +- .../src/telemetryEvents/index.ts | 2 +- ...repo-app-download-sub-generator.i18n.json} | 9 +- .../src/utils/constants.ts | 14 +- .../src/utils/download-utils.ts | 6 +- .../src/utils/event-hook.ts | 18 +- .../src/utils/file-helpers.ts | 130 ++++++++ .../src/utils/i18n.ts | 8 +- .../src/utils/logger.ts | 8 +- .../src/utils/validators.ts | 36 +-- .../test/app-config.test.ts | 101 +++--- .../test/app.test.ts | 45 +-- .../test/fixtures/downloaded-app/component.js | 2 +- .../downloaded-app/i18n/i18n.properties | 0 .../test/fixtures/downloaded-app/index.html | 6 +- .../fixtures/downloaded-app/manifest.json | 6 +- .../test/fixtures/downloaded-app/qfa.json | 27 ++ .../test/fixtures/index.ts | 0 .../test/fixtures/metadata.xml | 0 .../test/prompts/prompt-helpers.test.ts | 12 +- .../test/prompts/prompt-state.test.ts | 0 .../test/prompts/prompts.test.ts | 10 +- .../test/utils/download-utils.test.ts | 6 +- .../test/utils/event-hook.test.ts | 12 +- .../test/utils/file-helpers.test.ts | 5 +- .../test/utils/logger.test.ts | 20 +- .../test/utils/validators.test.ts | 52 ++-- .../tsconfig.eslint.json | 0 .../tsconfig.json | 6 - pnpm-lock.yaml | 289 +++++++++--------- sonar-project.properties | 2 + tsconfig.json | 6 +- 46 files changed, 586 insertions(+), 582 deletions(-) delete mode 100644 packages/bsp-app-download-sub-generator/src/utils/file-helpers.ts delete mode 100644 packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/qfa.json rename packages/{bsp-app-download-sub-generator => repo-app-download-sub-generator}/.eslintignore (100%) rename packages/{bsp-app-download-sub-generator => repo-app-download-sub-generator}/.eslintrc.js (100%) rename packages/{bsp-app-download-sub-generator => repo-app-download-sub-generator}/LICENSE (100%) rename packages/{bsp-app-download-sub-generator => repo-app-download-sub-generator}/README.md (63%) rename packages/{bsp-app-download-sub-generator => repo-app-download-sub-generator}/jest.config.js (100%) rename packages/{bsp-app-download-sub-generator => repo-app-download-sub-generator}/package.json (89%) rename packages/{bsp-app-download-sub-generator => repo-app-download-sub-generator}/src/app/app-config.ts (73%) rename packages/{bsp-app-download-sub-generator => repo-app-download-sub-generator}/src/app/index.ts (88%) rename packages/{bsp-app-download-sub-generator => repo-app-download-sub-generator}/src/app/types.ts (75%) rename packages/{bsp-app-download-sub-generator => repo-app-download-sub-generator}/src/prompts/prompt-helpers.ts (92%) rename packages/{bsp-app-download-sub-generator => repo-app-download-sub-generator}/src/prompts/prompt-state.ts (92%) rename packages/{bsp-app-download-sub-generator => repo-app-download-sub-generator}/src/prompts/prompts.ts (82%) rename packages/{bsp-app-download-sub-generator => repo-app-download-sub-generator}/src/telemetryEvents/index.ts (86%) rename packages/{bsp-app-download-sub-generator/src/translations/bsp-app-download-sub-generator.i18n.json => repo-app-download-sub-generator/src/translations/repo-app-download-sub-generator.i18n.json} (88%) rename packages/{bsp-app-download-sub-generator => repo-app-download-sub-generator}/src/utils/constants.ts (76%) rename packages/{bsp-app-download-sub-generator => repo-app-download-sub-generator}/src/utils/download-utils.ts (87%) rename packages/{bsp-app-download-sub-generator => repo-app-download-sub-generator}/src/utils/event-hook.ts (58%) create mode 100644 packages/repo-app-download-sub-generator/src/utils/file-helpers.ts rename packages/{bsp-app-download-sub-generator => repo-app-download-sub-generator}/src/utils/i18n.ts (68%) rename packages/{bsp-app-download-sub-generator => repo-app-download-sub-generator}/src/utils/logger.ts (87%) rename packages/{bsp-app-download-sub-generator => repo-app-download-sub-generator}/src/utils/validators.ts (66%) rename packages/{bsp-app-download-sub-generator => repo-app-download-sub-generator}/test/app-config.test.ts (73%) rename packages/{bsp-app-download-sub-generator => repo-app-download-sub-generator}/test/app.test.ts (91%) rename packages/{bsp-app-download-sub-generator => repo-app-download-sub-generator}/test/fixtures/downloaded-app/component.js (74%) rename packages/{bsp-app-download-sub-generator => repo-app-download-sub-generator}/test/fixtures/downloaded-app/i18n/i18n.properties (100%) rename packages/{bsp-app-download-sub-generator => repo-app-download-sub-generator}/test/fixtures/downloaded-app/index.html (86%) rename packages/{bsp-app-download-sub-generator => repo-app-download-sub-generator}/test/fixtures/downloaded-app/manifest.json (97%) create mode 100644 packages/repo-app-download-sub-generator/test/fixtures/downloaded-app/qfa.json rename packages/{bsp-app-download-sub-generator => repo-app-download-sub-generator}/test/fixtures/index.ts (100%) rename packages/{bsp-app-download-sub-generator => repo-app-download-sub-generator}/test/fixtures/metadata.xml (100%) rename packages/{bsp-app-download-sub-generator => repo-app-download-sub-generator}/test/prompts/prompt-helpers.test.ts (87%) rename packages/{bsp-app-download-sub-generator => repo-app-download-sub-generator}/test/prompts/prompt-state.test.ts (100%) rename packages/{bsp-app-download-sub-generator => repo-app-download-sub-generator}/test/prompts/prompts.test.ts (94%) rename packages/{bsp-app-download-sub-generator => repo-app-download-sub-generator}/test/utils/download-utils.test.ts (91%) rename packages/{bsp-app-download-sub-generator => repo-app-download-sub-generator}/test/utils/event-hook.test.ts (74%) rename packages/{bsp-app-download-sub-generator => repo-app-download-sub-generator}/test/utils/file-helpers.test.ts (92%) rename packages/{bsp-app-download-sub-generator => repo-app-download-sub-generator}/test/utils/logger.test.ts (64%) rename packages/{bsp-app-download-sub-generator => repo-app-download-sub-generator}/test/utils/validators.test.ts (62%) rename packages/{bsp-app-download-sub-generator => repo-app-download-sub-generator}/tsconfig.eslint.json (100%) rename packages/{bsp-app-download-sub-generator => repo-app-download-sub-generator}/tsconfig.json (90%) diff --git a/.vscode/launch.json b/.vscode/launch.json index dbb54849f6..5fc0f2ef94 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -433,10 +433,10 @@ { "type": "node", "request": "launch", - "name": "bsp-app-download-sub-generator: Launch Yeoman generators/app", - "program": "${workspaceFolder}/packages/bsp-app-download-sub-generator/node_modules/yo/lib/cli.js", + "name": "repo-app-download-sub-generator: Launch Yeoman generators/app", + "program": "${workspaceFolder}/packages/repo-app-download-sub-generator/node_modules/yo/lib/cli.js", "args": [ - "${workspaceFolder}/packages/bsp-app-download-sub-generator/generators/app/index.js" + "${workspaceFolder}/packages/repo-app-download-sub-generator/generators/app/index.js" ], "env": { }, diff --git a/packages/bsp-app-download-sub-generator/src/utils/file-helpers.ts b/packages/bsp-app-download-sub-generator/src/utils/file-helpers.ts deleted file mode 100644 index b59741d377..0000000000 --- a/packages/bsp-app-download-sub-generator/src/utils/file-helpers.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { adtSourceTemplateId } from './constants'; -import { join } from 'path'; -import type { Editor } from 'mem-fs-editor'; -import { FileName, DirName, type Manifest } from '@sap-ux/project-access'; -import { t } from './i18n'; -import BspAppDownloadLogger from './logger'; -import type { QfaJsonConfig } from '../app/types'; - -/** - * Reads and validates the `manifest.json` file. - * - * @param {string} extractedProjectPath - The path to the extracted project. - * @param {Editor} fs - The file system editor. - * @returns {Manifest} The validated manifest object. - */ -export function readManifest(extractedProjectPath: string, fs: Editor): Manifest { - const manifestPath = join(extractedProjectPath, FileName.Manifest); - if (!fs.exists(manifestPath)) { - BspAppDownloadLogger.logger?.error(t('error.readManifestErrors.manifestFileNotFound')); - } - const manifest = fs.readJSON(manifestPath) as unknown as Manifest; - if (!manifest) { - BspAppDownloadLogger.logger?.error(t('error.readManifestErrors.readManifestFailed')); - } - if (!manifest?.['sap.app']) { - BspAppDownloadLogger.logger?.error(t('error.readManifestErrors.sapAppNotDefined')); - } - if (manifest?.['sap.app']?.sourceTemplate?.id !== adtSourceTemplateId) { - BspAppDownloadLogger.logger?.error(t('error.readManifestErrors.sourceTemplateNotSupported')); - } - return manifest; -} - -/** - * Replaces the specified files in the `webapp` directory with the corresponding files from the `extractedPath`. - * - * @param {string} projectPath - The path to the downloaded App. - * @param {string} extractedPath - The path from which files will be copied. - * @param {Editor} fs - The file system editor instance to modify files in memory. - */ -export async function replaceWebappFiles(projectPath: string, extractedPath: string, fs: Editor): Promise { - try { - const webappPath = join(projectPath, DirName.Webapp); - // Define the paths of the files to be replaced - const filesToReplace = [ - { webappFile: FileName.Manifest, extractedFile: FileName.Manifest }, - { webappFile: join('i18n', 'i18n.properties'), extractedFile: join('i18n', 'i18n.properties') }, - { webappFile: 'index.html', extractedFile: 'index.html' }, - { webappFile: 'Component.js', extractedFile: 'component.js' } - ]; - // Loop through each file and perform the replacement - for (const { webappFile, extractedFile } of filesToReplace) { - const webappFilePath = join(webappPath, webappFile); - const extractedFilePath = join(extractedPath, extractedFile); - - // Check if the extracted file exists before replacing - if (fs.exists(extractedFilePath)) { - if (webappFile === FileName.Manifest) { - // Pretify manifest.json file - const manifestContent = fs.read(extractedFilePath); - const prettifiedContent = JSON.stringify(JSON.parse(manifestContent), null, 2); - fs.write(webappFilePath, prettifiedContent); - } else { - fs.copy(extractedFilePath, webappFilePath); - } - } else { - BspAppDownloadLogger.logger?.warn(t('warn.extractedFileNotFound', { extractedFilePath })); - } - } - } catch (error) { - BspAppDownloadLogger.logger?.error(t('error.replaceWebappFilesError', { error })); - } -} - -/** - * - * @param filePath - Path to the JSON file - * @param fs - File system editor instance - * @returns - Parsed JSON object - */ -export function makeValidJson(filePath: string, fs: Editor): QfaJsonConfig { - try { - // Read the file contents - const fileContents = fs.read(filePath); - // Replace property names without quotes with quoted property names - const validJsonString = fileContents.replace(/(\w+):/g, '"$1":'); - // Parse to ensure it's valid JSON - const validJson: QfaJsonConfig = JSON.parse(validJsonString); - return validJson; - } catch (error) { - throw new Error(t('error.errorProcessingJsonFile', { error })); - } -} diff --git a/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/qfa.json b/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/qfa.json deleted file mode 100644 index 2f493595e4..0000000000 --- a/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/qfa.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "metadata": { - "package": "$TMP", - "master_language": "EN" - }, - "service_binding_details": { - "name": "ZSB_TRAVEL_DRAFT_2", - "service_name": "/DMO/UI_TRAVEL_D_D", - "service_version": "0001", - "main_entity_name": "Travel", - "navigation_entity": "_Booking" - }, - "project_attribute": { - "module_name": "travel.approver.2", - "application_title": "Travel Approver 2.0" - }, - "deployment_details": { - "repository_name": "ZSB_TRVL_APR2", - "repository_description": "Travel Approver 2.0" - }, - "fiori_launchpad_configuration": { - "semantic_object": "", - "action": "", - "title": "", - "subtitle": "" - } -} diff --git a/packages/bsp-app-download-sub-generator/.eslintignore b/packages/repo-app-download-sub-generator/.eslintignore similarity index 100% rename from packages/bsp-app-download-sub-generator/.eslintignore rename to packages/repo-app-download-sub-generator/.eslintignore diff --git a/packages/bsp-app-download-sub-generator/.eslintrc.js b/packages/repo-app-download-sub-generator/.eslintrc.js similarity index 100% rename from packages/bsp-app-download-sub-generator/.eslintrc.js rename to packages/repo-app-download-sub-generator/.eslintrc.js diff --git a/packages/bsp-app-download-sub-generator/LICENSE b/packages/repo-app-download-sub-generator/LICENSE similarity index 100% rename from packages/bsp-app-download-sub-generator/LICENSE rename to packages/repo-app-download-sub-generator/LICENSE diff --git a/packages/bsp-app-download-sub-generator/README.md b/packages/repo-app-download-sub-generator/README.md similarity index 63% rename from packages/bsp-app-download-sub-generator/README.md rename to packages/repo-app-download-sub-generator/README.md index 9921252c3b..c264b6c4e5 100644 --- a/packages/bsp-app-download-sub-generator/README.md +++ b/packages/repo-app-download-sub-generator/README.md @@ -1,8 +1,8 @@ -# @sap-ux/bsp-app-download-sub-generator +# @sap-ux/repo-app-download-sub-generator ## Features -The SAP App download sub-generator enables users to download a basic LROP App from BSP repository. The sub-generator will download the app from the BSP repository and add it to the workspace. +The SAP App download sub-generator enables users to download a basic LROP App from an ABAP repository. The sub-generator will download the app from the repository and add it to the workspace. ## Installation @@ -10,7 +10,7 @@ The SAP App download sub-generator sub-generator is installed as part of the [@ ## Launch the SAP Reuse Library sub-generator -Open the Command Palette in MS Visual Studio Code ( CMD/CTRL + Shift + P ) and execute the Fiori: Download Fiori app from BSP repository. +Open the Command Palette in MS Visual Studio Code ( CMD/CTRL + Shift + P ) and execute the Fiori: Download ADT deployed app from UI5 ABAP repository. ## Keywords SAP Fiori Generator diff --git a/packages/bsp-app-download-sub-generator/jest.config.js b/packages/repo-app-download-sub-generator/jest.config.js similarity index 100% rename from packages/bsp-app-download-sub-generator/jest.config.js rename to packages/repo-app-download-sub-generator/jest.config.js diff --git a/packages/bsp-app-download-sub-generator/package.json b/packages/repo-app-download-sub-generator/package.json similarity index 89% rename from packages/bsp-app-download-sub-generator/package.json rename to packages/repo-app-download-sub-generator/package.json index b68c29dd80..b9d3e5190c 100644 --- a/packages/bsp-app-download-sub-generator/package.json +++ b/packages/repo-app-download-sub-generator/package.json @@ -1,11 +1,11 @@ { - "name": "@sap-ux/bsp-app-download-sub-generator", - "description": "Generator to support downloading ABAP Deployed Fiori App from BSP Repository", + "name": "@sap-ux/repo-app-download-sub-generator", + "description": "Generator to download LROP Fiori applications deployed from an ABAP repository.", "version": "0.1.40", "repository": { "type": "git", "url": "https://github.com/SAP/open-ux-tools.git", - "directory": "packages/bsp-app-download-sub-generator" + "directory": "packages/repo-app-download-sub-generator" }, "bugs": { "url": "https://github.com/SAP/open-ux-tools/issues?q=is%3Aopen+is%3Aissue" @@ -37,11 +37,9 @@ "@sap-ux/inquirer-common": "workspace:*", "@sap-ux/project-access": "workspace:*", "@sap-ux/odata-service-inquirer": "workspace:*", - "@sap-ux/ui5-application-writer": "workspace:*", "@sap-ux/fiori-elements-writer": "workspace:*", "@sap-ux/logger": "workspace:*", "@sap-ux/project-input-validator": "workspace:*", - "@sap-ux/ui5-application-inquirer": "workspace:*", "@sap-ux/launch-config": "workspace:*", "@sap-ux/fiori-tools-settings": "workspace:*", "@sap-ux/abap-deploy-config-writer": "workspace:*", diff --git a/packages/bsp-app-download-sub-generator/src/app/app-config.ts b/packages/repo-app-download-sub-generator/src/app/app-config.ts similarity index 73% rename from packages/bsp-app-download-sub-generator/src/app/app-config.ts rename to packages/repo-app-download-sub-generator/src/app/app-config.ts index 1911727206..56ceee6e28 100644 --- a/packages/bsp-app-download-sub-generator/src/app/app-config.ts +++ b/packages/repo-app-download-sub-generator/src/app/app-config.ts @@ -5,12 +5,12 @@ import type { Editor } from 'mem-fs-editor'; import { t } from '../utils/i18n'; import type { AppInfo, QfaJsonConfig } from '../app/types'; import { readManifest } from '../utils/file-helpers'; -import { supportedUi5VersionFallbacks, getUI5Versions } from '@sap-ux/ui5-info'; -import { getMinimumUI5Version } from '@sap-ux/project-access'; import { adtSourceTemplateId } from '../utils/constants'; import { PromptState } from '../prompts/prompt-state'; import type { AbapDeployConfig } from '@sap-ux/ui5-config'; -import BspAppDownloadLogger from '../utils/logger'; +import RepoAppDownloadLogger from '../utils/logger'; +import { FileName } from '@sap-ux/project-access'; +import { join } from 'path'; /** * Generates the deployment configuration for an ABAP application. @@ -27,10 +27,10 @@ export const getAbapDeployConfig = (app: AppInfo, qfaJson: QfaJsonConfig): AbapD destination: app.repoName }, app: { - name: qfaJson.deployment_details.repository_name, + name: qfaJson.deploymentDetails.repositoryName, package: qfaJson.metadata.package, - description: qfaJson.deployment_details.repository_description, - transport: qfaJson.deployment_details.transport_request ?? 'REPLACE_WITH_TRANSPORT' + description: qfaJson.deploymentDetails.repositoryDescription, + transport: 'REPLACE_WITH_TRANSPORT' } }; }; @@ -46,7 +46,7 @@ const fetchServiceMetadata = async (provider: AbapServiceProvider, serviceUrl: s try { return await provider.service(serviceUrl).metadata(); } catch (err) { - BspAppDownloadLogger.logger?.error(t('error.metadataFetchError', { error: err.message })); + RepoAppDownloadLogger.logger?.error(t('error.metadataFetchError', { error: err.message })); } }; @@ -68,10 +68,10 @@ export async function getAppConfig( fs: Editor ): Promise> { try { - const manifest = readManifest(extractedProjectPath, fs); + const manifest = readManifest(join(extractedProjectPath, FileName.Manifest), fs); const serviceProvider = PromptState.systemSelection?.connectedSystem?.serviceProvider as AbapServiceProvider; if (!manifest?.['sap.app']?.dataSources) { - BspAppDownloadLogger.logger?.error(t('error.dataSourcesNotFound')); + RepoAppDownloadLogger.logger?.error(t('error.dataSourcesNotFound')); } const odataVersion = @@ -84,9 +84,6 @@ export async function getAppConfig( serviceProvider, manifest?.['sap.app']?.dataSources?.mainService.uri ?? '' ); - const availableUI5Versions = await getUI5Versions({ includeMaintained: true }); - const manifestUi5Version = getMinimumUI5Version(manifest); - const ui5Version = availableUI5Versions.find((version) => version.version === manifestUi5Version); const appConfig: FioriElementsApp = { app: { id: app.appId, @@ -109,7 +106,7 @@ export async function getAppConfig( type: TemplateType.ListReportObjectPage, settings: { entityConfig: { - mainEntityName: qfaJson.service_binding_details.main_entity_name + mainEntityName: qfaJson.serviceBindingDetails.mainEntityName } } }, @@ -122,22 +119,11 @@ export async function getAppConfig( appOptions: { addAnnotations: odataVersion === OdataVersion.v4, addTests: true - }, - ui5: { - //todo: confirm this and also add to the manifest json being replaced - version: ui5Version ? manifestUi5Version : supportedUi5VersionFallbacks[0].version } }; - //todo: confirm this - if (qfaJson.service_binding_details.navigation_entity) { - appConfig.template.settings.entityConfig.navigationEntity = { - EntitySet: qfaJson.service_binding_details.navigation_entity, - Name: qfaJson.service_binding_details.navigation_entity - }; - } return appConfig; } catch (error) { - BspAppDownloadLogger.logger?.error(t('error.appConfigGenError', { error: error.message })); + RepoAppDownloadLogger.logger?.error(t('error.appConfigGenError', { error: error.message })); throw error; } } diff --git a/packages/bsp-app-download-sub-generator/src/app/index.ts b/packages/repo-app-download-sub-generator/src/app/index.ts similarity index 88% rename from packages/bsp-app-download-sub-generator/src/app/index.ts rename to packages/repo-app-download-sub-generator/src/app/index.ts index 2323b802e6..8d6145f0b2 100644 --- a/packages/bsp-app-download-sub-generator/src/app/index.ts +++ b/packages/repo-app-download-sub-generator/src/app/index.ts @@ -1,5 +1,5 @@ import Generator from 'yeoman-generator'; -import BspAppDownloadLogger from '../utils/logger'; +import RepoAppDownloadLogger from '../utils/logger'; import { AppWizard, Prompts, MessageType } from '@sap-devx/yeoman-ui-types'; import { isInternalFeaturesSettingEnabled } from '@sap-ux/feature-toggle'; import type { Logger } from '@sap-ux/logger'; @@ -11,9 +11,9 @@ import { downloadApp } from '../utils/download-utils'; import { EventName } from '../telemetryEvents'; import { getDefaultTargetFolder } from '@sap-ux/fiori-generator-shared'; import type { - BspAppDownloadOptions, - BspAppDownloadAnswers, - BspAppDownloadQuestions, + RepoAppDownloadOptions, + RepoAppDownloadAnswers, + RepoAppDownloadQuestions, QfaJsonConfig, QuickDeployedAppConfig } from './types'; @@ -34,12 +34,13 @@ import { PromptState } from '../prompts/prompt-state'; import { PromptNames } from './types'; import { getAbapDeployConfig, getAppConfig } from './app-config'; import type { AbapDeployConfig } from '@sap-ux/ui5-config'; -import { replaceWebappFiles, makeValidJson } from '../utils/file-helpers'; +import { replaceWebappFiles, makeValidJson, validateAndUpdateManifestUI5Version } from '../utils/file-helpers'; import { fetchAppListForSelectedSystem, extractAppData } from '../prompts/prompt-helpers'; import { isValidPromptState, validateQfaJsonFile } from '../utils/validators'; +import { FileName, DirName } from '@sap-ux/project-access'; /** - * Generator class for downloading a basic app from BSP repository. + * Generator class for downloading a basic app from a repository. * This class handles the process of app selection, downloading the app and generating a fiori app from the downloaded app */ export default class extends Generator { @@ -47,8 +48,8 @@ export default class extends Generator { private readonly vscode?: any; private readonly appRootPath: string; private readonly prompts: Prompts; - private answers: BspAppDownloadAnswers = defaultAnswers; - public options: BspAppDownloadOptions; + private answers: RepoAppDownloadAnswers = defaultAnswers; + public options: RepoAppDownloadOptions; private projectPath: string; private extractedProjectPath: string; setPromptsCallback: (fn: object) => void; @@ -59,7 +60,7 @@ export default class extends Generator { * @param args - arguments passed to the generator * @param opts - options passed to the generator */ - constructor(args: string | string[], opts: BspAppDownloadOptions) { + constructor(args: string | string[], opts: RepoAppDownloadOptions) { super(args, opts); // Initialise properties from options @@ -69,7 +70,7 @@ export default class extends Generator { this.options = opts; // Configure logging - BspAppDownloadLogger.configureLogging( + RepoAppDownloadLogger.configureLogging( this.rootGeneratorName(), this.log, this.options.logWrapper, @@ -108,8 +109,8 @@ export default class extends Generator { */ public async prompting(): Promise { const quickDeployedAppConfig = this.options?.data?.quickDeployedAppConfig; - const questions: BspAppDownloadQuestions[] = await getPrompts(this.appRootPath, quickDeployedAppConfig); - const answers: BspAppDownloadAnswers = await this.prompt(questions); + const questions: RepoAppDownloadQuestions[] = await getPrompts(this.appRootPath, quickDeployedAppConfig); + const answers: RepoAppDownloadAnswers = await this.prompt(questions); const { targetFolder } = answers; if (quickDeployedAppConfig?.appId) { // Handle quick deployed app download where prompts for system selection and app selection are not displayed @@ -119,12 +120,6 @@ export default class extends Generator { // Handle app download where prompts for system selection and app selection are shown Object.assign(this.answers, answers); } - if (isValidPromptState(this.answers.targetFolder, this.answers.selectedApp.appId)) { - this.projectPath = join(this.answers.targetFolder, this.answers.selectedApp.appId); - this.extractedProjectPath = join(this.projectPath, extractedFilePath); - // Trigger app download - await downloadApp(this.answers.selectedApp.repoName, this.extractedProjectPath, this.fs); - } } /** @@ -141,10 +136,9 @@ export default class extends Generator { quickDeployedAppConfig.appId ); if (!appList.length) { - BspAppDownloadLogger.logger?.error( + throw new Error( t('error.quickDeployedAppDownloadErrors.noAppsFound', { appId: quickDeployedAppConfig.appId }) ); - throw new Error(); } this.answers.selectedApp = extractAppData(appList[0]).value; this.answers.targetFolder = targetFolder; @@ -155,6 +149,12 @@ export default class extends Generator { * Writes the configuration files for the project, including deployment config, and README. */ public async writing(): Promise { + if (isValidPromptState(this.answers.targetFolder, this.answers.selectedApp.appId)) { + this.projectPath = join(this.answers.targetFolder, this.answers.selectedApp.appId); + this.extractedProjectPath = join(this.projectPath, extractedFilePath); + // Trigger app download + await downloadApp(this.answers.selectedApp.repoName, this.extractedProjectPath, this.fs); + } const qfaJsonFilePath = join(this.extractedProjectPath, qfaJsonFileName); if (this.fs.exists(qfaJsonFilePath)) { const qfaJson: QfaJsonConfig = makeValidJson(qfaJsonFilePath, this.fs); @@ -169,10 +169,6 @@ export default class extends Generator { const deployConfig: AbapDeployConfig = getAbapDeployConfig(this.answers.selectedApp, qfaJson); await generateDeployConfig(this.projectPath, deployConfig, undefined, this.fs); - // Generate README - const readMeConfig = this._getReadMeConfig(config); - generateReadMe(this.projectPath, readMeConfig, this.fs); - if (this.vscode) { // Generate Fiori launch config const fioriOptions = this._getLaunchConfig(config); @@ -181,16 +177,27 @@ export default class extends Generator { this.projectPath, fioriOptions, this.fs, - BspAppDownloadLogger.logger as unknown as Logger + RepoAppDownloadLogger.logger as unknown as Logger ); - writeApplicationInfoSettings(this.projectPath, this.fs); + writeApplicationInfoSettings(this.projectPath); } + + // Generate README + const readMeConfig = this._getReadMeConfig(config); + generateReadMe(this.projectPath, readMeConfig, this.fs); + // Replace webapp files with downloaded app files await replaceWebappFiles(this.projectPath, this.extractedProjectPath, this.fs); + + await validateAndUpdateManifestUI5Version( + join(this.projectPath, DirName.Webapp, FileName.Manifest), + this.fs + ); + // Clean up extracted project files this.fs.delete(this.extractedProjectPath); } else { - BspAppDownloadLogger.logger?.error(t('error.qfaJsonNotFound', { jsonFileName: qfaJsonFileName })); + RepoAppDownloadLogger.logger?.error(t('error.qfaJsonNotFound', { jsonFileName: qfaJsonFileName })); } } @@ -255,10 +262,10 @@ export default class extends Generator { try { await this._runNpmInstall(this.projectPath); } catch (error) { - BspAppDownloadLogger.logger?.error(t('error.installationErrors.npmInstall', { error })); + RepoAppDownloadLogger.logger?.error(t('error.installationErrors.npmInstall', { error })); } } else { - BspAppDownloadLogger.logger?.info(t('info.installationErrors.skippedInstallation')); + RepoAppDownloadLogger.logger?.info(t('info.installationErrors.skippedInstallation')); } } @@ -323,22 +330,22 @@ export default class extends Generator { */ async end(): Promise { try { - this.appWizard.showInformation(t('info.bspAppDownloadCompleteMsg'), MessageType.notification); + this.appWizard.showInformation(t('info.repoAppDownloadCompleteMsg'), MessageType.notification); await sendTelemetry( EventName.GENERATION_SUCCESS, TelemetryHelper.createTelemetryData({ - appType: 'bsp-app-download-sub-generator', + appType: 'repo-app-download-sub-generator', ...this.options.telemetryData }) ?? {} ).catch((error) => { - BspAppDownloadLogger.logger?.error(t('error.telemetry', { error: error.message })); + RepoAppDownloadLogger.logger?.error(t('error.telemetry', { error: error.message })); }); await this._handlePostAppGeneration(); } catch (error) { - BspAppDownloadLogger.logger?.error(t('error.endPhase', { error: error.message })); + RepoAppDownloadLogger.logger?.error(t('error.endPhase', { error: error.message })); } } } export { PromptNames }; -export type { BspAppDownloadOptions }; +export type { RepoAppDownloadOptions }; diff --git a/packages/bsp-app-download-sub-generator/src/app/types.ts b/packages/repo-app-download-sub-generator/src/app/types.ts similarity index 75% rename from packages/bsp-app-download-sub-generator/src/app/types.ts rename to packages/repo-app-download-sub-generator/src/app/types.ts index ac6bf216f6..f7eef94e5f 100644 --- a/packages/bsp-app-download-sub-generator/src/app/types.ts +++ b/packages/repo-app-download-sub-generator/src/app/types.ts @@ -9,7 +9,7 @@ import type { AutocompleteQuestionOptions } from 'inquirer-autocomplete-prompt'; /** * Quick deploy app config are applicable only in scenarios where an application - * deployed via ADT Quick Deploy is being downloaded from a BSP repository. + * deployed via ADT Quick Deploy is being downloaded from a repository. */ export interface QuickDeployedAppConfig { /** application Id to be downloaded. */ @@ -20,9 +20,9 @@ export interface QuickDeployedAppConfig { serviceProvider: AbapServiceProvider; } /** - * Options for downloading a BSP application. + * Options for downloading an application from repository. */ -export interface BspAppDownloadOptions extends Generator.GeneratorOptions { +export interface RepoAppDownloadOptions extends Generator.GeneratorOptions { /** VSCode instance for interacting with the VSCode environment. */ vscode?: VSCodeInstance; @@ -43,7 +43,7 @@ export interface BspAppDownloadOptions extends Generator.GeneratorOptions { } /** - * Answers related to system selection in the BSP application download process. + * Answers related to system selection in the application download process. */ export interface SystemSelectionAnswers { /** @@ -68,7 +68,7 @@ export interface SystemSelectionAnswers { * Represents a question in the app download process. * Extends `YUIQuestion` with optional autocomplete functionality. */ -export type BspAppDownloadQuestions = YUIQuestion & +export type RepoAppDownloadQuestions = YUIQuestion & Partial>; // Extract the type of a single element in the AppIndex array @@ -83,7 +83,7 @@ export interface AppInfo { } /** - * Enum representing the names of prompts used in the BSP application download process. + * Enum representing the names of prompts used in the application download process. */ export enum PromptNames { selectedApp = 'selectedApp', @@ -92,14 +92,14 @@ export enum PromptNames { } /** - * Structure of answers provided by the user for BSP application download prompts. + * Structure of answers provided by the user for application download prompts. */ -export interface BspAppDownloadAnswers { +export interface RepoAppDownloadAnswers { /** Selected backend system connection details. */ [PromptNames.systemSelection]: SystemSelectionAnswers; /** Information about the selected application for download. */ [PromptNames.selectedApp]: AppInfo; - /** Target folder where the BSP application will be generated. */ + /** Target folder where the application will be generated. */ [PromptNames.targetFolder]: string; } @@ -111,28 +111,27 @@ export interface BspAppDownloadAnswers { export interface QfaJsonConfig { metadata: { package: string; - master_language?: string; + masterLanguage?: string; }; - service_binding_details: { + serviceBindingDetails: { name?: string; - service_name: string; - service_version: string; - main_entity_name: string; - navigation_entity?: string; + serviceName: string; + serviceVersion: string; + mainEntityName: string; + navigationEntity?: string; }; - project_attribute: { - module_name: string; - application_title?: string; - minimum_ui5_version?: string; + projectAttribute: { + moduleName: string; + applicationTitle?: string; + minimumUi5Version?: string; template?: string; }; - deployment_details: { - repository_name: string; - repository_description?: string; - transport_request?: string; + deploymentDetails: { + repositoryName: string; + repositoryDescription?: string; }; - fiori_launchpad_configuration: { - semantic_object: string; + fioriLaunchpadConfiguration: { + semanticObject: string; action: string; title: string; subtitle?: string; diff --git a/packages/bsp-app-download-sub-generator/src/prompts/prompt-helpers.ts b/packages/repo-app-download-sub-generator/src/prompts/prompt-helpers.ts similarity index 92% rename from packages/bsp-app-download-sub-generator/src/prompts/prompt-helpers.ts rename to packages/repo-app-download-sub-generator/src/prompts/prompt-helpers.ts index 577e3553b0..74fdf5c6e9 100644 --- a/packages/bsp-app-download-sub-generator/src/prompts/prompt-helpers.ts +++ b/packages/repo-app-download-sub-generator/src/prompts/prompt-helpers.ts @@ -5,7 +5,7 @@ import type { AppInfo } from '../app/types'; import { PromptState } from './prompt-state'; import type { AppItem } from '../app/types'; import { t } from '../utils/i18n'; -import BspAppDownloadLogger from '../utils/logger'; +import RepoAppDownloadLogger from '../utils/logger'; /** * Returns the details for the YUI prompt. @@ -59,7 +59,7 @@ export const formatAppChoices = (appList: AppIndex): Array<{ name: string; value .filter((app: AppItem) => { const hasRequiredFields = app['sap.app/id'] && app['sap.app/title'] && app['repoName'] && app['url']; if (!hasRequiredFields) { - BspAppDownloadLogger.logger?.error(t('error.requiredFieldsMissing', { app: JSON.stringify(app) })); + RepoAppDownloadLogger.logger?.error(t('error.requiredFieldsMissing', { app: JSON.stringify(app) })); } return hasRequiredFields; }) @@ -83,7 +83,7 @@ async function getAppList(provider: AbapServiceProvider, appId?: string): Promis : appListSearchParams; return await provider.getAppIndex().search(searchParams, appListResultFields); } catch (error) { - BspAppDownloadLogger.logger?.error(t('error.applicationListFetchError', { error: error.message })); + RepoAppDownloadLogger.logger?.error(t('error.applicationListFetchError', { error: error.message })); return []; } } diff --git a/packages/bsp-app-download-sub-generator/src/prompts/prompt-state.ts b/packages/repo-app-download-sub-generator/src/prompts/prompt-state.ts similarity index 92% rename from packages/bsp-app-download-sub-generator/src/prompts/prompt-state.ts rename to packages/repo-app-download-sub-generator/src/prompts/prompt-state.ts index 7a971b271c..0df5282bb4 100644 --- a/packages/bsp-app-download-sub-generator/src/prompts/prompt-state.ts +++ b/packages/repo-app-download-sub-generator/src/prompts/prompt-state.ts @@ -1,7 +1,7 @@ import type { SystemSelectionAnswers } from '../app/types'; /** - * Much of the values returned by the bsp app downloader prompting are derived from prompt answers and are not direct answer values. + * Much of the values returned by the app downloader prompting are derived from prompt answers and are not direct answer values. * Since inquirer does not provide a way to return values that are not direct answers from prompts, this class will maintain the derived values * across prompts statically for the lifespan of the prompting session. * diff --git a/packages/bsp-app-download-sub-generator/src/prompts/prompts.ts b/packages/repo-app-download-sub-generator/src/prompts/prompts.ts similarity index 82% rename from packages/bsp-app-download-sub-generator/src/prompts/prompts.ts rename to packages/repo-app-download-sub-generator/src/prompts/prompts.ts index 06d1e2b67f..de548e5a99 100644 --- a/packages/bsp-app-download-sub-generator/src/prompts/prompts.ts +++ b/packages/repo-app-download-sub-generator/src/prompts/prompts.ts @@ -1,6 +1,6 @@ import type { AppIndex, AbapServiceProvider } from '@sap-ux/axios-extension'; import { getSystemSelectionQuestions } from '@sap-ux/odata-service-inquirer'; -import type { BspAppDownloadAnswers, BspAppDownloadQuestions, QuickDeployedAppConfig, AppInfo } from '../app/types'; +import type { RepoAppDownloadAnswers, RepoAppDownloadQuestions, QuickDeployedAppConfig, AppInfo } from '../app/types'; import { PromptNames } from '../app/types'; import { t } from '../utils/i18n'; import type { FileBrowserQuestion } from '@sap-ux/inquirer-common'; @@ -14,15 +14,15 @@ import { fetchAppListForSelectedSystem } from './prompt-helpers'; * * @param {string} [appRootPath] - The application root path. * @param {string} appId - The application ID. - * @returns {FileBrowserQuestion} The target folder prompt configuration. + * @returns {FileBrowserQuestion} The target folder prompt configuration. */ -const getTargetFolderPrompt = (appRootPath?: string, appId?: string): FileBrowserQuestion => { +const getTargetFolderPrompt = (appRootPath?: string, appId?: string): FileBrowserQuestion => { return { type: 'input', name: PromptNames.targetFolder, message: t('prompts.targetPath.message'), guiType: 'folder-browser', - when: (answers: BspAppDownloadAnswers) => { + when: (answers: RepoAppDownloadAnswers) => { // Display the prompt if appId is provided. This occurs when the generator is invoked // as part of the quick deployment process from ADT. if (appId) { @@ -37,12 +37,12 @@ const getTargetFolderPrompt = (appRootPath?: string, appId?: string): FileBrowse mandatory: true, breadcrumb: t('prompts.targetPath.breadcrumb') }, - validate: async (target, answers: BspAppDownloadAnswers): Promise => { + validate: async (target, answers: RepoAppDownloadAnswers): Promise => { const selectedAppId = answers.selectedApp?.appId ?? appId; return await validateFioriAppTargetFolder(target, selectedAppId, true); }, default: () => appRootPath - } as FileBrowserQuestion; + } as FileBrowserQuestion; }; /** @@ -50,23 +50,23 @@ const getTargetFolderPrompt = (appRootPath?: string, appId?: string): FileBrowse * * @param {string} [appRootPath] - The root path of the application. * @param {QuickDeployedAppConfig} [quickDeployedAppConfig] - quick deployed app config. - * @returns {Promise} A list of questions for user interaction. + * @returns {Promise} A list of questions for user interaction. */ export async function getPrompts( appRootPath?: string, quickDeployedAppConfig?: QuickDeployedAppConfig -): Promise { +): Promise { PromptState.reset(); // If quickDeployedAppConfig is provided, return only the target folder prompt if (quickDeployedAppConfig?.appId) { - return [getTargetFolderPrompt(appRootPath, quickDeployedAppConfig.appId)] as BspAppDownloadQuestions[]; + return [getTargetFolderPrompt(appRootPath, quickDeployedAppConfig.appId)] as RepoAppDownloadQuestions[]; } const systemQuestions = await getSystemSelectionQuestions({ serviceSelection: { hide: true } }, false); let appList: AppIndex = []; const appSelectionPrompt = [ { - when: async (answers: BspAppDownloadAnswers): Promise => { + when: async (answers: RepoAppDownloadAnswers): Promise => { if (answers[PromptNames.systemSelection]) { appList = await fetchAppListForSelectedSystem( systemQuestions.answers.connectedSystem?.serviceProvider as AbapServiceProvider @@ -88,5 +88,5 @@ export async function getPrompts( ]; const targetFolderPrompts = getTargetFolderPrompt(appRootPath, quickDeployedAppConfig?.appId); - return [...systemQuestions.prompts, ...appSelectionPrompt, targetFolderPrompts] as BspAppDownloadQuestions[]; + return [...systemQuestions.prompts, ...appSelectionPrompt, targetFolderPrompts] as RepoAppDownloadQuestions[]; } diff --git a/packages/bsp-app-download-sub-generator/src/telemetryEvents/index.ts b/packages/repo-app-download-sub-generator/src/telemetryEvents/index.ts similarity index 86% rename from packages/bsp-app-download-sub-generator/src/telemetryEvents/index.ts rename to packages/repo-app-download-sub-generator/src/telemetryEvents/index.ts index 61fa53f5ab..76148247df 100644 --- a/packages/bsp-app-download-sub-generator/src/telemetryEvents/index.ts +++ b/packages/repo-app-download-sub-generator/src/telemetryEvents/index.ts @@ -1,5 +1,5 @@ /** - * Event names for telemetry for the generator when downloading an app from BSP + * Event names for telemetry for the generator when downloading an app from repository */ export enum EventName { GENERATION_SUCCESS = 'GENERATION_SUCCESS' diff --git a/packages/bsp-app-download-sub-generator/src/translations/bsp-app-download-sub-generator.i18n.json b/packages/repo-app-download-sub-generator/src/translations/repo-app-download-sub-generator.i18n.json similarity index 88% rename from packages/bsp-app-download-sub-generator/src/translations/bsp-app-download-sub-generator.i18n.json rename to packages/repo-app-download-sub-generator/src/translations/repo-app-download-sub-generator.i18n.json index 33a81473a4..59207aa75e 100644 --- a/packages/bsp-app-download-sub-generator/src/translations/bsp-app-download-sub-generator.i18n.json +++ b/packages/repo-app-download-sub-generator/src/translations/repo-app-download-sub-generator.i18n.json @@ -1,6 +1,6 @@ { "error": { - "telemetry": "Failed to send telemetry data after downloading app from BSP. {{- error}}", + "telemetry": "Failed to send telemetry data after downloading app from UI5 ABAP Repository. {{- error}}", "qfaJsonNotFound": "{{- jsonFileName }} not found in the downloaded app", "replaceWebappFilesError": "Error replacing files in the downloaded app: {{- error}}", "requiredFieldsMissing": "Required fields are missing for app: {{- app }}. Check if the app is deployed correctly", @@ -35,7 +35,8 @@ "manifestFileNotFound": "Error: Manifest file not found in the downloaded app", "readManifestFailed": "Error: Failed to read manifest file", "sapAppNotDefined": "Error: sap.app not defined in the manifest file", - "sourceTemplateNotSupported": "Error: Source template not supported" + "sourceTemplateNotSupported": "Error: Source template not supported", + "invalidManifestStructureError": "Error: Invalid manifest structure: 'sap.ui5' or dependencies are missing." }, "quickDeployedAppDownloadErrors": { "noAppsFound": "No application with id {{ appId }} found in the system. Please check if the application is deployed correctly" @@ -49,7 +50,7 @@ "message": "App", "hint": "Select the app to download", "breadcrumb": "App", - "noAppsDeployed": "No basic applications deployed to this system can be downloaded. Please see for more details" + "noAppsDeployed": "No applications deployed to this system can be downloaded. Please see for more details" }, "targetPath": { "message": "Project folder path", @@ -61,6 +62,6 @@ "launchText": "In order to launch the generated app, simply run the following from the generated app root folder:\n\n```\n npm start\n```" }, "info": { - "bspAppDownloadCompleteMsg": "The selected application has been downloaded and updated to support SAP Fiori tools" + "repoAppDownloadCompleteMsg": "The selected application has been downloaded and updated to support SAP Fiori tools." } } diff --git a/packages/bsp-app-download-sub-generator/src/utils/constants.ts b/packages/repo-app-download-sub-generator/src/utils/constants.ts similarity index 76% rename from packages/bsp-app-download-sub-generator/src/utils/constants.ts rename to packages/repo-app-download-sub-generator/src/utils/constants.ts index 5008836d24..346df7c42c 100644 --- a/packages/bsp-app-download-sub-generator/src/utils/constants.ts +++ b/packages/repo-app-download-sub-generator/src/utils/constants.ts @@ -1,18 +1,18 @@ -import { PromptNames, type BspAppDownloadAnswers } from '../app/types'; +import { PromptNames, type RepoAppDownloadAnswers } from '../app/types'; // Title and description for the generator -export const generatorTitle = 'Basic App Download from BSP'; -export const generatorDescription = 'Download a basic LROP app from a BSP repository'; +export const generatorTitle = 'UI5 ABAP Repository'; +export const generatorDescription = 'Download a basic LROP app from a UI5 ABAP Repository'; // Name of the generator used for Fiori app download -export const generatorName = '@sap-ux/bsp-app-download-sub-generator'; -// The source template ID used for filtering the apps in the BSP repository +export const generatorName = '@sap-ux/repo-app-download-sub-generator'; +// The source template ID used for filtering the apps in the repository export const adtSourceTemplateId = '@sap.adt.sevicebinding.deploy:lrop'; // The name of the QFA JSON file provided with the downloaded app, containing all user inputs. export const qfaJsonFileName = 'qfa.json'; // Default initial answers to use as a fallback. -export const defaultAnswers: BspAppDownloadAnswers = { +export const defaultAnswers: RepoAppDownloadAnswers = { [PromptNames.systemSelection]: {}, [PromptNames.selectedApp]: { appId: '', @@ -24,7 +24,7 @@ export const defaultAnswers: BspAppDownloadAnswers = { [PromptNames.targetFolder]: '' }; -// Path for storing the extracted files from BSP +// Path for storing the extracted files from repository export const extractedFilePath = 'extractedFiles'; // Search parameters to filter applications by the source template ID diff --git a/packages/bsp-app-download-sub-generator/src/utils/download-utils.ts b/packages/repo-app-download-sub-generator/src/utils/download-utils.ts similarity index 87% rename from packages/bsp-app-download-sub-generator/src/utils/download-utils.ts rename to packages/repo-app-download-sub-generator/src/utils/download-utils.ts index 7207fe695e..91e32a3f70 100644 --- a/packages/bsp-app-download-sub-generator/src/utils/download-utils.ts +++ b/packages/repo-app-download-sub-generator/src/utils/download-utils.ts @@ -4,7 +4,7 @@ import { join } from 'path'; import type { Editor } from 'mem-fs-editor'; import { PromptState } from '../prompts/prompt-state'; import { t } from './i18n'; -import BspAppDownloadLogger from '../utils/logger'; +import RepoAppDownloadLogger from '../utils/logger'; /** * Extracts a ZIP archive to a temporary directory. @@ -25,7 +25,7 @@ async function extractZip(extractedProjectPath: string, archive: Buffer, fs: Edi } }); } catch (error) { - BspAppDownloadLogger.logger?.error(t('error.appDownloadErrors.zipExtractionError', { error: error.message })); + RepoAppDownloadLogger.logger?.error(t('error.appDownloadErrors.zipExtractionError', { error: error.message })); } } @@ -43,7 +43,7 @@ export async function downloadApp(repoName: string, extractedProjectPath: string if (Buffer.isBuffer(archive)) { await extractZip(extractedProjectPath, archive, fs); } else { - BspAppDownloadLogger.logger?.error(t('error.appDownloadErrors.downloadedFileNotBufferError')); + RepoAppDownloadLogger.logger?.error(t('error.appDownloadErrors.downloadedFileNotBufferError')); } } catch (error) { throw new Error(t('error.appDownloadErrors.appDownloadFailure', { error: error.message })); diff --git a/packages/bsp-app-download-sub-generator/src/utils/event-hook.ts b/packages/repo-app-download-sub-generator/src/utils/event-hook.ts similarity index 58% rename from packages/bsp-app-download-sub-generator/src/utils/event-hook.ts rename to packages/repo-app-download-sub-generator/src/utils/event-hook.ts index d004dd8b56..e9db291c14 100644 --- a/packages/bsp-app-download-sub-generator/src/utils/event-hook.ts +++ b/packages/repo-app-download-sub-generator/src/utils/event-hook.ts @@ -1,12 +1,12 @@ -import BspAppDownloadLogger from './logger'; +import RepoAppDownloadLogger from './logger'; import { t } from './i18n'; import type { VSCodeInstance } from '@sap-ux/fiori-generator-shared'; /** - * Context object for the BSP App generation process. + * Context object for the App generation process. * Contains the path of the project and the post-generation command. */ -export interface BspAppGenContext { +export interface RepoAppGenContext { // The file path for the generated project path: string; // The post-generation command to be executed @@ -16,25 +16,25 @@ export interface BspAppGenContext { } /** - * Executes the post-generation command for the BSP app. + * Executes the post-generation command for the app. * Runs the specified command in the context of the generated project, typically for tasks like refreshing or reloading the project in the editor. * - * @param {BspAppGenContext} context - The context containing the project path, post-generation command, and optional VSCode instance. + * @param {RepoAppGenContext} context - The context containing the project path, post-generation command, and optional VSCode instance. */ -export async function runPostAppGenHook(context: BspAppGenContext): Promise { +export async function runPostAppGenHook(context: RepoAppGenContext): Promise { try { // Ensure that context has necessary values before proceeding if (!context.vscodeInstance) { - BspAppDownloadLogger.logger?.error(t('error.eventHookErrors.vscodeInstanceMissing')); + RepoAppDownloadLogger.logger?.error(t('error.eventHookErrors.vscodeInstanceMissing')); } if (!context.postGenCommand || context.postGenCommand.trim() === '') { - BspAppDownloadLogger.logger?.error(t('error.eventHookErrors.postGenCommandMissing')); + RepoAppDownloadLogger.logger?.error(t('error.eventHookErrors.postGenCommandMissing')); } // Execute the post-generation command await context.vscodeInstance?.commands?.executeCommand?.(context.postGenCommand, { fsPath: context.path }); } catch (e) { - BspAppDownloadLogger.logger?.error(t('error.eventHookErrors.commandExecutionFailed', e.message)); + RepoAppDownloadLogger.logger?.error(t('error.eventHookErrors.commandExecutionFailed', e.message)); } } diff --git a/packages/repo-app-download-sub-generator/src/utils/file-helpers.ts b/packages/repo-app-download-sub-generator/src/utils/file-helpers.ts new file mode 100644 index 0000000000..56cd8d5676 --- /dev/null +++ b/packages/repo-app-download-sub-generator/src/utils/file-helpers.ts @@ -0,0 +1,130 @@ +import { adtSourceTemplateId } from './constants'; +import { join } from 'path'; +import type { Editor } from 'mem-fs-editor'; +import { FileName, DirName, type Manifest } from '@sap-ux/project-access'; +import { t } from './i18n'; +import RepoAppDownloadLogger from './logger'; +import type { QfaJsonConfig } from '../app/types'; +import { isInternalFeaturesSettingEnabled } from '@sap-ux/feature-toggle'; +import { getUI5Versions } from '@sap-ux/ui5-info'; + +/** + * + * @param filePath - Path to the JSON file + * @param fs - File system editor instance + * @returns - Parsed JSON object + */ +export function makeValidJson(filePath: string, fs: Editor): QfaJsonConfig { + try { + // Read the file contents + const fileContents = fs.read(filePath); + const validJson: QfaJsonConfig = JSON.parse(fileContents); + return validJson; + } catch (error) { + throw new Error(t('error.errorProcessingJsonFile', { error })); + } +} + +/** + * Reads and validates the `manifest.json` file. + * + * @param {string} manifesFilePath - Manifest file path. + * @param {Editor} fs - The file system editor. + * @returns {Manifest} The validated manifest object. + */ +export function readManifest(manifesFilePath: string, fs: Editor): Manifest { + if (!fs.exists(manifesFilePath)) { + RepoAppDownloadLogger.logger?.error(t('error.readManifestErrors.manifestFileNotFound')); + } + const manifest = fs.readJSON(manifesFilePath) as unknown as Manifest; + if (!manifest) { + RepoAppDownloadLogger.logger?.error(t('error.readManifestErrors.readManifestFailed')); + } + if (!manifest?.['sap.app']) { + RepoAppDownloadLogger.logger?.error(t('error.readManifestErrors.sapAppNotDefined')); + } + if (manifest?.['sap.app']?.sourceTemplate?.id !== adtSourceTemplateId) { + RepoAppDownloadLogger.logger?.error(t('error.readManifestErrors.sourceTemplateNotSupported')); + } + return manifest; +} + +/** + * Validates and updates the UI5 version in the manifest. + * + * - If the minUI5Version in the manifest is not found in the list of released UI5 versions, + * it updates the manifest with the closest released version. + * - If internal features are enabled, it sets the minUI5Version to '${sap.ui5.dist.version}'. + * + * @param {string} manifestFilePath - The manifest file path. + * @param fs + * @returns {Promise} - The updated manifest object. + * @throws {Error} - Throws an error if the manifest structure is invalid or no fallback version is available. + */ +export async function validateAndUpdateManifestUI5Version(manifestFilePath: string, fs: Editor): Promise { + const manifestJson = readManifest(manifestFilePath, fs); + let validatedManifest: Manifest; + + if (!manifestJson['sap.ui5'] || !manifestJson['sap.ui5'].dependencies) { + throw new Error(t('error.readManifestErrors.invalidManifestStructureError')); + } + + const manifestUi5Version = manifestJson['sap.ui5']?.dependencies?.minUI5Version; + const availableUI5Versions = await getUI5Versions({ includeMaintained: true }); + + // Check if the manifest version exists in the list of released versions + const ui5VersionAvailable = availableUI5Versions.find( + (ui5Version: { version: string }) => ui5Version.version === manifestUi5Version + ); + + if (ui5VersionAvailable) { + // Return the manifest as it is if the version is valid + validatedManifest = manifestJson; + } + // Handle internal features setting + if (isInternalFeaturesSettingEnabled()) { + manifestJson['sap.ui5'].dependencies.minUI5Version = '${sap.ui5.dist.version}'; + validatedManifest = manifestJson; + } else { + // Handle fallback to the closest released version + const closestAvailableUi5Version = availableUI5Versions[0]?.version; + manifestJson['sap.ui5'].dependencies.minUI5Version = closestAvailableUi5Version; + validatedManifest = manifestJson; + } + // update manifest at extracted path + fs.writeJSON(manifestFilePath, validatedManifest, undefined, 2); +} + +/** + * Replaces the specified files in the `webapp` directory with the corresponding files from the `extractedPath`. + * + * @param {string} projectPath - The path to the downloaded App. + * @param {string} extractedPath - The path from which files will be copied. + * @param {Editor} fs - The file system editor instance to modify files in memory. + */ +export async function replaceWebappFiles(projectPath: string, extractedPath: string, fs: Editor): Promise { + try { + const webappPath = join(projectPath, DirName.Webapp); + // Define the paths of the files to be replaced + const filesToReplace = [ + { webappFile: FileName.Manifest, extractedFile: FileName.Manifest }, + { webappFile: join('i18n', 'i18n.properties'), extractedFile: join('i18n', 'i18n.properties') }, + { webappFile: 'index.html', extractedFile: 'index.html' }, + { webappFile: 'Component.js', extractedFile: 'component.js' } + ]; + // Loop through each file and perform the replacement + for (const { webappFile, extractedFile } of filesToReplace) { + const webappFilePath = join(webappPath, webappFile); + const extractedFilePath = join(extractedPath, extractedFile); + + // Check if the extracted file exists before replacing + if (fs.exists(extractedFilePath)) { + fs.copy(extractedFilePath, webappFilePath); + } else { + RepoAppDownloadLogger.logger?.warn(t('warn.extractedFileNotFound', { extractedFilePath })); + } + } + } catch (error) { + RepoAppDownloadLogger.logger?.error(t('error.replaceWebappFilesError', { error })); + } +} diff --git a/packages/bsp-app-download-sub-generator/src/utils/i18n.ts b/packages/repo-app-download-sub-generator/src/utils/i18n.ts similarity index 68% rename from packages/bsp-app-download-sub-generator/src/utils/i18n.ts rename to packages/repo-app-download-sub-generator/src/utils/i18n.ts index 6912814463..ad6d44baa4 100644 --- a/packages/bsp-app-download-sub-generator/src/utils/i18n.ts +++ b/packages/repo-app-download-sub-generator/src/utils/i18n.ts @@ -1,15 +1,15 @@ import type { TOptions } from 'i18next'; import i18next from 'i18next'; -import translations from '../translations/bsp-app-download-sub-generator.i18n.json'; +import translations from '../translations/repo-app-download-sub-generator.i18n.json'; -const bspAppDownloadGeneratorNs = 'bsp-app-download-sub-generator'; +const repoAppDownloadGeneratorNs = 'repo-app-download-sub-generator'; /** * Initialize i18next with the translations for this module. */ export async function initI18n(): Promise { await i18next.init({ lng: 'en', fallbackLng: 'en' }, () => - i18next.addResourceBundle('en', bspAppDownloadGeneratorNs, translations) + i18next.addResourceBundle('en', repoAppDownloadGeneratorNs, translations) ); } @@ -22,7 +22,7 @@ export async function initI18n(): Promise { */ export function t(key: string, options?: TOptions): string { if (!options?.ns) { - options = Object.assign(options ?? {}, { ns: bspAppDownloadGeneratorNs }); + options = Object.assign(options ?? {}, { ns: repoAppDownloadGeneratorNs }); } return i18next.t(key, options); } diff --git a/packages/bsp-app-download-sub-generator/src/utils/logger.ts b/packages/repo-app-download-sub-generator/src/utils/logger.ts similarity index 87% rename from packages/bsp-app-download-sub-generator/src/utils/logger.ts rename to packages/repo-app-download-sub-generator/src/utils/logger.ts index d7b267483a..046f20590e 100644 --- a/packages/bsp-app-download-sub-generator/src/utils/logger.ts +++ b/packages/repo-app-download-sub-generator/src/utils/logger.ts @@ -5,7 +5,7 @@ import type { IVSCodeExtLogger, LogLevel } from '@vscode-logging/logger'; /** * Static logger prevents passing of logger references through all functions, as this is a cross-cutting concern. */ -export default class BspAppDownloadLogger { +export default class RepoAppDownloadLogger { private static _logger: ILogWrapper = DefaultLogger; /** @@ -14,7 +14,7 @@ export default class BspAppDownloadLogger { * @returns the logger */ public static get logger(): ILogWrapper { - return BspAppDownloadLogger._logger; + return RepoAppDownloadLogger._logger; } /** @@ -23,7 +23,7 @@ export default class BspAppDownloadLogger { * @param value the logger to set */ public static set logger(value: ILogWrapper) { - BspAppDownloadLogger._logger = value; + RepoAppDownloadLogger._logger = value; } /** @@ -45,6 +45,6 @@ export default class BspAppDownloadLogger { vscode?: unknown ): void { const logger = logWrapper ?? new LogWrapper(loggerName, yoLogger, logLevel, vscLogger, vscode); - BspAppDownloadLogger.logger = logger; + RepoAppDownloadLogger.logger = logger; } } diff --git a/packages/bsp-app-download-sub-generator/src/utils/validators.ts b/packages/repo-app-download-sub-generator/src/utils/validators.ts similarity index 66% rename from packages/bsp-app-download-sub-generator/src/utils/validators.ts rename to packages/repo-app-download-sub-generator/src/utils/validators.ts index e4cdec2c0a..363499f45b 100644 --- a/packages/bsp-app-download-sub-generator/src/utils/validators.ts +++ b/packages/repo-app-download-sub-generator/src/utils/validators.ts @@ -1,6 +1,6 @@ import { t } from '../utils/i18n'; import type { QfaJsonConfig } from '../app/types'; -import BspAppDownloadLogger from '../utils/logger'; +import RepoAppDownloadLogger from '../utils/logger'; import { PromptState } from '../prompts/prompt-state'; /** @@ -11,7 +11,7 @@ import { PromptState } from '../prompts/prompt-state'; */ const validateMetadata = (metadata: QfaJsonConfig['metadata']): boolean => { if (!metadata.package || typeof metadata.package !== 'string') { - BspAppDownloadLogger.logger?.error(t('error.invalidMetadataPackage')); + RepoAppDownloadLogger.logger?.error(t('error.invalidMetadataPackage')); return false; } return true; @@ -23,17 +23,17 @@ const validateMetadata = (metadata: QfaJsonConfig['metadata']): boolean => { * @param {QfaJsonConfig['serviceBindingDetails']} serviceBinding - The service binding details object. * @returns {boolean} - Returns true if valid, false otherwise. */ -const validateServiceBindingDetails = (serviceBinding: QfaJsonConfig['service_binding_details']): boolean => { - if (!serviceBinding.service_name || typeof serviceBinding.service_name !== 'string') { - BspAppDownloadLogger.logger?.error(t('error.invalidServiceName')); +const validateServiceBindingDetails = (serviceBinding: QfaJsonConfig['serviceBindingDetails']): boolean => { + if (!serviceBinding.serviceName || typeof serviceBinding.serviceName !== 'string') { + RepoAppDownloadLogger.logger?.error(t('error.invalidServiceName')); return false; } - if (!serviceBinding.service_version || typeof serviceBinding.service_version !== 'string') { - BspAppDownloadLogger.logger?.error(t('error.invalidServiceVersion')); + if (!serviceBinding.serviceVersion || typeof serviceBinding.serviceVersion !== 'string') { + RepoAppDownloadLogger.logger?.error(t('error.invalidServiceVersion')); return false; } - if (!serviceBinding.main_entity_name || typeof serviceBinding.main_entity_name !== 'string') { - BspAppDownloadLogger.logger?.error(t('error.invalidMainEntityName')); + if (!serviceBinding.mainEntityName || typeof serviceBinding.mainEntityName !== 'string') { + RepoAppDownloadLogger.logger?.error(t('error.invalidMainEntityName')); return false; } return true; @@ -45,9 +45,9 @@ const validateServiceBindingDetails = (serviceBinding: QfaJsonConfig['service_bi * @param {QfaJsonConfig['projectAttribute']} projectAttribute - The project attribute object. * @returns {boolean} - Returns true if valid, false otherwise. */ -const validateProjectAttribute = (projectAttribute: QfaJsonConfig['project_attribute']): boolean => { - if (!projectAttribute.module_name || typeof projectAttribute.module_name !== 'string') { - BspAppDownloadLogger.logger?.error(t('error.invalidModuleName')); +const validateProjectAttribute = (projectAttribute: QfaJsonConfig['projectAttribute']): boolean => { + if (!projectAttribute.moduleName || typeof projectAttribute.moduleName !== 'string') { + RepoAppDownloadLogger.logger?.error(t('error.invalidModuleName')); return false; } return true; @@ -59,9 +59,9 @@ const validateProjectAttribute = (projectAttribute: QfaJsonConfig['project_attri * @param {QfaJsonConfig['deploymentDetails']} deploymentDetails - The deployment details object. * @returns {boolean} - Returns true if valid, false otherwise. */ -const validateDeploymentDetails = (deploymentDetails: QfaJsonConfig['deployment_details']): boolean => { - if (!deploymentDetails.repository_name) { - BspAppDownloadLogger.logger?.error(t('error.invalidRepositoryName')); +const validateDeploymentDetails = (deploymentDetails: QfaJsonConfig['deploymentDetails']): boolean => { + if (!deploymentDetails.repositoryName) { + RepoAppDownloadLogger.logger?.error(t('error.invalidRepositoryName')); return false; } return true; @@ -76,9 +76,9 @@ const validateDeploymentDetails = (deploymentDetails: QfaJsonConfig['deployment_ export const validateQfaJsonFile = (config: QfaJsonConfig): boolean => { return ( validateMetadata(config.metadata) && - validateServiceBindingDetails(config.service_binding_details) && - validateProjectAttribute(config.project_attribute) && - validateDeploymentDetails(config.deployment_details) + validateServiceBindingDetails(config.serviceBindingDetails) && + validateProjectAttribute(config.projectAttribute) && + validateDeploymentDetails(config.deploymentDetails) ); }; diff --git a/packages/bsp-app-download-sub-generator/test/app-config.test.ts b/packages/repo-app-download-sub-generator/test/app-config.test.ts similarity index 73% rename from packages/bsp-app-download-sub-generator/test/app-config.test.ts rename to packages/repo-app-download-sub-generator/test/app-config.test.ts index 31ff360042..c0a52d9a63 100644 --- a/packages/bsp-app-download-sub-generator/test/app-config.test.ts +++ b/packages/repo-app-download-sub-generator/test/app-config.test.ts @@ -1,14 +1,14 @@ import { getAppConfig, getAbapDeployConfig } from '../src/app/app-config'; import type { AbapServiceProvider } from '@sap-ux/axios-extension'; import type { Editor } from 'mem-fs-editor'; -import { getLatestUI5Version } from '@sap-ux/ui5-info'; +import { getUI5Versions } from '@sap-ux/ui5-info'; import { getMinimumUI5Version } from '@sap-ux/project-access'; import { PromptState } from '../src/prompts/prompt-state'; import type { AppInfo, QfaJsonConfig } from '../src/app/types'; import { readManifest } from '../src/utils/file-helpers'; import { t } from '../src/utils/i18n'; import { adtSourceTemplateId } from '../src/utils/constants'; -import BspAppDownloadLogger from '../src/utils/logger'; +import RepoAppDownloadLogger from '../src/utils/logger'; import { TestFixture } from './fixtures'; import { join } from 'path'; import { qfaJsonFileName } from '../src/utils/constants'; @@ -26,7 +26,7 @@ jest.mock('../src/utils/file-helpers', () => ({ jest.mock('@sap-ux/ui5-info', () => ({ ...jest.requireActual('@sap-ux/ui5-info'), - getLatestUI5Version: jest.fn() + getUI5Versions: jest.fn() })); jest.mock('@sap-ux/project-access', () => ({ @@ -34,6 +34,11 @@ jest.mock('@sap-ux/project-access', () => ({ getMinimumUI5Version: jest.fn() })); + +jest.mock('@sap-ux/feature-toggle', () => ({ + isInternalFeaturesSettingEnabled: jest.fn(), +})); + const testFixture = new TestFixture(); const mockQfaJson: QfaJsonConfig = JSON.parse(testFixture.getContents(join('downloaded-app', qfaJsonFileName))); @@ -66,7 +71,7 @@ describe('getAppConfig', () => { type: expect.any(String), settings: { entityConfig: { - mainEntityName: mockQfaJson.service_binding_details.main_entity_name + mainEntityName: mockQfaJson.serviceBindingDetails.mainEntityName } } }, @@ -79,30 +84,30 @@ describe('getAppConfig', () => { appOptions: { addAnnotations: true, addTests: true - }, - ui5: { - version: mockQfaJson.project_attribute.minimum_ui5_version ?? '1.129.0' } }; + const mockManifest = { + 'sap.app': { + dataSources: { + mainService: { + uri: '/odata/service', + settings: { odataVersion: '4.0' } + } + }, + applicationVersion: { version: '1.0.0' } + } + }; + + const availableUI5Versions = [{ version: '1.88.0' }]; + beforeEach(() => { jest.clearAllMocks(); jest.resetAllMocks(); + (getUI5Versions as jest.Mock).mockResolvedValue(availableUI5Versions); }); it('should generate app configuration successfully', async () => { - const mockManifest = { - 'sap.app': { - dataSources: { - mainService: { - uri: '/odata/service', - settings: { odataVersion: '4.0' } - } - }, - applicationVersion: { version: '1.0.0' } - } - }; - const mockServiceProvider = { defaults: { baseURL: 'https://test-url.com', @@ -115,15 +120,14 @@ describe('getAppConfig', () => { }; (readManifest as jest.Mock).mockReturnValue(mockManifest); - (getLatestUI5Version as jest.Mock).mockResolvedValue('1.100.0'); (getMinimumUI5Version as jest.Mock).mockReturnValue('1.90.0'); const mockQfaJsonWithoutNavEntity = { ...mockQfaJson, - service_binding_details: { - name: mockQfaJson.service_binding_details.name, - service_name: mockQfaJson.service_binding_details.service_name, - service_version: mockQfaJson.service_binding_details.service_version, - main_entity_name: mockQfaJson.service_binding_details.main_entity_name, + serviceBindingDetails: { + name: mockQfaJson.serviceBindingDetails.name, + serviceName: mockQfaJson.serviceBindingDetails.serviceName, + serviceVersion: mockQfaJson.serviceBindingDetails.serviceVersion, + mainEntityName: mockQfaJson.serviceBindingDetails.mainEntityName, } } const result = await getAppConfig(mockApp, '/path/to/project', mockQfaJsonWithoutNavEntity, mockFs); @@ -155,34 +159,18 @@ describe('getAppConfig', () => { }; (readManifest as jest.Mock).mockReturnValue(mockManifest); - (getLatestUI5Version as jest.Mock).mockResolvedValue('1.100.0'); (getMinimumUI5Version as jest.Mock).mockReturnValue('1.90.0'); const mockQfaJsonJsonWithNavEntity = { ...mockQfaJson, service_binding_details: { - ...mockQfaJson.service_binding_details, - main_entity_name: mockQfaJson.service_binding_details.main_entity_name, - navigation_entity: mockQfaJson.service_binding_details.navigation_entity + ...mockQfaJson.serviceBindingDetails, + main_entity_name: mockQfaJson.serviceBindingDetails.mainEntityName, + navigation_entity: mockQfaJson.serviceBindingDetails.navigationEntity } } const result = await getAppConfig(mockApp, '/path/to/project', mockQfaJsonJsonWithNavEntity, mockFs); - const expectedAppConfigWithNavEntity = { - ...expectedAppConfig, - template: { - ...expectedAppConfig.template, - settings: { - entityConfig: { - mainEntityName: mockQfaJson.service_binding_details.main_entity_name, - navigationEntity: { - EntitySet: mockQfaJsonJsonWithNavEntity.service_binding_details.navigation_entity, - Name: mockQfaJsonJsonWithNavEntity.service_binding_details.navigation_entity - } - } - } - } - }; - expect(result).toEqual(expectedAppConfigWithNavEntity); + expect(result).toEqual(expectedAppConfig); }); it('should throw an error if manifest data sources are missing', async () => { @@ -192,15 +180,10 @@ describe('getAppConfig', () => { (readManifest as jest.Mock).mockReturnValue(mockManifest); const result = await getAppConfig(mockApp, '/path/to/project', mockQfaJson, mockFs); - expect(BspAppDownloadLogger.logger.error).toBeCalledWith(t('error.dataSourcesNotFound')); + expect(RepoAppDownloadLogger.logger.error).toBeCalledWith(t('error.dataSourcesNotFound')); }); it('should log an error if fetchServiceMetadata throws an error', async () => { - const mockProvider = { - service: jest.fn().mockReturnValue({ - metadata: jest.fn().mockRejectedValue(new Error('Metadata fetch failed')) - }) - } as unknown as AbapServiceProvider; const mockManifest = { 'sap.app': { dataSources: { @@ -230,7 +213,7 @@ describe('getAppConfig', () => { (readManifest as jest.Mock).mockReturnValue(mockManifest); await getAppConfig(mockApp, '/path/to/project', mockQfaJson, mockFs); - expect(BspAppDownloadLogger.logger?.error).toHaveBeenCalledWith(t('error.metadataFetchError', { error: errorMsg })); + expect(RepoAppDownloadLogger.logger?.error).toHaveBeenCalledWith(t('error.metadataFetchError', { error: errorMsg })); }); it('should generate app config when minUi5Version is not provided in manifest', async () => { @@ -264,18 +247,17 @@ describe('getAppConfig', () => { connectedSystem: { serviceProvider: mockServiceProvider } }; (readManifest as jest.Mock).mockReturnValue(mockManifest); - (getLatestUI5Version as jest.Mock).mockResolvedValue('1.100.0'); (getMinimumUI5Version as jest.Mock).mockReturnValue('1.90.0'); const mockQfaJsonJsonWithoutUi5Version = { ...mockQfaJson, - project_attribute: { - ...mockQfaJson.project_attribute, + projectAttribute: { + ...mockQfaJson.projectAttribute, minimum_ui5_version: null } } as unknown as QfaJsonConfig; await getAppConfig(mockApp, '/path/to/project', mockQfaJsonJsonWithoutUi5Version, mockFs); - expect(BspAppDownloadLogger.logger?.error).not.toHaveBeenCalled(); + expect(RepoAppDownloadLogger.logger?.error).not.toHaveBeenCalled(); }); }); @@ -297,9 +279,9 @@ describe('getAbapDeployConfig', () => { destination: 'TEST_REPO' }, app: { - name: 'ZSB_TRVL_APR2', - package: '$TMP', - description: 'Travel Approver 2.0', + name: 'TEST_REPOSITORY_NAME', + package: 'TEST_PACKAGE', + description: 'TEST_REPOSITORY_DESCRIPTION', transport: 'REPLACE_WITH_TRANSPORT' } }; @@ -307,4 +289,5 @@ describe('getAbapDeployConfig', () => { expect(result).toEqual(expectedConfig); }); }); + \ No newline at end of file diff --git a/packages/bsp-app-download-sub-generator/test/app.test.ts b/packages/repo-app-download-sub-generator/test/app.test.ts similarity index 91% rename from packages/bsp-app-download-sub-generator/test/app.test.ts rename to packages/repo-app-download-sub-generator/test/app.test.ts index 749a147ba1..9a8e14e48b 100644 --- a/packages/bsp-app-download-sub-generator/test/app.test.ts +++ b/packages/repo-app-download-sub-generator/test/app.test.ts @@ -1,7 +1,7 @@ import yeomanTest from 'yeoman-test'; import { AppWizard, MessageType } from '@sap-devx/yeoman-ui-types'; import { join } from 'path'; -import BspAppDownloadGenerator from '../src/app'; +import RepoAppDownloadGenerator from '../src/app'; import * as prompts from '../src/prompts/prompts'; import { PromptNames } from '../src/app/types'; import fs from 'fs'; @@ -14,7 +14,7 @@ import { removeSync } from 'fs-extra'; import { isValidPromptState } from '../src/utils/validators'; import { hostEnvironment, sendTelemetry } from '@sap-ux/fiori-generator-shared'; import { FileName, DirName } from '@sap-ux/project-access'; -import BspAppDownloadLogger from '../src/utils/logger'; +import RepoAppDownloadLogger from '../src/utils/logger'; import { t } from '../src/utils/i18n'; import { type AbapServiceProvider } from '@sap-ux/axios-extension'; import { fetchAppListForSelectedSystem } from '../src/prompts/prompt-helpers'; @@ -202,9 +202,9 @@ function verifyGeneratedFiles(testOutputDir: string, appId: string, testFixtureD ); } -describe('BSP App Download', () => { +describe('Repo App Download', () => { const testFixture = new TestFixture(); - const bspAppDownloadGenPath = join(__dirname, '../src/app/index.ts'); + const repoAppDownloadGenPath = join(__dirname, '../src/app/index.ts'); const testOutputDir = join(__dirname, 'test-output'); const metadata = testFixture.getContents('metadata.xml'); let appConfig: FioriElementsApp; @@ -215,7 +215,7 @@ describe('BSP App Download', () => { showError: jest.fn(), showInformation: jest.fn() }; - const appId = 'app-1', repoName = 'app-1-repo'; + const appId = 'test-app-id', repoName = 'app-1-repo'; const extractedProjectPath = join(testOutputDir, appId, extractedFilePath); const testFixtureDir = join(__dirname, 'fixtures', 'downloaded-app'); copyFilesToExtractedProjectPath(testFixtureDir, extractedProjectPath); @@ -239,13 +239,13 @@ describe('BSP App Download', () => { }; }); - it('Should successfully run BSP app download', async () => { + it('Should successfully run app download from repository', async () => { (isValidPromptState as jest.Mock).mockReturnValue(true); (getAppConfig as jest.Mock).mockResolvedValue(appConfig); await expect( yeomanTest - .run(BspAppDownloadGenerator, { - resolved: bspAppDownloadGenPath + .run(RepoAppDownloadGenerator, { + resolved: repoAppDownloadGenPath }) .cd('.') .withOptions({ @@ -268,8 +268,8 @@ describe('BSP App Download', () => { ) .resolves.not.toThrow(); verifyGeneratedFiles(testOutputDir, appId, testFixtureDir); - expect(mockAppWizard.showInformation).toHaveBeenCalledWith(t('info.bspAppDownloadCompleteMsg'), MessageType.notification); - expect(BspAppDownloadLogger.logger.info).toHaveBeenCalledWith(t('info.installationErrors.skippedInstallation')); + expect(mockAppWizard.showInformation).toHaveBeenCalledWith(t('info.repoAppDownloadCompleteMsg'), MessageType.notification); + expect(RepoAppDownloadLogger.logger.info).toHaveBeenCalledWith(t('info.installationErrors.skippedInstallation')); }); it('Should not throw error in end phase if telemetry fails', async () => { @@ -280,8 +280,8 @@ describe('BSP App Download', () => { await expect( yeomanTest - .run(BspAppDownloadGenerator, { - resolved: bspAppDownloadGenPath + .run(RepoAppDownloadGenerator, { + resolved: repoAppDownloadGenPath }) .cd('.') .withOptions({ @@ -302,7 +302,7 @@ describe('BSP App Download', () => { }) ) .resolves.not.toThrow(); - expect(BspAppDownloadLogger.logger.error).toHaveBeenCalledWith(t('error.telemetry', { error: errorMsg })); + expect(RepoAppDownloadLogger.logger.error).toHaveBeenCalledWith(t('error.telemetry', { error: errorMsg })); verifyGeneratedFiles(testOutputDir, appId, testFixtureDir); }); @@ -312,8 +312,8 @@ describe('BSP App Download', () => { await expect( yeomanTest - .run(BspAppDownloadGenerator, { - resolved: bspAppDownloadGenPath + .run(RepoAppDownloadGenerator, { + resolved: repoAppDownloadGenPath }) .cd('.') .withOptions({ @@ -340,7 +340,7 @@ describe('BSP App Download', () => { verifyGeneratedFiles(testOutputDir, appId, testFixtureDir); }); - it('Should successfully download a quick deployed app from BSP', async () => { + it('Should successfully download a quick deployed app from repostory', async () => { (isValidPromptState as jest.Mock).mockReturnValue(true); (getAppConfig as jest.Mock).mockResolvedValue(appConfig); (fetchAppListForSelectedSystem as jest.Mock).mockResolvedValue([ @@ -364,8 +364,8 @@ describe('BSP App Download', () => { await expect( yeomanTest - .run(BspAppDownloadGenerator, { - resolved: bspAppDownloadGenPath + .run(RepoAppDownloadGenerator, { + resolved: repoAppDownloadGenPath }) .cd('.') .withOptions({ @@ -416,8 +416,8 @@ describe('BSP App Download', () => { await expect( yeomanTest - .run(BspAppDownloadGenerator, { - resolved: bspAppDownloadGenPath + .run(RepoAppDownloadGenerator, { + resolved: repoAppDownloadGenPath }) .cd('.') .withOptions({ @@ -445,9 +445,10 @@ describe('BSP App Download', () => { targetFolder: testOutputDir }) ) - .rejects.toThrowError(); + .rejects.toThrowError( + t('error.quickDeployedAppDownloadErrors.noAppsFound', { appId: appConfig.app.id }) + ); expect(fetchAppListForSelectedSystem).toHaveBeenCalledWith(mockServiceProvider, appConfig.app.id); - expect(BspAppDownloadLogger.logger.error).toHaveBeenCalledWith(t('error.quickDeployedAppDownloadErrors.noAppsFound', { appId: appConfig.app.id })); expect(fs.existsSync(join(`${testOutputDir}/${appId}/${DirName.Webapp}`))).toBe(false); }); }); \ No newline at end of file diff --git a/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/component.js b/packages/repo-app-download-sub-generator/test/fixtures/downloaded-app/component.js similarity index 74% rename from packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/component.js rename to packages/repo-app-download-sub-generator/test/fixtures/downloaded-app/component.js index d3d5735986..a0f7626475 100644 --- a/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/component.js +++ b/packages/repo-app-download-sub-generator/test/fixtures/downloaded-app/component.js @@ -2,7 +2,7 @@ sap.ui.define( ["sap/fe/core/AppComponent"], function (Component){ "use strict"; - return Component.extend("travel.approver.2.Component",{ + return Component.extend("test-app-id.Component",{ metadata:{ manifest: "json" } diff --git a/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/i18n/i18n.properties b/packages/repo-app-download-sub-generator/test/fixtures/downloaded-app/i18n/i18n.properties similarity index 100% rename from packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/i18n/i18n.properties rename to packages/repo-app-download-sub-generator/test/fixtures/downloaded-app/i18n/i18n.properties diff --git a/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/index.html b/packages/repo-app-download-sub-generator/test/fixtures/downloaded-app/index.html similarity index 86% rename from packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/index.html rename to packages/repo-app-download-sub-generator/test/fixtures/downloaded-app/index.html index 9e5c0174cf..c970d9a6f5 100644 --- a/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/index.html +++ b/packages/repo-app-download-sub-generator/test/fixtures/downloaded-app/index.html @@ -12,7 +12,7 @@ data-sap-ui-libs="sap.ui.core, sap.m, sap.fe.templates, sap.uxap" data-sap-ui-preload="async" - data-sap-ui-resourceroots='{ "travel.approver.2": "." }' + data-sap-ui-resourceroots='{ "test-app-id": "." }' data-sap-ui-oninit="module:sap/ui/core/ComponentSupport" data-sap-ui-compatVersion="edge" data-sap-ui-async="true" @@ -22,9 +22,9 @@
diff --git a/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/manifest.json b/packages/repo-app-download-sub-generator/test/fixtures/downloaded-app/manifest.json similarity index 97% rename from packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/manifest.json rename to packages/repo-app-download-sub-generator/test/fixtures/downloaded-app/manifest.json index 1975a5235c..3eef953991 100644 --- a/packages/bsp-app-download-sub-generator/test/fixtures/downloaded-app/manifest.json +++ b/packages/repo-app-download-sub-generator/test/fixtures/downloaded-app/manifest.json @@ -1,7 +1,7 @@ { "_version": "1.65.0", "sap.app": { - "id": "travel.approver.2", + "id": "test-app-id", "type": "application", "i18n": "i18n/i18n.properties", "applicationVersion": { @@ -35,7 +35,7 @@ "css": [] }, "dependencies": { - "minUI5Version": "1.136.0", + "minUI5Version": "1.134.1", "libs": { "sap.fe.templates": {} }, @@ -45,7 +45,7 @@ "i18n": { "type": "sap.ui.model.resource.ResourceModel", "settings": { - "bundleName": "travel.approver.2.i18n.i18n" + "bundleName": "test-app-id.i18n.i18n" } }, "@i18n": { diff --git a/packages/repo-app-download-sub-generator/test/fixtures/downloaded-app/qfa.json b/packages/repo-app-download-sub-generator/test/fixtures/downloaded-app/qfa.json new file mode 100644 index 0000000000..dcccc91b5b --- /dev/null +++ b/packages/repo-app-download-sub-generator/test/fixtures/downloaded-app/qfa.json @@ -0,0 +1,27 @@ +{ + "metadata": { + "package": "TEST_PACKAGE", + "masterLanguage": "TEST_LANGUAGE" + }, + "serviceBindingDetails": { + "name": "TEST_SERVICE_NAME", + "serviceName": "/TEST/SERVICE/NAME", + "serviceVersion": "TEST_VERSION", + "mainEntityName": "TEST_ENTITY", + "navigationEntity": "TEST_NAVIGATION" + }, + "projectAttribute": { + "moduleName": "TEST_MODULE_NAME", + "applicationTitle": "TEST_APPLICATION_TITLE" + }, + "deploymentDetails": { + "repositoryName": "TEST_REPOSITORY_NAME", + "repositoryDescription": "TEST_REPOSITORY_DESCRIPTION" + }, + "fioriLaunchpadConfiguration": { + "semanticObject": "TEST_SEMANTIC_OBJECT", + "action": "TEST_ACTION", + "title": "TEST_TITLE", + "subtitle": "TEST_SUBTITLE" + } +} diff --git a/packages/bsp-app-download-sub-generator/test/fixtures/index.ts b/packages/repo-app-download-sub-generator/test/fixtures/index.ts similarity index 100% rename from packages/bsp-app-download-sub-generator/test/fixtures/index.ts rename to packages/repo-app-download-sub-generator/test/fixtures/index.ts diff --git a/packages/bsp-app-download-sub-generator/test/fixtures/metadata.xml b/packages/repo-app-download-sub-generator/test/fixtures/metadata.xml similarity index 100% rename from packages/bsp-app-download-sub-generator/test/fixtures/metadata.xml rename to packages/repo-app-download-sub-generator/test/fixtures/metadata.xml diff --git a/packages/bsp-app-download-sub-generator/test/prompts/prompt-helpers.test.ts b/packages/repo-app-download-sub-generator/test/prompts/prompt-helpers.test.ts similarity index 87% rename from packages/bsp-app-download-sub-generator/test/prompts/prompt-helpers.test.ts rename to packages/repo-app-download-sub-generator/test/prompts/prompt-helpers.test.ts index 93e9911cef..caa0e80cfa 100644 --- a/packages/bsp-app-download-sub-generator/test/prompts/prompt-helpers.test.ts +++ b/packages/repo-app-download-sub-generator/test/prompts/prompt-helpers.test.ts @@ -1,10 +1,10 @@ import { fetchAppListForSelectedSystem, formatAppChoices, getYUIDetails } from '../../src/prompts/prompt-helpers'; -import { PromptNames, BspAppDownloadAnswers, AppItem } from '../../src/app/types'; +import { PromptNames, RepoAppDownloadAnswers, AppItem } from '../../src/app/types'; import { PromptState } from '../../src/prompts/prompt-state'; import type { AbapServiceProvider, AppIndex } from '@sap-ux/axios-extension'; import { generatorTitle, generatorDescription } from '../../src/utils/constants'; import { t } from '../../src/utils/i18n'; -import BspAppDownloadLogger from '../../src/utils/logger'; +import RepoAppDownloadLogger from '../../src/utils/logger'; jest.mock('../../src/utils/logger', () => ({ logger: { @@ -19,7 +19,7 @@ describe('fetchAppListForSelectedSystem', () => { }) } as unknown as AbapServiceProvider; - const mockAnswers: BspAppDownloadAnswers = { + const mockAnswers: RepoAppDownloadAnswers = { [PromptNames.systemSelection]: { connectedSystem: { serviceProvider: mockServiceProvider @@ -57,7 +57,7 @@ describe('fetchAppListForSelectedSystem', () => { const error = new Error('Mock error'); mockServiceProvider.getAppIndex().search = jest.fn().mockRejectedValue(error); const result = await fetchAppListForSelectedSystem(mockServiceProvider, mockAnswers[PromptNames.selectedApp].appId); - expect(BspAppDownloadLogger.logger.error).toBeCalledWith(t('error.applicationListFetchError', { error: error.message })); + expect(RepoAppDownloadLogger.logger.error).toBeCalledWith(t('error.applicationListFetchError', { error: error.message })); expect(result).toEqual([]); }); }); @@ -99,13 +99,13 @@ describe('formatAppChoices', () => { it('should log error if required fields are missing', () => { const appList: AppIndex = [invalidApp]; const result = formatAppChoices(appList); - expect(BspAppDownloadLogger.logger.error).toBeCalledWith( t('error.requiredFieldsMissing', { app: JSON.stringify(appList) }) ); + expect(RepoAppDownloadLogger.logger.error).toBeCalledWith( t('error.requiredFieldsMissing', { app: JSON.stringify(appList) }) ); }); it('should handle a mix of valid and invalid apps by throwing an error', () => { const appList: AppIndex = [validApp, invalidApp]; const result = formatAppChoices(appList); - expect(BspAppDownloadLogger.logger.error).toBeCalledWith( t('error.requiredFieldsMissing', { app: JSON.stringify(appList) }) ); + expect(RepoAppDownloadLogger.logger.error).toBeCalledWith( t('error.requiredFieldsMissing', { app: JSON.stringify(appList) }) ); }); it('should return an empty array if the app list is empty', () => { diff --git a/packages/bsp-app-download-sub-generator/test/prompts/prompt-state.test.ts b/packages/repo-app-download-sub-generator/test/prompts/prompt-state.test.ts similarity index 100% rename from packages/bsp-app-download-sub-generator/test/prompts/prompt-state.test.ts rename to packages/repo-app-download-sub-generator/test/prompts/prompt-state.test.ts diff --git a/packages/bsp-app-download-sub-generator/test/prompts/prompts.test.ts b/packages/repo-app-download-sub-generator/test/prompts/prompts.test.ts similarity index 94% rename from packages/bsp-app-download-sub-generator/test/prompts/prompts.test.ts rename to packages/repo-app-download-sub-generator/test/prompts/prompts.test.ts index e32e6672c3..605463ef9e 100644 --- a/packages/bsp-app-download-sub-generator/test/prompts/prompts.test.ts +++ b/packages/repo-app-download-sub-generator/test/prompts/prompts.test.ts @@ -2,7 +2,7 @@ import { getPrompts } from '../../src/prompts/prompts'; import { getSystemSelectionQuestions } from '@sap-ux/odata-service-inquirer'; import { fetchAppListForSelectedSystem, formatAppChoices } from '../../src/prompts/prompt-helpers'; import { PromptNames } from '../../src/app/types'; -import type { BspAppDownloadAnswers, BspAppDownloadQuestions } from '../../src/app/types'; +import type { RepoAppDownloadAnswers, RepoAppDownloadQuestions } from '../../src/app/types'; import { join } from 'path'; import { t } from '../../src/utils/i18n'; import type { AbapServiceProvider } from '@sap-ux/axios-extension'; @@ -30,7 +30,7 @@ describe('getPrompts', () => { } as unknown as AbapServiceProvider; const mockAnswers = { selectedApp: { appId: 'app1' } - } as unknown as BspAppDownloadAnswers; + } as unknown as RepoAppDownloadAnswers; const mockAppList = [{ appId: 'app1', name: 'Test App' }]; beforeEach(() => { @@ -55,12 +55,12 @@ describe('getPrompts', () => { expect(systemPrompt?.name).toBe('system'); // app selection prompts - const appSelectionPrompt = prompts.find(p => p.name === PromptNames.selectedApp) as BspAppDownloadQuestions; + const appSelectionPrompt = prompts.find(p => p.name === PromptNames.selectedApp) as RepoAppDownloadQuestions; expect(appSelectionPrompt).toBeDefined(); if (typeof appSelectionPrompt?.when === 'function') { await expect(appSelectionPrompt.when({ [PromptNames.systemSelection]: { connectedSystem: { serviceProvider: mockServiceProvider } - } } as unknown as BspAppDownloadAnswers)).resolves.toBe(true); + } } as unknown as RepoAppDownloadAnswers)).resolves.toBe(true); }; if (appSelectionPrompt?.type === 'list') { const listPrompt = appSelectionPrompt as unknown as { choices: () => { name: string; value: string }[] }; @@ -92,7 +92,7 @@ describe('getPrompts', () => { const targetFolderPrompt = prompts.find(p => p.name === PromptNames.targetFolder); expect(targetFolderPrompt).toBeDefined(); if (typeof targetFolderPrompt?.when === 'function') { - expect(targetFolderPrompt.when( {} as unknown as BspAppDownloadAnswers)).toBe(false); + expect(targetFolderPrompt.when( {} as unknown as RepoAppDownloadAnswers)).toBe(false); }; }); diff --git a/packages/bsp-app-download-sub-generator/test/utils/download-utils.test.ts b/packages/repo-app-download-sub-generator/test/utils/download-utils.test.ts similarity index 91% rename from packages/bsp-app-download-sub-generator/test/utils/download-utils.test.ts rename to packages/repo-app-download-sub-generator/test/utils/download-utils.test.ts index 14742f46e1..e05daefbcd 100644 --- a/packages/bsp-app-download-sub-generator/test/utils/download-utils.test.ts +++ b/packages/repo-app-download-sub-generator/test/utils/download-utils.test.ts @@ -6,7 +6,7 @@ import type { Editor } from 'mem-fs-editor'; import type { AbapServiceProvider } from '@sap-ux/axios-extension'; import { join } from 'path'; import { t } from '../../src/utils/i18n'; -import BspAppDownloadLogger from '../../src/utils/logger'; +import RepoAppDownloadLogger from '../../src/utils/logger'; jest.mock('adm-zip'); jest.mock('../../src/utils/logger', () => ({ @@ -50,7 +50,7 @@ describe('download-utils', () => { it('should log an error if the downloaded file is not a Buffer', async () => { jest.spyOn(mockServiceProvider.getUi5AbapRepository(), 'downloadFiles').mockResolvedValue('not-a-buffer' as any); await downloadApp('repoName', extractedPath, mockFs); - expect(BspAppDownloadLogger.logger.error).toBeCalledWith(t('error.appDownloadErrors.downloadedFileNotBufferError')); + expect(RepoAppDownloadLogger.logger.error).toBeCalledWith(t('error.appDownloadErrors.downloadedFileNotBufferError')); }); it('should throw an error if the download fails', async () => { @@ -86,6 +86,6 @@ describe('download-utils', () => { }); jest.spyOn(mockServiceProvider.getUi5AbapRepository(), 'downloadFiles').mockResolvedValue(Buffer.from('app contents')); await downloadApp('repoName', extractedPath, mockFs); - expect(BspAppDownloadLogger.logger.error).toBeCalledWith(t('error.appDownloadErrors.zipExtractionError', { error: errorMessage })); + expect(RepoAppDownloadLogger.logger.error).toBeCalledWith(t('error.appDownloadErrors.zipExtractionError', { error: errorMessage })); }); }); \ No newline at end of file diff --git a/packages/bsp-app-download-sub-generator/test/utils/event-hook.test.ts b/packages/repo-app-download-sub-generator/test/utils/event-hook.test.ts similarity index 74% rename from packages/bsp-app-download-sub-generator/test/utils/event-hook.test.ts rename to packages/repo-app-download-sub-generator/test/utils/event-hook.test.ts index a7da8c5f4b..88d12e97da 100644 --- a/packages/bsp-app-download-sub-generator/test/utils/event-hook.test.ts +++ b/packages/repo-app-download-sub-generator/test/utils/event-hook.test.ts @@ -1,7 +1,7 @@ -import { runPostAppGenHook, type BspAppGenContext } from '../../src/utils/event-hook'; +import { runPostAppGenHook, type RepoAppGenContext } from '../../src/utils/event-hook'; import type { VSCodeInstance } from '@sap-ux/fiori-generator-shared'; import { t } from '../../src/utils/i18n'; -import BspAppDownloadLogger from '../../src/utils/logger'; +import RepoAppDownloadLogger from '../../src/utils/logger'; jest.mock('../../src/utils/logger', () => ({ logger: { @@ -10,7 +10,7 @@ jest.mock('../../src/utils/logger', () => ({ })); describe('runPostAppGenHook', () => { - let mockContext: BspAppGenContext; + let mockContext: RepoAppGenContext; beforeEach(() => { mockContext = { @@ -31,13 +31,13 @@ describe('runPostAppGenHook', () => { it('should log an error if vscodeInstance is missing', async () => { mockContext.vscodeInstance = undefined; await runPostAppGenHook(mockContext); - expect(BspAppDownloadLogger.logger.error).toBeCalledWith(t('error.eventHookErrors.vscodeInstanceMissing')); + expect(RepoAppDownloadLogger.logger.error).toBeCalledWith(t('error.eventHookErrors.vscodeInstanceMissing')); }); it('should log an error if postGenCommand is missing', async () => { mockContext.postGenCommand = ''; await runPostAppGenHook(mockContext); - expect(BspAppDownloadLogger.logger.error).toBeCalledWith(t('error.eventHookErrors.postGenCommandMissing')); + expect(RepoAppDownloadLogger.logger.error).toBeCalledWith(t('error.eventHookErrors.postGenCommandMissing')); }); it('should execute the post-generation command successfully', async () => { @@ -53,6 +53,6 @@ describe('runPostAppGenHook', () => { mockContext.vscodeInstance.commands.executeCommand = jest.fn().mockRejectedValue(mockError); } await runPostAppGenHook(mockContext); - expect(BspAppDownloadLogger.logger.error).toBeCalledWith(t('error.eventHookErrors.commandExecutionFailed')); + expect(RepoAppDownloadLogger.logger.error).toBeCalledWith(t('error.eventHookErrors.commandExecutionFailed')); }); }); \ No newline at end of file diff --git a/packages/bsp-app-download-sub-generator/test/utils/file-helpers.test.ts b/packages/repo-app-download-sub-generator/test/utils/file-helpers.test.ts similarity index 92% rename from packages/bsp-app-download-sub-generator/test/utils/file-helpers.test.ts rename to packages/repo-app-download-sub-generator/test/utils/file-helpers.test.ts index 5d697eee48..60ef845b89 100644 --- a/packages/bsp-app-download-sub-generator/test/utils/file-helpers.test.ts +++ b/packages/repo-app-download-sub-generator/test/utils/file-helpers.test.ts @@ -31,9 +31,10 @@ describe('readManifest', () => { } }; mockReadJSON.mockReturnValue(validManifest); - const result = readManifest(extractedProjectPath, mockFs); + const manifestFilePath = join(extractedProjectPath, 'manifest.json'); + const result = readManifest(manifestFilePath, mockFs); expect(result).toBe(validManifest); - expect(mockFs.readJSON).toHaveBeenCalledWith(join('project-path','manifest.json')); + expect(mockFs.readJSON).toHaveBeenCalledWith(manifestFilePath); }); it('should throw an error if manifest is not found', async () => { diff --git a/packages/bsp-app-download-sub-generator/test/utils/logger.test.ts b/packages/repo-app-download-sub-generator/test/utils/logger.test.ts similarity index 64% rename from packages/bsp-app-download-sub-generator/test/utils/logger.test.ts rename to packages/repo-app-download-sub-generator/test/utils/logger.test.ts index aa3ba49a89..19214ba03b 100644 --- a/packages/bsp-app-download-sub-generator/test/utils/logger.test.ts +++ b/packages/repo-app-download-sub-generator/test/utils/logger.test.ts @@ -1,32 +1,32 @@ -import BspAppDownloadLogger from '../../src/utils/logger'; +import RepoAppDownloadLogger from '../../src/utils/logger'; import { DefaultLogger, LogWrapper } from '@sap-ux/fiori-generator-shared'; import type { Logger } from 'yeoman-environment'; import type { IVSCodeExtLogger, LogLevel } from '@vscode-logging/logger'; -describe('BspAppDownloadLogger', () => { +describe('RepoAppDownloadLogger', () => { const testLoggerName = 'testLogger'; afterEach(() => { // Reset the logger to the default after each test - BspAppDownloadLogger.logger = DefaultLogger; + RepoAppDownloadLogger.logger = DefaultLogger; }); it('should return the default logger initially', () => { - expect(BspAppDownloadLogger.logger).toBe(DefaultLogger); + expect(RepoAppDownloadLogger.logger).toBe(DefaultLogger); }); it('should allow setting a custom logger', () => { const mockLogger = { log: jest.fn() } as unknown as LogWrapper; - BspAppDownloadLogger.logger = mockLogger; - expect(BspAppDownloadLogger.logger).toBe(mockLogger); + RepoAppDownloadLogger.logger = mockLogger; + expect(RepoAppDownloadLogger.logger).toBe(mockLogger); }); it('should configure the logger with provided parameters', () => { const mockYoLogger = { log: jest.fn() } as unknown as Logger; const mockLogWrapper = { log: jest.fn() } as unknown as LogWrapper; - BspAppDownloadLogger.configureLogging(testLoggerName, mockYoLogger, mockLogWrapper); + RepoAppDownloadLogger.configureLogging(testLoggerName, mockYoLogger, mockLogWrapper); - expect(BspAppDownloadLogger.logger).toBe(mockLogWrapper); + expect(RepoAppDownloadLogger.logger).toBe(mockLogWrapper); }); it('should create a new LogWrapper if none is provided', () => { @@ -41,7 +41,7 @@ describe('BspAppDownloadLogger', () => { }) } as unknown as IVSCodeExtLogger; - BspAppDownloadLogger.configureLogging(testLoggerName, mockYoLogger, undefined, mockLogLevel, mockVscLogger); - expect(BspAppDownloadLogger.logger).toBeInstanceOf(LogWrapper); + RepoAppDownloadLogger.configureLogging(testLoggerName, mockYoLogger, undefined, mockLogLevel, mockVscLogger); + expect(RepoAppDownloadLogger.logger).toBeInstanceOf(LogWrapper); }); }); \ No newline at end of file diff --git a/packages/bsp-app-download-sub-generator/test/utils/validators.test.ts b/packages/repo-app-download-sub-generator/test/utils/validators.test.ts similarity index 62% rename from packages/bsp-app-download-sub-generator/test/utils/validators.test.ts rename to packages/repo-app-download-sub-generator/test/utils/validators.test.ts index 8e6b49d055..1fc1b360af 100644 --- a/packages/bsp-app-download-sub-generator/test/utils/validators.test.ts +++ b/packages/repo-app-download-sub-generator/test/utils/validators.test.ts @@ -1,7 +1,7 @@ import { validateQfaJsonFile } from '../../src/utils/validators'; import { QfaJsonConfig } from '../../src/app/types'; import { t } from '../../src/utils/i18n'; -import BspAppDownloadLogger from '../../src/utils/logger'; +import RepoAppDownloadLogger from '../../src/utils/logger'; jest.mock('../../src/utils/logger', () => ({ logger: { @@ -12,15 +12,15 @@ jest.mock('../../src/utils/logger', () => ({ describe('validateQfaJsonFile', () => { const validConfig: QfaJsonConfig = { metadata: { package: 'valid-package' }, - service_binding_details: { - service_name: 'validService', - service_version: '1.0.0', - main_entity_name: 'validEntity', + serviceBindingDetails: { + serviceName: 'validService', + serviceVersion: '1.0.0', + mainEntityName: 'validEntity', }, - project_attribute: { module_name: 'validModule' }, - deployment_details: { repository_name: 'validRepository' }, - fiori_launchpad_configuration: { - semantic_object: 'semanticObject', + projectAttribute: { moduleName: 'validModule' }, + deploymentDetails: { repositoryName: 'validRepository' }, + fioriLaunchpadConfiguration: { + semanticObject: 'semanticObject', action: 'action', title: 'title' }, @@ -43,70 +43,70 @@ describe('validateQfaJsonFile', () => { const result = validateQfaJsonFile(invalidMetadataConfig); expect(result).toBe(false); - expect(BspAppDownloadLogger.logger.error).toBeCalledWith(t('error.invalidMetadataPackage')); + expect(RepoAppDownloadLogger.logger.error).toBeCalledWith(t('error.invalidMetadataPackage')); }); it('should return false and log an error when service binding details validation fails', () => { const invalidServiceBindingConfig = { ...validConfig, - service_binding_details: { - ...validConfig.service_binding_details, - service_name: '', // Invalid service name + serviceBindingDetails: { + ...validConfig.serviceBindingDetails, + serviceName: '', // Invalid service name } } as unknown as QfaJsonConfig; const result = validateQfaJsonFile(invalidServiceBindingConfig); expect(result).toBe(false); - expect(BspAppDownloadLogger.logger.error).toBeCalledWith(t('error.invalidServiceName')); + expect(RepoAppDownloadLogger.logger.error).toBeCalledWith(t('error.invalidServiceName')); }); it('should return false and log an error when service binding version is not provided', () => { const invalidServiceBindingConfig = { ...validConfig, - service_binding_details: { - ...validConfig.service_binding_details, - service_version: '' // Invalid service version + serviceBindingDetails: { + ...validConfig.serviceBindingDetails, + serviceVersion: '' // Invalid service version } } as unknown as QfaJsonConfig; const result = validateQfaJsonFile(invalidServiceBindingConfig); expect(result).toBe(false); - expect(BspAppDownloadLogger.logger.error).toBeCalledWith(t('error.invalidServiceVersion')); + expect(RepoAppDownloadLogger.logger.error).toBeCalledWith(t('error.invalidServiceVersion')); }); it('should return false and log an error when main entity name is missing', () => { const invalidServiceBindingConfig = { ...validConfig, - service_binding_details: { - ...validConfig.service_binding_details, - main_entity_name: '' // Invalid main entity name + serviceBindingDetails: { + ...validConfig.serviceBindingDetails, + mainEntityName: '' // Invalid main entity name } } as unknown as QfaJsonConfig; const result = validateQfaJsonFile(invalidServiceBindingConfig); expect(result).toBe(false); - expect(BspAppDownloadLogger.logger.error).toBeCalledWith(t('error.invalidMainEntityName')); + expect(RepoAppDownloadLogger.logger.error).toBeCalledWith(t('error.invalidMainEntityName')); }); it('should return false and log an error when project attribute validation fails', () => { const invalidProjectAttributeConfig = { ...validConfig, - project_attribute: { module_name: '' } // Invalid module name + projectAttribute: { moduleName: '' } // Invalid module name } as unknown as QfaJsonConfig; const result = validateQfaJsonFile(invalidProjectAttributeConfig); expect(result).toBe(false); - expect(BspAppDownloadLogger.logger.error).toBeCalledWith(t('error.invalidModuleName')); + expect(RepoAppDownloadLogger.logger.error).toBeCalledWith(t('error.invalidModuleName')); }); it('should return false and log an error when deployment details validation fails', () => { const invalidDeploymentDetailsConfig = { ...validConfig, - deployment_details: { repository_name: '' } // Invalid repository name + deploymentDetails: { repositoryName: '' } // Invalid repository name } as unknown as QfaJsonConfig; const result = validateQfaJsonFile(invalidDeploymentDetailsConfig); expect(result).toBe(false); - expect(BspAppDownloadLogger.logger.error).toBeCalledWith(t('error.invalidRepositoryName')); + expect(RepoAppDownloadLogger.logger.error).toBeCalledWith(t('error.invalidRepositoryName')); }); }); diff --git a/packages/bsp-app-download-sub-generator/tsconfig.eslint.json b/packages/repo-app-download-sub-generator/tsconfig.eslint.json similarity index 100% rename from packages/bsp-app-download-sub-generator/tsconfig.eslint.json rename to packages/repo-app-download-sub-generator/tsconfig.eslint.json diff --git a/packages/bsp-app-download-sub-generator/tsconfig.json b/packages/repo-app-download-sub-generator/tsconfig.json similarity index 90% rename from packages/bsp-app-download-sub-generator/tsconfig.json rename to packages/repo-app-download-sub-generator/tsconfig.json index 17d4a860b2..5c28d132d5 100644 --- a/packages/bsp-app-download-sub-generator/tsconfig.json +++ b/packages/repo-app-download-sub-generator/tsconfig.json @@ -57,12 +57,6 @@ { "path": "../store" }, - { - "path": "../ui5-application-inquirer" - }, - { - "path": "../ui5-application-writer" - }, { "path": "../ui5-config" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eb61739da5..4c2a30f90d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -904,151 +904,6 @@ importers: specifier: 2.2.2 version: 2.2.2 - packages/bsp-app-download-sub-generator: - dependencies: - '@sap-devx/yeoman-ui-types': - specifier: 1.14.4 - version: 1.14.4 - '@sap-ux/abap-deploy-config-writer': - specifier: workspace:* - version: link:../abap-deploy-config-writer - '@sap-ux/axios-extension': - specifier: workspace:* - version: link:../axios-extension - '@sap-ux/btp-utils': - specifier: workspace:* - version: link:../btp-utils - '@sap-ux/feature-toggle': - specifier: workspace:* - version: link:../feature-toggle - '@sap-ux/fiori-elements-writer': - specifier: workspace:* - version: link:../fiori-elements-writer - '@sap-ux/fiori-generator-shared': - specifier: workspace:* - version: link:../fiori-generator-shared - '@sap-ux/fiori-tools-settings': - specifier: workspace:* - version: link:../fiori-tools-settings - '@sap-ux/i18n': - specifier: workspace:* - version: link:../i18n - '@sap-ux/inquirer-common': - specifier: workspace:* - version: link:../inquirer-common - '@sap-ux/launch-config': - specifier: workspace:* - version: link:../launch-config - '@sap-ux/logger': - specifier: workspace:* - version: link:../logger - '@sap-ux/odata-service-inquirer': - specifier: workspace:* - version: link:../odata-service-inquirer - '@sap-ux/project-access': - specifier: workspace:* - version: link:../project-access - '@sap-ux/project-input-validator': - specifier: workspace:* - version: link:../project-input-validator - '@sap-ux/store': - specifier: workspace:* - version: link:../store - '@sap-ux/ui5-application-inquirer': - specifier: workspace:* - version: link:../ui5-application-inquirer - '@sap-ux/ui5-application-writer': - specifier: workspace:* - version: link:../ui5-application-writer - '@sap-ux/ui5-info': - specifier: workspace:* - version: link:../ui5-info - adm-zip: - specifier: 0.5.10 - version: 0.5.10 - i18next: - specifier: 23.5.1 - version: 23.5.1 - inquirer: - specifier: 8.2.6 - version: 8.2.6 - inquirer-autocomplete-prompt: - specifier: 2.0.1 - version: 2.0.1(inquirer@8.2.6) - yeoman-generator: - specifier: 5.10.0 - version: 5.10.0(mem-fs@2.1.0)(yeoman-environment@3.19.3) - devDependencies: - '@jest/types': - specifier: 29.6.3 - version: 29.6.3 - '@sap-ux/nodejs-utils': - specifier: workspace:* - version: link:../nodejs-utils - '@sap-ux/ui5-config': - specifier: workspace:* - version: link:../ui5-config - '@types/adm-zip': - specifier: 0.5.5 - version: 0.5.5 - '@types/fs-extra': - specifier: 9.0.13 - version: 9.0.13 - '@types/inquirer': - specifier: 8.2.6 - version: 8.2.6 - '@types/inquirer-autocomplete-prompt': - specifier: 2.0.1 - version: 2.0.1 - '@types/lodash': - specifier: 4.14.202 - version: 4.14.202 - '@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 - '@types/yeoman-generator': - specifier: 5.2.11 - version: 5.2.11 - '@types/yeoman-test': - specifier: 4.0.6 - version: 4.0.6 - '@vscode-logging/logger': - specifier: 2.0.0 - version: 2.0.0 - fs-extra: - specifier: 10.0.0 - version: 10.0.0 - lodash: - specifier: 4.17.21 - version: 4.17.21 - mem-fs-editor: - specifier: 9.4.0 - version: 9.4.0(mem-fs@2.1.0) - memfs: - specifier: 3.4.13 - version: 3.4.13 - rimraf: - specifier: 5.0.5 - version: 5.0.5 - typescript: - specifier: 5.3.3 - version: 5.3.3 - unionfs: - specifier: 4.4.0 - version: 4.4.0 - yeoman-test: - specifier: 6.3.0 - version: 6.3.0(mem-fs@2.1.0)(yeoman-environment@3.19.3)(yeoman-generator@5.10.0) - yo: - specifier: '4' - version: 4.3.1(mem-fs@2.1.0) - packages/btp-utils: dependencies: '@sap/bas-sdk': @@ -3417,6 +3272,145 @@ importers: specifier: 6.3.3 version: 6.3.3 + packages/repo-app-download-sub-generator: + dependencies: + '@sap-devx/yeoman-ui-types': + specifier: 1.14.4 + version: 1.14.4 + '@sap-ux/abap-deploy-config-writer': + specifier: workspace:* + version: link:../abap-deploy-config-writer + '@sap-ux/axios-extension': + specifier: workspace:* + version: link:../axios-extension + '@sap-ux/btp-utils': + specifier: workspace:* + version: link:../btp-utils + '@sap-ux/feature-toggle': + specifier: workspace:* + version: link:../feature-toggle + '@sap-ux/fiori-elements-writer': + specifier: workspace:* + version: link:../fiori-elements-writer + '@sap-ux/fiori-generator-shared': + specifier: workspace:* + version: link:../fiori-generator-shared + '@sap-ux/fiori-tools-settings': + specifier: workspace:* + version: link:../fiori-tools-settings + '@sap-ux/i18n': + specifier: workspace:* + version: link:../i18n + '@sap-ux/inquirer-common': + specifier: workspace:* + version: link:../inquirer-common + '@sap-ux/launch-config': + specifier: workspace:* + version: link:../launch-config + '@sap-ux/logger': + specifier: workspace:* + version: link:../logger + '@sap-ux/odata-service-inquirer': + specifier: workspace:* + version: link:../odata-service-inquirer + '@sap-ux/project-access': + specifier: workspace:* + version: link:../project-access + '@sap-ux/project-input-validator': + specifier: workspace:* + version: link:../project-input-validator + '@sap-ux/store': + specifier: workspace:* + version: link:../store + '@sap-ux/ui5-info': + specifier: workspace:* + version: link:../ui5-info + adm-zip: + specifier: 0.5.10 + version: 0.5.10 + i18next: + specifier: 23.5.1 + version: 23.5.1 + inquirer: + specifier: 8.2.6 + version: 8.2.6 + inquirer-autocomplete-prompt: + specifier: 2.0.1 + version: 2.0.1(inquirer@8.2.6) + yeoman-generator: + specifier: 5.10.0 + version: 5.10.0(mem-fs@2.1.0)(yeoman-environment@3.19.3) + devDependencies: + '@jest/types': + specifier: 29.6.3 + version: 29.6.3 + '@sap-ux/nodejs-utils': + specifier: workspace:* + version: link:../nodejs-utils + '@sap-ux/ui5-config': + specifier: workspace:* + version: link:../ui5-config + '@types/adm-zip': + specifier: 0.5.5 + version: 0.5.5 + '@types/fs-extra': + specifier: 9.0.13 + version: 9.0.13 + '@types/inquirer': + specifier: 8.2.6 + version: 8.2.6 + '@types/inquirer-autocomplete-prompt': + specifier: 2.0.1 + version: 2.0.1 + '@types/lodash': + specifier: 4.14.202 + version: 4.14.202 + '@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 + '@types/yeoman-generator': + specifier: 5.2.11 + version: 5.2.11 + '@types/yeoman-test': + specifier: 4.0.6 + version: 4.0.6 + '@vscode-logging/logger': + specifier: 2.0.0 + version: 2.0.0 + fs-extra: + specifier: 10.0.0 + version: 10.0.0 + lodash: + specifier: 4.17.21 + version: 4.17.21 + mem-fs-editor: + specifier: 9.4.0 + version: 9.4.0(mem-fs@2.1.0) + memfs: + specifier: 3.4.13 + version: 3.4.13 + rimraf: + specifier: 5.0.5 + version: 5.0.5 + typescript: + specifier: 5.3.3 + version: 5.3.3 + unionfs: + specifier: 4.4.0 + version: 4.4.0 + yeoman-test: + specifier: 6.3.0 + version: 6.3.0(mem-fs@2.1.0)(yeoman-environment@3.19.3)(yeoman-generator@5.10.0) + yo: + specifier: '4' + version: 4.3.1(mem-fs@2.1.0) + packages/serve-static-middleware: dependencies: '@sap-ux/logger': @@ -10688,7 +10682,7 @@ packages: '@types/mem-fs': 1.1.2 '@types/mem-fs-editor': 7.0.1 '@types/yeoman-environment': 2.10.11 - '@types/yeoman-generator': 5.2.11 + '@types/yeoman-generator': 5.2.14 dev: true /@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.57.0)(typescript@5.8.2): @@ -17482,6 +17476,7 @@ packages: /is-docker@3.0.0: resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true dev: true /is-extglob@2.1.1: @@ -25696,7 +25691,7 @@ packages: chalk: 4.1.2 cli-list: 0.2.0 configstore: 5.0.1 - cross-spawn: 7.0.5 + cross-spawn: 7.0.6 figures: 3.2.0 fullname: 4.0.1 global-agent: 3.0.0 diff --git a/sonar-project.properties b/sonar-project.properties index ed837c2fef..0d12c97311 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -42,6 +42,7 @@ sonar.javascript.lcov.reportPaths=packages/abap-deploy-config-inquirer/coverage/ packages/i18n/coverage/lcov.info, \ packages/environment-check/coverage/lcov.info, \ packages/eslint-plugin-fiori-tools/coverage/lcov.info, \ + packages/repo-app-download-sub-generator/coverage/lcov.info, \ packages/generator-adp/coverage/lcov.info, \ packages/logger/coverage/lcov.info, \ packages/mockserver-config-writer/coverage/lcov.info, \ @@ -99,6 +100,7 @@ sonar.testExecutionReportPaths=packages/abap-deploy-config-inquirer/coverage/son packages/axios-extension/coverage/sonar-report.xml, \ packages/backend-proxy-middleware/coverage/sonar-report.xml, \ packages/btp-utils/coverage/sonar-report.xml, \ + packages/repo-app-download-sub-generator/coverage/sonar-report.xml, \ packages/cap-config-writer/coverage/sonar-report.xml, \ packages/cards-editor-config-writer/coverage/sonar-report.xml, \ packages/cards-editor-middleware/coverage/sonar-report.xml, \ diff --git a/tsconfig.json b/tsconfig.json index ba32c81e05..58272a6d63 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -51,9 +51,6 @@ { "path": "packages/backend-proxy-middleware" }, - { - "path": "packages/bsp-app-download-sub-generator" - }, { "path": "packages/btp-utils" }, @@ -204,6 +201,9 @@ { "path": "packages/reload-middleware" }, + { + "path": "packages/repo-app-download-sub-generator" + }, { "path": "packages/serve-static-middleware" }, From 043d130c076c630c92580b13c99b9dddbc6e1b2f Mon Sep 17 00:00:00 2001 From: I743583 Date: Sun, 13 Apr 2025 12:09:30 +0100 Subject: [PATCH 24/41] add changeset --- .changeset/blue-wombats-yell.md | 6 ++++++ .../fiori-tools-settings/src/applicationInfoHandler.ts | 8 ++------ packages/repo-app-download-sub-generator/package.json | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) create mode 100644 .changeset/blue-wombats-yell.md diff --git a/.changeset/blue-wombats-yell.md b/.changeset/blue-wombats-yell.md new file mode 100644 index 0000000000..830dc38554 --- /dev/null +++ b/.changeset/blue-wombats-yell.md @@ -0,0 +1,6 @@ +--- +'@sap-ux/repo-app-download-sub-generator': minor +'@sap-ux/axios-extension': minor +--- + +Added a new sub-generator: `@sap-ux/repo-app-download-sub-generator` to support downloading ABAP deployed Fiori apps from the repository. Enhanced `@sap-ux/axios-extension` to support Base64 download data. diff --git a/packages/fiori-tools-settings/src/applicationInfoHandler.ts b/packages/fiori-tools-settings/src/applicationInfoHandler.ts index fb4399573c..87bbe129d8 100644 --- a/packages/fiori-tools-settings/src/applicationInfoHandler.ts +++ b/packages/fiori-tools-settings/src/applicationInfoHandler.ts @@ -50,9 +50,7 @@ export function writeApplicationInfoSettings(path: string, fs?: Editor) { appInfoContents.latestGeneratedFiles.push(path); fs.write(appInfoFilePath, JSON.stringify(appInfoContents, null, 2)); fs.commit((err) => { - if (err) { - console.log('Error in writting to AppInfo.json file', err); - } + console.log('Error in writting to AppInfo.json file', err); }); } @@ -70,9 +68,7 @@ export function deleteAppInfoSettings(fs?: Editor) { try { fs.delete(appInfoFilePath); fs.commit((err) => { - if (err) { - console.log('Failed to commit the deletion of the AppInfo.json file: ', err); - } + console.log('Failed to commit the deletion of the AppInfo.json file: ', err); }); } catch (err) { throw new Error(`Error deleting appInfo.json file: ${err}`); diff --git a/packages/repo-app-download-sub-generator/package.json b/packages/repo-app-download-sub-generator/package.json index b9d3e5190c..641ad23733 100644 --- a/packages/repo-app-download-sub-generator/package.json +++ b/packages/repo-app-download-sub-generator/package.json @@ -1,7 +1,7 @@ { "name": "@sap-ux/repo-app-download-sub-generator", "description": "Generator to download LROP Fiori applications deployed from an ABAP repository.", - "version": "0.1.40", + "version": "0.0.0", "repository": { "type": "git", "url": "https://github.com/SAP/open-ux-tools.git", From 27123a9f326ee6c1a2ac3e5424603fd58462e915 Mon Sep 17 00:00:00 2001 From: I743583 Date: Sun, 13 Apr 2025 13:00:41 +0100 Subject: [PATCH 25/41] fix sonar issues --- .../src/app/index.ts | 6 ++---- .../src/prompts/prompt-helpers.ts | 6 ++---- .../src/prompts/prompts.ts | 3 +-- .../src/utils/file-helpers.ts | 15 +++++---------- 4 files changed, 10 insertions(+), 20 deletions(-) diff --git a/packages/repo-app-download-sub-generator/src/app/index.ts b/packages/repo-app-download-sub-generator/src/app/index.ts index 8d6145f0b2..94a306d48f 100644 --- a/packages/repo-app-download-sub-generator/src/app/index.ts +++ b/packages/repo-app-download-sub-generator/src/app/index.ts @@ -6,10 +6,9 @@ import type { Logger } from '@sap-ux/logger'; import { sendTelemetry, TelemetryHelper } from '@sap-ux/fiori-generator-shared'; import { generatorTitle, extractedFilePath, generatorName, defaultAnswers, qfaJsonFileName } from '../utils/constants'; import { t } from '../utils/i18n'; -import { getYUIDetails } from '../prompts/prompt-helpers'; import { downloadApp } from '../utils/download-utils'; import { EventName } from '../telemetryEvents'; -import { getDefaultTargetFolder } from '@sap-ux/fiori-generator-shared'; +import { getDefaultTargetFolder, generateReadMe, type ReadMe } from '@sap-ux/fiori-generator-shared'; import type { RepoAppDownloadOptions, RepoAppDownloadAnswers, @@ -21,7 +20,6 @@ import { getPrompts } from '../prompts/prompts'; import { generate, TemplateType, type FioriElementsApp, type LROPSettings } from '@sap-ux/fiori-elements-writer'; import { join, basename } from 'path'; import { platform } from 'os'; -import { generateReadMe, type ReadMe } from '@sap-ux/fiori-generator-shared'; import { runPostAppGenHook } from '../utils/event-hook'; import { getDefaultUI5Theme } from '@sap-ux/ui5-info'; import type { DebugOptions, FioriOptions } from '@sap-ux/launch-config'; @@ -35,7 +33,7 @@ import { PromptNames } from './types'; import { getAbapDeployConfig, getAppConfig } from './app-config'; import type { AbapDeployConfig } from '@sap-ux/ui5-config'; import { replaceWebappFiles, makeValidJson, validateAndUpdateManifestUI5Version } from '../utils/file-helpers'; -import { fetchAppListForSelectedSystem, extractAppData } from '../prompts/prompt-helpers'; +import { fetchAppListForSelectedSystem, extractAppData, getYUIDetails } from '../prompts/prompt-helpers'; import { isValidPromptState, validateQfaJsonFile } from '../utils/validators'; import { FileName, DirName } from '@sap-ux/project-access'; diff --git a/packages/repo-app-download-sub-generator/src/prompts/prompt-helpers.ts b/packages/repo-app-download-sub-generator/src/prompts/prompt-helpers.ts index 74fdf5c6e9..5096b062a7 100644 --- a/packages/repo-app-download-sub-generator/src/prompts/prompt-helpers.ts +++ b/packages/repo-app-download-sub-generator/src/prompts/prompt-helpers.ts @@ -1,9 +1,7 @@ -import { generatorTitle, generatorDescription } from '../utils/constants'; -import { appListSearchParams, appListResultFields } from '../utils/constants'; +import { appListSearchParams, appListResultFields, generatorTitle, generatorDescription } from '../utils/constants'; import type { AbapServiceProvider, AppIndex } from '@sap-ux/axios-extension'; -import type { AppInfo } from '../app/types'; +import type { AppInfo, AppItem } from '../app/types'; import { PromptState } from './prompt-state'; -import type { AppItem } from '../app/types'; import { t } from '../utils/i18n'; import RepoAppDownloadLogger from '../utils/logger'; diff --git a/packages/repo-app-download-sub-generator/src/prompts/prompts.ts b/packages/repo-app-download-sub-generator/src/prompts/prompts.ts index de548e5a99..4f4379917e 100644 --- a/packages/repo-app-download-sub-generator/src/prompts/prompts.ts +++ b/packages/repo-app-download-sub-generator/src/prompts/prompts.ts @@ -4,10 +4,9 @@ import type { RepoAppDownloadAnswers, RepoAppDownloadQuestions, QuickDeployedApp import { PromptNames } from '../app/types'; import { t } from '../utils/i18n'; import type { FileBrowserQuestion } from '@sap-ux/inquirer-common'; -import { formatAppChoices } from './prompt-helpers'; import { validateFioriAppTargetFolder } from '@sap-ux/project-input-validator'; import { PromptState } from './prompt-state'; -import { fetchAppListForSelectedSystem } from './prompt-helpers'; +import { fetchAppListForSelectedSystem, formatAppChoices } from './prompt-helpers'; /** * Gets the target folder selection prompt. diff --git a/packages/repo-app-download-sub-generator/src/utils/file-helpers.ts b/packages/repo-app-download-sub-generator/src/utils/file-helpers.ts index 56cd8d5676..74b870de67 100644 --- a/packages/repo-app-download-sub-generator/src/utils/file-helpers.ts +++ b/packages/repo-app-download-sub-generator/src/utils/file-helpers.ts @@ -63,9 +63,7 @@ export function readManifest(manifesFilePath: string, fs: Editor): Manifest { */ export async function validateAndUpdateManifestUI5Version(manifestFilePath: string, fs: Editor): Promise { const manifestJson = readManifest(manifestFilePath, fs); - let validatedManifest: Manifest; - - if (!manifestJson['sap.ui5'] || !manifestJson['sap.ui5'].dependencies) { + if (!manifestJson['sap.ui5']?.dependencies) { throw new Error(t('error.readManifestErrors.invalidManifestStructureError')); } @@ -79,20 +77,17 @@ export async function validateAndUpdateManifestUI5Version(manifestFilePath: stri if (ui5VersionAvailable) { // Return the manifest as it is if the version is valid - validatedManifest = manifestJson; - } - // Handle internal features setting - if (isInternalFeaturesSettingEnabled()) { + // No changes needed + } else if (isInternalFeaturesSettingEnabled()) { + // Handle internal features setting manifestJson['sap.ui5'].dependencies.minUI5Version = '${sap.ui5.dist.version}'; - validatedManifest = manifestJson; } else { // Handle fallback to the closest released version const closestAvailableUi5Version = availableUI5Versions[0]?.version; manifestJson['sap.ui5'].dependencies.minUI5Version = closestAvailableUi5Version; - validatedManifest = manifestJson; } // update manifest at extracted path - fs.writeJSON(manifestFilePath, validatedManifest, undefined, 2); + fs.writeJSON(manifestFilePath, manifestJson, undefined, 2); } /** From 053e94e9ef53247e823c7b6c8d0f47170b68584e Mon Sep 17 00:00:00 2001 From: I743583 Date: Sun, 13 Apr 2025 13:08:02 +0100 Subject: [PATCH 26/41] sonar issue fixes --- .../axios-extension/src/abap/ui5-abap-repository-service.ts | 2 +- packages/repo-app-download-sub-generator/src/app/index.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/axios-extension/src/abap/ui5-abap-repository-service.ts b/packages/axios-extension/src/abap/ui5-abap-repository-service.ts index e54ea34a28..c3cd0d832b 100644 --- a/packages/axios-extension/src/abap/ui5-abap-repository-service.ts +++ b/packages/axios-extension/src/abap/ui5-abap-repository-service.ts @@ -152,7 +152,7 @@ export class Ui5AbapRepositoryService extends ODataService { return Buffer.from(str, 'base64').toString('base64') === str.trim(); } catch (e) { // If decoding fails, it's not valid Base64 - return false; + return false; //NOSONAR } } diff --git a/packages/repo-app-download-sub-generator/src/app/index.ts b/packages/repo-app-download-sub-generator/src/app/index.ts index 94a306d48f..70cd3659f8 100644 --- a/packages/repo-app-download-sub-generator/src/app/index.ts +++ b/packages/repo-app-download-sub-generator/src/app/index.ts @@ -46,7 +46,7 @@ export default class extends Generator { private readonly vscode?: any; private readonly appRootPath: string; private readonly prompts: Prompts; - private answers: RepoAppDownloadAnswers = defaultAnswers; + private readonly answers: RepoAppDownloadAnswers = defaultAnswers; public options: RepoAppDownloadOptions; private projectPath: string; private extractedProjectPath: string; @@ -209,7 +209,7 @@ export default class extends Generator { const readMeConfig: ReadMe = { appName: config.app.id, appTitle: config.app.title ?? '', - appNamespace: '', // todo: cant find namespace in manifest json - default? + appNamespace: config.app.id.substring(0, config.app.id.lastIndexOf('.')), appDescription: t('readMe.appDescription'), ui5Theme: getDefaultUI5Theme(config.ui5?.version), generatorName: generatorName, From c8e34ed7b116520015aa16f80cbc689e02332f1c Mon Sep 17 00:00:00 2001 From: I743583 Date: Sun, 13 Apr 2025 17:21:21 +0100 Subject: [PATCH 27/41] readme --- packages/repo-app-download-sub-generator/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/repo-app-download-sub-generator/README.md b/packages/repo-app-download-sub-generator/README.md index c264b6c4e5..034ebe2e97 100644 --- a/packages/repo-app-download-sub-generator/README.md +++ b/packages/repo-app-download-sub-generator/README.md @@ -10,7 +10,7 @@ The SAP App download sub-generator sub-generator is installed as part of the [@ ## Launch the SAP Reuse Library sub-generator -Open the Command Palette in MS Visual Studio Code ( CMD/CTRL + Shift + P ) and execute the Fiori: Download ADT deployed app from UI5 ABAP repository. +Open the Command Palette in MS Visual Studio Code ( CMD/CTRL + Shift + P ) and execute the Fiori: Download ADT deployed app from an UI5 ABAP repository. ## Keywords SAP Fiori Generator From b113237c5eeb1fac95a873e31c00be2a5ddece11 Mon Sep 17 00:00:00 2001 From: I743583 Date: Mon, 14 Apr 2025 09:52:19 +0100 Subject: [PATCH 28/41] fix: add more test --- .../src/app/index.ts | 3 +- .../repo-app-download-sub-generator.i18n.json | 2 +- .../src/utils/constants.ts | 5 +- .../src/utils/file-helpers.ts | 80 +------- .../src/utils/updates.ts | 83 ++++++++ .../test/app.test.ts | 4 - .../test/utils/file-helpers.test.ts | 8 +- .../test/utils/updates.test.ts | 189 ++++++++++++++++++ 8 files changed, 283 insertions(+), 91 deletions(-) create mode 100644 packages/repo-app-download-sub-generator/src/utils/updates.ts create mode 100644 packages/repo-app-download-sub-generator/test/utils/updates.test.ts diff --git a/packages/repo-app-download-sub-generator/src/app/index.ts b/packages/repo-app-download-sub-generator/src/app/index.ts index 70cd3659f8..d7af356c92 100644 --- a/packages/repo-app-download-sub-generator/src/app/index.ts +++ b/packages/repo-app-download-sub-generator/src/app/index.ts @@ -32,7 +32,8 @@ import { PromptState } from '../prompts/prompt-state'; import { PromptNames } from './types'; import { getAbapDeployConfig, getAppConfig } from './app-config'; import type { AbapDeployConfig } from '@sap-ux/ui5-config'; -import { replaceWebappFiles, makeValidJson, validateAndUpdateManifestUI5Version } from '../utils/file-helpers'; +import { makeValidJson } from '../utils/file-helpers'; +import { replaceWebappFiles, validateAndUpdateManifestUI5Version } from '../utils/updates'; import { fetchAppListForSelectedSystem, extractAppData, getYUIDetails } from '../prompts/prompt-helpers'; import { isValidPromptState, validateQfaJsonFile } from '../utils/validators'; import { FileName, DirName } from '@sap-ux/project-access'; diff --git a/packages/repo-app-download-sub-generator/src/translations/repo-app-download-sub-generator.i18n.json b/packages/repo-app-download-sub-generator/src/translations/repo-app-download-sub-generator.i18n.json index 59207aa75e..6dcb154ee0 100644 --- a/packages/repo-app-download-sub-generator/src/translations/repo-app-download-sub-generator.i18n.json +++ b/packages/repo-app-download-sub-generator/src/translations/repo-app-download-sub-generator.i18n.json @@ -36,7 +36,7 @@ "readManifestFailed": "Error: Failed to read manifest file", "sapAppNotDefined": "Error: sap.app not defined in the manifest file", "sourceTemplateNotSupported": "Error: Source template not supported", - "invalidManifestStructureError": "Error: Invalid manifest structure: 'sap.ui5' or dependencies are missing." + "invalidManifestStructureError": "Invalid manifest structure: 'sap.ui5' or dependencies are missing." }, "quickDeployedAppDownloadErrors": { "noAppsFound": "No application with id {{ appId }} found in the system. Please check if the application is deployed correctly" diff --git a/packages/repo-app-download-sub-generator/src/utils/constants.ts b/packages/repo-app-download-sub-generator/src/utils/constants.ts index 346df7c42c..b22222c711 100644 --- a/packages/repo-app-download-sub-generator/src/utils/constants.ts +++ b/packages/repo-app-download-sub-generator/src/utils/constants.ts @@ -1,8 +1,9 @@ import { PromptNames, type RepoAppDownloadAnswers } from '../app/types'; // Title and description for the generator -export const generatorTitle = 'UI5 ABAP Repository'; -export const generatorDescription = 'Download a basic LROP app from a UI5 ABAP Repository'; +export const generatorTitle = 'Download ADT deployed app from UI5 ABAP repository'; +export const generatorDescription = + 'Download an application that was generated with the ADT Quick Fiori Application generator'; // Name of the generator used for Fiori app download export const generatorName = '@sap-ux/repo-app-download-sub-generator'; diff --git a/packages/repo-app-download-sub-generator/src/utils/file-helpers.ts b/packages/repo-app-download-sub-generator/src/utils/file-helpers.ts index 74b870de67..e0f41627bb 100644 --- a/packages/repo-app-download-sub-generator/src/utils/file-helpers.ts +++ b/packages/repo-app-download-sub-generator/src/utils/file-helpers.ts @@ -1,12 +1,9 @@ import { adtSourceTemplateId } from './constants'; -import { join } from 'path'; import type { Editor } from 'mem-fs-editor'; -import { FileName, DirName, type Manifest } from '@sap-ux/project-access'; +import { type Manifest } from '@sap-ux/project-access'; import { t } from './i18n'; import RepoAppDownloadLogger from './logger'; import type { QfaJsonConfig } from '../app/types'; -import { isInternalFeaturesSettingEnabled } from '@sap-ux/feature-toggle'; -import { getUI5Versions } from '@sap-ux/ui5-info'; /** * @@ -48,78 +45,3 @@ export function readManifest(manifesFilePath: string, fs: Editor): Manifest { } return manifest; } - -/** - * Validates and updates the UI5 version in the manifest. - * - * - If the minUI5Version in the manifest is not found in the list of released UI5 versions, - * it updates the manifest with the closest released version. - * - If internal features are enabled, it sets the minUI5Version to '${sap.ui5.dist.version}'. - * - * @param {string} manifestFilePath - The manifest file path. - * @param fs - * @returns {Promise} - The updated manifest object. - * @throws {Error} - Throws an error if the manifest structure is invalid or no fallback version is available. - */ -export async function validateAndUpdateManifestUI5Version(manifestFilePath: string, fs: Editor): Promise { - const manifestJson = readManifest(manifestFilePath, fs); - if (!manifestJson['sap.ui5']?.dependencies) { - throw new Error(t('error.readManifestErrors.invalidManifestStructureError')); - } - - const manifestUi5Version = manifestJson['sap.ui5']?.dependencies?.minUI5Version; - const availableUI5Versions = await getUI5Versions({ includeMaintained: true }); - - // Check if the manifest version exists in the list of released versions - const ui5VersionAvailable = availableUI5Versions.find( - (ui5Version: { version: string }) => ui5Version.version === manifestUi5Version - ); - - if (ui5VersionAvailable) { - // Return the manifest as it is if the version is valid - // No changes needed - } else if (isInternalFeaturesSettingEnabled()) { - // Handle internal features setting - manifestJson['sap.ui5'].dependencies.minUI5Version = '${sap.ui5.dist.version}'; - } else { - // Handle fallback to the closest released version - const closestAvailableUi5Version = availableUI5Versions[0]?.version; - manifestJson['sap.ui5'].dependencies.minUI5Version = closestAvailableUi5Version; - } - // update manifest at extracted path - fs.writeJSON(manifestFilePath, manifestJson, undefined, 2); -} - -/** - * Replaces the specified files in the `webapp` directory with the corresponding files from the `extractedPath`. - * - * @param {string} projectPath - The path to the downloaded App. - * @param {string} extractedPath - The path from which files will be copied. - * @param {Editor} fs - The file system editor instance to modify files in memory. - */ -export async function replaceWebappFiles(projectPath: string, extractedPath: string, fs: Editor): Promise { - try { - const webappPath = join(projectPath, DirName.Webapp); - // Define the paths of the files to be replaced - const filesToReplace = [ - { webappFile: FileName.Manifest, extractedFile: FileName.Manifest }, - { webappFile: join('i18n', 'i18n.properties'), extractedFile: join('i18n', 'i18n.properties') }, - { webappFile: 'index.html', extractedFile: 'index.html' }, - { webappFile: 'Component.js', extractedFile: 'component.js' } - ]; - // Loop through each file and perform the replacement - for (const { webappFile, extractedFile } of filesToReplace) { - const webappFilePath = join(webappPath, webappFile); - const extractedFilePath = join(extractedPath, extractedFile); - - // Check if the extracted file exists before replacing - if (fs.exists(extractedFilePath)) { - fs.copy(extractedFilePath, webappFilePath); - } else { - RepoAppDownloadLogger.logger?.warn(t('warn.extractedFileNotFound', { extractedFilePath })); - } - } - } catch (error) { - RepoAppDownloadLogger.logger?.error(t('error.replaceWebappFilesError', { error })); - } -} diff --git a/packages/repo-app-download-sub-generator/src/utils/updates.ts b/packages/repo-app-download-sub-generator/src/utils/updates.ts new file mode 100644 index 0000000000..3b80953c4d --- /dev/null +++ b/packages/repo-app-download-sub-generator/src/utils/updates.ts @@ -0,0 +1,83 @@ +import { join } from 'path'; +import type { Editor } from 'mem-fs-editor'; +import { FileName, DirName } from '@sap-ux/project-access'; +import { t } from './i18n'; +import RepoAppDownloadLogger from './logger'; +import { isInternalFeaturesSettingEnabled } from '@sap-ux/feature-toggle'; +import { getUI5Versions } from '@sap-ux/ui5-info'; +import { readManifest } from './file-helpers'; + +/** + * Validates and updates the UI5 version in the manifest. + * + * - If the minUI5Version in the manifest is not found in the list of released UI5 versions, + * it updates the manifest with the closest released version. + * - If internal features are enabled, it sets the minUI5Version to '${sap.ui5.dist.version}'. + * + * @param {string} manifestFilePath - The manifest file path. + * @param fs + * @returns {Promise} - The updated manifest object. + * @throws {Error} - Throws an error if the manifest structure is invalid or no fallback version is available. + */ +export async function validateAndUpdateManifestUI5Version(manifestFilePath: string, fs: Editor): Promise { + const manifestJson = readManifest(manifestFilePath, fs); + if (!manifestJson?.['sap.ui5']?.dependencies) { + throw new Error(t('error.readManifestErrors.invalidManifestStructureError')); + } + + const manifestUi5Version = manifestJson['sap.ui5']?.dependencies?.minUI5Version; + const availableUI5Versions = await getUI5Versions({ includeMaintained: true }); + + // Check if the manifest version exists in the list of released versions + const ui5VersionAvailable = availableUI5Versions.find( + (ui5Version: { version: string }) => ui5Version.version === manifestUi5Version + ); + + if (ui5VersionAvailable) { + // Return the manifest as it is if the version is valid + // No changes needed + } else if (isInternalFeaturesSettingEnabled()) { + // Handle internal features setting + manifestJson['sap.ui5'].dependencies.minUI5Version = '${sap.ui5.dist.version}'; + } else { + // Handle fallback to the closest released version + const closestAvailableUi5Version = availableUI5Versions[0]?.version; + manifestJson['sap.ui5'].dependencies.minUI5Version = closestAvailableUi5Version; + } + // update manifest at extracted path + fs.writeJSON(manifestFilePath, manifestJson, undefined, 2); +} + +/** + * Replaces the specified files in the `webapp` directory with the corresponding files from the `extractedPath`. + * + * @param {string} projectPath - The path to the downloaded App. + * @param {string} extractedPath - The path from which files will be copied. + * @param {Editor} fs - The file system editor instance to modify files in memory. + */ +export async function replaceWebappFiles(projectPath: string, extractedPath: string, fs: Editor): Promise { + try { + const webappPath = join(projectPath, DirName.Webapp); + // Define the paths of the files to be replaced + const filesToReplace = [ + { webappFile: FileName.Manifest, extractedFile: FileName.Manifest }, + { webappFile: join('i18n', 'i18n.properties'), extractedFile: join('i18n', 'i18n.properties') }, + { webappFile: 'index.html', extractedFile: 'index.html' }, + { webappFile: 'Component.js', extractedFile: 'component.js' } + ]; + // Loop through each file and perform the replacement + for (const { webappFile, extractedFile } of filesToReplace) { + const webappFilePath = join(webappPath, webappFile); + const extractedFilePath = join(extractedPath, extractedFile); + + // Check if the extracted file exists before replacing + if (fs.exists(extractedFilePath)) { + fs.copy(extractedFilePath, webappFilePath); + } else { + RepoAppDownloadLogger.logger?.warn(t('warn.extractedFileNotFound', { extractedFilePath })); + } + } + } catch (error) { + RepoAppDownloadLogger.logger?.error(t('error.replaceWebappFilesError', { error })); + } +} diff --git a/packages/repo-app-download-sub-generator/test/app.test.ts b/packages/repo-app-download-sub-generator/test/app.test.ts index 9a8e14e48b..76d313fc59 100644 --- a/packages/repo-app-download-sub-generator/test/app.test.ts +++ b/packages/repo-app-download-sub-generator/test/app.test.ts @@ -35,10 +35,6 @@ jest.mock('../src/utils/logger', () => ({ configureLogging: jest.fn() })); -jest.mock('../src/utils/file-helpers', () => ({ - ...jest.requireActual('../src/utils/file-helpers'), - readManifest: jest.fn() -})); jest.mock('../src/utils/download-utils'); jest.mock('../src/app/app-config', () => ({ ...jest.requireActual('../src/app/app-config'), diff --git a/packages/repo-app-download-sub-generator/test/utils/file-helpers.test.ts b/packages/repo-app-download-sub-generator/test/utils/file-helpers.test.ts index 60ef845b89..72ede10be6 100644 --- a/packages/repo-app-download-sub-generator/test/utils/file-helpers.test.ts +++ b/packages/repo-app-download-sub-generator/test/utils/file-helpers.test.ts @@ -2,7 +2,7 @@ import { readManifest } from '../../src/utils/file-helpers'; import type { Editor } from 'mem-fs-editor'; import { t } from '../../src/utils/i18n'; import { adtSourceTemplateId } from '../../src/utils/constants'; -import BspAppDownloadLogger from '../../src/utils/logger'; +import RepoAppDownloadLogger from '../../src/utils/logger'; import { join } from 'path'; jest.mock('../../src/utils/logger', () => ({ @@ -40,7 +40,7 @@ describe('readManifest', () => { it('should throw an error if manifest is not found', async () => { mockReadJSON.mockReturnValue(null); readManifest(extractedProjectPath, mockFs) - expect(BspAppDownloadLogger.logger.error).toBeCalledWith(t('error.readManifestErrors.readManifestFailed')); + expect(RepoAppDownloadLogger.logger.error).toBeCalledWith(t('error.readManifestErrors.readManifestFailed')); }); it('should throw an error if "sap.app" is not defined in the manifest', async () => { @@ -50,7 +50,7 @@ describe('readManifest', () => { // Mock fs readJSON function to return a manifest without 'sap.app' mockReadJSON.mockReturnValue(invalidManifestNoSapApp); readManifest(extractedProjectPath, mockFs) - expect(BspAppDownloadLogger.logger.error).toBeCalledWith(t('error.readManifestErrors.sapAppNotDefined')); + expect(RepoAppDownloadLogger.logger.error).toBeCalledWith(t('error.readManifestErrors.sapAppNotDefined')); }); it('should throw an error if the sourceTemplate.id is not supported', async () => { @@ -65,6 +65,6 @@ describe('readManifest', () => { // Mock fs readJSON function to return a manifest with an unsupported sourceTemplate.id mockReadJSON.mockReturnValue(invalidManifestWrongTemplate); readManifest(extractedProjectPath, mockFs) - expect(BspAppDownloadLogger.logger.error).toBeCalledWith(t('error.readManifestErrors.sourceTemplateNotSupported')); + expect(RepoAppDownloadLogger.logger.error).toBeCalledWith(t('error.readManifestErrors.sourceTemplateNotSupported')); }); }); diff --git a/packages/repo-app-download-sub-generator/test/utils/updates.test.ts b/packages/repo-app-download-sub-generator/test/utils/updates.test.ts new file mode 100644 index 0000000000..ad14f47325 --- /dev/null +++ b/packages/repo-app-download-sub-generator/test/utils/updates.test.ts @@ -0,0 +1,189 @@ +import { validateAndUpdateManifestUI5Version, replaceWebappFiles } from '../../src/utils/updates'; +import type { Editor } from 'mem-fs-editor'; +import { t } from '../../src/utils/i18n'; +import { getUI5Versions } from '@sap-ux/ui5-info'; +import { isInternalFeaturesSettingEnabled } from '@sap-ux/feature-toggle'; +import type { Manifest } from '@sap-ux/project-access'; +import { readManifest } from '../../src/utils/file-helpers'; +import { join } from 'path'; +import { FileName, DirName } from '@sap-ux/project-access'; +import RepoAppDownloadLogger from '../../src/utils/logger'; + +jest.mock('@sap-ux/ui5-info', () => ({ + ...jest.requireActual('@sap-ux/ui5-info'), + getUI5Versions: jest.fn() +})); + +jest.mock('@sap-ux/feature-toggle', () => ({ + ...jest.requireActual('@sap-ux/feature-toggle'), + isInternalFeaturesSettingEnabled: jest.fn() +})); + +jest.mock('../../src/utils/file-helpers', () => ({ + ...jest.requireActual('../../src/utils/file-helpers'), + readManifest: jest.fn() +})); + +jest.mock('../../src/utils/logger', () => ({ + logger: { + error: jest.fn(), + warn: jest.fn() + } +})); + +describe('validateAndUpdateManifestUI5Version', () => { + let fs: jest.Mocked; + + beforeEach(() => { + fs = { + writeJSON: jest.fn(), + exists: jest.fn(), + readJSON: jest.fn(), + } as unknown as jest.Mocked; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should throw an error if manifest structure is invalid', async () => { + (readManifest as jest.Mock).mockReturnValue({ + 'sap.ui5': {} + } as unknown as Manifest); + (getUI5Versions as jest.Mock).mockResolvedValue([{ version: '1.90.0' }]); + + await expect(validateAndUpdateManifestUI5Version('path/to/manifest.json', fs)).rejects.toThrow( + t('error.readManifestErrors.invalidManifestStructureError') + ); + }); + + it('should not modify the manifest if minUI5Version is valid', async () => { + const manifest = { + 'sap.ui5': { + dependencies: { + minUI5Version: '1.90.0', + } + } + }; + (readManifest as jest.Mock).mockReturnValue(manifest); + (getUI5Versions as jest.Mock).mockResolvedValue([{ version: '1.90.0' }]); + + await validateAndUpdateManifestUI5Version('path/to/manifest.json', fs); + expect(fs.writeJSON).toHaveBeenCalledWith('path/to/manifest.json', manifest, undefined, 2); + }); + + it('should update minUI5Version to internal version if internal features are enabled', async () => { + const manifest = { + 'sap.ui5': { + dependencies: { + minUI5Version: '1.80.0', + }, + }, + }; + (readManifest as jest.Mock).mockReturnValue(manifest); + (getUI5Versions as jest.Mock).mockResolvedValue([{ version: '1.90.0' }]); + (isInternalFeaturesSettingEnabled as jest.Mock).mockReturnValue(true); + + await validateAndUpdateManifestUI5Version('path/to/manifest.json', fs); + + expect(fs.writeJSON).toHaveBeenCalledWith( + 'path/to/manifest.json', + { + 'sap.ui5': { + dependencies: { + minUI5Version: '${sap.ui5.dist.version}', + }, + }, + }, + undefined, + 2 + ); + }); + + it('should update minUI5Version to the closest available version if invalid', async () => { + const manifest = { + 'sap.ui5': { + dependencies: { + minUI5Version: '1.70.0', + }, + }, + }; + (readManifest as jest.Mock).mockReturnValue(manifest); + (getUI5Versions as jest.Mock).mockResolvedValue([{ version: '1.90.0' }]); + (isInternalFeaturesSettingEnabled as jest.Mock).mockReturnValue(false); + + await validateAndUpdateManifestUI5Version('path/to/manifest.json', fs); + + expect(fs.writeJSON).toHaveBeenCalledWith( + 'path/to/manifest.json', + { + 'sap.ui5': { + dependencies: { + minUI5Version: '1.90.0', + }, + }, + }, + undefined, + 2 + ); + }); +}); + +describe('replaceWebappFiles', () => { + let fs: jest.Mocked; + + beforeEach(() => { + fs = { + exists: jest.fn(), + copy: jest.fn() + } as unknown as jest.Mocked; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should copy files from extractedPath to webappPath if they exist', async () => { + const projectPath = '/project'; + const extractedPath = '/extracted'; + const webappPath = join(`${projectPath}/${DirName.Webapp}`); + + // Mock fs.exists to return true for all files + fs.exists.mockReturnValue(true); + await replaceWebappFiles(projectPath, extractedPath, fs); + + // Verify that fs.copy is called for each file + expect(fs.copy).toHaveBeenCalledWith(join(`${extractedPath}/${FileName.Manifest}`), join(`${webappPath}/${FileName.Manifest}`)); + expect(fs.copy).toHaveBeenCalledWith(join(`${extractedPath}/i18n/i18n.properties`), join(`${webappPath}/i18n/i18n.properties`)); + expect(fs.copy).toHaveBeenCalledWith(join(`${extractedPath}/index.html`), join(`${webappPath}/index.html`)); + expect(fs.copy).toHaveBeenCalledWith(join(`${extractedPath}/component.js`), join(`${webappPath}/Component.js`)); + }); + + it('should log a warning if a file does not exist in extractedPath', async () => { + const projectPath = '/project'; + const extractedPath = '/extracted'; + const webappPath = join(`${projectPath}/${DirName.Webapp}`); + + // Mock fs.exists to return false for one file + fs.exists.mockImplementation((filePath) => filePath !== join(`${extractedPath}/${FileName.Manifest}`)); + await replaceWebappFiles(projectPath, extractedPath, fs); + + // Verify that fs.copy is not called for the missing file + expect(fs.copy).not.toHaveBeenCalledWith(join(`${extractedPath}/${FileName.Manifest}`), join(`${webappPath}/${FileName.Manifest}`)); + expect(RepoAppDownloadLogger.logger?.warn).toHaveBeenCalledWith( + t('warn.extractedFileNotFound', { extractedFilePath: join(`${extractedPath}/${FileName.Manifest}`) }) + ); + }); + + it('should log an error if an exception occurs', async () => { + const projectPath = '/project'; + const extractedPath = '/extracted'; + fs.exists.mockImplementation(() => { + throw new Error('Test error'); + }); + await replaceWebappFiles(projectPath, extractedPath, fs); + expect(RepoAppDownloadLogger.logger?.error).toHaveBeenCalledWith( + t('error.replaceWebappFilesError', { error: new Error('Test error') }) + ); + }); +}); \ No newline at end of file From dd1ab6612823b8ba177f5fd7fe4230d026cf9355 Mon Sep 17 00:00:00 2001 From: I743583 Date: Mon, 14 Apr 2025 10:14:57 +0100 Subject: [PATCH 29/41] add fioriAppSourcetemplateId to the fiori supported apps --- .../repo-app-download-sub-generator/src/app/app-config.ts | 4 ++-- .../repo-app-download-sub-generator/src/utils/constants.ts | 2 ++ .../repo-app-download-sub-generator/test/app-config.test.ts | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/repo-app-download-sub-generator/src/app/app-config.ts b/packages/repo-app-download-sub-generator/src/app/app-config.ts index 56ceee6e28..947532480c 100644 --- a/packages/repo-app-download-sub-generator/src/app/app-config.ts +++ b/packages/repo-app-download-sub-generator/src/app/app-config.ts @@ -5,7 +5,7 @@ import type { Editor } from 'mem-fs-editor'; import { t } from '../utils/i18n'; import type { AppInfo, QfaJsonConfig } from '../app/types'; import { readManifest } from '../utils/file-helpers'; -import { adtSourceTemplateId } from '../utils/constants'; +import { fioriAppSourcetemplateId } from '../utils/constants'; import { PromptState } from '../prompts/prompt-state'; import type { AbapDeployConfig } from '@sap-ux/ui5-config'; import RepoAppDownloadLogger from '../utils/logger'; @@ -90,7 +90,7 @@ export async function getAppConfig( title: app.title, description: app.description, sourceTemplate: { - id: adtSourceTemplateId + id: fioriAppSourcetemplateId }, projectType: 'EDMXBackend', flpAppId: `${app.appId.replace(/[-_.#]/g, '')}-tile` diff --git a/packages/repo-app-download-sub-generator/src/utils/constants.ts b/packages/repo-app-download-sub-generator/src/utils/constants.ts index b22222c711..fea6cceb8c 100644 --- a/packages/repo-app-download-sub-generator/src/utils/constants.ts +++ b/packages/repo-app-download-sub-generator/src/utils/constants.ts @@ -9,6 +9,8 @@ export const generatorDescription = export const generatorName = '@sap-ux/repo-app-download-sub-generator'; // The source template ID used for filtering the apps in the repository export const adtSourceTemplateId = '@sap.adt.sevicebinding.deploy:lrop'; +// The source template ID used for Fiori app generation +export const fioriAppSourcetemplateId = '@sap/generator-fiori:lrop'; // The name of the QFA JSON file provided with the downloaded app, containing all user inputs. export const qfaJsonFileName = 'qfa.json'; diff --git a/packages/repo-app-download-sub-generator/test/app-config.test.ts b/packages/repo-app-download-sub-generator/test/app-config.test.ts index c0a52d9a63..f6071ab856 100644 --- a/packages/repo-app-download-sub-generator/test/app-config.test.ts +++ b/packages/repo-app-download-sub-generator/test/app-config.test.ts @@ -7,7 +7,7 @@ import { PromptState } from '../src/prompts/prompt-state'; import type { AppInfo, QfaJsonConfig } from '../src/app/types'; import { readManifest } from '../src/utils/file-helpers'; import { t } from '../src/utils/i18n'; -import { adtSourceTemplateId } from '../src/utils/constants'; +import { fioriAppSourcetemplateId } from '../src/utils/constants'; import RepoAppDownloadLogger from '../src/utils/logger'; import { TestFixture } from './fixtures'; import { join } from 'path'; @@ -57,7 +57,7 @@ describe('getAppConfig', () => { title: mockApp.title, description: mockApp.description, flpAppId: `${mockApp.appId}-tile`, - sourceTemplate: { id: adtSourceTemplateId }, + sourceTemplate: { id: fioriAppSourcetemplateId }, projectType: 'EDMXBackend' }, package: { From a14ebf10bce37d6b89fef9bdbbd7ddad4f27f2a5 Mon Sep 17 00:00:00 2001 From: I743583 Date: Mon, 14 Apr 2025 10:56:19 +0100 Subject: [PATCH 30/41] fix source template id tests --- .../repo-app-download-sub-generator.i18n.json | 2 +- .../src/utils/updates.ts | 5 +++- .../test/app.test.ts | 19 +++++++------ .../test/utils/updates.test.ts | 28 ++++++++++++++++++- .../test/utils/validators.test.ts | 2 +- 5 files changed, 44 insertions(+), 12 deletions(-) diff --git a/packages/repo-app-download-sub-generator/src/translations/repo-app-download-sub-generator.i18n.json b/packages/repo-app-download-sub-generator/src/translations/repo-app-download-sub-generator.i18n.json index 6dcb154ee0..95cf812f78 100644 --- a/packages/repo-app-download-sub-generator/src/translations/repo-app-download-sub-generator.i18n.json +++ b/packages/repo-app-download-sub-generator/src/translations/repo-app-download-sub-generator.i18n.json @@ -36,7 +36,7 @@ "readManifestFailed": "Error: Failed to read manifest file", "sapAppNotDefined": "Error: sap.app not defined in the manifest file", "sourceTemplateNotSupported": "Error: Source template not supported", - "invalidManifestStructureError": "Invalid manifest structure: 'sap.ui5' or dependencies are missing." + "invalidManifestStructureError": "Invalid manifest structure: 'sap.ui5' or 'sap.app' are missing." }, "quickDeployedAppDownloadErrors": { "noAppsFound": "No application with id {{ appId }} found in the system. Please check if the application is deployed correctly" diff --git a/packages/repo-app-download-sub-generator/src/utils/updates.ts b/packages/repo-app-download-sub-generator/src/utils/updates.ts index 3b80953c4d..aa7959c741 100644 --- a/packages/repo-app-download-sub-generator/src/utils/updates.ts +++ b/packages/repo-app-download-sub-generator/src/utils/updates.ts @@ -6,6 +6,7 @@ import RepoAppDownloadLogger from './logger'; import { isInternalFeaturesSettingEnabled } from '@sap-ux/feature-toggle'; import { getUI5Versions } from '@sap-ux/ui5-info'; import { readManifest } from './file-helpers'; +import { fioriAppSourcetemplateId } from './constants'; /** * Validates and updates the UI5 version in the manifest. @@ -21,7 +22,8 @@ import { readManifest } from './file-helpers'; */ export async function validateAndUpdateManifestUI5Version(manifestFilePath: string, fs: Editor): Promise { const manifestJson = readManifest(manifestFilePath, fs); - if (!manifestJson?.['sap.ui5']?.dependencies) { + if (!manifestJson?.['sap.ui5']?.dependencies || !manifestJson?.['sap.app']?.sourceTemplate) { + // Check if the manifest structure is valid) { throw new Error(t('error.readManifestErrors.invalidManifestStructureError')); } @@ -44,6 +46,7 @@ export async function validateAndUpdateManifestUI5Version(manifestFilePath: stri const closestAvailableUi5Version = availableUI5Versions[0]?.version; manifestJson['sap.ui5'].dependencies.minUI5Version = closestAvailableUi5Version; } + manifestJson['sap.app'].sourceTemplate.id = fioriAppSourcetemplateId; // update manifest at extracted path fs.writeJSON(manifestFilePath, manifestJson, undefined, 2); } diff --git a/packages/repo-app-download-sub-generator/test/app.test.ts b/packages/repo-app-download-sub-generator/test/app.test.ts index 76d313fc59..7319c72094 100644 --- a/packages/repo-app-download-sub-generator/test/app.test.ts +++ b/packages/repo-app-download-sub-generator/test/app.test.ts @@ -9,11 +9,11 @@ import { TestFixture } from './fixtures'; import { getAppConfig } from '../src/app/app-config'; import { OdataVersion } from '@sap-ux/odata-service-inquirer'; import { TemplateType, type FioriElementsApp, type LROPSettings } from '@sap-ux/fiori-elements-writer'; -import { adtSourceTemplateId, extractedFilePath } from '../src/utils/constants'; +import { adtSourceTemplateId, fioriAppSourcetemplateId, extractedFilePath } from '../src/utils/constants'; import { removeSync } from 'fs-extra'; import { isValidPromptState } from '../src/utils/validators'; import { hostEnvironment, sendTelemetry } from '@sap-ux/fiori-generator-shared'; -import { FileName, DirName } from '@sap-ux/project-access'; +import { FileName, DirName, type Manifest } from '@sap-ux/project-access'; import RepoAppDownloadLogger from '../src/utils/logger'; import { t } from '../src/utils/i18n'; import { type AbapServiceProvider } from '@sap-ux/axios-extension'; @@ -182,12 +182,15 @@ function verifyGeneratedFiles(testOutputDir: string, appId: string, testFixtureD const filePath = join(projectPath, file); expect(fs.existsSync(filePath)).toBe(true); }); - - const projectManifest = JSON.stringify( - JSON.parse(fs.readFileSync(join(projectPath, DirName.Webapp, FileName.Manifest), 'utf-8')) - ); - const extractedManifest = JSON.stringify( - JSON.parse(fs.readFileSync(join(testFixtureDir, FileName.Manifest), 'utf-8')) + // after converting to fiori app, manifest will be updated with fiori app source template id + const extractedManifest = JSON.parse( + fs.readFileSync(join(testFixtureDir, FileName.Manifest), 'utf-8') + ) as Manifest; + if (extractedManifest && extractedManifest['sap.app'] && extractedManifest['sap.app'].sourceTemplate) { + extractedManifest['sap.app'].sourceTemplate.id = fioriAppSourcetemplateId; + } + const projectManifest = JSON.parse( + fs.readFileSync(join(projectPath, DirName.Webapp, FileName.Manifest), 'utf-8') ); expect(projectManifest).toEqual(extractedManifest); expect(fs.readFileSync(join(projectPath, DirName.Webapp, 'i18n', 'i18n.properties'), 'utf-8')).toBe( diff --git a/packages/repo-app-download-sub-generator/test/utils/updates.test.ts b/packages/repo-app-download-sub-generator/test/utils/updates.test.ts index ad14f47325..1d5a609c66 100644 --- a/packages/repo-app-download-sub-generator/test/utils/updates.test.ts +++ b/packages/repo-app-download-sub-generator/test/utils/updates.test.ts @@ -8,6 +8,7 @@ import { readManifest } from '../../src/utils/file-helpers'; import { join } from 'path'; import { FileName, DirName } from '@sap-ux/project-access'; import RepoAppDownloadLogger from '../../src/utils/logger'; +import { fioriAppSourcetemplateId } from '../../src/utils/constants'; jest.mock('@sap-ux/ui5-info', () => ({ ...jest.requireActual('@sap-ux/ui5-info'), @@ -63,6 +64,11 @@ describe('validateAndUpdateManifestUI5Version', () => { dependencies: { minUI5Version: '1.90.0', } + }, + 'sap.app': { + sourceTemplate: { + id: fioriAppSourcetemplateId + } } }; (readManifest as jest.Mock).mockReturnValue(manifest); @@ -79,6 +85,11 @@ describe('validateAndUpdateManifestUI5Version', () => { minUI5Version: '1.80.0', }, }, + 'sap.app': { + sourceTemplate: { + id: fioriAppSourcetemplateId + } + } }; (readManifest as jest.Mock).mockReturnValue(manifest); (getUI5Versions as jest.Mock).mockResolvedValue([{ version: '1.90.0' }]); @@ -94,6 +105,11 @@ describe('validateAndUpdateManifestUI5Version', () => { minUI5Version: '${sap.ui5.dist.version}', }, }, + 'sap.app': { + sourceTemplate: { + id: fioriAppSourcetemplateId + } + } }, undefined, 2 @@ -105,8 +121,13 @@ describe('validateAndUpdateManifestUI5Version', () => { 'sap.ui5': { dependencies: { minUI5Version: '1.70.0', - }, + } }, + 'sap.app': { + sourceTemplate: { + id: fioriAppSourcetemplateId + } + } }; (readManifest as jest.Mock).mockReturnValue(manifest); (getUI5Versions as jest.Mock).mockResolvedValue([{ version: '1.90.0' }]); @@ -122,6 +143,11 @@ describe('validateAndUpdateManifestUI5Version', () => { minUI5Version: '1.90.0', }, }, + 'sap.app': { + sourceTemplate: { + id: fioriAppSourcetemplateId + } + } }, undefined, 2 diff --git a/packages/repo-app-download-sub-generator/test/utils/validators.test.ts b/packages/repo-app-download-sub-generator/test/utils/validators.test.ts index 1fc1b360af..5322f5bb80 100644 --- a/packages/repo-app-download-sub-generator/test/utils/validators.test.ts +++ b/packages/repo-app-download-sub-generator/test/utils/validators.test.ts @@ -23,7 +23,7 @@ describe('validateQfaJsonFile', () => { semanticObject: 'semanticObject', action: 'action', title: 'title' - }, + } }; afterEach(() => { From be5b56d203c18cd3d1a36784e797b0bc4a37e0cc Mon Sep 17 00:00:00 2001 From: I743583 Date: Mon, 14 Apr 2025 13:00:20 +0100 Subject: [PATCH 31/41] fix read me and overwrite yeoman --- packages/repo-app-download-sub-generator/src/app/index.ts | 4 ++++ .../translations/repo-app-download-sub-generator.i18n.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/repo-app-download-sub-generator/src/app/index.ts b/packages/repo-app-download-sub-generator/src/app/index.ts index d7af356c92..40e6496e2b 100644 --- a/packages/repo-app-download-sub-generator/src/app/index.ts +++ b/packages/repo-app-download-sub-generator/src/app/index.ts @@ -37,6 +37,7 @@ import { replaceWebappFiles, validateAndUpdateManifestUI5Version } from '../util import { fetchAppListForSelectedSystem, extractAppData, getYUIDetails } from '../prompts/prompt-helpers'; import { isValidPromptState, validateQfaJsonFile } from '../utils/validators'; import { FileName, DirName } from '@sap-ux/project-access'; +import type { YeomanEnvironment } from '@sap-ux/fiori-generator-shared'; /** * Generator class for downloading a basic app from a repository. @@ -92,6 +93,9 @@ export default class extends Generator { * Initialises necessary settings and telemetry for the generator. */ public async initializing(): Promise { + if ((this.env as unknown as YeomanEnvironment).conflicter) { + (this.env as unknown as YeomanEnvironment).conflicter.force = this.options.force ?? true; + } // Initialise telemetry settings await TelemetryHelper.initTelemetrySettings({ consumerModule: { diff --git a/packages/repo-app-download-sub-generator/src/translations/repo-app-download-sub-generator.i18n.json b/packages/repo-app-download-sub-generator/src/translations/repo-app-download-sub-generator.i18n.json index 95cf812f78..4f5a6ba549 100644 --- a/packages/repo-app-download-sub-generator/src/translations/repo-app-download-sub-generator.i18n.json +++ b/packages/repo-app-download-sub-generator/src/translations/repo-app-download-sub-generator.i18n.json @@ -58,7 +58,7 @@ } }, "readMe": { - "appDescription" : "This application was converted from an ABAP basic app that was deployed from ADT", + "appDescription" : "This application was converted from a SAP Fiori app that was deployed from ADT using the ADT Quick Fiori Application generator", "launchText": "In order to launch the generated app, simply run the following from the generated app root folder:\n\n```\n npm start\n```" }, "info": { From d9dabfd8bcd8ac881cba38f0b0a0210f96863403 Mon Sep 17 00:00:00 2001 From: I743583 Date: Mon, 14 Apr 2025 13:18:03 +0100 Subject: [PATCH 32/41] sonar fix --- .../repo-app-download-sub-generator/src/app/index.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/repo-app-download-sub-generator/src/app/index.ts b/packages/repo-app-download-sub-generator/src/app/index.ts index 40e6496e2b..dede196fef 100644 --- a/packages/repo-app-download-sub-generator/src/app/index.ts +++ b/packages/repo-app-download-sub-generator/src/app/index.ts @@ -3,12 +3,18 @@ import RepoAppDownloadLogger from '../utils/logger'; import { AppWizard, Prompts, MessageType } from '@sap-devx/yeoman-ui-types'; import { isInternalFeaturesSettingEnabled } from '@sap-ux/feature-toggle'; import type { Logger } from '@sap-ux/logger'; -import { sendTelemetry, TelemetryHelper } from '@sap-ux/fiori-generator-shared'; import { generatorTitle, extractedFilePath, generatorName, defaultAnswers, qfaJsonFileName } from '../utils/constants'; import { t } from '../utils/i18n'; import { downloadApp } from '../utils/download-utils'; import { EventName } from '../telemetryEvents'; -import { getDefaultTargetFolder, generateReadMe, type ReadMe } from '@sap-ux/fiori-generator-shared'; +import { + getDefaultTargetFolder, + generateReadMe, + type ReadMe, + type YeomanEnvironment, + sendTelemetry, + TelemetryHelper +} from '@sap-ux/fiori-generator-shared'; import type { RepoAppDownloadOptions, RepoAppDownloadAnswers, @@ -37,7 +43,6 @@ import { replaceWebappFiles, validateAndUpdateManifestUI5Version } from '../util import { fetchAppListForSelectedSystem, extractAppData, getYUIDetails } from '../prompts/prompt-helpers'; import { isValidPromptState, validateQfaJsonFile } from '../utils/validators'; import { FileName, DirName } from '@sap-ux/project-access'; -import type { YeomanEnvironment } from '@sap-ux/fiori-generator-shared'; /** * Generator class for downloading a basic app from a repository. From f8f3957890da4d01e544cc0135937a66b085a9e2 Mon Sep 17 00:00:00 2001 From: I743583 Date: Mon, 14 Apr 2025 18:30:47 +0100 Subject: [PATCH 33/41] changing workflow for quick deploy app --- .../src/app/index.ts | 27 +-- .../src/app/types.ts | 7 +- .../src/prompts/prompt-helpers.ts | 1 + .../src/prompts/prompts.ts | 198 +++++++++++++++--- .../repo-app-download-sub-generator.i18n.json | 2 +- 5 files changed, 176 insertions(+), 59 deletions(-) diff --git a/packages/repo-app-download-sub-generator/src/app/index.ts b/packages/repo-app-download-sub-generator/src/app/index.ts index dede196fef..602ac700a3 100644 --- a/packages/repo-app-download-sub-generator/src/app/index.ts +++ b/packages/repo-app-download-sub-generator/src/app/index.ts @@ -123,36 +123,15 @@ export default class extends Generator { if (quickDeployedAppConfig?.appId) { // Handle quick deployed app download where prompts for system selection and app selection are not displayed // Only target folder prompt is shown - await this._handleQuickDeployedAppDownload(quickDeployedAppConfig, targetFolder); + this.answers.targetFolder = targetFolder; + this.answers.systemSelection = PromptState.systemSelection; + this.answers.selectedApp = answers.selectedApp; } else { // Handle app download where prompts for system selection and app selection are shown Object.assign(this.answers, answers); } } - /** - * - * @param quickDeployedAppConfig - The configuration for the quick deployed app. - * @param targetFolder - The target folder where the app will be downloaded. - */ - private async _handleQuickDeployedAppDownload( - quickDeployedAppConfig: QuickDeployedAppConfig, - targetFolder: string - ): Promise { - const appList = await fetchAppListForSelectedSystem( - quickDeployedAppConfig.serviceProvider, - quickDeployedAppConfig.appId - ); - if (!appList.length) { - throw new Error( - t('error.quickDeployedAppDownloadErrors.noAppsFound', { appId: quickDeployedAppConfig.appId }) - ); - } - this.answers.selectedApp = extractAppData(appList[0]).value; - this.answers.targetFolder = targetFolder; - this.answers.systemSelection = PromptState.systemSelection; - } - /** * Writes the configuration files for the project, including deployment config, and README. */ diff --git a/packages/repo-app-download-sub-generator/src/app/types.ts b/packages/repo-app-download-sub-generator/src/app/types.ts index f7eef94e5f..5c9b5dc08e 100644 --- a/packages/repo-app-download-sub-generator/src/app/types.ts +++ b/packages/repo-app-download-sub-generator/src/app/types.ts @@ -17,7 +17,12 @@ export interface QuickDeployedAppConfig { /** appUrl is the URL pointing to the application */ appUrl?: string; /** service provider is used to identify the system from which the app is downloaded from. */ - serviceProvider: AbapServiceProvider; + serviceProviderInfo?: { + /** system url */ + serviceUrl: string; + /** system name to pre populate App ID prompt */ + name: string; + }; } /** * Options for downloading an application from repository. diff --git a/packages/repo-app-download-sub-generator/src/prompts/prompt-helpers.ts b/packages/repo-app-download-sub-generator/src/prompts/prompt-helpers.ts index 5096b062a7..9f0654e0ab 100644 --- a/packages/repo-app-download-sub-generator/src/prompts/prompt-helpers.ts +++ b/packages/repo-app-download-sub-generator/src/prompts/prompt-helpers.ts @@ -79,6 +79,7 @@ async function getAppList(provider: AbapServiceProvider, appId?: string): Promis 'sap.app/id': appId } : appListSearchParams; + console.log("--- provider ---", provider); return await provider.getAppIndex().search(searchParams, appListResultFields); } catch (error) { RepoAppDownloadLogger.logger?.error(t('error.applicationListFetchError', { error: error.message })); diff --git a/packages/repo-app-download-sub-generator/src/prompts/prompts.ts b/packages/repo-app-download-sub-generator/src/prompts/prompts.ts index 4f4379917e..f0841cc839 100644 --- a/packages/repo-app-download-sub-generator/src/prompts/prompts.ts +++ b/packages/repo-app-download-sub-generator/src/prompts/prompts.ts @@ -1,5 +1,5 @@ import type { AppIndex, AbapServiceProvider } from '@sap-ux/axios-extension'; -import { getSystemSelectionQuestions } from '@sap-ux/odata-service-inquirer'; +import { getSystemSelectionQuestions, promptNames } from '@sap-ux/odata-service-inquirer'; import type { RepoAppDownloadAnswers, RepoAppDownloadQuestions, QuickDeployedAppConfig, AppInfo } from '../app/types'; import { PromptNames } from '../app/types'; import { t } from '../utils/i18n'; @@ -7,6 +7,7 @@ import type { FileBrowserQuestion } from '@sap-ux/inquirer-common'; import { validateFioriAppTargetFolder } from '@sap-ux/project-input-validator'; import { PromptState } from './prompt-state'; import { fetchAppListForSelectedSystem, formatAppChoices } from './prompt-helpers'; +import { ListQuestion } from 'inquirer'; /** * Gets the target folder selection prompt. @@ -44,48 +45,179 @@ const getTargetFolderPrompt = (appRootPath?: string, appId?: string): FileBrowse } as FileBrowserQuestion; }; +// function getDefaultStuff (getDefaultStuff: QuickDeployedAppConfig | undefined): { defaultSystem: string; defaultAppId: string } { +// let defaultSystem: string = ''; +// let defaultAppId: string = ''; +// if(getDefaultStuff?.appId && getDefaultStuff?.serviceProvider) { +// // If appId is provided but serviceProvider is not, set the serviceProvider to the default value +// //defaultSystem = getDefaultStuff.serviceProvider.; +// defaultAppId = getDefaultStuff.appId; +// defaultSystem = getDefaultStuff.serviceProvider.name; +// } +// return { defaultSystem, defaultAppId}; +// } + +// /** +// * Retrieves questions for selecting system, app lists and target path where app will be generated. +// * +// * @param {string} [appRootPath] - The root path of the application. +// * @param {QuickDeployedAppConfig} [quickDeployedAppConfig] - quick deployed app config. +// * @returns {Promise} A list of questions for user interaction. +// */ +// export async function getPrompts( +// appRootPath?: string, +// quickDeployedAppConfig?: QuickDeployedAppConfig +// ): Promise { +// PromptState.reset(); + +// const { defaultSystem} = getDefaultStuff(quickDeployedAppConfig) +// let systemQuestions = await getSystemSelectionQuestions( { serviceSelection: { hide: true } }, false); +// if (quickDeployedAppConfig?.appId) { +// const filteredSystemQuestion = systemQuestions.prompts.find(p => p.name === promptNames.systemSelection); +// let defaultIndex = -1; +// if (filteredSystemQuestion) { +// //const choices = (filteredSystemQuestion as ListQuestion).choices; +// // Filter the choices based on the default system +// let choices = (filteredSystemQuestion as ListQuestion).choices; +// if (Array.isArray(choices)) { +// // Filter the choices based on the default system +// defaultIndex = choices.findIndex((choice: any) => choice.value.system.name === defaultSystem); +// filteredSystemQuestion.default = defaultIndex !== -1 ? defaultIndex : undefined; // Assign default index if found +// systemQuestions.prompts = [filteredSystemQuestion]; +// } + +// filteredSystemQuestion.default = defaultIndex !== -1 ? defaultIndex : undefined; // Assign default index if found +// systemQuestions.prompts = [filteredSystemQuestion]; +// } +// } + + +// let appList: AppIndex = []; +// const appSelectionPrompt = [ +// { +// when: async (answers: RepoAppDownloadAnswers): Promise => { +// if (answers[PromptNames.systemSelection]) { +// debugger; +// if(quickDeployedAppConfig?.appId) { +// appList = await fetchAppListForSelectedSystem( +// systemQuestions.answers.connectedSystem?.serviceProvider as AbapServiceProvider, +// quickDeployedAppConfig.appId +// ); +// } +// else { +// appList = await fetchAppListForSelectedSystem( +// systemQuestions.answers.connectedSystem?.serviceProvider as AbapServiceProvider +// ); +// } +// } +// // display app selection prompt only if user has selected a system +// return !!systemQuestions.answers.connectedSystem?.serviceProvider; +// }, +// type: 'list', +// name: PromptNames.selectedApp, +// default: () => quickDeployedAppConfig?.appId ? 0 : undefined, +// guiOptions: { +// mandatory: !!appList.length, +// breadcrumb: t('prompts.appSelection.breadcrumb') +// }, +// message: t('prompts.appSelection.message'), +// choices: (): { name: string; value: AppInfo }[] => (appList.length ? formatAppChoices(appList) : []), +// validate: (): string | boolean => (appList.length ? true : t('prompts.appSelection.noAppsDeployed')) +// } +// ]; + +// const targetFolderPrompts = getTargetFolderPrompt(appRootPath, quickDeployedAppConfig?.appId); +// return [...systemQuestions.prompts, ...appSelectionPrompt, targetFolderPrompts] as RepoAppDownloadQuestions[]; +// } + + +/** + * Extracts default system from the quick deployed app configuration. + * + * @param {QuickDeployedAppConfig | undefined} quickDeployedAppConfig - The quick deployed app configuration. + * @returns {string} The default system. + */ +function extractDefaultSystem(quickDeployedAppConfig: QuickDeployedAppConfig | undefined): string { + let defaultSystem = ''; + + if (quickDeployedAppConfig?.appId && quickDeployedAppConfig?.serviceProviderInfo) { + defaultSystem = quickDeployedAppConfig.serviceProviderInfo.name; + } + + return defaultSystem; +} + /** - * Retrieves questions for selecting system, app lists and target path where app will be generated. + * Retrieves prompts for selecting a system, app list, and target folder where the app will be generated. * * @param {string} [appRootPath] - The root path of the application. - * @param {QuickDeployedAppConfig} [quickDeployedAppConfig] - quick deployed app config. - * @returns {Promise} A list of questions for user interaction. + * @param {QuickDeployedAppConfig} [quickDeployedAppConfig] - The quick deployed app configuration. + * @returns {Promise} A list of prompts for user interaction. */ export async function getPrompts( appRootPath?: string, quickDeployedAppConfig?: QuickDeployedAppConfig ): Promise { - PromptState.reset(); - // If quickDeployedAppConfig is provided, return only the target folder prompt - if (quickDeployedAppConfig?.appId) { - return [getTargetFolderPrompt(appRootPath, quickDeployedAppConfig.appId)] as RepoAppDownloadQuestions[]; - } + try { + PromptState.reset(); + + const defaultSystem = extractDefaultSystem(quickDeployedAppConfig); + const systemQuestions = await getSystemSelectionQuestions({ serviceSelection: { hide: true } }, false); + + // Filter system questions and set default system if applicable + if (quickDeployedAppConfig?.appId) { + const filteredSystemQuestion = systemQuestions.prompts.find(p => p.name === promptNames.systemSelection); + + if (filteredSystemQuestion) { + const choices = (filteredSystemQuestion as ListQuestion).choices; - const systemQuestions = await getSystemSelectionQuestions({ serviceSelection: { hide: true } }, false); - let appList: AppIndex = []; - const appSelectionPrompt = [ - { - when: async (answers: RepoAppDownloadAnswers): Promise => { - if (answers[PromptNames.systemSelection]) { - appList = await fetchAppListForSelectedSystem( - systemQuestions.answers.connectedSystem?.serviceProvider as AbapServiceProvider - ); + if (Array.isArray(choices)) { + const defaultIndex = choices.findIndex((choice: any) => choice.value.system.name === defaultSystem); + filteredSystemQuestion.default = defaultIndex !== -1 ? defaultIndex : undefined; + systemQuestions.prompts = [filteredSystemQuestion]; } - // display app selection prompt only if user has selected a system - return !!systemQuestions.answers.connectedSystem?.serviceProvider; - }, - type: 'list', - name: PromptNames.selectedApp, - guiOptions: { - mandatory: !!appList.length, - breadcrumb: t('prompts.appSelection.breadcrumb') - }, - message: t('prompts.appSelection.message'), - choices: (): { name: string; value: AppInfo }[] => (appList.length ? formatAppChoices(appList) : []), - validate: (): string | boolean => (appList.length ? true : t('prompts.appSelection.noAppsDeployed')) + } } - ]; - const targetFolderPrompts = getTargetFolderPrompt(appRootPath, quickDeployedAppConfig?.appId); - return [...systemQuestions.prompts, ...appSelectionPrompt, targetFolderPrompts] as RepoAppDownloadQuestions[]; + let appList: AppIndex = []; + const appSelectionPrompt = [ + { + when: async (answers: RepoAppDownloadAnswers): Promise => { + if (answers[PromptNames.systemSelection]) { + if (quickDeployedAppConfig?.appId) { + appList = await fetchAppListForSelectedSystem( + systemQuestions.answers.connectedSystem?.serviceProvider as AbapServiceProvider, + quickDeployedAppConfig.appId + ); + } else { + appList = await fetchAppListForSelectedSystem( + systemQuestions.answers.connectedSystem?.serviceProvider as AbapServiceProvider + ); + } + } + return !!systemQuestions.answers.connectedSystem?.serviceProvider; + }, + type: 'list', + name: PromptNames.selectedApp, + default: () => (quickDeployedAppConfig?.appId ? 0 : undefined), + guiOptions: { + mandatory: !!appList.length, + breadcrumb: t('prompts.appSelection.breadcrumb'), + }, + message: t('prompts.appSelection.message'), + choices: (): { name: string; value: AppInfo }[] => (appList.length ? formatAppChoices(appList) : []), + validate: (): string | boolean => { + if (quickDeployedAppConfig?.appId && !appList.length) { + return t('prompts.quickDeployedAppDownloadErrors.noAppsFound'); + } + else return (appList.length ? true : t('prompts.appSelection.noAppsDeployed')) + }, + }, + ]; + + const targetFolderPrompts = getTargetFolderPrompt(appRootPath, quickDeployedAppConfig?.appId); + return [...systemQuestions.prompts, ...appSelectionPrompt, targetFolderPrompts] as RepoAppDownloadQuestions[]; + } catch (error) { + throw new Error(`Failed to generate prompts: ${error.message}`); + } } diff --git a/packages/repo-app-download-sub-generator/src/translations/repo-app-download-sub-generator.i18n.json b/packages/repo-app-download-sub-generator/src/translations/repo-app-download-sub-generator.i18n.json index 4f5a6ba549..a562a6a0c1 100644 --- a/packages/repo-app-download-sub-generator/src/translations/repo-app-download-sub-generator.i18n.json +++ b/packages/repo-app-download-sub-generator/src/translations/repo-app-download-sub-generator.i18n.json @@ -39,7 +39,7 @@ "invalidManifestStructureError": "Invalid manifest structure: 'sap.ui5' or 'sap.app' are missing." }, "quickDeployedAppDownloadErrors": { - "noAppsFound": "No application with id {{ appId }} found in the system. Please check if the application is deployed correctly" + "noAppsFound": "No application with id {{ appId }} found in the system. Please check if the application is deployed correctly or select another app" } }, "warn": { From 2b0377bd57369801bc69426488cfccfb896d51b4 Mon Sep 17 00:00:00 2001 From: I743583 Date: Mon, 14 Apr 2025 19:58:21 +0100 Subject: [PATCH 34/41] change workflow --- .../src/app/index.ts | 10 +- .../src/app/types.ts | 15 ++- .../src/prompts/prompt-helpers.ts | 1 - .../src/prompts/prompts.ts | 97 +------------------ .../test/app.test.ts | 59 +---------- .../test/prompts/prompts.test.ts | 17 +++- 6 files changed, 37 insertions(+), 162 deletions(-) diff --git a/packages/repo-app-download-sub-generator/src/app/index.ts b/packages/repo-app-download-sub-generator/src/app/index.ts index 602ac700a3..64558aa613 100644 --- a/packages/repo-app-download-sub-generator/src/app/index.ts +++ b/packages/repo-app-download-sub-generator/src/app/index.ts @@ -15,13 +15,7 @@ import { sendTelemetry, TelemetryHelper } from '@sap-ux/fiori-generator-shared'; -import type { - RepoAppDownloadOptions, - RepoAppDownloadAnswers, - RepoAppDownloadQuestions, - QfaJsonConfig, - QuickDeployedAppConfig -} from './types'; +import type { RepoAppDownloadOptions, RepoAppDownloadAnswers, RepoAppDownloadQuestions, QfaJsonConfig } from './types'; import { getPrompts } from '../prompts/prompts'; import { generate, TemplateType, type FioriElementsApp, type LROPSettings } from '@sap-ux/fiori-elements-writer'; import { join, basename } from 'path'; @@ -40,7 +34,7 @@ import { getAbapDeployConfig, getAppConfig } from './app-config'; import type { AbapDeployConfig } from '@sap-ux/ui5-config'; import { makeValidJson } from '../utils/file-helpers'; import { replaceWebappFiles, validateAndUpdateManifestUI5Version } from '../utils/updates'; -import { fetchAppListForSelectedSystem, extractAppData, getYUIDetails } from '../prompts/prompt-helpers'; +import { getYUIDetails } from '../prompts/prompt-helpers'; import { isValidPromptState, validateQfaJsonFile } from '../utils/validators'; import { FileName, DirName } from '@sap-ux/project-access'; diff --git a/packages/repo-app-download-sub-generator/src/app/types.ts b/packages/repo-app-download-sub-generator/src/app/types.ts index 5c9b5dc08e..bb28f56823 100644 --- a/packages/repo-app-download-sub-generator/src/app/types.ts +++ b/packages/repo-app-download-sub-generator/src/app/types.ts @@ -16,14 +16,21 @@ export interface QuickDeployedAppConfig { appId: string; /** appUrl is the URL pointing to the application */ appUrl?: string; - /** service provider is used to identify the system from which the app is downloaded from. */ + /** + * Information about the system from which the application is to be downloaded. + */ serviceProviderInfo?: { - /** system url */ - serviceUrl: string; - /** system name to pre populate App ID prompt */ + /** + * The base URL of the system providing the application. + */ + serviceUrl?: string; + /** + * The name of the system providing the application. + */ name: string; }; } + /** * Options for downloading an application from repository. */ diff --git a/packages/repo-app-download-sub-generator/src/prompts/prompt-helpers.ts b/packages/repo-app-download-sub-generator/src/prompts/prompt-helpers.ts index 9f0654e0ab..5096b062a7 100644 --- a/packages/repo-app-download-sub-generator/src/prompts/prompt-helpers.ts +++ b/packages/repo-app-download-sub-generator/src/prompts/prompt-helpers.ts @@ -79,7 +79,6 @@ async function getAppList(provider: AbapServiceProvider, appId?: string): Promis 'sap.app/id': appId } : appListSearchParams; - console.log("--- provider ---", provider); return await provider.getAppIndex().search(searchParams, appListResultFields); } catch (error) { RepoAppDownloadLogger.logger?.error(t('error.applicationListFetchError', { error: error.message })); diff --git a/packages/repo-app-download-sub-generator/src/prompts/prompts.ts b/packages/repo-app-download-sub-generator/src/prompts/prompts.ts index f0841cc839..bb2c5eb377 100644 --- a/packages/repo-app-download-sub-generator/src/prompts/prompts.ts +++ b/packages/repo-app-download-sub-generator/src/prompts/prompts.ts @@ -45,99 +45,13 @@ const getTargetFolderPrompt = (appRootPath?: string, appId?: string): FileBrowse } as FileBrowserQuestion; }; -// function getDefaultStuff (getDefaultStuff: QuickDeployedAppConfig | undefined): { defaultSystem: string; defaultAppId: string } { -// let defaultSystem: string = ''; -// let defaultAppId: string = ''; -// if(getDefaultStuff?.appId && getDefaultStuff?.serviceProvider) { -// // If appId is provided but serviceProvider is not, set the serviceProvider to the default value -// //defaultSystem = getDefaultStuff.serviceProvider.; -// defaultAppId = getDefaultStuff.appId; -// defaultSystem = getDefaultStuff.serviceProvider.name; -// } -// return { defaultSystem, defaultAppId}; -// } - -// /** -// * Retrieves questions for selecting system, app lists and target path where app will be generated. -// * -// * @param {string} [appRootPath] - The root path of the application. -// * @param {QuickDeployedAppConfig} [quickDeployedAppConfig] - quick deployed app config. -// * @returns {Promise} A list of questions for user interaction. -// */ -// export async function getPrompts( -// appRootPath?: string, -// quickDeployedAppConfig?: QuickDeployedAppConfig -// ): Promise { -// PromptState.reset(); - -// const { defaultSystem} = getDefaultStuff(quickDeployedAppConfig) -// let systemQuestions = await getSystemSelectionQuestions( { serviceSelection: { hide: true } }, false); -// if (quickDeployedAppConfig?.appId) { -// const filteredSystemQuestion = systemQuestions.prompts.find(p => p.name === promptNames.systemSelection); -// let defaultIndex = -1; -// if (filteredSystemQuestion) { -// //const choices = (filteredSystemQuestion as ListQuestion).choices; -// // Filter the choices based on the default system -// let choices = (filteredSystemQuestion as ListQuestion).choices; -// if (Array.isArray(choices)) { -// // Filter the choices based on the default system -// defaultIndex = choices.findIndex((choice: any) => choice.value.system.name === defaultSystem); -// filteredSystemQuestion.default = defaultIndex !== -1 ? defaultIndex : undefined; // Assign default index if found -// systemQuestions.prompts = [filteredSystemQuestion]; -// } - -// filteredSystemQuestion.default = defaultIndex !== -1 ? defaultIndex : undefined; // Assign default index if found -// systemQuestions.prompts = [filteredSystemQuestion]; -// } -// } - - -// let appList: AppIndex = []; -// const appSelectionPrompt = [ -// { -// when: async (answers: RepoAppDownloadAnswers): Promise => { -// if (answers[PromptNames.systemSelection]) { -// debugger; -// if(quickDeployedAppConfig?.appId) { -// appList = await fetchAppListForSelectedSystem( -// systemQuestions.answers.connectedSystem?.serviceProvider as AbapServiceProvider, -// quickDeployedAppConfig.appId -// ); -// } -// else { -// appList = await fetchAppListForSelectedSystem( -// systemQuestions.answers.connectedSystem?.serviceProvider as AbapServiceProvider -// ); -// } -// } -// // display app selection prompt only if user has selected a system -// return !!systemQuestions.answers.connectedSystem?.serviceProvider; -// }, -// type: 'list', -// name: PromptNames.selectedApp, -// default: () => quickDeployedAppConfig?.appId ? 0 : undefined, -// guiOptions: { -// mandatory: !!appList.length, -// breadcrumb: t('prompts.appSelection.breadcrumb') -// }, -// message: t('prompts.appSelection.message'), -// choices: (): { name: string; value: AppInfo }[] => (appList.length ? formatAppChoices(appList) : []), -// validate: (): string | boolean => (appList.length ? true : t('prompts.appSelection.noAppsDeployed')) -// } -// ]; - -// const targetFolderPrompts = getTargetFolderPrompt(appRootPath, quickDeployedAppConfig?.appId); -// return [...systemQuestions.prompts, ...appSelectionPrompt, targetFolderPrompts] as RepoAppDownloadQuestions[]; -// } - - /** * Extracts default system from the quick deployed app configuration. * * @param {QuickDeployedAppConfig | undefined} quickDeployedAppConfig - The quick deployed app configuration. * @returns {string} The default system. */ -function extractDefaultSystem(quickDeployedAppConfig: QuickDeployedAppConfig | undefined): string { +function extractDefaultSystem(quickDeployedAppConfig?: QuickDeployedAppConfig): string { let defaultSystem = ''; if (quickDeployedAppConfig?.appId && quickDeployedAppConfig?.serviceProviderInfo) { @@ -160,13 +74,12 @@ export async function getPrompts( ): Promise { try { PromptState.reset(); - - const defaultSystem = extractDefaultSystem(quickDeployedAppConfig); + debugger; const systemQuestions = await getSystemSelectionQuestions({ serviceSelection: { hide: true } }, false); - // Filter system questions and set default system if applicable if (quickDeployedAppConfig?.appId) { - const filteredSystemQuestion = systemQuestions.prompts.find(p => p.name === promptNames.systemSelection); + const defaultSystem = extractDefaultSystem(quickDeployedAppConfig); + const filteredSystemQuestion = systemQuestions.prompts.find(p => p.name === PromptNames.systemSelection); if (filteredSystemQuestion) { const choices = (filteredSystemQuestion as ListQuestion).choices; @@ -208,7 +121,7 @@ export async function getPrompts( choices: (): { name: string; value: AppInfo }[] => (appList.length ? formatAppChoices(appList) : []), validate: (): string | boolean => { if (quickDeployedAppConfig?.appId && !appList.length) { - return t('prompts.quickDeployedAppDownloadErrors.noAppsFound'); + return t('error.quickDeployedAppDownloadErrors.noAppsFound'); } else return (appList.length ? true : t('prompts.appSelection.noAppsDeployed')) }, diff --git a/packages/repo-app-download-sub-generator/test/app.test.ts b/packages/repo-app-download-sub-generator/test/app.test.ts index 7319c72094..cf290e3b04 100644 --- a/packages/repo-app-download-sub-generator/test/app.test.ts +++ b/packages/repo-app-download-sub-generator/test/app.test.ts @@ -377,7 +377,10 @@ describe('Repo App Download', () => { quickDeployedAppConfig: { appId: appConfig.app.id, appUrl: 'https://app-url.com/app', - serviceProvider: mockServiceProvider + serviceProviderInfo: { + serviceUrl: 'https://test-url.com', + name: 'system3' + } } } }) @@ -394,60 +397,6 @@ describe('Repo App Download', () => { }) ) .resolves.not.toThrow(); - expect(fetchAppListForSelectedSystem).toHaveBeenCalledWith(mockServiceProvider, appConfig.app.id); verifyGeneratedFiles(testOutputDir, appId, testFixtureDir); }); - - it('Should throw error when fetchAppListForSelectedSystem fetches no app', async () => { - (isValidPromptState as jest.Mock).mockReturnValue(true); - (getAppConfig as jest.Mock).mockResolvedValue(appConfig); - (fetchAppListForSelectedSystem as jest.Mock).mockResolvedValue([]); - const mockServiceProvider = { - defaults: { baseURL: 'https://test-url.com' }, - service: jest.fn().mockReturnValue({ - metadata: jest.fn().mockResolvedValue({ - dataServices: { - schema: [] - } - }) - }) - } as unknown as AbapServiceProvider; - - await expect( - yeomanTest - .run(RepoAppDownloadGenerator, { - resolved: repoAppDownloadGenPath - }) - .cd('.') - .withOptions({ - appRootPath: testOutputDir, - appWizard: mockAppWizard, - vscode: mockVSCode, - data: { - postGenCommand: 'test-post-gen-command', - quickDeployedAppConfig: { - appId: appConfig.app.id, - appUrl: 'https://app-url.com/app', - serviceProvider: mockServiceProvider - } - } - }) - .withPrompts({ - systemSelection: 'system3', - selectedApp: { - appId: appConfig.app.id, - title: appConfig.app.title, - description: appConfig.app.description, - repoName: repoName, - url: 'url-1' - }, - targetFolder: testOutputDir - }) - ) - .rejects.toThrowError( - t('error.quickDeployedAppDownloadErrors.noAppsFound', { appId: appConfig.app.id }) - ); - expect(fetchAppListForSelectedSystem).toHaveBeenCalledWith(mockServiceProvider, appConfig.app.id); - expect(fs.existsSync(join(`${testOutputDir}/${appId}/${DirName.Webapp}`))).toBe(false); - }); }); \ No newline at end of file diff --git a/packages/repo-app-download-sub-generator/test/prompts/prompts.test.ts b/packages/repo-app-download-sub-generator/test/prompts/prompts.test.ts index 605463ef9e..7c1e9e3d44 100644 --- a/packages/repo-app-download-sub-generator/test/prompts/prompts.test.ts +++ b/packages/repo-app-download-sub-generator/test/prompts/prompts.test.ts @@ -2,7 +2,7 @@ import { getPrompts } from '../../src/prompts/prompts'; import { getSystemSelectionQuestions } from '@sap-ux/odata-service-inquirer'; import { fetchAppListForSelectedSystem, formatAppChoices } from '../../src/prompts/prompt-helpers'; import { PromptNames } from '../../src/app/types'; -import type { RepoAppDownloadAnswers, RepoAppDownloadQuestions } from '../../src/app/types'; +import type { QuickDeployedAppConfig, RepoAppDownloadAnswers, RepoAppDownloadQuestions } from '../../src/app/types'; import { join } from 'path'; import { t } from '../../src/utils/i18n'; import type { AbapServiceProvider } from '@sap-ux/axios-extension'; @@ -31,7 +31,7 @@ describe('getPrompts', () => { const mockAnswers = { selectedApp: { appId: 'app1' } } as unknown as RepoAppDownloadAnswers; - const mockAppList = [{ appId: 'app1', name: 'Test App' }]; + const mockAppList = [{ appId: 'app1', name: 'Test App' }, { appId: 'app2', name: 'Test App 2' }]; beforeEach(() => { (getSystemSelectionQuestions as jest.Mock).mockResolvedValue({ @@ -135,5 +135,18 @@ describe('getPrompts', () => { const result = await targetFolderPrompt?.default(); expect(result).toBe(appRootPath); }); + + it('should return pre filled system questions, app selection, and target folder prompts when quick deployed app config is provided', async () => { + const quickDeployedAppConfig: QuickDeployedAppConfig = { + appId: 'app1', + serviceProviderInfo: { + name: 'system3' + } + } + const prompts = await getPrompts(appRootPath, quickDeployedAppConfig); + }); }); + + +//prompts.ts | 37.5 | 20 | 40 | 38.46 | 29,55-60,76-83,96,106,115,126,141-147,161-221 \ No newline at end of file From 028198b1782d36ac8e076b3cd3427544f9ae8182 Mon Sep 17 00:00:00 2001 From: I743583 Date: Tue, 15 Apr 2025 08:38:16 +0100 Subject: [PATCH 35/41] adding tests to prompts.test --- .../repo-app-download-sub-generator/README.md | 2 +- .../package.json | 4 +- .../src/app/index.ts | 17 +- .../src/app/types.ts | 12 +- .../src/prompts/prompt-state.ts | 18 ++ .../src/prompts/prompts.ts | 40 ++- .../repo-app-download-sub-generator.i18n.json | 2 +- .../src/utils/download-utils.ts | 21 +- .../test/prompts/prompt-state.test.ts | 30 +- .../test/prompts/prompts.test.ts | 271 ++++++++++-------- .../test/utils/download-utils.test.ts | 140 +++++---- 11 files changed, 337 insertions(+), 220 deletions(-) diff --git a/packages/repo-app-download-sub-generator/README.md b/packages/repo-app-download-sub-generator/README.md index 034ebe2e97..1d41683cbb 100644 --- a/packages/repo-app-download-sub-generator/README.md +++ b/packages/repo-app-download-sub-generator/README.md @@ -6,7 +6,7 @@ The SAP App download sub-generator enables users to download a basic LROP App fr ## Installation -The SAP App download sub-generator sub-generator is installed as part of the [@sap/generator-fiori](https://www.npmjs.com/package/@sap/generator-fiori) generator and cannot be used stand alone. +The SAP App download sub-generator is installed as part of the [@sap/generator-fiori](https://www.npmjs.com/package/@sap/generator-fiori) generator and cannot be used stand alone. ## Launch the SAP Reuse Library sub-generator diff --git a/packages/repo-app-download-sub-generator/package.json b/packages/repo-app-download-sub-generator/package.json index 641ad23733..847e2127c5 100644 --- a/packages/repo-app-download-sub-generator/package.json +++ b/packages/repo-app-download-sub-generator/package.json @@ -50,8 +50,7 @@ "adm-zip": "0.5.10", "i18next": "23.5.1", "inquirer": "8.2.6", - "yeoman-generator": "5.10.0", - "inquirer-autocomplete-prompt": "2.0.1" + "yeoman-generator": "5.10.0" }, "devDependencies": { "@jest/types": "29.6.3", @@ -60,6 +59,7 @@ "@types/mem-fs-editor": "7.0.1", "@types/yeoman-generator": "5.2.11", "@types/yeoman-environment": "2.10.11", + "inquirer-autocomplete-prompt": "2.0.1", "@types/inquirer-autocomplete-prompt": "2.0.1", "@types/yeoman-test": "4.0.6", "@sap-ux/nodejs-utils": "workspace:*", diff --git a/packages/repo-app-download-sub-generator/src/app/index.ts b/packages/repo-app-download-sub-generator/src/app/index.ts index 64558aa613..282eb721a1 100644 --- a/packages/repo-app-download-sub-generator/src/app/index.ts +++ b/packages/repo-app-download-sub-generator/src/app/index.ts @@ -5,7 +5,7 @@ import { isInternalFeaturesSettingEnabled } from '@sap-ux/feature-toggle'; import type { Logger } from '@sap-ux/logger'; import { generatorTitle, extractedFilePath, generatorName, defaultAnswers, qfaJsonFileName } from '../utils/constants'; import { t } from '../utils/i18n'; -import { downloadApp } from '../utils/download-utils'; +import { extractZip } from '../utils/download-utils'; import { EventName } from '../telemetryEvents'; import { getDefaultTargetFolder, @@ -124,18 +124,21 @@ export default class extends Generator { // Handle app download where prompts for system selection and app selection are shown Object.assign(this.answers, answers); } + if (isValidPromptState(this.answers.targetFolder, this.answers.selectedApp.appId)) { + this.projectPath = join(this.answers.targetFolder, this.answers.selectedApp.appId); + this.extractedProjectPath = join(this.projectPath, extractedFilePath); + } } /** * Writes the configuration files for the project, including deployment config, and README. */ public async writing(): Promise { - if (isValidPromptState(this.answers.targetFolder, this.answers.selectedApp.appId)) { - this.projectPath = join(this.answers.targetFolder, this.answers.selectedApp.appId); - this.extractedProjectPath = join(this.projectPath, extractedFilePath); - // Trigger app download - await downloadApp(this.answers.selectedApp.repoName, this.extractedProjectPath, this.fs); - } + // Extract downloaded app + const archive = PromptState.downloadedAppPackage; + await extractZip(this.extractedProjectPath, archive, this.fs); + + // Check if the qfa.json file const qfaJsonFilePath = join(this.extractedProjectPath, qfaJsonFileName); if (this.fs.exists(qfaJsonFilePath)) { const qfaJson: QfaJsonConfig = makeValidJson(qfaJsonFilePath, this.fs); diff --git a/packages/repo-app-download-sub-generator/src/app/types.ts b/packages/repo-app-download-sub-generator/src/app/types.ts index bb28f56823..dc642f0b43 100644 --- a/packages/repo-app-download-sub-generator/src/app/types.ts +++ b/packages/repo-app-download-sub-generator/src/app/types.ts @@ -16,16 +16,16 @@ export interface QuickDeployedAppConfig { appId: string; /** appUrl is the URL pointing to the application */ appUrl?: string; - /** - * Information about the system from which the application is to be downloaded. + /** + * Information about the system from which the application is to be downloaded. */ serviceProviderInfo?: { - /** - * The base URL of the system providing the application. + /** + * The base URL of the system providing the application. */ serviceUrl?: string; - /** - * The name of the system providing the application. + /** + * The name of the system providing the application. */ name: string; }; diff --git a/packages/repo-app-download-sub-generator/src/prompts/prompt-state.ts b/packages/repo-app-download-sub-generator/src/prompts/prompt-state.ts index 0df5282bb4..575d359d9f 100644 --- a/packages/repo-app-download-sub-generator/src/prompts/prompt-state.ts +++ b/packages/repo-app-download-sub-generator/src/prompts/prompt-state.ts @@ -8,6 +8,7 @@ import type { SystemSelectionAnswers } from '../app/types'; */ export class PromptState { private static _systemSelection: SystemSelectionAnswers = {}; + private static _downloadedAppPackage?: Buffer; /** * Returns the current state of the service config. @@ -27,6 +28,22 @@ export class PromptState { this._systemSelection = value; } + /** + * Set the downloaded app package. + */ + public static set downloadedAppPackage(archive: Buffer) { + this._downloadedAppPackage = archive; + } + + /** + * Returns the downloaded app package. + * + * @returns {Buffer} downloaded app package + */ + public static get downloadedAppPackage(): Buffer { + return this._downloadedAppPackage ?? Buffer.alloc(0); + } + /** * Get the baseURL from the connected system's service provider defaults. * @@ -47,5 +64,6 @@ export class PromptState { static reset(): void { PromptState.systemSelection = {}; + PromptState._downloadedAppPackage = undefined; } } diff --git a/packages/repo-app-download-sub-generator/src/prompts/prompts.ts b/packages/repo-app-download-sub-generator/src/prompts/prompts.ts index bb2c5eb377..cfd95b55f3 100644 --- a/packages/repo-app-download-sub-generator/src/prompts/prompts.ts +++ b/packages/repo-app-download-sub-generator/src/prompts/prompts.ts @@ -1,5 +1,5 @@ import type { AppIndex, AbapServiceProvider } from '@sap-ux/axios-extension'; -import { getSystemSelectionQuestions, promptNames } from '@sap-ux/odata-service-inquirer'; +import { getSystemSelectionQuestions } from '@sap-ux/odata-service-inquirer'; import type { RepoAppDownloadAnswers, RepoAppDownloadQuestions, QuickDeployedAppConfig, AppInfo } from '../app/types'; import { PromptNames } from '../app/types'; import { t } from '../utils/i18n'; @@ -7,7 +7,8 @@ import type { FileBrowserQuestion } from '@sap-ux/inquirer-common'; import { validateFioriAppTargetFolder } from '@sap-ux/project-input-validator'; import { PromptState } from './prompt-state'; import { fetchAppListForSelectedSystem, formatAppChoices } from './prompt-helpers'; -import { ListQuestion } from 'inquirer'; +import type { ListQuestion } from 'inquirer'; +import { downloadApp } from '../utils/download-utils'; /** * Gets the target folder selection prompt. @@ -74,12 +75,12 @@ export async function getPrompts( ): Promise { try { PromptState.reset(); - debugger; + const systemQuestions = await getSystemSelectionQuestions({ serviceSelection: { hide: true } }, false); // Filter system questions and set default system if applicable if (quickDeployedAppConfig?.appId) { const defaultSystem = extractDefaultSystem(quickDeployedAppConfig); - const filteredSystemQuestion = systemQuestions.prompts.find(p => p.name === PromptNames.systemSelection); + const filteredSystemQuestion = systemQuestions.prompts.find((p) => p.name === PromptNames.systemSelection); if (filteredSystemQuestion) { const choices = (filteredSystemQuestion as ListQuestion).choices; @@ -115,17 +116,34 @@ export async function getPrompts( default: () => (quickDeployedAppConfig?.appId ? 0 : undefined), guiOptions: { mandatory: !!appList.length, - breadcrumb: t('prompts.appSelection.breadcrumb'), + breadcrumb: t('prompts.appSelection.breadcrumb') }, message: t('prompts.appSelection.message'), choices: (): { name: string; value: AppInfo }[] => (appList.length ? formatAppChoices(appList) : []), - validate: (): string | boolean => { - if (quickDeployedAppConfig?.appId && !appList.length) { - return t('error.quickDeployedAppDownloadErrors.noAppsFound'); + validate: async (answers: AppInfo): Promise => { + // Quick deploy config exists but no apps found + if (quickDeployedAppConfig?.appId && appList.length === 0) { + return t('error.quickDeployedAppDownloadErrors.noAppsFound', { + appId: quickDeployedAppConfig.appId + }); } - else return (appList.length ? true : t('prompts.appSelection.noAppsDeployed')) - }, - }, + + // No apps available at all + if (appList.length === 0) { + return t('prompts.appSelection.noAppsDeployed'); + } + + // Valid app selected, try to download + if (answers?.appId) { + try { + await downloadApp(answers.repoName); + return true; + } catch (error) { + throw new Error(t('error.appDownloadErrors.appDownloadFailure', { error: error.message })); + } + } + } + } ]; const targetFolderPrompts = getTargetFolderPrompt(appRootPath, quickDeployedAppConfig?.appId); diff --git a/packages/repo-app-download-sub-generator/src/translations/repo-app-download-sub-generator.i18n.json b/packages/repo-app-download-sub-generator/src/translations/repo-app-download-sub-generator.i18n.json index a562a6a0c1..5a7e7c68c6 100644 --- a/packages/repo-app-download-sub-generator/src/translations/repo-app-download-sub-generator.i18n.json +++ b/packages/repo-app-download-sub-generator/src/translations/repo-app-download-sub-generator.i18n.json @@ -50,7 +50,7 @@ "message": "App", "hint": "Select the app to download", "breadcrumb": "App", - "noAppsDeployed": "No applications deployed to this system can be downloaded. Please see for more details" + "noAppsDeployed": "No applications deployed to this system can be downloaded. {{- help}}" }, "targetPath": { "message": "Project folder path", diff --git a/packages/repo-app-download-sub-generator/src/utils/download-utils.ts b/packages/repo-app-download-sub-generator/src/utils/download-utils.ts index 91e32a3f70..f7790c952f 100644 --- a/packages/repo-app-download-sub-generator/src/utils/download-utils.ts +++ b/packages/repo-app-download-sub-generator/src/utils/download-utils.ts @@ -13,7 +13,7 @@ import RepoAppDownloadLogger from '../utils/logger'; * @param {Buffer} archive - The ZIP archive buffer. * @param {Editor} fs - The file system editor. */ -async function extractZip(extractedProjectPath: string, archive: Buffer, fs: Editor): Promise { +export async function extractZip(extractedProjectPath: string, archive: Buffer, fs: Editor): Promise { try { const zip = new AdmZip(archive); zip.getEntries().forEach(function (zipEntry) { @@ -33,19 +33,10 @@ async function extractZip(extractedProjectPath: string, archive: Buffer, fs: Edi * Downloads application files from the ABAP repository. * * @param {string} repoName - The repository name of the application. - * @param {string} extractedProjectPath - The path where the application should be extracted. - * @param {Editor} fs - The file system editor. */ -export async function downloadApp(repoName: string, extractedProjectPath: string, fs: Editor): Promise { - try { - const serviceProvider = PromptState.systemSelection?.connectedSystem?.serviceProvider as AbapServiceProvider; - const archive = await serviceProvider.getUi5AbapRepository().downloadFiles(repoName); - if (Buffer.isBuffer(archive)) { - await extractZip(extractedProjectPath, archive, fs); - } else { - RepoAppDownloadLogger.logger?.error(t('error.appDownloadErrors.downloadedFileNotBufferError')); - } - } catch (error) { - throw new Error(t('error.appDownloadErrors.appDownloadFailure', { error: error.message })); - } +export async function downloadApp(repoName: string): Promise { + const serviceProvider = PromptState.systemSelection?.connectedSystem?.serviceProvider as AbapServiceProvider; + const downloadedAppPackage = await serviceProvider.getUi5AbapRepository().downloadFiles(repoName); + // store downloaded package in prompt state + PromptState.downloadedAppPackage = downloadedAppPackage; } diff --git a/packages/repo-app-download-sub-generator/test/prompts/prompt-state.test.ts b/packages/repo-app-download-sub-generator/test/prompts/prompt-state.test.ts index ed1f3e7fe8..999beb9322 100644 --- a/packages/repo-app-download-sub-generator/test/prompts/prompt-state.test.ts +++ b/packages/repo-app-download-sub-generator/test/prompts/prompt-state.test.ts @@ -3,11 +3,13 @@ import type { SystemSelectionAnswers } from '../../src/app/types'; import type { AbapServiceProvider } from '@sap-ux/axios-extension'; describe('PromptState', () => { - const mockServiceProvider = { - getAppIndex: jest.fn().mockReturnValue({ - search: jest.fn().mockResolvedValue([{ id: 'app1' }, { id: 'app2' }]) - }) + defaults: { + baseURL: 'https://mock.sap-system.com', + params: { + 'sap-client': '100' + } + } } as unknown as AbapServiceProvider; let mockSystemSelection: SystemSelectionAnswers; beforeEach(() => { @@ -34,4 +36,24 @@ describe('PromptState', () => { PromptState.reset(); expect(PromptState.systemSelection).toEqual({}); }); + + it('should set and get downloadedAppPackage correctly', () => { + const mockBuffer = Buffer.from('mock zip content'); + PromptState.downloadedAppPackage = mockBuffer; + expect(PromptState.downloadedAppPackage).toBe(mockBuffer); + }); + + it('should return undefined for downloadedAppPackage if not set', () => { + expect(PromptState.downloadedAppPackage.length).toBe(0); + }); + + it('should return baseURL from connected system', () => { + PromptState.systemSelection = mockSystemSelection; + expect(PromptState.baseURL).toBe('https://mock.sap-system.com'); + }); + + it('should return sapClient from connected system', () => { + PromptState.systemSelection = mockSystemSelection; + expect(PromptState.sapClient).toBe('100'); + }); }); \ No newline at end of file diff --git a/packages/repo-app-download-sub-generator/test/prompts/prompts.test.ts b/packages/repo-app-download-sub-generator/test/prompts/prompts.test.ts index 7c1e9e3d44..a787a874d9 100644 --- a/packages/repo-app-download-sub-generator/test/prompts/prompts.test.ts +++ b/packages/repo-app-download-sub-generator/test/prompts/prompts.test.ts @@ -1,152 +1,199 @@ -import { getPrompts } from '../../src/prompts/prompts'; -import { getSystemSelectionQuestions } from '@sap-ux/odata-service-inquirer'; -import { fetchAppListForSelectedSystem, formatAppChoices } from '../../src/prompts/prompt-helpers'; -import { PromptNames } from '../../src/app/types'; -import type { QuickDeployedAppConfig, RepoAppDownloadAnswers, RepoAppDownloadQuestions } from '../../src/app/types'; -import { join } from 'path'; import { t } from '../../src/utils/i18n'; -import type { AbapServiceProvider } from '@sap-ux/axios-extension'; import { validateFioriAppTargetFolder } from '@sap-ux/project-input-validator'; +import { getPrompts } from '../../src/prompts/prompts'; +import { PromptNames } from '../../src/app/types'; +import { PromptState } from '../../src/prompts/prompt-state'; +import * as helpers from '../../src/prompts/prompt-helpers'; +import * as downloadUtils from '../../src/utils/download-utils'; +import * as validator from '@sap-ux/project-input-validator'; jest.mock('@sap-ux/odata-service-inquirer', () => ({ - getSystemSelectionQuestions: jest.fn() + getSystemSelectionQuestions: jest.fn().mockResolvedValue({ + prompts: [{ + name: 'systemSelection', + type: 'list', + choices: [{ name: 'Sys', value: { system: { name: 'Sys' } } }] + }], + answers: { + connectedSystem: { serviceProvider: {} } + } + }) +})); +jest.mock('../../src/prompts/prompt-helpers', () => ({ + fetchAppListForSelectedSystem: jest.fn().mockResolvedValue([ + { appId: 'app1', repoName: 'repo1' }, + { appId: 'app2', repoName: 'repo2' } + ]), + formatAppChoices: jest.fn().mockReturnValue([ + { name: 'App 1', value: { appId: 'app1', repoName: 'repo1' } }, + { name: 'App 2', value: { appId: 'app2', repoName: 'repo2' } } + ]) +})); + +jest.mock('../../src/utils/download-utils', () => ({ + downloadApp: jest.fn() })); jest.mock('@sap-ux/project-input-validator', () => ({ - validateFioriAppTargetFolder: jest.fn() + validateFioriAppTargetFolder: jest.fn().mockResolvedValue(true) })); -jest.mock('../../src/prompts/prompt-helpers', () => ({ - fetchAppListForSelectedSystem: jest.fn(), - formatAppChoices: jest.fn() +jest.mock('@sap-ux/project-input-validator', () => ({ + validateFioriAppTargetFolder: jest.fn().mockResolvedValue(true) })); describe('getPrompts', () => { - const appRootPath = join('/mock/path'); - const mockServiceProvider = { - getAppIndex: jest.fn().mockReturnValue({ - search: jest.fn().mockResolvedValue([{ id: 'app1' }, { id: 'app2' }]) - }) - } as unknown as AbapServiceProvider; - const mockAnswers = { - selectedApp: { appId: 'app1' } - } as unknown as RepoAppDownloadAnswers; - const mockAppList = [{ appId: 'app1', name: 'Test App' }, { appId: 'app2', name: 'Test App 2' }]; + const mockGetSystemSelectionQuestions = require('@sap-ux/odata-service-inquirer').getSystemSelectionQuestions; + const mockFetchAppList = helpers.fetchAppListForSelectedSystem as jest.Mock; + const mockDownloadApp = downloadUtils.downloadApp as jest.Mock; beforeEach(() => { - (getSystemSelectionQuestions as jest.Mock).mockResolvedValue({ - prompts: [{ type: 'input', name: 'system' }], + jest.clearAllMocks(); + jest.resetAllMocks(); + PromptState.reset(); + }); + + it('should return prompts including system, app, and target folder', async () => { + mockGetSystemSelectionQuestions.mockResolvedValue({ + prompts: [{ + name: PromptNames.systemSelection, + type: 'list', + choices: [{ name: 'System 1', value: { system: { name: 'MockSystem' } } }] + }], answers: { - connectedSystem: { serviceProvider: mockServiceProvider } + connectedSystem: { serviceProvider: {} } } }); - (fetchAppListForSelectedSystem as jest.Mock).mockResolvedValue([{ appId: 'app1', name: 'Test App' }]); - (formatAppChoices as jest.Mock).mockReturnValue(mockAppList); - }); - it('should return system questions, app selection, and target folder prompts', async () => { - const prompts = await getPrompts(appRootPath); - expect(prompts.length).toBeGreaterThanOrEqual(2); + mockFetchAppList.mockResolvedValue([{ appId: 'app1', repoName: 'repo1' }]); + mockDownloadApp.mockResolvedValue(undefined); - // system prompts - const systemPrompt = prompts.find(p => p.name === 'system'); - expect(systemPrompt).toBeDefined(); - expect(systemPrompt?.type).toBe('input'); - expect(systemPrompt?.name).toBe('system'); + const prompts = await getPrompts('/app/path'); + expect(prompts).toBeInstanceOf(Array); + expect(prompts.find(p => p.name === PromptNames.systemSelection)).toBeTruthy(); + expect(prompts.find(p => p.name === PromptNames.selectedApp)).toBeTruthy(); + expect(prompts.find(p => p.name === PromptNames.targetFolder)).toBeTruthy(); + }); - // app selection prompts - const appSelectionPrompt = prompts.find(p => p.name === PromptNames.selectedApp) as RepoAppDownloadQuestions; - expect(appSelectionPrompt).toBeDefined(); - if (typeof appSelectionPrompt?.when === 'function') { - await expect(appSelectionPrompt.when({ [PromptNames.systemSelection]: { - connectedSystem: { serviceProvider: mockServiceProvider } - } } as unknown as RepoAppDownloadAnswers)).resolves.toBe(true); - }; - if (appSelectionPrompt?.type === 'list') { - const listPrompt = appSelectionPrompt as unknown as { choices: () => { name: string; value: string }[] }; - expect(listPrompt.choices()).toEqual(mockAppList); + it('should preselect default system if quickDeployedAppConfig is provided', async () => { + const quickDeployedAppConfig = { + appId: 'app1', + serviceProviderInfo: { + name: 'DefaultSystem' + } }; - expect(appSelectionPrompt && appSelectionPrompt.validate && appSelectionPrompt.validate(mockAppList)).toBe(true); - expect(appSelectionPrompt?.guiOptions?.breadcrumb).toBe(t('prompts.appSelection.breadcrumb')); - - // target folder prompt - const targetFolderPrompt = prompts.find(p => p.name === PromptNames.targetFolder); - expect(targetFolderPrompt).toBeDefined(); - }); - it('should handle no apps available scenario', async () => { - (fetchAppListForSelectedSystem as jest.Mock).mockResolvedValue([]); + mockGetSystemSelectionQuestions.mockResolvedValue({ + prompts: [{ + name: PromptNames.systemSelection, + type: 'list', + choices: [ + { name: 'System A', value: { system: { name: 'SystemA' } } }, + { name: 'Default System', value: { system: { name: 'DefaultSystem' } } } + ] + }], + answers: { + connectedSystem: { serviceProvider: {} } + } + }); - const prompts = await getPrompts(appRootPath); + mockFetchAppList.mockResolvedValue([{ appId: 'app1', repoName: 'repo1' }]); + const prompts = await getPrompts(undefined, quickDeployedAppConfig); - const appSelectionPrompt = prompts.find(p => p.name === PromptNames.selectedApp); - expect(appSelectionPrompt).toBeDefined(); - // no apps deployed message should be displayed - expect(appSelectionPrompt && appSelectionPrompt.validate && appSelectionPrompt.validate('')).toBe(t('prompts.appSelection.noAppsDeployed')); - if (appSelectionPrompt?.type === 'list') { - const listPrompt = appSelectionPrompt as unknown as { choices: () => { name: string; value: string }[] }; - expect(listPrompt.choices()).toEqual([]); - }; - - // target folder prompt should not be displayed - const targetFolderPrompt = prompts.find(p => p.name === PromptNames.targetFolder); - expect(targetFolderPrompt).toBeDefined(); - if (typeof targetFolderPrompt?.when === 'function') { - expect(targetFolderPrompt.when( {} as unknown as RepoAppDownloadAnswers)).toBe(false); - }; + const systemPrompt = prompts.find(p => p.name === PromptNames.systemSelection) as any; + expect(systemPrompt.default).toBe(1); }); - it('should validate the target folder path when it is valid', async () => { - // Mock validateFioriAppTargetFolder to return true (valid path) - (validateFioriAppTargetFolder as jest.Mock).mockResolvedValue(true); - const prompts = await getPrompts(appRootPath); + it('should throw an error if downloadApp fails', async () => { + mockGetSystemSelectionQuestions.mockResolvedValue({ + prompts: [{ + name: PromptNames.systemSelection, + type: 'list', + choices: [{ name: 'Sys', value: { system: { name: 'Sys' } } }] + }], + answers: { + connectedSystem: { serviceProvider: {} } + } + }); - // target folder prompt - const targetFolderPrompt = prompts.find(p => p.name === PromptNames.targetFolder); - expect(targetFolderPrompt).toBeDefined(); - const result = targetFolderPrompt !== undefined && targetFolderPrompt.validate ? await targetFolderPrompt.validate(appRootPath, mockAnswers) : undefined; + const appList = [{ appId: 'app1', repoName: 'repo1' }]; + mockFetchAppList.mockResolvedValue(appList); + mockDownloadApp.mockRejectedValue(new Error('Download failed')); - // Assert that validation returns true - expect(result).toBe(true); - expect(validateFioriAppTargetFolder).toHaveBeenCalledWith(appRootPath, 'app1', true); - }); - - it('should return error message when the target folder path is invalid', async () => { - const errorMessage = `The project folder path already contains an SAP Fiori application in the folder: ${appRootPath}. Please choose a different folder and try again.`; - (validateFioriAppTargetFolder as jest.Mock).mockResolvedValue(errorMessage); - const prompts = await getPrompts(appRootPath); + const prompts = await getPrompts(); - // target folder prompt - const targetFolderPrompt = prompts.find(p => p.name === PromptNames.targetFolder); - expect(targetFolderPrompt).toBeDefined(); - const result = targetFolderPrompt !== undefined && targetFolderPrompt.validate ? await targetFolderPrompt.validate(appRootPath, mockAnswers) : undefined; + const appPrompt = prompts.find(p => p.name === PromptNames.selectedApp) as any; + + await appPrompt.when({ + [PromptNames.systemSelection]: { system: { name: 'Sys' } } + }); - // Assert that validation returns the error message - expect(result).toBe(errorMessage); - expect(validateFioriAppTargetFolder).toHaveBeenCalledWith(appRootPath, 'app1', true); + await expect(appPrompt.validate({ appId: 'app1', repoName: 'repo1' })).rejects.toThrow( + t('error.appDownloadErrors.appDownloadFailure', { error: 'Download failed' }) + ); }); - - it('should return default path when no target folder is provided', async () => { - const prompts = await getPrompts(appRootPath); - // target folder prompt - const targetFolderPrompt = prompts.find(p => p.name === PromptNames.targetFolder); - expect(targetFolderPrompt).toBeDefined(); - const result = await targetFolderPrompt?.default(); - expect(result).toBe(appRootPath); + it('should use validateFioriAppTargetFolder in folder prompt', async () => { + mockGetSystemSelectionQuestions.mockResolvedValue({ + prompts: [], + answers: {} + }); + const prompts = await getPrompts('/some/path'); + const projectPathPrompt = prompts.find(p => p.name === PromptNames.targetFolder) as any; + await projectPathPrompt.validate('/some/path', { + selectedApp: { appId: 'id1' } + }); + expect(validateFioriAppTargetFolder).toHaveBeenCalledWith('/some/path', 'id1', true); }); - it('should return pre filled system questions, app selection, and target folder prompts when quick deployed app config is provided', async () => { - const quickDeployedAppConfig: QuickDeployedAppConfig = { - appId: 'app1', - serviceProviderInfo: { - name: 'system3' + it('should handle quickDeployedAppConfig and return the correct prompts', async () => { + mockGetSystemSelectionQuestions.mockResolvedValue({ + prompts: [{ + name: PromptNames.systemSelection, + type: 'list', + choices: [{ name: 'System 1', value: { system: { name: 'MockSystem' } } }] + }], + answers: { + connectedSystem: { serviceProvider: {} } } + }); + const quickDeployedAppConfig = { + appId: 'app1', + serviceProviderInfo: { name: 'System 1' } + }; + // Call getPrompts with quickDeployedAppConfig + const prompts = await getPrompts('/some/path', quickDeployedAppConfig); + + // Ensure prompts are returned correctly + expect(prompts).toBeDefined(); + + // Check if the system selection prompt exists + const systemSelectionPrompt = prompts.find(p => p.name === PromptNames.systemSelection); + expect(systemSelectionPrompt).toBeDefined(); + + // Check if the system selection prompt is filtered correctly + if (systemSelectionPrompt) { + const listPrompt = systemSelectionPrompt as unknown as { choices: () => { name: string; value: {} }[] }; + expect(listPrompt.choices).toEqual([ + { name: 'System 1', value: { system: { name: 'MockSystem' } } } + ]); + } + + // Check if the app selection prompt exists and is populated correctly + const appSelectionPrompt = prompts.find(p => p.name === PromptNames.selectedApp); + expect(appSelectionPrompt).toBeDefined(); + if (appSelectionPrompt) { + const listPrompt = appSelectionPrompt as unknown as { choices: () => { name: string; value: {} }[] }; + expect(appSelectionPrompt.when).toBeTruthy(); + } + + // Check if the target folder prompt exists and is included + const targetFolderPrompt = prompts.find(p => p.name === PromptNames.targetFolder); + expect(targetFolderPrompt).toBeDefined(); + if (targetFolderPrompt) { + expect(targetFolderPrompt.when).toBeTruthy(); } - const prompts = await getPrompts(appRootPath, quickDeployedAppConfig); }); - }); -//prompts.ts | 37.5 | 20 | 40 | 38.46 | 29,55-60,76-83,96,106,115,126,141-147,161-221 \ No newline at end of file diff --git a/packages/repo-app-download-sub-generator/test/utils/download-utils.test.ts b/packages/repo-app-download-sub-generator/test/utils/download-utils.test.ts index e05daefbcd..ff20036d85 100644 --- a/packages/repo-app-download-sub-generator/test/utils/download-utils.test.ts +++ b/packages/repo-app-download-sub-generator/test/utils/download-utils.test.ts @@ -1,12 +1,9 @@ -import { downloadApp } from '../../src/utils/download-utils'; +import { downloadApp, extractZip } from '../../src/utils/download-utils'; import AdmZip from 'adm-zip'; import { PromptState } from '../../src/prompts/prompt-state'; -import type { Logger } from '@sap-ux/logger'; -import type { Editor } from 'mem-fs-editor'; -import type { AbapServiceProvider } from '@sap-ux/axios-extension'; -import { join } from 'path'; import { t } from '../../src/utils/i18n'; import RepoAppDownloadLogger from '../../src/utils/logger'; +import type { SystemSelectionAnswers } from '../../src/app/types'; jest.mock('adm-zip'); jest.mock('../../src/utils/logger', () => ({ @@ -15,77 +12,98 @@ jest.mock('../../src/utils/logger', () => ({ } })); -describe('download-utils', () => { - let mockFs: Editor; - let mockServiceProvider: AbapServiceProvider; +describe('extractZip', () => { + let mockZip: any; + let mockEntry1: any; + let mockEntry2: any; + let mockFs: any; beforeEach(() => { - mockFs = { - write: jest.fn(), - } as unknown as Editor; - - mockServiceProvider = { - getUi5AbapRepository: jest.fn().mockReturnValue({ - downloadFiles: jest.fn(), - }), - } as unknown as AbapServiceProvider; + mockEntry1 = { + isDirectory: false, + entryName: 'file1.txt', + getData: jest.fn(() => Buffer.from('File 1 content')) + }; + mockEntry2 = { + isDirectory: false, + entryName: 'folder/file2.txt', + getData: jest.fn(() => Buffer.from('File 2 content')) + }; + mockZip = { + getEntries: jest.fn(() => [mockEntry1, mockEntry2]) + }; - (PromptState.systemSelection as any) = { - connectedSystem: { - serviceProvider: mockServiceProvider, - }, + (AdmZip as jest.Mock).mockImplementation(() => mockZip); + mockFs = { + write: jest.fn() }; }); - afterEach(() => { - jest.clearAllMocks(); - }); + it('should extract files from zip and write them using fs', async () => { + const extractedPath = '/tmp/project'; + const dummyBuffer = Buffer.from('fake zip content'); + + await extractZip(extractedPath, dummyBuffer, mockFs); - const extractedPath = join('path/to/extract'); - it('should download and extract the application files', async () => { - await downloadApp('repoName', extractedPath, mockFs); - expect(mockServiceProvider.getUi5AbapRepository().downloadFiles).toHaveBeenCalledWith('repoName'); + expect(mockZip.getEntries).toHaveBeenCalled(); + + expect(mockFs.write).toHaveBeenCalledWith( + '/tmp/project/file1.txt', + 'File 1 content' + ); + + expect(mockFs.write).toHaveBeenCalledWith( + '/tmp/project/folder/file2.txt', + 'File 2 content' + ); }); - it('should log an error if the downloaded file is not a Buffer', async () => { - jest.spyOn(mockServiceProvider.getUi5AbapRepository(), 'downloadFiles').mockResolvedValue('not-a-buffer' as any); - await downloadApp('repoName', extractedPath, mockFs); - expect(RepoAppDownloadLogger.logger.error).toBeCalledWith(t('error.appDownloadErrors.downloadedFileNotBufferError')); + it('should log an error if zip extraction fails', async () => { + const errorMessage = 'Zip corrupted!'; + (AdmZip as jest.Mock).mockImplementation(() => { + throw new Error(errorMessage); + }); + + const dummyBuffer = Buffer.from('broken zip'); + await extractZip('/tmp/fail', dummyBuffer, mockFs); + + expect(RepoAppDownloadLogger.logger.error).toHaveBeenCalledWith( + t('error.appDownloadErrors.zipExtractionError', { error: errorMessage }) + ); }); +}); - it('should throw an error if the download fails', async () => { - const errorMessage = 'Mock download error'; - jest.spyOn(mockServiceProvider.getUi5AbapRepository(), 'downloadFiles').mockRejectedValue(new Error(errorMessage)); - await expect(downloadApp('repoName', extractedPath, mockFs)).rejects.toThrowError( - t('error.appDownloadErrors.appDownloadFailure', { error: errorMessage })); +describe('downloadApp', () => { + const mockDownloadFiles = jest.fn(); + const mockGetUi5AbapRepository = jest.fn(() => ({ + downloadFiles: mockDownloadFiles + })); + const mockServiceProvider = { + getUi5AbapRepository: mockGetUi5AbapRepository + }; + + beforeEach(() => { + mockDownloadFiles.mockReset(); + mockGetUi5AbapRepository.mockClear(); + PromptState.systemSelection = { + connectedSystem: { + serviceProvider: mockServiceProvider + } + } as any; }); - it('should extract files from a ZIP archive and write them to the file system', async () => { - const appContents = 'app contents', appName = 'app-name'; - const mockZipEntry = { - isDirectory: false, - entryName: appName, - getData: jest.fn().mockReturnValue(Buffer.from(appContents)) - }; - const mockZip = { - getEntries: jest.fn().mockReturnValue([mockZipEntry]), - }; - (AdmZip as jest.Mock).mockImplementation(() => mockZip); + it('should download app and store it in PromptState', async () => { + const mockPackage = { name: 'app-1', files: ['files.js'] }; + mockDownloadFiles.mockResolvedValue(mockPackage); - jest.spyOn(mockServiceProvider.getUi5AbapRepository(), 'downloadFiles').mockResolvedValue(Buffer.from(appContents)); - await downloadApp('repoName', extractedPath, mockFs); + await downloadApp('repo-1'); - expect(mockZip.getEntries).toHaveBeenCalled(); - expect(mockFs.write).toHaveBeenCalledWith(join(`${extractedPath}/${appName}`), appContents); + expect(mockDownloadFiles).toHaveBeenCalledWith('repo-1'); + expect(PromptState.downloadedAppPackage).toEqual(mockPackage); }); - it('should log an error if extraction fails', async () => { - const errorMessage = 'Mock extraction error'; - (AdmZip as jest.Mock).mockImplementation(() => { - throw new Error(errorMessage); - }); - jest.spyOn(mockServiceProvider.getUi5AbapRepository(), 'downloadFiles').mockResolvedValue(Buffer.from('app contents')); - await downloadApp('repoName', extractedPath, mockFs); - expect(RepoAppDownloadLogger.logger.error).toBeCalledWith(t('error.appDownloadErrors.zipExtractionError', { error: errorMessage })); + it('should throw if serviceProvider is undefined', async () => { + PromptState.systemSelection = undefined as unknown as Partial; + await expect(downloadApp('repo-1')).rejects.toThrow(); }); -}); \ No newline at end of file +}); From 7367e83af8942427fe540cccaea85d088c2416ca Mon Sep 17 00:00:00 2001 From: I743583 Date: Tue, 15 Apr 2025 08:47:46 +0100 Subject: [PATCH 36/41] pnpm recursive isntall --- .../test/prompts/prompts.test.ts | 1 - pnpm-lock.yaml | 7 +++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/repo-app-download-sub-generator/test/prompts/prompts.test.ts b/packages/repo-app-download-sub-generator/test/prompts/prompts.test.ts index a787a874d9..797618a517 100644 --- a/packages/repo-app-download-sub-generator/test/prompts/prompts.test.ts +++ b/packages/repo-app-download-sub-generator/test/prompts/prompts.test.ts @@ -5,7 +5,6 @@ import { PromptNames } from '../../src/app/types'; import { PromptState } from '../../src/prompts/prompt-state'; import * as helpers from '../../src/prompts/prompt-helpers'; import * as downloadUtils from '../../src/utils/download-utils'; -import * as validator from '@sap-ux/project-input-validator'; jest.mock('@sap-ux/odata-service-inquirer', () => ({ getSystemSelectionQuestions: jest.fn().mockResolvedValue({ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 709f6b7408..5c677489e6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3346,9 +3346,6 @@ importers: inquirer: specifier: 8.2.6 version: 8.2.6 - inquirer-autocomplete-prompt: - specifier: 2.0.1 - version: 2.0.1(inquirer@8.2.6) yeoman-generator: specifier: 5.10.0 version: 5.10.0(mem-fs@2.1.0)(yeoman-environment@3.19.3) @@ -3398,6 +3395,9 @@ importers: fs-extra: specifier: 10.0.0 version: 10.0.0 + inquirer-autocomplete-prompt: + specifier: 2.0.1 + version: 2.0.1(inquirer@8.2.6) lodash: specifier: 4.17.21 version: 4.17.21 @@ -17259,7 +17259,6 @@ packages: picocolors: 1.1.1 run-async: 2.4.1 rxjs: 7.8.1 - dev: false /inquirer@1.2.3: resolution: {integrity: sha512-diSnpgfv/Ozq6QKuV2mUcwZ+D24b03J3W6EVxzvtkCWJTPrH2gKLsqgSW0vzRMZZFhFdhnvzka0RUJxIm7AOxQ==} From 1f7efb32119c2a79069d2769f86cd1e6b15fb7d9 Mon Sep 17 00:00:00 2001 From: I743583 Date: Tue, 15 Apr 2025 09:53:19 +0100 Subject: [PATCH 37/41] pnpm recursive install fix --- packages/repo-app-download-sub-generator/package.json | 1 - packages/repo-app-download-sub-generator/src/utils/updates.ts | 2 +- packages/repo-app-download-sub-generator/tsconfig.json | 3 --- pnpm-lock.yaml | 3 --- 4 files changed, 1 insertion(+), 8 deletions(-) diff --git a/packages/repo-app-download-sub-generator/package.json b/packages/repo-app-download-sub-generator/package.json index 847e2127c5..5680a36fce 100644 --- a/packages/repo-app-download-sub-generator/package.json +++ b/packages/repo-app-download-sub-generator/package.json @@ -33,7 +33,6 @@ "@sap-devx/yeoman-ui-types": "1.14.4", "@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/odata-service-inquirer": "workspace:*", diff --git a/packages/repo-app-download-sub-generator/src/utils/updates.ts b/packages/repo-app-download-sub-generator/src/utils/updates.ts index aa7959c741..df3e7b616f 100644 --- a/packages/repo-app-download-sub-generator/src/utils/updates.ts +++ b/packages/repo-app-download-sub-generator/src/utils/updates.ts @@ -16,7 +16,7 @@ import { fioriAppSourcetemplateId } from './constants'; * - If internal features are enabled, it sets the minUI5Version to '${sap.ui5.dist.version}'. * * @param {string} manifestFilePath - The manifest file path. - * @param fs + * @param {Editor} fs - The file system editor instance. * @returns {Promise} - The updated manifest object. * @throws {Error} - Throws an error if the manifest structure is invalid or no fallback version is available. */ diff --git a/packages/repo-app-download-sub-generator/tsconfig.json b/packages/repo-app-download-sub-generator/tsconfig.json index 5c28d132d5..3034b18067 100644 --- a/packages/repo-app-download-sub-generator/tsconfig.json +++ b/packages/repo-app-download-sub-generator/tsconfig.json @@ -30,9 +30,6 @@ { "path": "../fiori-tools-settings" }, - { - "path": "../i18n" - }, { "path": "../inquirer-common" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5c677489e6..bb81655f02 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3310,9 +3310,6 @@ importers: '@sap-ux/fiori-tools-settings': specifier: workspace:* version: link:../fiori-tools-settings - '@sap-ux/i18n': - specifier: workspace:* - version: link:../i18n '@sap-ux/inquirer-common': specifier: workspace:* version: link:../inquirer-common From 9f714ea6f39084d7039afd727a3c088067a36426 Mon Sep 17 00:00:00 2001 From: I743583 Date: Tue, 15 Apr 2025 10:25:48 +0100 Subject: [PATCH 38/41] small change on review --- .../src/prompts/prompts.ts | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/packages/repo-app-download-sub-generator/src/prompts/prompts.ts b/packages/repo-app-download-sub-generator/src/prompts/prompts.ts index cfd95b55f3..4c6cde489a 100644 --- a/packages/repo-app-download-sub-generator/src/prompts/prompts.ts +++ b/packages/repo-app-download-sub-generator/src/prompts/prompts.ts @@ -98,16 +98,9 @@ export async function getPrompts( { when: async (answers: RepoAppDownloadAnswers): Promise => { if (answers[PromptNames.systemSelection]) { - if (quickDeployedAppConfig?.appId) { - appList = await fetchAppListForSelectedSystem( - systemQuestions.answers.connectedSystem?.serviceProvider as AbapServiceProvider, - quickDeployedAppConfig.appId - ); - } else { - appList = await fetchAppListForSelectedSystem( - systemQuestions.answers.connectedSystem?.serviceProvider as AbapServiceProvider - ); - } + appList = await fetchAppListForSelectedSystem( + systemQuestions.answers.connectedSystem?.serviceProvider as AbapServiceProvider + ); } return !!systemQuestions.answers.connectedSystem?.serviceProvider; }, From 993dbb18cdc10d5fafcbe18d39af24a0bab67d22 Mon Sep 17 00:00:00 2001 From: I743583 Date: Tue, 15 Apr 2025 10:28:11 +0100 Subject: [PATCH 39/41] refactor --- .../repo-app-download-sub-generator/src/prompts/prompts.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/repo-app-download-sub-generator/src/prompts/prompts.ts b/packages/repo-app-download-sub-generator/src/prompts/prompts.ts index 4c6cde489a..93416891a4 100644 --- a/packages/repo-app-download-sub-generator/src/prompts/prompts.ts +++ b/packages/repo-app-download-sub-generator/src/prompts/prompts.ts @@ -99,7 +99,8 @@ export async function getPrompts( when: async (answers: RepoAppDownloadAnswers): Promise => { if (answers[PromptNames.systemSelection]) { appList = await fetchAppListForSelectedSystem( - systemQuestions.answers.connectedSystem?.serviceProvider as AbapServiceProvider + systemQuestions.answers.connectedSystem?.serviceProvider as AbapServiceProvider, + quickDeployedAppConfig?.appId ); } return !!systemQuestions.answers.connectedSystem?.serviceProvider; From a331507388a7701387e55a4a364fbd1bf46043d4 Mon Sep 17 00:00:00 2001 From: I743583 Date: Tue, 15 Apr 2025 13:57:32 +0100 Subject: [PATCH 40/41] improvising service provider logic --- .../src/prompts/prompts.ts | 42 ++++--------------- .../test/prompts/prompts.test.ts | 35 ++-------------- 2 files changed, 12 insertions(+), 65 deletions(-) diff --git a/packages/repo-app-download-sub-generator/src/prompts/prompts.ts b/packages/repo-app-download-sub-generator/src/prompts/prompts.ts index 93416891a4..4ab31e2458 100644 --- a/packages/repo-app-download-sub-generator/src/prompts/prompts.ts +++ b/packages/repo-app-download-sub-generator/src/prompts/prompts.ts @@ -7,7 +7,6 @@ import type { FileBrowserQuestion } from '@sap-ux/inquirer-common'; import { validateFioriAppTargetFolder } from '@sap-ux/project-input-validator'; import { PromptState } from './prompt-state'; import { fetchAppListForSelectedSystem, formatAppChoices } from './prompt-helpers'; -import type { ListQuestion } from 'inquirer'; import { downloadApp } from '../utils/download-utils'; /** @@ -46,22 +45,6 @@ const getTargetFolderPrompt = (appRootPath?: string, appId?: string): FileBrowse } as FileBrowserQuestion; }; -/** - * Extracts default system from the quick deployed app configuration. - * - * @param {QuickDeployedAppConfig | undefined} quickDeployedAppConfig - The quick deployed app configuration. - * @returns {string} The default system. - */ -function extractDefaultSystem(quickDeployedAppConfig?: QuickDeployedAppConfig): string { - let defaultSystem = ''; - - if (quickDeployedAppConfig?.appId && quickDeployedAppConfig?.serviceProviderInfo) { - defaultSystem = quickDeployedAppConfig.serviceProviderInfo.name; - } - - return defaultSystem; -} - /** * Retrieves prompts for selecting a system, app list, and target folder where the app will be generated. * @@ -76,22 +59,13 @@ export async function getPrompts( try { PromptState.reset(); - const systemQuestions = await getSystemSelectionQuestions({ serviceSelection: { hide: true } }, false); - // Filter system questions and set default system if applicable - if (quickDeployedAppConfig?.appId) { - const defaultSystem = extractDefaultSystem(quickDeployedAppConfig); - const filteredSystemQuestion = systemQuestions.prompts.find((p) => p.name === PromptNames.systemSelection); - - if (filteredSystemQuestion) { - const choices = (filteredSystemQuestion as ListQuestion).choices; - - if (Array.isArray(choices)) { - const defaultIndex = choices.findIndex((choice: any) => choice.value.system.name === defaultSystem); - filteredSystemQuestion.default = defaultIndex !== -1 ? defaultIndex : undefined; - systemQuestions.prompts = [filteredSystemQuestion]; - } - } - } + const systemQuestions = await getSystemSelectionQuestions( + { + serviceSelection: { hide: true }, + systemSelection: { defaultChoice: quickDeployedAppConfig?.serviceProviderInfo?.name } + }, + false + ); let appList: AppIndex = []; const appSelectionPrompt = [ @@ -133,7 +107,7 @@ export async function getPrompts( await downloadApp(answers.repoName); return true; } catch (error) { - throw new Error(t('error.appDownloadErrors.appDownloadFailure', { error: error.message })); + return t('error.appDownloadErrors.appDownloadFailure', { error: error.message }); } } } diff --git a/packages/repo-app-download-sub-generator/test/prompts/prompts.test.ts b/packages/repo-app-download-sub-generator/test/prompts/prompts.test.ts index 797618a517..8950b72d64 100644 --- a/packages/repo-app-download-sub-generator/test/prompts/prompts.test.ts +++ b/packages/repo-app-download-sub-generator/test/prompts/prompts.test.ts @@ -5,6 +5,7 @@ import { PromptNames } from '../../src/app/types'; import { PromptState } from '../../src/prompts/prompt-state'; import * as helpers from '../../src/prompts/prompt-helpers'; import * as downloadUtils from '../../src/utils/download-utils'; +import RepoAppDownloadLogger from '../../src/utils/logger'; jest.mock('@sap-ux/odata-service-inquirer', () => ({ getSystemSelectionQuestions: jest.fn().mockResolvedValue({ @@ -89,7 +90,8 @@ describe('getPrompts', () => { choices: [ { name: 'System A', value: { system: { name: 'SystemA' } } }, { name: 'Default System', value: { system: { name: 'DefaultSystem' } } } - ] + ], + default: 'DefaultSystem' }], answers: { connectedSystem: { serviceProvider: {} } @@ -100,38 +102,9 @@ describe('getPrompts', () => { const prompts = await getPrompts(undefined, quickDeployedAppConfig); const systemPrompt = prompts.find(p => p.name === PromptNames.systemSelection) as any; - expect(systemPrompt.default).toBe(1); + expect(systemPrompt.default).toBe('DefaultSystem'); }); - it('should throw an error if downloadApp fails', async () => { - mockGetSystemSelectionQuestions.mockResolvedValue({ - prompts: [{ - name: PromptNames.systemSelection, - type: 'list', - choices: [{ name: 'Sys', value: { system: { name: 'Sys' } } }] - }], - answers: { - connectedSystem: { serviceProvider: {} } - } - }); - - const appList = [{ appId: 'app1', repoName: 'repo1' }]; - mockFetchAppList.mockResolvedValue(appList); - mockDownloadApp.mockRejectedValue(new Error('Download failed')); - - const prompts = await getPrompts(); - - const appPrompt = prompts.find(p => p.name === PromptNames.selectedApp) as any; - - await appPrompt.when({ - [PromptNames.systemSelection]: { system: { name: 'Sys' } } - }); - - await expect(appPrompt.validate({ appId: 'app1', repoName: 'repo1' })).rejects.toThrow( - t('error.appDownloadErrors.appDownloadFailure', { error: 'Download failed' }) - ); - }); - it('should use validateFioriAppTargetFolder in folder prompt', async () => { mockGetSystemSelectionQuestions.mockResolvedValue({ prompts: [], From 6402f95dbe631b40af3137ac87d1718784627c45 Mon Sep 17 00:00:00 2001 From: I743583 Date: Tue, 15 Apr 2025 14:35:13 +0100 Subject: [PATCH 41/41] lint fix --- .../repo-app-download-sub-generator/src/prompts/prompts.ts | 3 ++- .../test/prompts/prompts.test.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/repo-app-download-sub-generator/src/prompts/prompts.ts b/packages/repo-app-download-sub-generator/src/prompts/prompts.ts index 4ab31e2458..0862ace4fa 100644 --- a/packages/repo-app-download-sub-generator/src/prompts/prompts.ts +++ b/packages/repo-app-download-sub-generator/src/prompts/prompts.ts @@ -88,7 +88,7 @@ export async function getPrompts( }, message: t('prompts.appSelection.message'), choices: (): { name: string; value: AppInfo }[] => (appList.length ? formatAppChoices(appList) : []), - validate: async (answers: AppInfo): Promise => { + validate: async (answers: AppInfo): Promise => { // Quick deploy config exists but no apps found if (quickDeployedAppConfig?.appId && appList.length === 0) { return t('error.quickDeployedAppDownloadErrors.noAppsFound', { @@ -110,6 +110,7 @@ export async function getPrompts( return t('error.appDownloadErrors.appDownloadFailure', { error: error.message }); } } + return false; } } ]; diff --git a/packages/repo-app-download-sub-generator/test/prompts/prompts.test.ts b/packages/repo-app-download-sub-generator/test/prompts/prompts.test.ts index 8950b72d64..5795690631 100644 --- a/packages/repo-app-download-sub-generator/test/prompts/prompts.test.ts +++ b/packages/repo-app-download-sub-generator/test/prompts/prompts.test.ts @@ -19,6 +19,7 @@ jest.mock('@sap-ux/odata-service-inquirer', () => ({ } }) })); + jest.mock('../../src/prompts/prompt-helpers', () => ({ fetchAppListForSelectedSystem: jest.fn().mockResolvedValue([ { appId: 'app1', repoName: 'repo1' }, @@ -104,7 +105,7 @@ describe('getPrompts', () => { const systemPrompt = prompts.find(p => p.name === PromptNames.systemSelection) as any; expect(systemPrompt.default).toBe('DefaultSystem'); }); - + it('should use validateFioriAppTargetFolder in folder prompt', async () => { mockGetSystemSelectionQuestions.mockResolvedValue({ prompts: [],