Skip to content

Commit

Permalink
feat(ui-devkit): Support module path mappings for UI extensions
Browse files Browse the repository at this point in the history
  • Loading branch information
vrosa committed Jan 18, 2023
1 parent 35dbf82 commit 2c86f20
Show file tree
Hide file tree
Showing 3 changed files with 174 additions and 10 deletions.
41 changes: 33 additions & 8 deletions packages/ui-devkit/src/compiler/scaffold.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
GLOBAL_STYLES_OUTPUT_DIR,
MODULES_OUTPUT_DIR,
SHARED_EXTENSIONS_FILE,
STATIC_ASSETS_OUTPUT_DIR,
} from './constants';
import { getAllTranslationFiles, mergeExtensionTranslations } from './translations';
import {
Expand All @@ -25,6 +24,7 @@ import {
copyUiDevkit,
isAdminUiExtension,
isGlobalStylesExtension,
isModulePathMappingExtension,
isSassVariableOverridesExtension,
isStaticAssetExtension,
isTranslationExtension,
Expand All @@ -35,7 +35,9 @@ import {

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

const modulePathMappingExtension = extensions.find(isModulePathMappingExtension);
copyAdminUiSource(outputPath, modulePathMappingExtension?.modulePathMapping);

const adminUiExtensions = extensions.filter(isAdminUiExtension);
const normalizedExtensions = normalizeExtensions(adminUiExtensions);
Expand Down Expand Up @@ -193,15 +195,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)) {
addModulePathMappingIfDefined(tsconfigFilePath, modulePathMapping);
return;
}

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

Expand All @@ -216,13 +220,34 @@ function copyAdminUiSource(outputPath: string) {
fs.removeSync(outputPath);
fs.ensureDirSync(outputPath);
fs.copySync(scaffoldDir, outputPath);
addModulePathMappingIfDefined(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 addModulePathMappingIfDefined(
tsconfigFilePath: string,
modulePathMapping: Record<string, string> | 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<string, string[]>);
tsconfig.compilerOptions.paths = tsPaths;
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
138 changes: 136 additions & 2 deletions packages/ui-devkit/src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ export type Extension =
| TranslationExtension
| StaticAssetExtension
| GlobalStylesExtension
| SassVariableOverridesExtension;
| SassVariableOverridesExtension
| ModulePathMappingExtension;

/**
* @description
Expand Down Expand Up @@ -82,6 +83,138 @@ 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<string, string>;
}

/**
* @description
* Defines extensions to the Admin UI application by specifying additional
Expand All @@ -102,7 +235,8 @@ export interface AdminUiExtension
/**
* @description
* An optional ID for the extension module. Only used internally for generating
* import paths to your module. If not specified, a unique hash will be used as the id.
* import paths to your module or if {@link ModulePathMappingExtension} is defined.
* If not specified, a unique hash will be used as the id.
*/
id?: string;

Expand Down
5 changes: 5 additions & 0 deletions packages/ui-devkit/src/compiler/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
AdminUiExtension,
Extension,
GlobalStylesExtension,
ModulePathMappingExtension,
SassVariableOverridesExtension,
StaticAssetDefinition,
StaticAssetExtension,
Expand Down Expand Up @@ -111,3 +112,7 @@ 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');
}

0 comments on commit 2c86f20

Please sign in to comment.