Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ui5-writer): abstract functions #1040

Merged
merged 16 commits into from
Jun 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/beige-news-compete.md
Original file line number Diff line number Diff line change
@@ -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
1 change: 0 additions & 1 deletion packages/fiori-elements-writer/test/lrop.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
21 changes: 21 additions & 0 deletions packages/project-access/src/file/file-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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<string[] | []> {
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);
}
2 changes: 1 addition & 1 deletion packages/project-access/src/file/index.ts
Original file line number Diff line number Diff line change
@@ -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';
1 change: 1 addition & 0 deletions packages/project-access/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ export {
loadModuleFromProject,
readUi5Yaml
} from './project';
export { getFilePaths } from './file';
export * from './types';
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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));
});
});
});
76 changes: 1 addition & 75 deletions packages/ui5-application-writer/src/data/defaults.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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<B, E>(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.
*
Expand Down Expand Up @@ -120,45 +85,6 @@ export function mergeUi5(ui5: Partial<UI5>, options?: Partial<AppOptions>): 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.
*
Expand Down
3 changes: 2 additions & 1 deletion packages/ui5-application-writer/src/data/index.ts
Original file line number Diff line number Diff line change
@@ -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';

/**
Expand Down
17 changes: 0 additions & 17 deletions packages/ui5-application-writer/src/files.ts

This file was deleted.

7 changes: 3 additions & 4 deletions packages/ui5-application-writer/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -122,7 +121,7 @@ async function enableTypescript(basePath: string, fs?: Editor): Promise<Editor>
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());

Expand Down
39 changes: 20 additions & 19 deletions packages/ui5-application-writer/src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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);
Expand All @@ -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<void> } = {
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)
};

/**
Expand All @@ -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]);
Expand All @@ -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]);
Expand All @@ -103,18 +104,18 @@ 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,
tmplPath: string,
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 });
}
});
}
}
}
49 changes: 3 additions & 46 deletions packages/ui5-application-writer/test/data.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => ({
Expand All @@ -15,50 +16,6 @@ jest.mock('child_process', () => ({
})
}));

describe('mergeObjects', () => {
const base: Partial<Package> = {
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<UI5>; expected: UI5 }[] = [
// 0
Expand Down
Loading