Skip to content

Commit

Permalink
feat(@ngtools/webpack): add NGCC as part of the workflow
Browse files Browse the repository at this point in the history
When add module is resolved, it will try to convert the module using the `NGCC` API.

NGCC will be run hooked to the compiler during the module resolution, using the Compiler Host methods 'resolveTypeReferenceDirectives' and 'resolveModuleNames'. It will process a single entry for each module and is based on the first match from the Webpack mainFields.

When Ivy is enabled we also append the '_ivy_ngcc' suffixed properties
to the mainFields so that Webpack resolver will resolve ngcc processed
modules first.
  • Loading branch information
Alan Agius authored and mgechev committed Apr 1, 2019
1 parent 655626c commit d2e22e9
Show file tree
Hide file tree
Showing 6 changed files with 233 additions and 9 deletions.
56 changes: 54 additions & 2 deletions packages/ngtools/webpack/src/angular_compiler_plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
PLATFORM,
} from './interfaces';
import { LazyRouteMap, findLazyRoutes } from './lazy_routes';
import { NgccProcessor } from './ngcc_processor';
import { TypeScriptPathsPlugin } from './paths-plugin';
import { WebpackResourceLoader } from './resource_loader';
import {
Expand All @@ -68,7 +69,7 @@ import {
MESSAGE_KIND,
UpdateMessage,
} from './type_checker_messages';
import { workaroundResolve } from './utils';
import { flattenArray, workaroundResolve } from './utils';
import {
VirtualFileSystemDecorator,
VirtualWatchFileSystemDecorator,
Expand Down Expand Up @@ -123,6 +124,8 @@ export class AngularCompilerPlugin {
// Logging.
private _logger: logging.Logger;

private _mainFields: string[] = [];

constructor(options: AngularCompilerPluginOptions) {
this._options = Object.assign({}, options);
this._setupOptions(this._options);
Expand Down Expand Up @@ -594,6 +597,16 @@ export class AngularCompilerPlugin {
// Registration hook for webpack plugin.
// tslint:disable-next-line:no-big-function
apply(compiler: Compiler & { watchMode?: boolean }) {
// The below is require by NGCC processor
// since we need to know which fields we need to process
compiler.hooks.environment.tap('angular-compiler', () => {
const { options } = compiler;
const mainFields = options.resolve && options.resolve.mainFields;
if (mainFields) {
this._mainFields = flattenArray(mainFields);
}
});

// cleanup if not watching
compiler.hooks.thisCompilation.tap('angular-compiler', compilation => {
compilation.hooks.finishModules.tap('angular-compiler', () => {
Expand Down Expand Up @@ -645,14 +658,33 @@ export class AngularCompilerPlugin {
}
}

let ngccProcessor: NgccProcessor | undefined;
if (this._compilerOptions.enableIvy) {
let ngcc: typeof import('@angular/compiler-cli/ngcc') | undefined;
try {
// this is done for the sole reason that @ngtools/webpack
// support versions of Angular that don't have NGCC API
ngcc = require('@angular/compiler-cli/ngcc');
} catch {
}

if (ngcc) {
ngccProcessor = new NgccProcessor(
ngcc,
this._mainFields,
compilerWithFileSystems.inputFileSystem,
);
}
}

// Create the webpack compiler host.
const webpackCompilerHost = new WebpackCompilerHost(
this._compilerOptions,
this._basePath,
host,
true,
this._options.directTemplateLoading,
this._platform,
ngccProcessor,
);

// Create and set a new WebpackResourceLoader in AOT
Expand Down Expand Up @@ -764,6 +796,26 @@ export class AngularCompilerPlugin {
});

compiler.hooks.afterResolvers.tap('angular-compiler', compiler => {
if (this._compilerOptions.enableIvy) {
// When Ivy is enabled we need to add the fields added by NGCC
// to take precedence over the provided mainFields.
// NGCC adds fields in package.json suffixed with '_ivy_ngcc'
// Example: module -> module__ivy_ngcc
// tslint:disable-next-line:no-any
(compiler as any).resolverFactory.hooks.resolveOptions
.for('normal')
// tslint:disable-next-line:no-any
.tap('WebpackOptionsApply', (resolveOptions: any) => {
const mainFields = (resolveOptions.mainFields as string[])
.map(f => [`${f}_ivy_ngcc`, f]);

return {
...resolveOptions,
mainFields: flattenArray(mainFields),
};
});
}

// tslint:disable-next-line:no-any
(compiler as any).resolverFactory.hooks.resolver
.for('normal')
Expand Down
50 changes: 43 additions & 7 deletions packages/ngtools/webpack/src/compiler_host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ import {
} from '@angular-devkit/core';
import { Stats } from 'fs';
import * as ts from 'typescript';
import { NgccProcessor } from './ngcc_processor';
import { WebpackResourceLoader } from './resource_loader';
import { workaroundResolve } from './utils';


export interface OnErrorFn {
Expand Down Expand Up @@ -47,6 +49,7 @@ export class WebpackCompilerHost implements ts.CompilerHost {
host: virtualFs.Host,
private readonly cacheSourceFiles: boolean,
private readonly directTemplateLoading = false,
private readonly ngccProcessor?: NgccProcessor,
) {
this._syncHost = new virtualFs.SyncDelegateHost(host);
this._memoryHost = new virtualFs.SyncDelegateHost(new virtualFs.SimpleMemoryHost());
Expand Down Expand Up @@ -354,13 +357,46 @@ export class WebpackCompilerHost implements ts.CompilerHost {
trace(message: string) {
console.log(message);
}
}

resolveModuleNames(
moduleNames: string[],
containingFile: string,
): (ts.ResolvedModule | undefined)[] {
return moduleNames.map(moduleName => {
const { resolvedModule } = ts.resolveModuleName(
moduleName,
workaroundResolve(containingFile),
this._options,
this,
);

if (this._options.enableIvy && resolvedModule && this.ngccProcessor) {
this.ngccProcessor.processModule(moduleName, resolvedModule);
}

return resolvedModule;
});
}

resolveTypeReferenceDirectives(
typeReferenceDirectiveNames: string[],
containingFile: string,
redirectedReference?: ts.ResolvedProjectReference,
): (ts.ResolvedTypeReferenceDirective | undefined)[] {
return typeReferenceDirectiveNames.map(moduleName => {
const { resolvedTypeReferenceDirective } = ts.resolveTypeReferenceDirective(
moduleName,
workaroundResolve(containingFile),
this._options,
this,
redirectedReference,
);

if (this._options.enableIvy && resolvedTypeReferenceDirective && this.ngccProcessor) {
this.ngccProcessor.processModule(moduleName, resolvedTypeReferenceDirective);
}

// `TsCompilerAotCompilerTypeCheckHostAdapter` in @angular/compiler-cli seems to resolve module
// names directly via `resolveModuleName`, which prevents full Path usage.
// To work around this we must provide the same path format as TS internally uses in
// the SourceFile paths.
export function workaroundResolve(path: Path | string) {
return getSystemPath(normalize(path)).replace(/\\/g, '/');
return resolvedTypeReferenceDirective;
});
}
}
1 change: 1 addition & 0 deletions packages/ngtools/webpack/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

export * from './angular_compiler_plugin';
export * from './interfaces';
export { ngcLoader as default } from './loader';

export const NgToolsLoader = __filename;
97 changes: 97 additions & 0 deletions packages/ngtools/webpack/src/ngcc_processor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import * as ts from 'typescript';
import { InputFileSystem } from 'webpack';
import { time, timeEnd } from './benchmark';
import { workaroundResolve } from './utils';

// We cannot create a plugin for this, because NGTSC requires addition type
// information which ngcc creates when processing a package which was compiled with NGC.

// Example of such errors:
// ERROR in node_modules/@angular/platform-browser/platform-browser.d.ts(42,22):
// error TS-996002: Appears in the NgModule.imports of AppModule,
// but could not be resolved to an NgModule class

// We now transform a package and it's typings when NGTSC is resolving a module.

export class NgccProcessor {
private _processedModules = new Set<string>();

constructor(
private readonly ngcc: typeof import('@angular/compiler-cli/ngcc'),
private readonly propertiesToConsider: string[],
private readonly inputFileSystem: InputFileSystem,
) {
}

processModule(
moduleName: string,
resolvedModule: ts.ResolvedModule | ts.ResolvedTypeReferenceDirective,
): void {
const resolvedFileName = resolvedModule.resolvedFileName;
if (
!resolvedFileName
|| moduleName.startsWith('.')
|| this._processedModules.has(moduleName)) {
// Skip when module is unknown, relative or NGCC compiler is not found or already processed.
return;
}

const packageJsonPath = this.tryResolvePackage(moduleName, resolvedFileName);
if (!packageJsonPath) {
// add it to processed so the second time round we skip this.
this._processedModules.add(moduleName);

return;
}
const normalizedJsonPath = workaroundResolve(packageJsonPath);

const timeLabel = `NgccProcessor.processModule.ngcc.process+${moduleName}`;
time(timeLabel);
this.ngcc.process({
basePath: normalizedJsonPath.substring(0, normalizedJsonPath.indexOf(moduleName)),
targetEntryPointPath: moduleName,
propertiesToConsider: this.propertiesToConsider,
compileAllFormats: false,
createNewEntryPointFormats: true,
});
timeEnd(timeLabel);

// Purge this file from cache, since NGCC add new mainFields. Ex: module_ivy_ngcc
// which are unknown in the cached file.

// tslint:disable-next-line:no-any
(this.inputFileSystem as any).purge(packageJsonPath);

this._processedModules.add(moduleName);
}

/**
* Try resolve a package.json file from the resolved .d.ts file.
*/
private tryResolvePackage(moduleName: string, resolvedFileName: string): string | undefined {
try {
// This is based on the logic in the NGCC compiler
// tslint:disable-next-line:max-line-length
// See: https://github.com/angular/angular/blob/b93c1dffa17e4e6900b3ab1b9e554b6da92be0de/packages/compiler-cli/src/ngcc/src/packages/dependency_host.ts#L85-L121
const packageJsonPath = require.resolve(`${moduleName}/package.json`,
{
paths: [resolvedFileName],
},
);

return packageJsonPath;
} catch {
// if it fails this might be a deep import which doesn't have a package.json
// Ex: @angular/compiler/src/i18n/i18n_ast/package.json
return undefined;
}
}
}
20 changes: 20 additions & 0 deletions packages/ngtools/webpack/src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import { Path, getSystemPath, normalize } from '@angular-devkit/core';

// `TsCompilerAotCompilerTypeCheckHostAdapter` in @angular/compiler-cli seems to resolve module
// names directly via `resolveModuleName`, which prevents full Path usage.
// To work around this we must provide the same path format as TS internally uses in
// the SourceFile paths.
export function workaroundResolve(path: Path | string) {
return getSystemPath(normalize(path)).replace(/\\/g, '/');
}

export function flattenArray<T>(value: Array<T | T[]>): T[] {
return [].concat.apply([], value);
}
18 changes: 18 additions & 0 deletions packages/ngtools/webpack/src/utils_spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import { flattenArray } from './utils';

describe('@ngtools/webpack utils', () => {
describe('flattenArray', () => {
it('should flatten an array', () => {
const arr = flattenArray(['module', ['browser', 'main']]);
expect(arr).toEqual(['module', 'browser', 'main']);
});
});
});

0 comments on commit d2e22e9

Please sign in to comment.