From d266c618f90cc939513b25cb18292d88b7a60f1f Mon Sep 17 00:00:00 2001 From: Vinicius Rosa Date: Thu, 19 Jan 2023 17:39:12 +1100 Subject: [PATCH] feat(ui-devkit): Update solution not to require extension id --- packages/ui-devkit/src/compiler/scaffold.ts | 53 +++-- packages/ui-devkit/src/compiler/types.ts | 230 ++++++++------------ packages/ui-devkit/src/compiler/utils.ts | 8 +- 3 files changed, 130 insertions(+), 161 deletions(-) diff --git a/packages/ui-devkit/src/compiler/scaffold.ts b/packages/ui-devkit/src/compiler/scaffold.ts index dfe580559e..e74c5a2f58 100644 --- a/packages/ui-devkit/src/compiler/scaffold.ts +++ b/packages/ui-devkit/src/compiler/scaffold.ts @@ -11,9 +11,9 @@ import { } from './constants'; import { getAllTranslationFiles, mergeExtensionTranslations } from './translations'; import { - AdminUiExtension, AdminUiExtensionLazyModule, AdminUiExtensionSharedModule, + AdminUiExtensionWithId, Extension, GlobalStylesExtension, SassVariableOverridesExtension, @@ -24,7 +24,6 @@ import { copyUiDevkit, isAdminUiExtension, isGlobalStylesExtension, - isModulePathMappingExtension, isSassVariableOverridesExtension, isStaticAssetExtension, isTranslationExtension, @@ -36,11 +35,12 @@ import { export async function setupScaffold(outputPath: string, extensions: Extension[]) { deleteExistingExtensionModules(outputPath); - const modulePathMappingExtension = extensions.find(isModulePathMappingExtension); - copyAdminUiSource(outputPath, modulePathMappingExtension?.modulePathMapping); - const adminUiExtensions = extensions.filter(isAdminUiExtension); const normalizedExtensions = normalizeExtensions(adminUiExtensions); + + const modulePathMapping = generateModulePathMapping(normalizedExtensions); + copyAdminUiSource(outputPath, modulePathMapping); + await copyExtensionModules(outputPath, normalizedExtensions); const staticAssetExtensions = extensions.filter(isStaticAssetExtension); @@ -72,11 +72,30 @@ function deleteExistingExtensionModules(outputPath: string) { fs.removeSync(path.join(outputPath, MODULES_OUTPUT_DIR)); } +/** + * Generates a module path mapping object for all extensions with a "pathAlias" + * property declared (if any). + */ +function generateModulePathMapping(extensions: Array) { + const extensionsWithAlias = extensions.filter(e => e.pathAlias); + if (extensionsWithAlias.length === 0) { + return undefined; + } + + return extensionsWithAlias.reduce((acc, e) => { + // for imports from the index file if there is one + acc[e.pathAlias as string] = [`src/extensions/${e.id}`]; + // direct access to files / deep imports + acc[`${e.pathAlias as string}/*`] = [`src/extensions/${e.id}/*`]; + return acc; + }, {} as Record); +} + /** * Copies all files from the extensionPaths of the configured extensions into the * admin-ui source tree. */ -async function copyExtensionModules(outputPath: string, extensions: Array>) { +async function copyExtensionModules(outputPath: string, extensions: Array) { const extensionRoutesSource = generateLazyExtensionRoutes(extensions); fs.writeFileSync(path.join(outputPath, EXTENSION_ROUTES_FILE), extensionRoutesSource, 'utf8'); const sharedExtensionModulesSource = generateSharedExtensionModule(extensions); @@ -144,9 +163,9 @@ export async function copyGlobalStyleFile(outputPath: string, stylePath: string) await fs.copyFile(stylePath, styleOutputPath); } -function generateLazyExtensionRoutes(extensions: Array>): string { +function generateLazyExtensionRoutes(extensions: Array): string { const routes: string[] = []; - for (const extension of extensions as Array>) { + for (const extension of extensions as Array) { for (const module of extension.ngModules) { if (module.type === 'lazy') { routes.push(` { @@ -161,7 +180,7 @@ function generateLazyExtensionRoutes(extensions: Array>) { +function generateSharedExtensionModule(extensions: Array) { return `import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; ${extensions @@ -198,11 +217,11 @@ function getModuleFilePath( * Copies the Admin UI sources & static assets to the outputPath if it does not already * exist there. */ -function copyAdminUiSource(outputPath: string, modulePathMapping: Record | undefined) { +function copyAdminUiSource(outputPath: string, modulePathMapping: Record | undefined) { const tsconfigFilePath = path.join(outputPath, 'tsconfig.json'); const indexFilePath = path.join(outputPath, '/src/index.html'); if (fs.existsSync(tsconfigFilePath) && fs.existsSync(indexFilePath)) { - addModulePathMappingIfDefined(tsconfigFilePath, modulePathMapping); + configureModulePathMapping(tsconfigFilePath, modulePathMapping); return; } @@ -220,7 +239,7 @@ function copyAdminUiSource(outputPath: string, modulePathMapping: Record | undefined, + modulePathMapping: Record | undefined, ) { if (!modulePathMapping) { return; } const tsconfig = require(tsconfigFilePath); - const tsPaths = Object.entries(modulePathMapping).reduce((acc, [key, value]) => { - acc[key] = [`src/extensions/${value}`]; - return acc; - }, {} as Record); - tsconfig.compilerOptions.paths = tsPaths; + tsconfig.compilerOptions.paths = modulePathMapping; fs.writeFileSync(tsconfigFilePath, JSON.stringify(tsconfig, null, 2)); } diff --git a/packages/ui-devkit/src/compiler/types.ts b/packages/ui-devkit/src/compiler/types.ts index a93e75099a..190473e84e 100644 --- a/packages/ui-devkit/src/compiler/types.ts +++ b/packages/ui-devkit/src/compiler/types.ts @@ -5,8 +5,7 @@ export type Extension = | TranslationExtension | StaticAssetExtension | GlobalStylesExtension - | SassVariableOverridesExtension - | ModulePathMappingExtension; + | SassVariableOverridesExtension; /** * @description @@ -83,138 +82,6 @@ export interface SassVariableOverridesExtension { sassVariableOverrides: string; } -/** - * @description - * Defines an extension which specifies module path mapping to allow an {@link AdminUiExtension} import code - * from another AdminUiExtension. - * - * By default, Angular modules declared in an AdminUiExtension do not have access to code outside the directory - * defined by the `extensionPath` property except for `node_modules`. A scenario in which that can be useful though - * is on a monorepo codebase where a common NgModule is shared across different plugins, each defined in its own - * package. An example can be found below - note that the main `tsconfig.json` also maps the target module but using - * a path relative to the project's root folder. The UI module is not part of the main TypeScript build task as explained - * in [Extending the Admin UI](https://www.vendure.io/docs/plugins/extending-the-admin-ui/) but having `paths` - * properly configured helps with usual IDE code editing features such as code completion and quick navigation, as - * well as linting. - * - * @example - * ```json - * // tsconfig.json - * { - * "compilerOptions": { - * "baseUrl": ".", - * "paths": { - * "@common-ui-module/ui": ["packages/common-ui-module/src/ui/ui-shared.module.ts"] - * } - * } - * } - * ``` - * - * ```ts - * // packages/common-ui-module/src/ui/ui-shared.module.ts - * import { NgModule } from '@angular/core'; - * import { SharedModule } from '@vendure/admin-ui/core'; - * - * import { CommonUiComponent } from './components/common-ui/common-ui.component'; - * - * export { CommonUiComponent }; - * - * \@NgModule({ - * imports: [SharedModule], - * exports: [CommonUiComponent], - * declarations: [CommonUiComponent], - * }) - * export class CommonSharedUiModule {} - * ``` - * - * ```ts - * // packages/common-ui-module/src/index.ts - * import path from 'path'; - * - * import { AdminUiExtension } from '@vendure/ui-devkit/compiler'; - * - * export const uiExtensions: AdminUiExtension = { - * id: 'common-ui', // this is important - * extensionPath: path.join(__dirname, 'ui'), - * ngModules: [ - * { - * type: 'shared' as const, - * ngModuleFileName: 'ui-shared.module.ts', - * ngModuleName: 'CommonSharedUiModule', - * }, - * ], - * }; - * ``` - * - * ```ts - * // packages/sample-plugin/src/ui/ui-extension.module.ts - * import { NgModule } from '@angular/core'; - * import { SharedModule } from '@vendure/admin-ui/core'; - * import { CommonSharedUiModule, CommonUiComponent } from '@common-ui-module/ui'; - * - * \@NgModule({ - * imports: [ - * SharedModule, - * CommonSharedUiModule, - * RouterModule.forChild([ - * { - * path: '', - * pathMatch: 'full', - * component: CommonUiComponent, - * }, - * ]), - * ], - * }) - * export class SampleUiExtensionModule {} - * ``` - * - * ```ts - * // vendure-config.ts - * import path from 'path'; - * import { AdminUiPlugin } from '@vendure/admin-ui-plugin'; - * import { VendureConfig } from '@vendure/core'; - * import { compileUiExtensions } from '@vendure/ui-devkit/compiler'; - * import { uiExtensions as commonUiExtensions } from '@common-ui-module/ui'; - * - * export const config: VendureConfig = { - * // ... - * plugins: [ - * AdminUiPlugin.init({ - * app: compileUiExtensions({ - * outputPath: path.join(__dirname, '../admin-ui'), - * extensions: [{ - * modulePathMapping: { - * // 'common-ui' is the id given to the common-ui-module UI extension - * // 'ui-shared.module.ts' is the file in 'ui' directory that we want to import from - * '@common-module/ui': 'common-ui/ui-shared.module.ts', - * }, - * }], - * commonUiExtensions, - * // UI extensions for SamplePlugin, which uses CommonSharedUiModule, should also be imported - * // and declared here - * }), - * }), - * ], - * }; - * ``` - * - * @docsCategory UiDevkit - * @docsPage AdminUiExtension - */ -export interface ModulePathMappingExtension { - /** - * @description - * Optional object which defines one or more module name mappings in a similar way to TypeScript's - * [path mapping](https://www.typescriptlang.org/docs/handbook/module-resolution.html#path-mapping) with - * the following differences: - * - * * The path value is a single string instead of an array of strings - * * Paths are resolved relative to the "extensions" folder of the compiled Admin UI app and reference the id - * property of the target AdminUiExtension - */ - modulePathMapping: Record; -} - /** * @description * Defines extensions to the Admin UI application by specifying additional @@ -235,8 +102,7 @@ export interface AdminUiExtension /** * @description * An optional ID for the extension module. Only used internally for generating - * import paths to your module or if {@link ModulePathMappingExtension} is defined. - * If not specified, a unique hash will be used as the id. + * import paths to your module. If not specified, a unique hash will be used as the id. */ id?: string; @@ -247,11 +113,99 @@ export interface AdminUiExtension * scss style sheets etc. */ extensionPath: string; + /** * @description * One or more Angular modules which extend the default Admin UI. */ ngModules: Array; + + /** + * @description + * An optional alias for the module so it can be referenced by other UI extension modules. + * + * By default, Angular modules declared in an AdminUiExtension do not have access to code outside the directory + * defined by the `extensionPath`. A scenario in which that can be useful though is in a monorepo codebase where + * a common NgModule is shared across different plugins, each defined in its own package. An example can be found + * below - note that the main `tsconfig.json` also maps the target module but using a path relative to the project's + * root folder. The UI module is not part of the main TypeScript build task as explained in + * [Extending the Admin UI](https://www.vendure.io/docs/plugins/extending-the-admin-ui/) but having `paths` + * properly configured helps with usual IDE code editing features such as code completion and quick navigation, as + * well as linting. + * + * @example + * ```ts + * // packages/common-ui-module/src/ui/ui-shared.module.ts + * import { NgModule } from '@angular/core'; + * import { SharedModule } from '@vendure/admin-ui/core'; + * import { CommonUiComponent } from './components/common-ui/common-ui.component'; + * + * export { CommonUiComponent }; + * + * \@NgModule({ + * imports: [SharedModule], + * exports: [CommonUiComponent], + * declarations: [CommonUiComponent], + * }) + * export class CommonSharedUiModule {} + * ``` + * + * ```ts + * // packages/common-ui-module/src/index.ts + * import path from 'path'; + * + * import { AdminUiExtension } from '@vendure/ui-devkit/compiler'; + * + * export const uiExtensions: AdminUiExtension = { + * pathAlias: '@common-ui-module', // this is the important part + * extensionPath: path.join(__dirname, 'ui'), + * ngModules: [ + * { + * type: 'shared' as const, + * ngModuleFileName: 'ui-shared.module.ts', + * ngModuleName: 'CommonSharedUiModule', + * }, + * ], + * }; + * ``` + * + * ```json + * // tsconfig.json + * { + * "compilerOptions": { + * "baseUrl": ".", + * "paths": { + * "@common-ui-module/*": ["packages/common-ui-module/src/ui/*"] + * } + * } + * } + * ``` + * + * ```ts + * // packages/sample-plugin/src/ui/ui-extension.module.ts + * import { NgModule } from '@angular/core'; + * import { SharedModule } from '@vendure/admin-ui/core'; + * // the import below works both in the context of the custom Admin UI app as well as the main project + * // '@common-ui-module' is the value of "pathAlias" and 'ui-shared.module' is the file we want to reference inside "extensionPath" + * import { CommonSharedUiModule, CommonUiComponent } from '@common-ui-module/ui-shared.module'; + * + * \@NgModule({ + * imports: [ + * SharedModule, + * CommonSharedUiModule, + * RouterModule.forChild([ + * { + * path: '', + * pathMatch: 'full', + * component: CommonUiComponent, + * }, + * ]), + * ], + * }) + * export class SampleUiExtensionModule {} + * ``` + */ + pathAlias?: string; } /** @@ -414,3 +368,7 @@ export interface BrandingOptions { largeLogoPath?: string; faviconPath?: string; } + +export interface AdminUiExtensionWithId extends AdminUiExtension { + id: string; +} diff --git a/packages/ui-devkit/src/compiler/utils.ts b/packages/ui-devkit/src/compiler/utils.ts index 96f93056c8..4ec4a973a4 100644 --- a/packages/ui-devkit/src/compiler/utils.ts +++ b/packages/ui-devkit/src/compiler/utils.ts @@ -8,9 +8,9 @@ import * as path from 'path'; import { STATIC_ASSETS_OUTPUT_DIR } from './constants'; import { AdminUiExtension, + AdminUiExtensionWithId, Extension, GlobalStylesExtension, - ModulePathMappingExtension, SassVariableOverridesExtension, StaticAssetDefinition, StaticAssetExtension, @@ -80,7 +80,7 @@ export async function copyStaticAsset(outputPath: string, staticAssetDef: Static * If not defined by the user, a deterministic ID is generated * from a hash of the extension config. */ -export function normalizeExtensions(extensions?: AdminUiExtension[]): Array> { +export function normalizeExtensions(extensions?: AdminUiExtension[]): Array { return (extensions || []).map(e => { let id = e.id; if (!id) { @@ -112,7 +112,3 @@ export function isGlobalStylesExtension(input: Extension): input is GlobalStyles export function isSassVariableOverridesExtension(input: Extension): input is SassVariableOverridesExtension { return input.hasOwnProperty('sassVariableOverrides'); } - -export function isModulePathMappingExtension(input: Extension): input is ModulePathMappingExtension { - return input.hasOwnProperty('modulePathMapping'); -}