@sap/generator-fiori
Version:
Create an SAPUI5 application using SAP Fiori elements or a freestyle approach
565 lines (541 loc) • 24.2 kB
JavaScript
"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 }}"}}');
/***/ })
};
;