UNPKG

@sap/generator-fiori

Version:

Create an SAPUI5 application using SAP Fiori elements or a freestyle approach

565 lines (541 loc) 24.2 kB
"use strict"; exports.id = 944; exports.ids = [944]; exports.modules = { /***/ 35445: /***/ ((__unused_webpack_module, exports, __webpack_require__) => { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.readManifest = readManifest; exports.generateOPAFiles = generateOPAFiles; exports.generatePageObjectFile = generatePageObjectFile; const path_1 = __webpack_require__(16928); const mem_fs_1 = __webpack_require__(64812); const mem_fs_editor_1 = __webpack_require__(90718); const types_1 = __webpack_require__(30420); const i18n_1 = __webpack_require__(60577); const project_access_1 = __webpack_require__(20787); /** * Reads the manifest for an app. * * @param fs - a reference to a mem-fs editor * @param basePath - the root folder of the app * @returns the manifest object. An exception is thrown if the manifest cannot be read. */ function readManifest(fs, basePath) { const manifest = fs.readJSON((0, path_1.join)(basePath, project_access_1.DirName.Webapp, project_access_1.FileName.Manifest)); if (!manifest) { throw new types_1.ValidationError((0, i18n_1.t)('error.cannotReadManifest', { filePath: (0, path_1.join)(basePath, project_access_1.DirName.Webapp, project_access_1.FileName.Manifest) })); } return manifest; } /** * Retrieves the application type of the main datasource (FreeStyle, FE V2 or FE V4). * * @param manifest - the app descriptor of the app * @returns {{ applicationType: string, hideFilterBar: boolean }} An object containing the application type and hideFilterBar flag. An exception is thrown if it can't be found or if it's not supported */ function getAppTypeAndHideFilterBarFromManifest(manifest) { const appTargets = manifest['sap.ui5']?.routing?.targets; let hideFilterBar = false; let isFEV4 = false; for (const targetKey in appTargets) { const target = appTargets[targetKey]; if (target.type === 'Component' && target.name && target.name in types_1.SupportedPageTypes) { isFEV4 = true; if (types_1.SupportedPageTypes[target.name] === 'ListReport') { hideFilterBar = target.options?.settings?.hideFilterBar ?? false; } } } if (!isFEV4) { throw new types_1.ValidationError((0, i18n_1.t)('error.badApplicationType')); } return { applicationType: 'v4', hideFilterBar }; // For the time being, only FE V4 is supported } /** * Retrieves appID and appPath from the manifest. * * @param manifest - the app descriptor of the app * @param forcedAppID - the appID in case we don't want to read it from the manifest * @returns appID and appPath */ function getAppFromManifest(manifest, forcedAppID) { const appID = forcedAppID ?? manifest['sap.app']?.id; const appPath = appID?.split('.').join('/'); if (!appID || !appPath) { throw new types_1.ValidationError((0, i18n_1.t)('error.cannotReadAppID')); } return { appID, appPath }; } /** * Create the page configuration object from the app descriptor and the target key. * * @param manifest - the app descriptor of the app * @param targetKey - the key of the target in the manifest * @param forcedAppID - the appID in case we don't want to read it from the manifest * @returns Page configuration object, or undefined if the target type is not supported */ function createPageConfig(manifest, targetKey, forcedAppID) { const appTargets = manifest['sap.ui5']?.routing?.targets; const target = appTargets && appTargets[targetKey]; const { appID, appPath } = getAppFromManifest(manifest, forcedAppID); if (target?.type === 'Component' && target?.name && target.name in types_1.SupportedPageTypes && target?.id && (target?.options?.settings?.entitySet || target?.options?.settings?.contextPath)) { const pageConfig = { appPath, appID, targetKey, componentID: target.id, template: types_1.SupportedPageTypes[target.name], isStartup: false }; if (target.options.settings.contextPath) { pageConfig.contextPath = target.options.settings.contextPath; } else if (target.options.settings.entitySet) { pageConfig.entitySet = target.options.settings.entitySet; } return pageConfig; } else { return undefined; } } /** * Create the configuration object from the app descriptor. * * @param manifest - the app descriptor of the target app * @param opaConfig - parameters for the generation * @param opaConfig.scriptName - the name of the OPA journey file. If not specified, 'FirstJourney' will be used * @param opaConfig.htmlTarget - the name of the html file that will be used in the OPA journey file. If not specified, 'index.html' will be used * @param opaConfig.appID - the appID. If not specified, will be read from the manifest in sap.app/id * @param hideFilterBar - whether the filter bar should be hidden in the generated tests * @returns OPA test configuration object */ function createConfig(manifest, opaConfig, hideFilterBar) { // General application info const { appID, appPath } = getAppFromManifest(manifest, opaConfig.appID); const config = { appID, appPath, pages: [], opaJourneyFileName: opaConfig.scriptName ?? 'FirstJourney', htmlTarget: opaConfig.htmlTarget ?? 'index.html', hideFilterBar }; // Identify startup targets from the routes const appRoutes = (manifest['sap.ui5']?.routing?.routes ?? []); // Find the route with an empty pattern (except for the trailing query part) const startupRoute = appRoutes.find((route) => { return route.pattern.replace(':?query:', '') === ''; }); let startupTargets = startupRoute?.target ?? []; if (!Array.isArray(startupTargets)) { startupTargets = [startupTargets]; } // Create page configurations in supported cases const appTargets = manifest['sap.ui5']?.routing?.targets; for (const targetKey in appTargets) { const pageConfig = createPageConfig(manifest, targetKey, opaConfig.appID); if (pageConfig) { pageConfig.isStartup = startupTargets.includes(targetKey); config.pages.push(pageConfig); } } return config; } /** * Finds the initial ListReport page and the first Object page from the app. * * @param pages - the page configs of the app * @param manifest - the app descriptor of the target app * @returns the page fonfigs for the LR and the OP if they're found */ function findLROP(pages, manifest) { const pageLR = pages.find((page) => { return page.isStartup && page.template === 'ListReport'; }); if (!pageLR) { return {}; } const appTargets = manifest['sap.ui5']?.routing?.targets; const appRoutes = (manifest['sap.ui5']?.routing?.routes ?? []); const target = appTargets?.[pageLR.targetKey]; if (!target?.options?.settings?.navigation) { return { pageLR }; // No navigation from LR } // Find all targets that can be navigated from the LR page const navigatedTargetKeys = []; for (const navKey in target.options.settings.navigation) { const navObject = target.options.settings.navigation[navKey]; const navigatedRoute = navObject.detail?.route && appRoutes.find((route) => { return route.name === navObject.detail?.route; }); if (Array.isArray(navigatedRoute?.target)) { navigatedTargetKeys.push(...navigatedRoute.target); } else if (navigatedRoute?.target) { navigatedTargetKeys.push(navigatedRoute.target); } } // Find the first navigated page that is valid and not the starting LR let pageOP; for (let i = 0; i < navigatedTargetKeys.length && !pageOP; i++) { if (navigatedTargetKeys[i] === pageLR.targetKey) { continue; // This can happen in the FCL case where the LR is also part of the route's targets to the OP } pageOP = pages.find((page) => { return page.targetKey === navigatedTargetKeys[i]; }); } return { pageLR, pageOP }; } /** * Writes a page object in a mem-fs-editor. * * @param pageConfig - the page configuration object * @param rootTemplateDirPath - template root directory * @param testOutDirPath - output test directory (.../webapp/test) * @param fs - a reference to a mem-fs editor */ function writePageObject(pageConfig, rootTemplateDirPath, testOutDirPath, fs) { fs.copyTpl((0, path_1.join)(rootTemplateDirPath, `integration/pages/${pageConfig.template}.js`), (0, path_1.join)(testOutDirPath, `integration/pages/${pageConfig.targetKey}.js`), pageConfig, undefined, { globOptions: { dot: true } }); } /** * Generate OPA test files for a Fiori elements for OData V4 application. * Note: this can potentially overwrite existing files in the webapp/test folder. * * @param basePath - the absolute target path where the application will be generated * @param opaConfig - parameters for the generation * @param opaConfig.scriptName - the name of the OPA journey file. If not specified, 'FirstJourney' will be used * @param opaConfig.htmlTarget - the name of the html that will be used in OPA journey file. If not specified, 'index.html' will be used * @param opaConfig.appID - the appID. If not specified, will be read from the manifest in sap.app/id * @param fs - an optional reference to a mem-fs editor * @returns Reference to a mem-fs-editor */ function generateOPAFiles(basePath, opaConfig, fs) { const editor = fs ?? (0, mem_fs_editor_1.create)((0, mem_fs_1.create)()); const manifest = readManifest(editor, basePath); const { applicationType, hideFilterBar } = getAppTypeAndHideFilterBarFromManifest(manifest); const config = createConfig(manifest, opaConfig, hideFilterBar); const rootCommonTemplateDirPath = (0, path_1.join)(__dirname, '../templates/common'); const rootV4TemplateDirPath = (0, path_1.join)(__dirname, `../templates/${applicationType}`); // Only v4 is supported for the time being const testOutDirPath = (0, path_1.join)(basePath, 'webapp/test'); // Common test files editor.copyTpl((0, path_1.join)(rootCommonTemplateDirPath), testOutDirPath, // unit tests are not added for Fiori elements app { appId: config.appID }, undefined, { globOptions: { dot: true } }); // Integration (OPA) test files - version-specific editor.copyTpl((0, path_1.join)(rootV4TemplateDirPath, 'integration', 'opaTests.*.*'), (0, path_1.join)(testOutDirPath, 'integration'), config, undefined, { globOptions: { dot: true } }); // Pages files (one for each page in the app) config.pages.forEach((page) => { writePageObject(page, rootV4TemplateDirPath, testOutDirPath, editor); }); // OPA Journey file const startPages = config.pages.filter((page) => page.isStartup).map((page) => page.targetKey); const LROP = findLROP(config.pages, manifest); const journeyParams = { startPages, startLR: LROP.pageLR?.targetKey, navigatedOP: LROP.pageOP?.targetKey, hideFilterBar: config.hideFilterBar }; editor.copyTpl((0, path_1.join)(rootV4TemplateDirPath, 'integration/FirstJourney.js'), (0, path_1.join)(testOutDirPath, `integration/${config.opaJourneyFileName}.js`), journeyParams, undefined, { globOptions: { dot: true } }); return editor; } /** * Generate a page object file for a Fiori elements for OData V4 application. * Note: this doesn't modify other existing files in the webapp/test folder. * * @param basePath - the absolute target path where the application will be generated * @param pageObjectParameters - parameters for the page * @param pageObjectParameters.targetKey - the key of the target in the manifest file corresponding to the page * @param pageObjectParameters.appID - the appID. If not specified, will be read from the manifest in sap.app/id * @param fs - an optional reference to a mem-fs editor * @returns Reference to a mem-fs-editor */ function generatePageObjectFile(basePath, pageObjectParameters, fs) { const editor = fs || (0, mem_fs_editor_1.create)((0, mem_fs_1.create)()); const manifest = readManifest(editor, basePath); const { applicationType } = getAppTypeAndHideFilterBarFromManifest(manifest); const pageConfig = createPageConfig(manifest, pageObjectParameters.targetKey, pageObjectParameters.appID); if (pageConfig) { const rootTemplateDirPath = (0, path_1.join)(__dirname, `../templates/${applicationType}`); // Only v4 is supported for the time being const testOutDirPath = (0, path_1.join)(basePath, 'webapp/test'); writePageObject(pageConfig, rootTemplateDirPath, testOutDirPath, editor); } else { throw new types_1.ValidationError((0, i18n_1.t)('error.cannotGeneratePageFile', { targetKey: pageObjectParameters.targetKey })); } return editor; } //# sourceMappingURL=fiori-elements-opa-writer.js.map /***/ }), /***/ 53591: /***/ ((__unused_webpack_module, exports, __webpack_require__) => { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.generateFreestyleOPAFiles = generateFreestyleOPAFiles; const path_1 = __webpack_require__(16928); const mem_fs_1 = __webpack_require__(64812); const mem_fs_editor_1 = __webpack_require__(90718); const project_access_1 = __webpack_require__(20787); const i18n_1 = __webpack_require__(60577); const ui5_application_writer_1 = __webpack_require__(57727); /** * Updates tsconfig.json to include paths for unit and integration tests. * * @param {Editor} fs - The file system editor instance. * @param {string} destinationRoot - The root directory where tsconfig.json exists. * @param log */ function writeOPATsconfigJsonUpdates(fs, destinationRoot, log) { try { const tsconfig = fs.readJSON((0, path_1.join)(destinationRoot, project_access_1.FileName.Tsconfig)) ?? {}; tsconfig.compilerOptions = tsconfig.compilerOptions || {}; tsconfig.compilerOptions.paths = tsconfig.compilerOptions.paths || {}; tsconfig.compilerOptions.paths['unit/*'] = ['./webapp/test/unit/*']; tsconfig.compilerOptions.paths['integration/*'] = ['./webapp/test/integration/*']; fs.writeJSON((0, path_1.join)(destinationRoot, project_access_1.FileName.Tsconfig), tsconfig); } catch (error) { log?.error((0, i18n_1.t)('error.errorWritingTsConfig', { error: error })); } } /** * Gets the template UI5 version based on the provided UI5 version. * * @param ui5Version - The UI5 version. * @returns template UI5 version. */ function getTemplateUi5Version(ui5Version) { const templateLtsVersion_1_120 = '1.120.0'; if (!ui5Version) { return templateLtsVersion_1_120; } return (0, ui5_application_writer_1.compareUI5VersionGte)(ui5Version, templateLtsVersion_1_120) ? templateLtsVersion_1_120 : ui5_application_writer_1.ui5LtsVersion_1_71; } /** * Filters files based on the template UI5 version. * * @param files - Array of file paths. * @param templateUi5Version - The current template Ui5 Version. * @returns Files that either are testsuite files or reside in the current template UI5 version folder. */ function filterByUi5Version(files, templateUi5Version) { return files.filter((filePath) => { // Always include testsuite files. if (filePath.includes('testsuite.qunit')) { return true; } // For all other files, include only those in the current UI5 version directory. return filePath.includes(`${path_1.sep}${templateUi5Version}${path_1.sep}`); }); } /** * Filters files based on the TypeScript setting. * * @param files - Array of file paths. * @param isTypeScript - If true, include .ts files; if false, include .js files. * @returns Files filtered based on the file extension. */ function filterByTypeScript(files, isTypeScript) { return files.filter((filePath) => { if (filePath.endsWith('.ts')) { return isTypeScript; } if (filePath.endsWith('.js')) { return !isTypeScript; } // Keep all .html file types return true; }); } /** * Determines the destination file path based on the provided file path. * * @param {string} filePath - The original file path. * @param {string} freestyleTemplateDir - The directory file path to be removed from the path. * @param {string} commonTemplateDir - The directory file to be removed for common templates path. * @param {string} templateUi5Version - The UI5 version to be replaced in the path. * @returns {string} - The transformed destination file path. */ function getDestFilePath(filePath, freestyleTemplateDir, commonTemplateDir, templateUi5Version) { if (filePath.includes(freestyleTemplateDir)) { return filePath.replace(freestyleTemplateDir, '').replace(`${path_1.sep}${templateUi5Version}${path_1.sep}`, path_1.sep); } else if (filePath.includes(commonTemplateDir)) { return filePath.replace(commonTemplateDir, ''); } else { return filePath; } } /** * Generates and copies freestyle test files based on configuration. * * @param {string} basePath - The base directory path. * @param {FFOPAConfig} opaConfig - Configuration object. * @param {Editor} fs - Optional file system editor instance. * @param {Logger} log - Optional logger instance. * @returns {Editor} - The modified file system editor. */ async function generateFreestyleOPAFiles(basePath, opaConfig, fs, log) { const fsEditor = fs ?? (0, mem_fs_editor_1.create)((0, mem_fs_1.create)()); const { enableTypeScript, ui5Version, viewName, appId } = opaConfig; const freestyleTemplateDir = (0, path_1.join)(__dirname, '../templates/freestyle/webapp/test'); const commonTemplateDir = (0, path_1.join)(__dirname, '../templates/common'); const testOutDir = (0, path_1.join)(basePath, 'webapp/test'); const templateUi5Version = getTemplateUi5Version(ui5Version); const appIdWithSlash = appId.replace(/[.]/g, '/'); // Replace all dots with slashes const navigationIntent = appId.replace(/[./\\\-\s]/g, ''); // Remove all dots, slashes, dashes, and spaces const templateFiles = await (0, project_access_1.getFilePaths)(freestyleTemplateDir); const isTypeScript = Boolean(enableTypeScript); const templateFilteredFiles = filterByUi5Version(templateFiles, templateUi5Version); const filteredFiles = filterByTypeScript(templateFilteredFiles, isTypeScript); // copy common templates const commonFiles = await (0, project_access_1.getFilePaths)(commonTemplateDir); const filteredCommonFiles = commonFiles.filter((filePath) => filePath.endsWith('.html')); filteredFiles.push(...filteredCommonFiles); const config = { ...opaConfig, viewNamePage: `${viewName}Page`, appIdWithSlash, navigationIntent, ui5Theme: opaConfig.ui5Theme ?? '' }; // Rename files: // - viewName.js files are renamed to include the view name in their file path // - viewName.ts files are renamed with the view name appended with 'Page' const renameMap = { [(0, path_1.join)('/integration/pages/viewName.js')]: (0, path_1.join)(`integration/pages/${viewName}.js`), [(0, path_1.join)('/integration/pages/viewName.ts')]: (0, path_1.join)(`integration/pages/${viewName}Page.ts`), [(0, path_1.join)('/unit/controller/viewName.controller.js')]: (0, path_1.join)(`unit/controller/${viewName}.controller.js`), [(0, path_1.join)('/unit/controller/viewName.controller.ts')]: (0, path_1.join)(`unit/controller/${viewName}Page.controller.ts`) }; // copy templates let freestyleTestTemplatesCopied = false; try { filteredFiles.forEach((filePath) => { // remove template UI5 version from the path const destFilePath = getDestFilePath(filePath, freestyleTemplateDir, commonTemplateDir, templateUi5Version); const destinationFilePath = (0, path_1.join)(testOutDir, renameMap?.[destFilePath] ?? destFilePath); fsEditor.copyTpl(filePath, destinationFilePath, config, undefined, { globOptions: { dot: true } }); }); freestyleTestTemplatesCopied = true; } catch (error) { log?.error((0, i18n_1.t)('error.errorCopyingFreestyleTestTemplates', { error: error })); } if (freestyleTestTemplatesCopied && isTypeScript) { writeOPATsconfigJsonUpdates(fsEditor, basePath, log); } return fsEditor; } //# sourceMappingURL=fiori-freestyle-opa-writer.js.map /***/ }), /***/ 60577: /***/ (function(__unused_webpack_module, exports, __webpack_require__) { var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.initI18n = initI18n; exports.t = t; const i18next_1 = __importDefault(__webpack_require__(71899)); const ui5_test_writer_i18n_json_1 = __importDefault(__webpack_require__(5125)); const NS = 'ui5-test-writer'; /** * Initialize i18next with the translations for this module. */ async function initI18n() { await i18next_1.default.init({ resources: { en: { [NS]: ui5_test_writer_i18n_json_1.default } }, lng: 'en', fallbackLng: 'en', defaultNS: NS, ns: [NS] }); } /** * Helper function facading the call to i18next. * * @param key i18n key * @param options additional options * @returns {string} localized string stored for the given key */ function t(key, options) { return i18next_1.default.t(key, options); } initI18n().catch(() => { // Ignore any errors since the write will still work }); //# sourceMappingURL=i18n.js.map /***/ }), /***/ 53069: /***/ ((__unused_webpack_module, exports, __webpack_require__) => { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.generateFreestyleOPAFiles = exports.generateOPAFiles = void 0; var fiori_elements_opa_writer_1 = __webpack_require__(35445); Object.defineProperty(exports, "generateOPAFiles", ({ enumerable: true, get: function () { return fiori_elements_opa_writer_1.generateOPAFiles; } })); var fiori_freestyle_opa_writer_1 = __webpack_require__(53591); Object.defineProperty(exports, "generateFreestyleOPAFiles", ({ enumerable: true, get: function () { return fiori_freestyle_opa_writer_1.generateFreestyleOPAFiles; } })); //# sourceMappingURL=index.js.map /***/ }), /***/ 30420: /***/ ((__unused_webpack_module, exports) => { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.ValidationError = exports.SupportedPageTypes = void 0; exports.SupportedPageTypes = { 'sap.fe.templates.ListReport': 'ListReport', 'sap.fe.templates.ObjectPage': 'ObjectPage', 'sap.fe.core.fpm': 'FPM' }; /** * General validation error thrown if app config options contain invalid combinations */ class ValidationError extends Error { /** * ValidationError constructor. * * @param message - the error message */ constructor(message) { super(`Validation error: ${message}`); this.name = this.constructor.name; } } exports.ValidationError = ValidationError; //# sourceMappingURL=types.js.map /***/ }), /***/ 5125: /***/ ((module) => { module.exports = /*#__PURE__*/JSON.parse('{"error":{"cannotReadManifest":"Cannot read manifest file {{ filePath }}","cannotReadAppID":"Cannot read appID in the manifest file","badApplicationType":"Cannot determine application type from the manifest, or unsupported type","cannotGeneratePageFile":"Cannot generate page file for target {{ targetKey }}","errorCopyingFreestyleTestTemplates":"Error copying freestyle test templates: {{ error }}","errorWritingTsConfig":"Error writing tsconfig.json: {{ error }}"}}'); /***/ }) }; ;