From 31207b953e76e5b9099cfa9bcec473fb5ae85447 Mon Sep 17 00:00:00 2001 From: Cian Morrin <99677697+cianmSAP@users.noreply.github.com> Date: Thu, 1 Jun 2023 09:09:34 +0100 Subject: [PATCH] feat(ui5-writer): abstract functions (#1040) * feat(ui5-writer): abstract functions * feat(ui5-writer): changeset * feat(ui5-writer): remove unused import * feat(ui5-writer): add project access test * feat(ui5-writer): make getFilePaths async * Revert "feat(ui5-writer): make getFilePaths async" This reverts commit 9debc7e86929e3486d35ef0733aff2d6c39ab9af. * feat(ui5-writer): make getFilePaths async * Linting auto fix commit * feat(ui5-writer): fix import * feat(ui5-writer): pnpm lock yaml --------- Co-authored-by: github-actions[bot] --- .changeset/beige-news-compete.md | 7 ++ .../fiori-elements-writer/test/lrop.test.ts | 1 - .../project-access/src/file/file-search.ts | 21 +++++ packages/project-access/src/file/index.ts | 2 +- packages/project-access/src/index.ts | 1 + ...find-files.test.ts => file-search.test.ts} | 22 +++++- .../src/data/defaults.ts | 76 +------------------ .../ui5-application-writer/src/data/index.ts | 3 +- packages/ui5-application-writer/src/files.ts | 17 ----- packages/ui5-application-writer/src/index.ts | 7 +- .../ui5-application-writer/src/options.ts | 39 +++++----- .../ui5-application-writer/test/data.test.ts | 49 +----------- packages/ui5-config/package.json | 8 +- packages/ui5-config/src/defaults.ts | 16 ++++ packages/ui5-config/src/index.ts | 3 +- packages/ui5-config/src/utils.ts | 61 +++++++++++++++ .../test/utils.test.ts} | 48 +++++++++++- pnpm-lock.yaml | 20 +++-- 18 files changed, 225 insertions(+), 176 deletions(-) create mode 100644 .changeset/beige-news-compete.md rename packages/project-access/test/file/{find-files.test.ts => file-search.test.ts} (78%) delete mode 100644 packages/ui5-application-writer/src/files.ts create mode 100644 packages/ui5-config/src/defaults.ts create mode 100644 packages/ui5-config/src/utils.ts rename packages/{ui5-application-writer/test/defaults.test.ts => ui5-config/test/utils.test.ts} (61%) diff --git a/.changeset/beige-news-compete.md b/.changeset/beige-news-compete.md new file mode 100644 index 0000000000..9a302e74b7 --- /dev/null +++ b/.changeset/beige-news-compete.md @@ -0,0 +1,7 @@ +--- +'@sap-ux/project-access': minor +'@sap-ux/ui5-application-writer': minor +'@sap-ux/ui5-config': minor +--- + +abstract ui5-app-writer functions into appropriate modules diff --git a/packages/fiori-elements-writer/test/lrop.test.ts b/packages/fiori-elements-writer/test/lrop.test.ts index e011c0b73d..487e65c50f 100644 --- a/packages/fiori-elements-writer/test/lrop.test.ts +++ b/packages/fiori-elements-writer/test/lrop.test.ts @@ -13,7 +13,6 @@ import { projectChecks, updatePackageJSONDependencyToUseLocalPath } from './common'; -import { UI5_DEFAULT } from '@sap-ux/ui5-application-writer/src/data/defaults'; const TEST_NAME = 'lropTemplates'; if (debug?.enabled) { diff --git a/packages/project-access/src/file/file-search.ts b/packages/project-access/src/file/file-search.ts index 9da3694994..e2e094626e 100644 --- a/packages/project-access/src/file/file-search.ts +++ b/packages/project-access/src/file/file-search.ts @@ -2,6 +2,7 @@ import type { Editor, FileMap } from 'mem-fs-editor'; import { basename, dirname, extname, join } from 'path'; import { default as find } from 'findit2'; import { fileExists } from './file-access'; +import { promises as fs } from 'fs'; /** * Get deleted and modified files from mem-fs editor filtered by query and 'by' (name|extension). @@ -136,3 +137,23 @@ export async function findFileUp(fileName: string, startPath: string, fs?: Edito return dirname(startPath) !== startPath ? findFileUp(fileName, dirname(startPath), fs) : undefined; } } + +/** + * @description Returns a flat list of all file paths under a directory tree, + * recursing through all subdirectories + * @param {string} dir - the directory to walk + * @returns {string[]} - array of file path strings + * @throws if an error occurs reading a file path + */ +export async function getFilePaths(dir: string): Promise { + const entries = await fs.readdir(dir); + + const filePathsPromises = entries.map(async (entry) => { + const entryPath = join(dir, entry); + const isDirectory = (await fs.stat(entryPath)).isDirectory(); + return isDirectory ? getFilePaths(entryPath) : entryPath; + }); + + const filePaths = await Promise.all(filePathsPromises); + return ([] as string[]).concat(...filePaths); +} diff --git a/packages/project-access/src/file/index.ts b/packages/project-access/src/file/index.ts index 126ce8e5d8..07d45370db 100644 --- a/packages/project-access/src/file/index.ts +++ b/packages/project-access/src/file/index.ts @@ -1,2 +1,2 @@ export { fileExists, readFile, readJSON } from './file-access'; -export { findBy, findFiles, findFilesByExtension, findFileUp } from './file-search'; +export { findBy, findFiles, findFilesByExtension, findFileUp, getFilePaths } from './file-search'; diff --git a/packages/project-access/src/index.ts b/packages/project-access/src/index.ts index 31e5c1bb63..b3faebc516 100644 --- a/packages/project-access/src/index.ts +++ b/packages/project-access/src/index.ts @@ -14,4 +14,5 @@ export { loadModuleFromProject, readUi5Yaml } from './project'; +export { getFilePaths } from './file'; export * from './types'; diff --git a/packages/project-access/test/file/find-files.test.ts b/packages/project-access/test/file/file-search.test.ts similarity index 78% rename from packages/project-access/test/file/find-files.test.ts rename to packages/project-access/test/file/file-search.test.ts index 3eaab9d0c6..afce6258c1 100644 --- a/packages/project-access/test/file/find-files.test.ts +++ b/packages/project-access/test/file/file-search.test.ts @@ -1,5 +1,5 @@ import { join } from 'path'; -import { findFiles, findFilesByExtension, findFileUp } from '../../src/file'; +import { findFiles, findFilesByExtension, findFileUp, getFilePaths } from '../../src/file'; import { create as createStorage } from 'mem-fs'; import { create } from 'mem-fs-editor'; @@ -91,4 +91,24 @@ describe('findFiles', () => { expect(await findFileUp(file, root, fs)).toBe(join(root, file)); }); }); + + describe('getFilePaths', () => { + test('files in the file folder', async () => { + const expectedPaths = [ + expect.stringContaining(join('file/childA/child')), + expect.stringContaining(join('file/childA/child.extension')), + expect.stringContaining(join('file/childA/firstchild')), + expect.stringContaining(join('file/childB/child')), + expect.stringContaining(join('file/childC/child')), + expect.stringContaining(join('file/childC/nested1/nochild')), + expect.stringContaining(join('file/childC/nested2/child')), + expect.stringContaining(join('file/root.extension')), + expect.stringContaining(join('file/rootfile')) + ]; + + const filePaths = await getFilePaths(root); + + expect(filePaths).toEqual(expect.arrayContaining(expectedPaths)); + }); + }); }); diff --git a/packages/ui5-application-writer/src/data/defaults.ts b/packages/ui5-application-writer/src/data/defaults.ts index 0aedc13731..ff0eb9b584 100644 --- a/packages/ui5-application-writer/src/data/defaults.ts +++ b/packages/ui5-application-writer/src/data/defaults.ts @@ -1,3 +1,4 @@ +import { UI5_DEFAULT, getEsmTypesVersion, getTypesVersion } from '@sap-ux/ui5-config'; import type { App, AppOptions, Package, UI5, UI5Framework } from '../types'; import versionToManifestDescMapping from '@ui5/manifest/mapping.json'; // from https://github.com/SAP/ui5-manifest/blob/master/mapping.json import { getUI5Libs } from './ui5Libs'; @@ -29,42 +30,6 @@ export function packageDefaults(version?: string, description?: string): Partial }; } -/** - * Merges two objects. All properties from base and from extension will be present. - * Overlapping properties will be used from extension. Arrays will be concatenated and de-duped. - * - * @param base - any object definition - * @param extension - another object definition - * @returns - a merged package defintion - */ -export function mergeObjects(base: B, extension: E): B & E { - return merge({}, base, extension, (objValue: unknown, srcValue: unknown) => { - // merge and de-dup arrays - if (objValue instanceof Array && srcValue instanceof Array) { - return [...new Set([...objValue, ...srcValue])]; - } else { - return undefined; - } - }); -} - -export const enum UI5_DEFAULT { - DEFAULT_UI5_VERSION = '', - DEFAULT_LOCAL_UI5_VERSION = '1.95.0', - MIN_UI5_VERSION = '1.60.0', - MIN_LOCAL_SAPUI5_VERSION = '1.76.0', - MIN_LOCAL_OPENUI5_VERSION = '1.52.5', - SAPUI5_CDN = 'https://ui5.sap.com', - OPENUI5_CDN = 'https://openui5.hana.ondemand.com', - TYPES_VERSION_SINCE = '1.76.0', - TYPES_VERSION_BEST_MIN = '1.102.0', - TYPES_VERSION_PREVIOUS = '1.71.15', - TYPES_VERSION_BEST = '1.108.0', - ESM_TYPES_VERSION_SINCE = '1.94.0', - MANIFEST_VERSION = '1.12.0', - BASE_COMPONENT = 'sap/ui/core/UIComponent' -} - /** * Returns an app instance merged with default properties. * @@ -120,45 +85,6 @@ export function mergeUi5(ui5: Partial, options?: Partial): UI5 return Object.assign({}, ui5, merged) as UI5; } -/** - * Get the best types version for the given minUI5Version for https://www.npmjs.com/package/@sapui5/ts-types where specific versions are missing. - * - * @param minUI5Version the minimum UI5 version that needs to be supported - * @returns semantic version representing the types version. - */ -export function getTypesVersion(minUI5Version?: string) { - const version = semVer.coerce(minUI5Version); - if (!version) { - return `~${UI5_DEFAULT.TYPES_VERSION_BEST}`; - } else if (semVer.gte(version, UI5_DEFAULT.TYPES_VERSION_BEST)) { - return `~${UI5_DEFAULT.TYPES_VERSION_BEST}`; - } else { - return semVer.gte(version, UI5_DEFAULT.TYPES_VERSION_SINCE) - ? `~${semVer.major(version)}.${semVer.minor(version)}.${semVer.patch(version)}` - : UI5_DEFAULT.TYPES_VERSION_PREVIOUS; - } -} - -/** - * Get the best types version for the given minUI5Version within a selective range, starting at 1.90.0 - * for https://www.npmjs.com/package/@sapui5/ts-types-esm - * For the latest versions the LTS S/4 on-premise version (1.102.x) is used, for anything before we - * match the versions as far back as available. - * - * @param minUI5Version the minimum UI5 version that needs to be supported - * @returns semantic version representing the types version. - */ -export function getEsmTypesVersion(minUI5Version?: string) { - const version = semVer.coerce(minUI5Version); - if (!version || semVer.gte(version, UI5_DEFAULT.TYPES_VERSION_BEST_MIN)) { - return `~${UI5_DEFAULT.TYPES_VERSION_BEST}`; - } else { - return semVer.gte(version, UI5_DEFAULT.ESM_TYPES_VERSION_SINCE) - ? `~${semVer.major(version)}.${semVer.minor(version)}.0` - : `~${UI5_DEFAULT.ESM_TYPES_VERSION_SINCE}`; - } -} - /** * Gets the miminum UI5 version based on the specified version. * diff --git a/packages/ui5-application-writer/src/data/index.ts b/packages/ui5-application-writer/src/data/index.ts index a3f9f16c49..e1e78d7423 100644 --- a/packages/ui5-application-writer/src/data/index.ts +++ b/packages/ui5-application-writer/src/data/index.ts @@ -1,5 +1,6 @@ import type { App, UI5, AppOptions, Package, Ui5App } from '../types'; -import { mergeApp, packageDefaults, mergeUi5, mergeObjects, getSpecTagVersion } from './defaults'; +import { mergeObjects } from '@sap-ux/ui5-config'; +import { mergeApp, packageDefaults, mergeUi5, getSpecTagVersion } from './defaults'; import { validate } from './validators'; /** diff --git a/packages/ui5-application-writer/src/files.ts b/packages/ui5-application-writer/src/files.ts deleted file mode 100644 index de1988b56b..0000000000 --- a/packages/ui5-application-writer/src/files.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { readdirSync, statSync } from 'fs'; -import { join } from 'path'; - -/** - * @description Returns a flat list of all file paths under a directory tree, - * recursing through all subdirectories - * @param {string} dir - the directory to walk - * @returns {string[]} - array of file path strings - * @throws if an error occurs reading a file path - */ -export function getFilePaths(dir: string): string[] | [] { - return readdirSync(dir).reduce((files: string[], entry: string) => { - const entryPath = join(dir, entry); - const isDirectory = statSync(entryPath).isDirectory(); - return isDirectory ? [...files, ...getFilePaths(entryPath)] : [...files, entryPath]; - }, []); -} diff --git a/packages/ui5-application-writer/src/index.ts b/packages/ui5-application-writer/src/index.ts index e819e74e22..2a08d46b06 100644 --- a/packages/ui5-application-writer/src/index.ts +++ b/packages/ui5-application-writer/src/index.ts @@ -3,13 +3,12 @@ import { create as createStorage } from 'mem-fs'; import type { Editor } from 'mem-fs-editor'; import { create } from 'mem-fs-editor'; import type { App, AppOptions, Package, UI5 } from './types'; -import { UI5Config } from '@sap-ux/ui5-config'; +import { UI5Config, getEsmTypesVersion } from '@sap-ux/ui5-config'; import type { Manifest } from '@sap-ux/project-access'; import { mergeWithDefaults } from './data'; import { ui5TSSupport } from './data/ui5Libs'; import { applyOptionalFeatures, enableTypescript as enableTypescriptOption } from './options'; import { Ui5App } from './types'; -import { getEsmTypesVersion } from './data/defaults'; /** * Writes the template to the memfs editor instance. @@ -60,7 +59,7 @@ async function generate(basePath: string, ui5AppConfig: Ui5App, fs?: Editor): Pr ui5LocalConfig.addFioriToolsAppReloadMiddleware(); // Add optional features - applyOptionalFeatures(ui5App, fs, basePath, tmplPath, [ui5Config, ui5LocalConfig]); + await applyOptionalFeatures(ui5App, fs, basePath, tmplPath, [ui5Config, ui5LocalConfig]); // write ui5 yamls fs.write(ui5ConfigPath, ui5Config.toString()); @@ -122,7 +121,7 @@ async function enableTypescript(basePath: string, fs?: Editor): Promise typesVersion: getEsmTypesVersion(manifest['sap.ui5']?.dependencies?.minUI5Version) } }; - enableTypescriptOption({ basePath, fs, ui5Configs: [ui5Config], tmplPath, ui5App }, true); + await enableTypescriptOption({ basePath, fs, ui5Configs: [ui5Config], tmplPath, ui5App }, true); fs.write(ui5ConfigPath, ui5Config.toString()); diff --git a/packages/ui5-application-writer/src/options.ts b/packages/ui5-application-writer/src/options.ts index 3723322324..ad1bb3b683 100644 --- a/packages/ui5-application-writer/src/options.ts +++ b/packages/ui5-application-writer/src/options.ts @@ -2,10 +2,10 @@ import { join } from 'path'; import type { Editor } from 'mem-fs-editor'; import { render } from 'ejs'; import type { Ui5App } from './types'; -import { getFilePaths } from './files'; +import { getFilePaths } from '@sap-ux/project-access'; import type { UI5Config } from '@sap-ux/ui5-config'; import { ui5NPMSupport, ui5TSSupport } from './data/ui5Libs'; -import { mergeObjects, UI5_DEFAULT } from './data/defaults'; +import { mergeObjects, UI5_DEFAULT } from '@sap-ux/ui5-config'; /** * Input required to enable optional features. @@ -28,9 +28,10 @@ export interface FeatureInput { * @param input.basePath project base path * @param input.tmplPath template basepath */ -function copyTemplates(name: string, { ui5App, fs, basePath, tmplPath }: FeatureInput) { +async function copyTemplates(name: string, { ui5App, fs, basePath, tmplPath }: FeatureInput) { const optTmplDirPath = join(tmplPath, 'optional', `${name}`); - const optTmplFilePaths = getFilePaths(optTmplDirPath); + const optTmplFilePaths = await getFilePaths(optTmplDirPath); + optTmplFilePaths.forEach((optTmplFilePath) => { const relPath = optTmplFilePath.replace(optTmplDirPath, ''); const outPath = join(basePath, relPath); @@ -51,13 +52,13 @@ function copyTemplates(name: string, { ui5App, fs, basePath, tmplPath }: Feature /** * Factory functions for applying optional features. */ -const factories: { [key: string]: (input: FeatureInput) => void } = { - codeAssist: (input: FeatureInput) => copyTemplates('codeAssist', input), - eslint: (input: FeatureInput) => copyTemplates('eslint', input), - loadReuseLibs: (input: FeatureInput) => copyTemplates('loadReuseLibs', input), - sapux: (input: FeatureInput) => copyTemplates('sapux', input), - typescript: enableTypescript, - npmPackageConsumption: enableNpmPackageConsumption +const factories: { [key: string]: (input: FeatureInput) => Promise } = { + codeAssist: async (input: FeatureInput) => await copyTemplates('codeAssist', input), + eslint: async (input: FeatureInput) => await copyTemplates('eslint', input), + loadReuseLibs: async (input: FeatureInput) => await copyTemplates('loadReuseLibs', input), + sapux: async (input: FeatureInput) => await copyTemplates('sapux', input), + typescript: async (input: FeatureInput) => await enableTypescript(input), + npmPackageConsumption: async (input: FeatureInput) => await enableNpmPackageConsumption(input) }; /** @@ -66,9 +67,9 @@ const factories: { [key: string]: (input: FeatureInput) => void } = { * @param input Input required to enable the optional typescript features * @param keepOldComponent if set to true then the old Component.js will be renamed but kept. */ -export function enableTypescript(input: FeatureInput, keepOldComponent: boolean = false) { +export async function enableTypescript(input: FeatureInput, keepOldComponent: boolean = false) { input.ui5App.app.baseComponent = input.ui5App.app.baseComponent ?? UI5_DEFAULT.BASE_COMPONENT; - copyTemplates('typescript', input); + await copyTemplates('typescript', input); input.ui5Configs.forEach((ui5Config) => { ui5Config.addCustomMiddleware([ui5TSSupport.middleware]); ui5Config.addCustomTasks([ui5TSSupport.task]); @@ -86,8 +87,8 @@ export function enableTypescript(input: FeatureInput, keepOldComponent: boolean * * @param input Input required to enable the optional npm modules import */ -export function enableNpmPackageConsumption(input: FeatureInput) { - copyTemplates('npmPackageConsumption', input); +export async function enableNpmPackageConsumption(input: FeatureInput) { + await copyTemplates('npmPackageConsumption', input); input.ui5Configs.forEach((ui5Config) => { ui5Config.addCustomMiddleware([ui5NPMSupport.middleware]); ui5Config.addCustomTasks([ui5NPMSupport.task]); @@ -103,7 +104,7 @@ export function enableNpmPackageConsumption(input: FeatureInput) { * @param tmplPath template basepath * @param ui5Configs available UI5 configs */ -export function applyOptionalFeatures( +export async function applyOptionalFeatures( ui5App: Ui5App, fs: Editor, basePath: string, @@ -111,10 +112,10 @@ export function applyOptionalFeatures( ui5Configs: UI5Config[] ) { if (ui5App.appOptions) { - Object.entries(ui5App.appOptions).forEach(([key, value]) => { + for (const [key, value] of Object.entries(ui5App.appOptions)) { if (value === true) { - factories[key]?.({ ui5App, fs, basePath, tmplPath, ui5Configs }); + await factories[key]?.({ ui5App, fs, basePath, tmplPath, ui5Configs }); } - }); + } } } diff --git a/packages/ui5-application-writer/test/data.test.ts b/packages/ui5-application-writer/test/data.test.ts index ff8b8c61b2..6ef7a4a3b5 100644 --- a/packages/ui5-application-writer/test/data.test.ts +++ b/packages/ui5-application-writer/test/data.test.ts @@ -1,6 +1,7 @@ -import { UI5_DEFAULT, mergeUi5, defaultUI5Libs, mergeApp, getSpecTagVersion, mergeObjects } from '../src/data/defaults'; +import { UI5_DEFAULT } from '@sap-ux/ui5-config'; +import { mergeUi5, defaultUI5Libs, mergeApp, getSpecTagVersion } from '../src/data/defaults'; import { mergeWithDefaults } from '../src/data/index'; -import type { App, Package, UI5, Ui5App } from '../src/types'; +import type { App, UI5, Ui5App } from '../src/types'; const mockSpecVersions = JSON.stringify({ latest: '1.102.3', 'UI5-1.71': '1.71.64', 'UI5-1.92': '1.92.1' }); jest.mock('child_process', () => ({ @@ -15,50 +16,6 @@ jest.mock('child_process', () => ({ }) })); -describe('mergeObjects', () => { - const base: Partial = { - scripts: { - first: 'first' - }, - ui5: { - dependencies: ['module-1'] - } - }; - - test('additional ui5 dependencies (array merge)', () => { - const extension: Package = { - name: 'test', - ui5: { - dependencies: ['module-2'] - } - }; - const merged = mergeObjects(base, extension); - expect(merged.ui5?.dependencies).toStrictEqual(['module-1', 'module-2']); - }); - - test('duplicated ui5 dependencies (array merge)', () => { - const extension: Package = { - name: 'test', - ui5: { - dependencies: ['module-1', 'module-2'] - } - }; - const merged = mergeObjects(base, extension); - expect(merged.ui5?.dependencies).toStrictEqual(['module-1', 'module-2']); - }); - - test('overwrite property', () => { - const extension: Package = { - name: 'test', - scripts: { - first: 'second' - } - }; - const merged = mergeObjects(base, extension); - expect(merged.scripts?.first).toBe(extension.scripts?.first); - }); -}); - describe('Setting defaults', () => { const testData: { input: Partial; expected: UI5 }[] = [ // 0 diff --git a/packages/ui5-config/package.json b/packages/ui5-config/package.json index f834199ad1..53fdda7f9e 100644 --- a/packages/ui5-config/package.json +++ b/packages/ui5-config/package.json @@ -31,11 +31,15 @@ "!dist/**/*.map" ], "dependencies": { - "@sap-ux/yaml": "workspace:*" + "@sap-ux/yaml": "workspace:*", + "lodash": "4.17.21", + "semver": "7.3.5" }, "devDependencies": { "mem-fs": "2.1.0", - "mem-fs-editor": "9.4.0" + "mem-fs-editor": "9.4.0", + "@types/lodash": "4.14.176", + "@types/semver": "7.3.9" }, "engines": { "pnpm": ">=6.26.1 < 7.0.0 || >=7.1.0", diff --git a/packages/ui5-config/src/defaults.ts b/packages/ui5-config/src/defaults.ts new file mode 100644 index 0000000000..8287fae7a3 --- /dev/null +++ b/packages/ui5-config/src/defaults.ts @@ -0,0 +1,16 @@ +export const enum UI5_DEFAULT { + DEFAULT_UI5_VERSION = '', + DEFAULT_LOCAL_UI5_VERSION = '1.95.0', + MIN_UI5_VERSION = '1.60.0', + MIN_LOCAL_SAPUI5_VERSION = '1.76.0', + MIN_LOCAL_OPENUI5_VERSION = '1.52.5', + SAPUI5_CDN = 'https://ui5.sap.com', + OPENUI5_CDN = 'https://openui5.hana.ondemand.com', + TYPES_VERSION_SINCE = '1.76.0', + TYPES_VERSION_BEST_MIN = '1.102.0', + TYPES_VERSION_PREVIOUS = '1.71.15', + TYPES_VERSION_BEST = '1.108.0', + ESM_TYPES_VERSION_SINCE = '1.94.0', + MANIFEST_VERSION = '1.12.0', + BASE_COMPONENT = 'sap/ui/core/UIComponent' +} diff --git a/packages/ui5-config/src/index.ts b/packages/ui5-config/src/index.ts index 1a42184ddf..3e3a95e4e8 100644 --- a/packages/ui5-config/src/index.ts +++ b/packages/ui5-config/src/index.ts @@ -15,5 +15,6 @@ export { UI5ProxyConfig, UI5ProxyConfigTarget } from './types'; - +export { UI5_DEFAULT } from './defaults'; +export { mergeObjects, getEsmTypesVersion, getTypesVersion } from './utils'; export { errorCode as yamlErrorCode, YAMLError } from '@sap-ux/yaml'; diff --git a/packages/ui5-config/src/utils.ts b/packages/ui5-config/src/utils.ts new file mode 100644 index 0000000000..5419e31f86 --- /dev/null +++ b/packages/ui5-config/src/utils.ts @@ -0,0 +1,61 @@ +import merge from 'lodash/mergeWith'; +import semVer from 'semver'; +import { UI5_DEFAULT } from './defaults'; + +/** + * Merges two objects. All properties from base and from extension will be present. + * Overlapping properties will be used from extension. Arrays will be concatenated and de-duped. + * + * @param base - any object definition + * @param extension - another object definition + * @returns - a merged package defintion + */ +export function mergeObjects(base: B, extension: E): B & E { + return merge({}, base, extension, (objValue: unknown, srcValue: unknown) => { + // merge and de-dup arrays + if (objValue instanceof Array && srcValue instanceof Array) { + return [...new Set([...objValue, ...srcValue])]; + } else { + return undefined; + } + }); +} + +/** + * Get the best types version for the given minUI5Version within a selective range, starting at 1.90.0 + * for https://www.npmjs.com/package/@sapui5/ts-types-esm + * For the latest versions the LTS S/4 on-premise version (1.102.x) is used, for anything before we + * match the versions as far back as available. + * + * @param minUI5Version the minimum UI5 version that needs to be supported + * @returns semantic version representing the types version. + */ +export function getEsmTypesVersion(minUI5Version?: string) { + const version = semVer.coerce(minUI5Version); + if (!version || semVer.gte(version, UI5_DEFAULT.TYPES_VERSION_BEST_MIN)) { + return `~${UI5_DEFAULT.TYPES_VERSION_BEST}`; + } else { + return semVer.gte(version, UI5_DEFAULT.ESM_TYPES_VERSION_SINCE) + ? `~${semVer.major(version)}.${semVer.minor(version)}.0` + : `~${UI5_DEFAULT.ESM_TYPES_VERSION_SINCE}`; + } +} + +/** + * Get the best types version for the given minUI5Version for https://www.npmjs.com/package/@sapui5/ts-types where specific versions are missing. + * + * @param minUI5Version the minimum UI5 version that needs to be supported + * @returns semantic version representing the types version. + */ +export function getTypesVersion(minUI5Version?: string) { + const version = semVer.coerce(minUI5Version); + if (!version) { + return `~${UI5_DEFAULT.TYPES_VERSION_BEST}`; + } else if (semVer.gte(version, UI5_DEFAULT.TYPES_VERSION_BEST)) { + return `~${UI5_DEFAULT.TYPES_VERSION_BEST}`; + } else { + return semVer.gte(version, UI5_DEFAULT.TYPES_VERSION_SINCE) + ? `~${semVer.major(version)}.${semVer.minor(version)}.${semVer.patch(version)}` + : UI5_DEFAULT.TYPES_VERSION_PREVIOUS; + } +} diff --git a/packages/ui5-application-writer/test/defaults.test.ts b/packages/ui5-config/test/utils.test.ts similarity index 61% rename from packages/ui5-application-writer/test/defaults.test.ts rename to packages/ui5-config/test/utils.test.ts index a6d5578446..d1eec426ab 100644 --- a/packages/ui5-application-writer/test/defaults.test.ts +++ b/packages/ui5-config/test/utils.test.ts @@ -1,6 +1,50 @@ -import { getEsmTypesVersion, getTypesVersion, UI5_DEFAULT } from '../src/data/defaults'; +import { UI5_DEFAULT } from '../src/defaults'; +import { getEsmTypesVersion, getTypesVersion, mergeObjects } from '../src/utils'; -describe('Defaults', () => { +describe('mergeObjects', () => { + const base = { + scripts: { + first: 'first' + }, + ui5: { + dependencies: ['module-1'] + } + }; + + test('additional ui5 dependencies (array merge)', () => { + const extension = { + name: 'test', + ui5: { + dependencies: ['module-2'] + } + }; + const merged = mergeObjects(base, extension); + expect(merged.ui5?.dependencies).toStrictEqual(['module-1', 'module-2']); + }); + + test('duplicated ui5 dependencies (array merge)', () => { + const extension = { + name: 'test', + ui5: { + dependencies: ['module-1', 'module-2'] + } + }; + const merged = mergeObjects(base, extension); + expect(merged.ui5?.dependencies).toStrictEqual(['module-1', 'module-2']); + }); + + test('overwrite property', () => { + const extension = { + name: 'test', + scripts: { + first: 'second' + } + }; + const merged = mergeObjects(base, extension); + expect(merged.scripts?.first).toBe(extension.scripts?.first); + }); +}); +describe('getEsmTypesVersion, getTypesVersion', () => { const esmTypesVersionSince = `~${UI5_DEFAULT.ESM_TYPES_VERSION_SINCE}`; const typesVersionBest = `~${UI5_DEFAULT.TYPES_VERSION_BEST}`; const minU5Version = UI5_DEFAULT.TYPES_VERSION_PREVIOUS; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6f85456609..9b97d93504 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1011,7 +1011,19 @@ importers: '@sap-ux/yaml': specifier: workspace:* version: link:../yaml + lodash: + specifier: 4.17.21 + version: 4.17.21 + semver: + specifier: 7.3.5 + version: 7.3.5 devDependencies: + '@types/lodash': + specifier: 4.14.176 + version: 4.14.176 + '@types/semver': + specifier: 7.3.9 + version: 7.3.9 mem-fs: specifier: 2.1.0 version: 2.1.0 @@ -4644,7 +4656,7 @@ packages: '@babel/preset-env': 7.19.1(@babel/core@7.20.12) '@babel/types': 7.20.7 '@mdx-js/mdx': 1.6.22 - '@types/lodash': 4.14.178 + '@types/lodash': 4.14.176 js-string-escape: 1.0.1 loader-utils: 2.0.4 lodash: 4.17.21 @@ -5202,10 +5214,6 @@ packages: resolution: {integrity: sha512-xZmuPTa3rlZoIbtDUyJKZQimJV3bxCmzMIO2c9Pz9afyDro6kr7R79GwcB6mRhuoPmV2p1Vb66WOJH7F886WKQ==} dev: true - /@types/lodash@4.14.178: - resolution: {integrity: sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw==} - dev: true - /@types/mdast@3.0.10: resolution: {integrity: sha512-W864tg/Osz1+9f4lrGTZpCSO5/z4608eUp19tbozkq2HJK6i3z1kT0H9tlADXuYIb1YYOBByU4Jsqkk75q48qA==} dependencies: @@ -16592,7 +16600,7 @@ packages: json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 - semver: 7.3.8 + semver: 7.3.5 typescript: 4.9.4 yargs-parser: 21.1.1 dev: true