Ver Fonte

feat(ui-devkit): Add experimental wrapper for shared ui providers

Michael Bromley há 2 anos atrás
pai
commit
daf6f8c1a4

+ 75 - 52
packages/ui-devkit/src/compiler/compile.ts

@@ -10,7 +10,6 @@ import { DEFAULT_BASE_HREF, MODULES_OUTPUT_DIR } from './constants';
 import { copyGlobalStyleFile, setBaseHref, setupScaffold } from './scaffold';
 import { getAllTranslationFiles, mergeExtensionTranslations } from './translations';
 import {
-    Extension,
     StaticAssetDefinition,
     UiExtensionCompilerOptions,
     UiExtensionCompilerProcessArgument,
@@ -36,53 +35,58 @@ import {
 export function compileUiExtensions(
     options: UiExtensionCompilerOptions,
 ): AdminUiAppConfig | AdminUiAppDevModeConfig {
-    const { outputPath, baseHref, devMode, watchPort, extensions, command, additionalProcessArguments } =
-        options;
-    const usingYarn = options.command && options.command === 'npm' ? false : shouldUseYarn();
+    const { devMode, watchPort, command } = options;
+    const usingYarn = command && command === 'npm' ? false : shouldUseYarn();
     if (devMode) {
-        return runWatchMode(
-            outputPath,
-            baseHref || DEFAULT_BASE_HREF,
-            watchPort || 4200,
-            extensions,
+        return runWatchMode({
+            watchPort: watchPort || 4200,
             usingYarn,
-            additionalProcessArguments,
-        );
+            ...options,
+        });
     } else {
-        return runCompileMode(
-            outputPath,
-            baseHref || DEFAULT_BASE_HREF,
-            extensions,
+        return runCompileMode({
             usingYarn,
-            additionalProcessArguments,
-        );
+            ...options,
+        });
     }
 }
 
-function runCompileMode(
-    outputPath: string,
-    baseHref: string,
-    extensions: Extension[],
-    usingYarn: boolean,
-    args?: UiExtensionCompilerProcessArgument[],
-): AdminUiAppConfig {
-    const cmd = usingYarn ? 'yarn' : 'npm';
+function runCompileMode({
+    outputPath,
+    baseHref,
+    extensions,
+    usingYarn,
+    additionalProcessArguments,
+    ngCompilerPath,
+}: UiExtensionCompilerOptions & { usingYarn: boolean }): AdminUiAppConfig {
     const distPath = path.join(outputPath, 'dist');
 
     const compile = () =>
         new Promise<void>(async (resolve, reject) => {
             await setupScaffold(outputPath, extensions);
-            await setBaseHref(outputPath, baseHref);
-            const commandArgs = ['run', 'build', ...buildProcessArguments(args)];
-            if (!usingYarn) {
-                // npm requires `--` before any command line args being passed to a script
-                commandArgs.splice(2, 0, '--');
+            await setBaseHref(outputPath, baseHref || DEFAULT_BASE_HREF);
+
+            let cmd = usingYarn ? 'yarn' : 'npm';
+            let commandArgs = ['run', 'build'];
+            if (ngCompilerPath) {
+                cmd = 'node';
+                commandArgs = [ngCompilerPath, 'build', '--configuration production'];
+            } else {
+                if (!usingYarn) {
+                    // npm requires `--` before any command line args being passed to a script
+                    commandArgs.splice(2, 0, '--');
+                }
             }
-            const buildProcess = spawn(cmd, commandArgs, {
-                cwd: outputPath,
-                shell: true,
-                stdio: 'inherit',
-            });
+            console.log(`Running ${cmd} ${commandArgs.join(' ')}`);
+            const buildProcess = spawn(
+                cmd,
+                [...commandArgs, ...buildProcessArguments(additionalProcessArguments)],
+                {
+                    cwd: outputPath,
+                    shell: true,
+                    stdio: 'inherit',
+                },
+            );
 
             buildProcess.on('close', code => {
                 if (code !== 0) {
@@ -96,19 +100,19 @@ function runCompileMode(
     return {
         path: distPath,
         compile,
-        route: baseHrefToRoute(baseHref),
+        route: baseHrefToRoute(baseHref || DEFAULT_BASE_HREF),
     };
 }
 
-function runWatchMode(
-    outputPath: string,
-    baseHref: string,
-    port: number,
-    extensions: Extension[],
-    usingYarn: boolean,
-    args?: UiExtensionCompilerProcessArgument[],
-): AdminUiAppDevModeConfig {
-    const cmd = usingYarn ? 'yarn' : 'npm';
+function runWatchMode({
+    outputPath,
+    baseHref,
+    watchPort,
+    extensions,
+    usingYarn,
+    additionalProcessArguments,
+    ngCompilerPath,
+}: UiExtensionCompilerOptions & { usingYarn: boolean }): AdminUiAppDevModeConfig {
     const devkitPath = require.resolve('@vendure/ui-devkit');
     let buildProcess: ChildProcess;
     let watcher: FSWatcher | undefined;
@@ -118,17 +122,31 @@ function runWatchMode(
     const compile = () =>
         new Promise<void>(async (resolve, reject) => {
             await setupScaffold(outputPath, extensions);
-            await setBaseHref(outputPath, baseHref);
+            await setBaseHref(outputPath, baseHref || DEFAULT_BASE_HREF);
             const adminUiExtensions = extensions.filter(isAdminUiExtension);
             const normalizedExtensions = normalizeExtensions(adminUiExtensions);
             const globalStylesExtensions = extensions.filter(isGlobalStylesExtension);
             const staticAssetExtensions = extensions.filter(isStaticAssetExtension);
             const allTranslationFiles = getAllTranslationFiles(extensions.filter(isTranslationExtension));
-            buildProcess = spawn(cmd, ['run', 'start', `--port=${port}`, ...buildProcessArguments(args)], {
-                cwd: outputPath,
-                shell: true,
-                stdio: 'inherit',
-            });
+            let cmd = usingYarn ? 'yarn' : 'npm';
+            let commandArgs = ['run', 'start'];
+            if (ngCompilerPath) {
+                cmd = 'node';
+                commandArgs = [ngCompilerPath, 'serve'];
+            }
+            buildProcess = spawn(
+                cmd,
+                [
+                    ...commandArgs,
+                    `--port=${watchPort || 4200}`,
+                    ...buildProcessArguments(additionalProcessArguments),
+                ],
+                {
+                    cwd: outputPath,
+                    shell: true,
+                    stdio: 'inherit',
+                },
+            );
 
             buildProcess.on('close', code => {
                 if (code !== 0) {
@@ -252,7 +270,12 @@ function runWatchMode(
     };
 
     process.on('SIGINT', close);
-    return { sourcePath: outputPath, port, compile, route: baseHrefToRoute(baseHref) };
+    return {
+        sourcePath: outputPath,
+        port: watchPort || 4200,
+        compile,
+        route: baseHrefToRoute(baseHref || DEFAULT_BASE_HREF),
+    };
 }
 
 function buildProcessArguments(args?: UiExtensionCompilerProcessArgument[]): string[] {

+ 1 - 0
packages/ui-devkit/src/compiler/index.ts

@@ -1,3 +1,4 @@
 export * from './compile';
 export * from './helpers';
 export * from './types';
+export * from './wrappers';

+ 31 - 7
packages/ui-devkit/src/compiler/scaffold.ts

@@ -18,6 +18,7 @@ import {
     Extension,
     GlobalStylesExtension,
     SassVariableOverridesExtension,
+    SharedUiProvidersExtension,
     StaticAssetExtension,
 } from './types';
 import {
@@ -26,6 +27,7 @@ import {
     isAdminUiExtension,
     isGlobalStylesExtension,
     isSassVariableOverridesExtension,
+    isSharedUiProvidersExtension,
     isStaticAssetExtension,
     isTranslationExtension,
     logger,
@@ -37,12 +39,13 @@ export async function setupScaffold(outputPath: string, extensions: Extension[])
     deleteExistingExtensionModules(outputPath);
 
     const adminUiExtensions = extensions.filter(isAdminUiExtension);
+    const sharedUiProvidersExtensions = extensions.filter(isSharedUiProvidersExtension);
     const normalizedExtensions = normalizeExtensions(adminUiExtensions);
 
     const modulePathMapping = generateModulePathMapping(normalizedExtensions);
     copyAdminUiSource(outputPath, modulePathMapping);
 
-    await copyExtensionModules(outputPath, normalizedExtensions);
+    await copyExtensionModules(outputPath, [...normalizedExtensions, ...sharedUiProvidersExtensions]);
 
     const staticAssetExtensions = extensions.filter(isStaticAssetExtension);
     await copyStaticAssets(outputPath, staticAssetExtensions);
@@ -88,13 +91,18 @@ function generateModulePathMapping(extensions: AdminUiExtensionWithId[]) {
  * Copies all files from the extensionPaths of the configured extensions into the
  * admin-ui source tree.
  */
-async function copyExtensionModules(outputPath: string, extensions: AdminUiExtensionWithId[]) {
-    const extensionRoutesSource = generateLazyExtensionRoutes(extensions);
+async function copyExtensionModules(
+    outputPath: string,
+    extensions: Array<AdminUiExtensionWithId | SharedUiProvidersExtension>,
+) {
+    const adminUiExtensions = extensions.filter(isAdminUiExtension) as AdminUiExtensionWithId[];
+    const sharedUiProvidersExtensions = extensions.filter(isSharedUiProvidersExtension);
+    const extensionRoutesSource = generateLazyExtensionRoutes(adminUiExtensions);
     fs.writeFileSync(path.join(outputPath, EXTENSION_ROUTES_FILE), extensionRoutesSource, 'utf8');
     const sharedExtensionModulesSource = generateSharedExtensionModule(extensions);
     fs.writeFileSync(path.join(outputPath, SHARED_EXTENSIONS_FILE), sharedExtensionModulesSource, 'utf8');
 
-    for (const extension of extensions) {
+    for (const extension of adminUiExtensions) {
         const dest = path.join(outputPath, MODULES_OUTPUT_DIR, extension.id);
         if (!extension.exclude) {
             fs.copySync(extension.extensionPath, dest);
@@ -108,6 +116,10 @@ async function copyExtensionModules(outputPath: string, extensions: AdminUiExten
             filter: name => name === extension.extensionPath || exclude.every(e => e !== name),
         });
     }
+    for (const extension of sharedUiProvidersExtensions) {
+        const dest = path.join(outputPath, MODULES_OUTPUT_DIR, `${extension.id}.ts`);
+        fs.copySync(extension.sharedProviders, dest);
+    }
 }
 
 async function copyStaticAssets(outputPath: string, extensions: Array<Partial<StaticAssetExtension>>) {
@@ -183,10 +195,18 @@ function generateLazyExtensionRoutes(extensions: AdminUiExtensionWithId[]): stri
     return `export const extensionRoutes = [${routes.join(',\n')}];\n`;
 }
 
-function generateSharedExtensionModule(extensions: AdminUiExtensionWithId[]) {
+function generateSharedExtensionModule(
+    extensions: Array<AdminUiExtensionWithId | SharedUiProvidersExtension>,
+) {
+    const sharedProviderExtensions = extensions.filter((e): e is SharedUiProvidersExtension =>
+        e.hasOwnProperty('sharedProviders'),
+    );
+    const adminUiExtensions = extensions.filter((e): e is AdminUiExtensionWithId =>
+        e.hasOwnProperty('extensionPath'),
+    );
     return `import { NgModule } from '@angular/core';
 import { CommonModule } from '@angular/common';
-${extensions
+${adminUiExtensions
     .map(e =>
         e.ngModules
             .filter(m => m.type === 'shared')
@@ -194,9 +214,12 @@ ${extensions
             .join(''),
     )
     .join('')}
+${sharedProviderExtensions
+    .map((m, i) => `import SharedProviders${i} from './extensions/${m.id}';\n`)
+    .join('')}
 
 @NgModule({
-    imports: [CommonModule, ${extensions
+    imports: [CommonModule, ${adminUiExtensions
         .map(e =>
             e.ngModules
                 .filter(m => m.type === 'shared')
@@ -204,6 +227,7 @@ ${extensions
                 .join(', '),
         )
         .join(', ')}],
+    providers: [${sharedProviderExtensions.map((m, i) => `...SharedProviders${i}`).join(', ')}],
 })
 export class SharedExtensionsModule {}
 `;

+ 36 - 1
packages/ui-devkit/src/compiler/types.ts

@@ -5,7 +5,8 @@ export type Extension =
     | TranslationExtension
     | StaticAssetExtension
     | GlobalStylesExtension
-    | SassVariableOverridesExtension;
+    | SassVariableOverridesExtension
+    | SharedUiProvidersExtension;
 
 /**
  * @description
@@ -82,6 +83,19 @@ export interface SassVariableOverridesExtension {
     sassVariableOverrides: string;
 }
 
+/**
+ * @description
+ * Defines an extension which contains only shared providers such as nav menu items, custom form inputs,
+ * custom detail components, action bar items, custom history entry components.
+ *
+ * @docsCategory UiDevkit
+ * @docsPage AdminUiExtension
+ */
+export interface SharedUiProvidersExtension {
+    id: string;
+    sharedProviders: string;
+}
+
 /**
  * @description
  * Defines extensions to the Admin UI application by specifying additional
@@ -316,6 +330,27 @@ export interface UiExtensionCompilerOptions {
      * translations with which to extend the Admin UI.
      */
     extensions: Extension[];
+    /**
+     * @description
+     * Allows you to manually specify the path to the Angular CLI compiler script. This can be useful in scenarios
+     * where for some reason the built-in start/build scripts are unable to locate the `ng` command.
+     *
+     * This option should not usually be required.
+     *
+     * @example
+     * ```ts
+     * compileUiExtensions({
+     *     ngCompilerPath: path.join(__dirname, '../../node_modules/@angular/cli/bin/ng.js'),
+     *     outputPath: path.join(__dirname, '../admin-ui'),
+     *     extensions: [
+     *       // ...
+     *     ],
+     * })
+     * ```
+     *
+     * @since 2.1.0
+     */
+    ngCompilerPath?: string | undefined;
     /**
      * @description
      * Set to `true` in order to compile the Admin UI in development mode (using the Angular CLI

+ 5 - 0
packages/ui-devkit/src/compiler/utils.ts

@@ -12,6 +12,7 @@ import {
     Extension,
     GlobalStylesExtension,
     SassVariableOverridesExtension,
+    SharedUiProvidersExtension,
     StaticAssetDefinition,
     StaticAssetExtension,
     TranslationExtension,
@@ -96,6 +97,10 @@ export function isAdminUiExtension(input: Extension): input is AdminUiExtension
     return input.hasOwnProperty('extensionPath');
 }
 
+export function isSharedUiProvidersExtension(input: Extension): input is SharedUiProvidersExtension {
+    return input.hasOwnProperty('sharedProviders');
+}
+
 export function isTranslationExtension(input: Extension): input is TranslationExtension {
     return input.hasOwnProperty('translations');
 }

+ 30 - 0
packages/ui-devkit/src/compiler/wrappers.ts

@@ -0,0 +1,30 @@
+import { normalizeString } from '@vendure/common/lib/normalize-string';
+import path from 'path';
+
+import { SharedUiProvidersExtension } from './types';
+
+export function uiExtensions(filePath: string): SharedUiProvidersExtension {
+    return {
+        id: getIdFromFilePath(filePath),
+        sharedProviders: filePath,
+    };
+}
+
+function getIdFromFilePath(filePath: string): string {
+    const { dir, name } = path.parse(filePath);
+    const parts = safelySplitFilePath(dir).slice(-3);
+    const id = normalizeString([...parts, name].join('_'), '_');
+    return id;
+}
+
+function safelySplitFilePath(filePath: string) {
+    try {
+        const normalizedPath = path.normalize(filePath);
+        const directoryParts = normalizedPath.split(path.sep);
+        return directoryParts.filter(part => part !== '.' && part !== '..');
+    } catch (error) {
+        // eslint-disable-next-line no-console
+        console.error('An error occurred while splitting the file path:', error);
+        return [];
+    }
+}