Skip to content

Commit

Permalink
fix(aot): Use the proper path when statically analyzing lazy routes. (#…
Browse files Browse the repository at this point in the history
…2992)

Before, we were using paths relative to base at all time, but these
might not be the paths we get in System.import(), therefore we have to
keep the relative path.

Also fix e2e tests.

BREAKING CHANGES: Using relative paths might lead to path clashing. We
now properly output an error in this case.

Fixes #2452
Fixes #2735
Fixes #2900
  • Loading branch information
hansl authored Nov 3, 2016
1 parent 37a1225 commit 88131a0
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 32 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "angular-cli",
"version": "1.0.0-beta.19",
"version": "1.0.0-beta.19-3",
"description": "CLI tool for Angular",
"main": "packages/angular-cli/lib/cli/index.js",
"trackingCode": "UA-8594346-19",
Expand Down
2 changes: 1 addition & 1 deletion packages/angular-cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "angular-cli",
"version": "1.0.0-beta.19",
"version": "1.0.0-beta.19-3",
"description": "CLI tool for Angular",
"main": "lib/cli/index.js",
"trackingCode": "UA-8594346-19",
Expand Down
2 changes: 1 addition & 1 deletion packages/angular-cli/upgrade/version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ function _hasOldCliBuildFile() {


export class Version {
constructor(private _version: string) {}
constructor(private _version: string = null) {}

private _parse() {
return this.isKnown()
Expand Down
2 changes: 1 addition & 1 deletion packages/webpack/src/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ export function ngcLoader(source: string) {
if (plugin && plugin instanceof AotPlugin) {
const cb: any = this.async();

plugin.done
Promise.resolve()
.then(() => _removeDecorators(this.resource, source))
.then(sourceText => _replaceBootstrap(this.resource, sourceText, plugin))
.then(sourceText => {
Expand Down
118 changes: 91 additions & 27 deletions packages/webpack/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {WebpackResourceLoader} from './resource_loader';
import {createResolveDependenciesFromContextMap} from './utils';
import {WebpackCompilerHost} from './compiler_host';
import {resolveEntryModuleFromMain} from './entry_resolver';
import {StaticSymbol} from '@angular/compiler-cli';


/**
Expand All @@ -25,6 +26,18 @@ export interface AotPluginOptions {
}


export interface LazyRoute {
moduleRoute: ModuleRoute;
moduleRelativePath: string;
moduleAbsolutePath: string;
}


export interface LazyRouteMap {
[path: string]: LazyRoute;
}


export class ModuleRoute {
constructor(public readonly path: string, public readonly className: string = null) {}

Expand Down Expand Up @@ -57,6 +70,7 @@ export class AotPlugin {

private _typeCheck: boolean = true;
private _basePath: string;
private _genDir: string;


constructor(options: AotPluginOptions) {
Expand All @@ -68,7 +82,7 @@ export class AotPlugin {
get compilerOptions() { return this._compilerOptions; }
get done() { return this._donePromise; }
get entryModule() { return this._entryModule; }
get genDir() { return this._basePath; }
get genDir() { return this._genDir; }
get program() { return this._program; }
get typeCheck() { return this._typeCheck; }

Expand Down Expand Up @@ -113,6 +127,7 @@ export class AotPlugin {
genDir
});
this._basePath = basePath;
this._genDir = genDir;

if (options.hasOwnProperty('typeChecking')) {
this._typeCheck = options.typeChecking;
Expand Down Expand Up @@ -168,9 +183,9 @@ export class AotPlugin {

// Virtual file system.
compiler.resolvers.normal.plugin('resolve', (request: any, cb?: () => void) => {
// Populate the file system cache with the virtual module.
this._compilerHost.populateWebpackResolver(compiler.resolvers.normal);
if (cb) {
if (request.request.match(/\.ts$/)) {
this.done.then(() => cb());
} else {
cb();
}
});
Expand Down Expand Up @@ -227,49 +242,85 @@ export class AotPlugin {
throw new Error(message);
}
})
.then(() => {
// Populate the file system cache with the virtual module.
this._compilerHost.populateWebpackResolver(this._compiler.resolvers.normal);
})
.then(() => {
// Process the lazy routes
this._lazyRoutes =
this._processNgModule(this._entryModule, null)
.map(module => ModuleRoute.fromString(module))
.reduce((lazyRoutes: any, module: ModuleRoute) => {
lazyRoutes[`${module.path}.ngfactory`] = path.join(
this.genDir, module.path + '.ngfactory.ts');
return lazyRoutes;
}, {});
this._lazyRoutes = {};
const allLazyRoutes = this._processNgModule(this._entryModule, null);
Object.keys(allLazyRoutes)
.forEach(k => {
const lazyRoute = allLazyRoutes[k];
this._lazyRoutes[k + '.ngfactory'] = lazyRoute.moduleAbsolutePath + '.ngfactory.ts';
});
})
.then(() => cb(), (err: any) => cb(err));
.then(() => cb(), (err: any) => { cb(err); });
}

private _resolveModule(module: ModuleRoute, containingFile: string) {
private _resolveModulePath(module: ModuleRoute, containingFile: string) {
if (module.path.startsWith('.')) {
return path.join(path.dirname(containingFile), module.path);
}
return module.path;
}

private _processNgModule(module: ModuleRoute, containingFile: string | null): string[] {
private _processNgModule(module: ModuleRoute, containingFile: string | null): LazyRouteMap {
const modulePath = containingFile ? module.path : ('./' + path.basename(module.path));
if (containingFile === null) {
containingFile = module.path + '.ts';
}
const relativeModulePath = this._resolveModulePath(module, containingFile);

const resolvedModulePath = this._resolveModule(module, containingFile);
const staticSymbol = this._reflectorHost
.findDeclaration(modulePath, module.className, containingFile);
const entryNgModuleMetadata = this.getNgModuleMetadata(staticSymbol);
const loadChildren = this.extractLoadChildren(entryNgModuleMetadata);
const result = loadChildren.map(route => {
return this._resolveModule(new ModuleRoute(route), resolvedModulePath);
});
const loadChildrenRoute: LazyRoute[] = this.extractLoadChildren(entryNgModuleMetadata)
.map(route => {
const mr = ModuleRoute.fromString(route);
const relativePath = this._resolveModulePath(mr, relativeModulePath);
const absolutePath = path.join(this.genDir, relativePath);
return {
moduleRoute: mr,
moduleRelativePath: relativePath,
moduleAbsolutePath: absolutePath
};
});
const resultMap: LazyRouteMap = loadChildrenRoute
.reduce((acc: LazyRouteMap, curr: LazyRoute) => {
const key = curr.moduleRoute.path;
if (acc[key]) {
if (acc[key].moduleAbsolutePath != curr.moduleAbsolutePath) {
throw new Error(`Duplicated path in loadChildren detected: "${key}" is used in 2 ` +
'loadChildren, but they point to different modules. Webpack cannot distinguish ' +
'between the two based on context and would fail to load the proper one.');
}
} else {
acc[key] = curr;
}
return acc;
}, {});

// Also concatenate every child of child modules.
for (const route of loadChildren) {
const childModule = ModuleRoute.fromString(route);
const children = this._processNgModule(childModule, resolvedModulePath + '.ts');
result.push(...children);
for (const lazyRoute of loadChildrenRoute) {
const mr = lazyRoute.moduleRoute;
const children = this._processNgModule(mr, relativeModulePath);
Object.keys(children).forEach(p => {
const child = children[p];
const key = child.moduleRoute.path;
if (resultMap[key]) {
if (resultMap[key].moduleAbsolutePath != child.moduleAbsolutePath) {
throw new Error(`Duplicated path in loadChildren detected: "${key}" is used in 2 ` +
'loadChildren, but they point to different modules. Webpack cannot distinguish ' +
'between the two based on context and would fail to load the proper one.');
}
} else {
resultMap[key] = child;
}
});
}
return result;
return resultMap;
}

private getNgModuleMetadata(staticSymbol: ngCompiler.StaticSymbol) {
Expand All @@ -281,10 +332,23 @@ export class AotPlugin {
}

private extractLoadChildren(ngModuleDecorator: any): any[] {
const routes = ngModuleDecorator.imports.reduce((mem: any[], m: any) => {
const routes = (ngModuleDecorator.imports || []).reduce((mem: any[], m: any) => {
return mem.concat(this.collectRoutes(m.providers));
}, this.collectRoutes(ngModuleDecorator.providers));
return this.collectLoadChildren(routes);
return this.collectLoadChildren(routes)
.concat((ngModuleDecorator.imports || [])
// Also recursively extractLoadChildren of modules we import.
.map((staticSymbol: any) => {
if (staticSymbol instanceof StaticSymbol) {
const entryNgModuleMetadata = this.getNgModuleMetadata(staticSymbol);
return this.extractLoadChildren(entryNgModuleMetadata);
} else {
return [];
}
})
// Poor man's flat map.
.reduce((acc: any[], i: any) => acc.concat(i), []))
.filter(x => !!x);
}

private collectRoutes(providers: any[]): any[] {
Expand Down
5 changes: 4 additions & 1 deletion tests/e2e_runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ testsToRun.reduce((previous, relativeName) => {
return Promise.resolve()
.then(() => printHeader(currentFileName))
.then(() => fn(argv, () => clean = false))
.then(() => console.log(' ----'))
.then(() => {
// Only clean after a real test, not a setup step. Also skip cleaning if the test
// requested an exception.
Expand All @@ -104,7 +105,9 @@ testsToRun.reduce((previous, relativeName) => {
})
.then(() => printFooter(currentFileName, start),
(err) => {
printFooter(currentFileName, start); throw err;
printFooter(currentFileName, start);
console.error(err);
throw err;
});
});
}, Promise.resolve())
Expand Down

1 comment on commit 88131a0

@sasxa
Copy link

@sasxa sasxa commented on 88131a0 Nov 4, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hansl Is compilerOptions.baseUrl from tsconfig.json the base mentioned in commit description?
When I use ng b --aot I'm getting this error:

ERROR in ./src/main.ts
Module build failed: SyntaxError: Unexpected token u in JSON at position 0
    at JSON.parse (<anonymous>)
    at _transpile (c:\dev\projects\angular-cli\packages\webpack\src\loader.ts:110:25)
    at c:\dev\projects\angular-cli\packages\webpack\src\loader.ts:125:26
 @ multi main

Is this the breaknig change mentioned?

My project compiles without errors when I use @angular/compiler-cli directly (via gulp task).

Please sign in to comment.