Browse Source

feat(ui-devkit): Run detect and run ngcc on first compilation

ngcc is needed to ensure the Angular libraries can be compiled with the Ivy renderer. The ui compiler will automatically detect whether the ngcc step is needed.
Michael Bromley 5 years ago
parent
commit
b5a57a81d8

+ 83 - 45
packages/ui-devkit/src/compiler/compile.ts

@@ -1,53 +1,23 @@
-import {
-    AdminUiApp,
-    AdminUiAppDevMode,
-    AdminUiExtension,
-    AdminUiExtensionLazyModule,
-    AdminUiExtensionSharedModule,
-} from '@vendure/common/lib/shared-types';
+/* tslint:disable:no-console */
+import { AdminUiApp, AdminUiAppDevMode } from '@vendure/common/lib/shared-types';
 import { ChildProcess, execSync, spawn } from 'child_process';
 import { FSWatcher, watch as chokidarWatch } from 'chokidar';
 import { createHash } from 'crypto';
 import * as fs from 'fs-extra';
 import * as path from 'path';
 
+import {
+    AdminUiExtension,
+    AdminUiExtensionLazyModule,
+    AdminUiExtensionSharedModule,
+    UiExtensionCompilerOptions,
+} from './types';
+
 const STATIC_ASSETS_OUTPUT_DIR = 'static-assets';
 const MODULES_OUTPUT_DIR = 'src/extensions';
 const EXTENSION_ROUTES_FILE = 'src/extension.routes.ts';
 const SHARED_EXTENSIONS_FILE = 'src/shared-extensions.module.ts';
 
-export interface UiExtensionCompilerOptions {
-    /**
-     * @description
-     * The directory into which the sources for the extended Admin UI will be copied.
-     */
-    outputPath: string;
-    /**
-     * @description
-     * An array of objects which configure extension Angular modules
-     * to be compiled into and made available by the AdminUi application.
-     */
-    extensions: AdminUiExtension[];
-    /**
-     * @description
-     * Set to `true` in order to compile the Admin UI in development mode (using the Angular CLI
-     * [ng serve](https://angular.io/cli/serve) command). When in watch mode, any changes to
-     * UI extension files will be watched and trigger a rebuild of the Admin UI with live
-     * reloading.
-     *
-     * @default false
-     */
-    watch?: boolean;
-    /**
-     * @description
-     * In watch mode, allows the port of the dev server to be specified. Defaults to the Angular CLI default
-     * of `4200`.
-     *
-     * @default 4200 | undefined
-     */
-    watchPort?: number;
-}
-
 /**
  * Builds the admin-ui app using the Angular CLI `ng build --prod` command.
  */
@@ -69,8 +39,8 @@ function runCompileMode(outputPath: string, extensions: AdminUiExtension[]): Adm
     const distPath = path.join(outputPath, 'dist');
 
     const compile = () =>
-        new Promise<void>((resolve, reject) => {
-            setupScaffold(outputPath, extensions);
+        new Promise<void>(async (resolve, reject) => {
+            await setupScaffold(outputPath, extensions);
             const buildProcess = spawn(cmd, ['run', 'build', `--outputPath=${distPath}`], {
                 cwd: outputPath,
                 shell: true,
@@ -97,9 +67,12 @@ function runWatchMode(outputPath: string, port: number, extensions: AdminUiExten
     const devkitPath = require.resolve('@vendure/ui-devkit');
     let buildProcess: ChildProcess;
     let watcher: FSWatcher | undefined;
+    let close: () => void = () => {
+        /* */
+    };
     const compile = () =>
-        new Promise<void>((resolve, reject) => {
-            setupScaffold(outputPath, extensions);
+        new Promise<void>(async (resolve, reject) => {
+            await setupScaffold(outputPath, extensions);
             const normalizedExtensions = normalizeExtensions(extensions);
             buildProcess = spawn(cmd, ['run', 'start', `--port=${port}`, `--poll=1000`], {
                 cwd: outputPath,
@@ -107,6 +80,15 @@ function runWatchMode(outputPath: string, port: number, extensions: AdminUiExten
                 stdio: 'inherit',
             });
 
+            buildProcess.on('close', code => {
+                if (code !== 0) {
+                    reject(code);
+                } else {
+                    resolve();
+                }
+                close();
+            });
+
             for (const extension of normalizedExtensions) {
                 if (!watcher) {
                     watcher = chokidarWatch(extension.extensionPath, {
@@ -145,9 +127,10 @@ function runWatchMode(outputPath: string, port: number, extensions: AdminUiExten
                     }
                 });
             }
+            resolve();
         });
 
-    const close = () => {
+    close = () => {
         if (watcher) {
             watcher.close();
         }
@@ -158,11 +141,19 @@ function runWatchMode(outputPath: string, port: number, extensions: AdminUiExten
     return { sourcePath: outputPath, port, onClose: close, compile };
 }
 
-function setupScaffold(outputPath: string, extensions: AdminUiExtension[]) {
+async function setupScaffold(outputPath: string, extensions: AdminUiExtension[]) {
     deleteExistingExtensionModules(outputPath);
     copySourceIfNotExists(outputPath);
     copyExtensionModules(outputPath, normalizeExtensions(extensions));
     copyUiDevkit(outputPath);
+    try {
+        await checkIfNgccWasRun();
+    } catch (e) {
+        const cmd = shouldUseYarn() ? 'yarn ngcc' : 'npx ngcc';
+        console.log(
+            `An error occurred when running ngcc. Try removing node_modules, re-installing, and then manually running "${cmd}" in the project root.`,
+        );
+    }
 }
 
 /**
@@ -312,6 +303,53 @@ function copySourceIfNotExists(outputPath: string) {
     fs.copySync(adminUiSrc, outputSrc);
 }
 
+/**
+ * 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
+ * where the angular libs are in a higher-level node_modules folder currently results in the error
+ * NG6002, see https://github.com/angular/angular/issues/35747.
+ *
+ * However, when ngcc is run from the root, it works.
+ */
+async function checkIfNgccWasRun(): Promise<void> {
+    const coreUmdFile = require.resolve('@vendure/admin-ui/core');
+    if (!coreUmdFile) {
+        console.log(`Could not resolve the "@vendure/admin-ui/core" package!`);
+        return;
+    }
+    // ngcc creates backup files when it has been run
+    const ivyFile = coreUmdFile + '.__ivy_ngcc_bak';
+    if (fs.existsSync(ivyFile)) {
+        return;
+    }
+    // Looks like ngcc has not been run, so attempt to do so.
+    const rootDir = coreUmdFile.split('node_modules')[0];
+    return new Promise((resolve, reject) => {
+        console.log(
+            'Running the Angular Ivy compatibility compiler (ngcc) on Vendure Admin UI dependencies ' +
+                '(this is only needed on the first run)...',
+        );
+        const cmd = shouldUseYarn() ? 'yarn' : 'npx';
+        const ngccProcess = spawn(
+            cmd,
+            ['ngcc', ' --properties es2015 browser module main', '--first-only', '--create-ivy-entry-points'],
+            {
+                cwd: rootDir,
+                shell: true,
+                stdio: 'inherit',
+            },
+        );
+
+        ngccProcess.on('close', code => {
+            if (code !== 0) {
+                reject(code);
+            } else {
+                resolve();
+            }
+        });
+    });
+}
+
 export function shouldUseYarn(): boolean {
     try {
         execSync('yarnpkg --version', { stdio: 'ignore' });

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

@@ -1 +1,2 @@
 export * from './compile';
+export * from './types';

+ 135 - 0
packages/ui-devkit/src/compiler/types.ts

@@ -0,0 +1,135 @@
+/**
+ * @description
+ * Defines extensions to the Admin UI application by specifying additional
+ * Angular [NgModules](https://angular.io/guide/ngmodules) which are compiled
+ * into the application.
+ *
+ * See [Extending the Admin UI](/docs/developer-guide/plugins/extending-the-admin-ui/) for
+ * detailed instructions.
+ *
+ * @docsCategory UiDevkit
+ */
+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.
+     */
+    id?: string;
+
+    /**
+     * @description
+     * The path to the directory containing the extension module(s). The entire contents of this directory
+     * will be copied into the Admin UI app, including all TypeScript source files, html templates,
+     * scss style sheets etc.
+     */
+    extensionPath: string;
+    /**
+     * @description
+     * One or more Angular modules which extend the default Admin UI.
+     */
+    ngModules: Array<AdminUiExtensionSharedModule | AdminUiExtensionLazyModule>;
+
+    /**
+     * @description
+     * Optional array of paths to static assets which will be copied over to the Admin UI app's `/static`
+     * directory.
+     */
+    staticAssets?: string[];
+}
+
+/**
+ * @description
+ * Configuration defining a single NgModule with which to extend the Admin UI.
+ *
+ * @docsCategory UiDevkit
+ */
+export interface AdminUiExtensionSharedModule {
+    /**
+     * @description
+     * Shared modules are directly imported into the main AppModule of the Admin UI
+     * and should be used to declare custom form components and define custom
+     * navigation items.
+     */
+    type: 'shared';
+    /**
+     * @description
+     * The name of the file containing the extension module class.
+     */
+    ngModuleFileName: string;
+    /**
+     * @description
+     * The name of the extension module class.
+     */
+    ngModuleName: string;
+}
+
+/**
+ * @description
+ * Configuration defining a single NgModule with which to extend the Admin UI.
+ *
+ * @docsCategory UiDevkit
+ */
+export interface AdminUiExtensionLazyModule {
+    /**
+     * @description
+     * Lazy modules are lazy-loaded at the `/extensions/` route and should be used for
+     * modules which define new views for the Admin UI.
+     */
+    type: 'lazy';
+    /**
+     * @description
+     * The route specifies the route at which the module will be lazy-loaded. E.g. a value
+     * of `'foo'` will cause the module to lazy-load when the `/extensions/foo` route
+     * is activated.
+     */
+    route: string;
+    /**
+     * @description
+     * The name of the file containing the extension module class.
+     */
+    ngModuleFileName: string;
+    /**
+     * @description
+     * The name of the extension module class.
+     */
+    ngModuleName: string;
+}
+
+/**
+ * @description
+ * Options to configure how the Admin UI should be compiled.
+ *
+ * @docsCategory UiDevkit
+ */
+export interface UiExtensionCompilerOptions {
+    /**
+     * @description
+     * The directory into which the sources for the extended Admin UI will be copied.
+     */
+    outputPath: string;
+    /**
+     * @description
+     * An array of objects which configure extension Angular modules
+     * to be compiled into and made available by the AdminUi application.
+     */
+    extensions: AdminUiExtension[];
+    /**
+     * @description
+     * Set to `true` in order to compile the Admin UI in development mode (using the Angular CLI
+     * [ng serve](https://angular.io/cli/serve) command). When in watch mode, any changes to
+     * UI extension files will be watched and trigger a rebuild of the Admin UI with live
+     * reloading.
+     *
+     * @default false
+     */
+    watch?: boolean;
+    /**
+     * @description
+     * In watch mode, allows the port of the dev server to be specified. Defaults to the Angular CLI default
+     * of `4200`.
+     *
+     * @default 4200 | undefined
+     */
+    watchPort?: number;
+}