Skip to content

Commit

Permalink
feat(ui-devkit): Support module path mappings for UI extensions (#1994)
Browse files Browse the repository at this point in the history
  • Loading branch information
vrosa authored Jan 19, 2023
1 parent 35dbf82 commit 6d57c86
Show file tree
Hide file tree
Showing 3 changed files with 147 additions and 14 deletions.
66 changes: 53 additions & 13 deletions packages/ui-devkit/src/compiler/scaffold.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,12 @@ import {
GLOBAL_STYLES_OUTPUT_DIR,
MODULES_OUTPUT_DIR,
SHARED_EXTENSIONS_FILE,
STATIC_ASSETS_OUTPUT_DIR,
} from './constants';
import { getAllTranslationFiles, mergeExtensionTranslations } from './translations';
import {
AdminUiExtension,
AdminUiExtensionLazyModule,
AdminUiExtensionSharedModule,
AdminUiExtensionWithId,
Extension,
GlobalStylesExtension,
SassVariableOverridesExtension,
Expand All @@ -35,10 +34,13 @@ import {

export async function setupScaffold(outputPath: string, extensions: Extension[]) {
deleteExistingExtensionModules(outputPath);
copyAdminUiSource(outputPath);

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);
Expand Down Expand Up @@ -70,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<AdminUiExtensionWithId>) {
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<string, string[]>);
}

/**
* Copies all files from the extensionPaths of the configured extensions into the
* admin-ui source tree.
*/
async function copyExtensionModules(outputPath: string, extensions: Array<Required<AdminUiExtension>>) {
async function copyExtensionModules(outputPath: string, extensions: Array<AdminUiExtensionWithId>) {
const extensionRoutesSource = generateLazyExtensionRoutes(extensions);
fs.writeFileSync(path.join(outputPath, EXTENSION_ROUTES_FILE), extensionRoutesSource, 'utf8');
const sharedExtensionModulesSource = generateSharedExtensionModule(extensions);
Expand Down Expand Up @@ -142,9 +163,9 @@ export async function copyGlobalStyleFile(outputPath: string, stylePath: string)
await fs.copyFile(stylePath, styleOutputPath);
}

function generateLazyExtensionRoutes(extensions: Array<Required<AdminUiExtension>>): string {
function generateLazyExtensionRoutes(extensions: Array<AdminUiExtensionWithId>): string {
const routes: string[] = [];
for (const extension of extensions as Array<Required<AdminUiExtension>>) {
for (const extension of extensions as Array<AdminUiExtensionWithId>) {
for (const module of extension.ngModules) {
if (module.type === 'lazy') {
routes.push(` {
Expand All @@ -159,7 +180,7 @@ function generateLazyExtensionRoutes(extensions: Array<Required<AdminUiExtension
return `export const extensionRoutes = [${routes.join(',\n')}];\n`;
}

function generateSharedExtensionModule(extensions: Array<Required<AdminUiExtension>>) {
function generateSharedExtensionModule(extensions: Array<AdminUiExtensionWithId>) {
return `import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
${extensions
Expand Down Expand Up @@ -193,15 +214,17 @@ function getModuleFilePath(
}

/**
* Copy the Admin UI sources & static assets to the outputPath if it does not already
* exists there.
* Copies the Admin UI sources & static assets to the outputPath if it does not already
* exist there.
*/
function copyAdminUiSource(outputPath: string) {
const angularJsonFile = path.join(outputPath, 'angular.json');
const indexFile = path.join(outputPath, '/src/index.html');
if (fs.existsSync(angularJsonFile) && fs.existsSync(indexFile)) {
function copyAdminUiSource(outputPath: string, modulePathMapping: Record<string, string[]> | undefined) {
const tsconfigFilePath = path.join(outputPath, 'tsconfig.json');
const indexFilePath = path.join(outputPath, '/src/index.html');
if (fs.existsSync(tsconfigFilePath) && fs.existsSync(indexFilePath)) {
configureModulePathMapping(tsconfigFilePath, modulePathMapping);
return;
}

const scaffoldDir = path.join(__dirname, '../scaffold');
const adminUiSrc = path.join(require.resolve('@vendure/admin-ui'), '../../static');

Expand All @@ -216,13 +239,30 @@ function copyAdminUiSource(outputPath: string) {
fs.removeSync(outputPath);
fs.ensureDirSync(outputPath);
fs.copySync(scaffoldDir, outputPath);
configureModulePathMapping(tsconfigFilePath, modulePathMapping);

// copy source files from admin-ui package
const outputSrc = path.join(outputPath, 'src');
fs.ensureDirSync(outputSrc);
fs.copySync(adminUiSrc, outputSrc);
}

/**
* Adds module path mapping to the bundled tsconfig.json file if defined as a UI extension.
*/
function configureModulePathMapping(
tsconfigFilePath: string,
modulePathMapping: Record<string, string[]> | undefined,
) {
if (!modulePathMapping) {
return;
}

const tsconfig = require(tsconfigFilePath);
tsconfig.compilerOptions.paths = modulePathMapping;
fs.writeFileSync(tsconfigFilePath, JSON.stringify(tsconfig, null, 2));
}

/**
* Attempts to find out it the ngcc compiler has been run on the Angular packages, and if not,
* attemps to run it. This is done this way because attempting to run ngcc from a sub-directory
Expand Down
92 changes: 92 additions & 0 deletions packages/ui-devkit/src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,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<AdminUiExtensionSharedModule | AdminUiExtensionLazyModule>;

/**
* @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;
}

/**
Expand Down Expand Up @@ -280,3 +368,7 @@ export interface BrandingOptions {
largeLogoPath?: string;
faviconPath?: string;
}

export interface AdminUiExtensionWithId extends AdminUiExtension {
id: string;
}
3 changes: 2 additions & 1 deletion packages/ui-devkit/src/compiler/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import * as path from 'path';
import { STATIC_ASSETS_OUTPUT_DIR } from './constants';
import {
AdminUiExtension,
AdminUiExtensionWithId,
Extension,
GlobalStylesExtension,
SassVariableOverridesExtension,
Expand Down Expand Up @@ -79,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<Required<AdminUiExtension>> {
export function normalizeExtensions(extensions?: AdminUiExtension[]): Array<AdminUiExtensionWithId> {
return (extensions || []).map(e => {
let id = e.id;
if (!id) {
Expand Down

0 comments on commit 6d57c86

Please sign in to comment.