Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

revert: fix(@ngtools/webpack): emit lazy routes errors on rebuilds #13309

Merged
merged 2 commits into from
Jan 8, 2019
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -6,9 +6,9 @@
* found in the LICENSE file at https://angular.io/license
*/

import { DefaultTimeout, TestLogger, runTargetSpec } from '@angular-devkit/architect/testing';
import { DefaultTimeout, runTargetSpec } from '@angular-devkit/architect/testing';
import { join, normalize } from '@angular-devkit/core';
import { take, tap } from 'rxjs/operators';
import { tap } from 'rxjs/operators';
import { BrowserBuilderSchema } from '../../src';
import { browserTargetSpec, host } from '../utils';

@@ -70,7 +70,6 @@ export const lazyModuleImport: { [path: string]: string } = {
`,
};

// tslint:disable-next-line:no-big-function
describe('Browser Builder lazy modules', () => {

const outputPath = normalize('dist');
@@ -90,50 +89,6 @@ describe('Browser Builder lazy modules', () => {
).toPromise().then(done, done.fail);
});

it('should show error when lazy route is invalid on watch mode AOT', (done) => {
host.writeMultipleFiles(lazyModuleFiles);
host.writeMultipleFiles(lazyModuleImport);
host.replaceInFile(
'src/app/app.module.ts',
'lazy.module#LazyModule',
'invalid.module#LazyModule',
);

const logger = new TestLogger('rebuild-lazy-errors');
const overrides = { watch: true, aot: true };
runTargetSpec(host, browserTargetSpec, overrides, DefaultTimeout, logger).pipe(
tap((buildEvent) => expect(buildEvent.success).toBe(false)),
tap(() => {
expect(logger.includes('Could not resolve module')).toBe(true);
logger.clear();
host.appendToFile('src/main.ts', ' ');
}),
take(2),
).toPromise().then(done, done.fail);
});

it('should show error when lazy route is invalid on watch mode JIT', (done) => {
host.writeMultipleFiles(lazyModuleFiles);
host.writeMultipleFiles(lazyModuleImport);
host.replaceInFile(
'src/app/app.module.ts',
'lazy.module#LazyModule',
'invalid.module#LazyModule',
);

const logger = new TestLogger('rebuild-lazy-errors');
const overrides = { watch: true, aot: false };
runTargetSpec(host, browserTargetSpec, overrides, DefaultTimeout, logger).pipe(
tap((buildEvent) => expect(buildEvent.success).toBe(false)),
tap(() => {
expect(logger.includes('Could not resolve module')).toBe(true);
logger.clear();
host.appendToFile('src/main.ts', ' ');
}),
take(2),
).toPromise().then(done, done.fail);
});

it('supports lazy bundle for lazy routes with AOT', (done) => {
host.writeMultipleFiles(lazyModuleFiles);
host.writeMultipleFiles(lazyModuleImport);
Original file line number Diff line number Diff line change
@@ -198,33 +198,6 @@ describe('Browser Builder rebuilds', () => {
).toPromise().then(done, done.fail);
});

it('rebuilds shows error', (done) => {
host.replaceInFile('./src/app/app.component.ts', 'AppComponent', 'AppComponentZ');

const overrides = { watch: true, aot: false };
let buildCount = 1;
const logger = new TestLogger('rebuild-errors');

runTargetSpec(host, browserTargetSpec, overrides, DefaultTimeout * 3, logger).pipe(
tap((buildEvent) => {
switch (buildCount) {
case 1:
expect(buildEvent.success).toBe(false);
expect(logger.includes('AppComponent cannot be used as an entry component')).toBe(true);
logger.clear();

host.replaceInFile('./src/app/app.component.ts', 'AppComponentZ', 'AppComponent');
break;

default:
expect(buildEvent.success).toBe(true);
break;
}
buildCount ++;
}),
take(2),
).toPromise().then(done, done.fail);
});

it('rebuilds after errors in AOT', (done) => {
// Save the original contents of `./src/app/app.component.ts`.
45 changes: 41 additions & 4 deletions packages/ngtools/webpack/src/angular_compiler_plugin.ts
Original file line number Diff line number Diff line change
@@ -39,10 +39,10 @@ import { time, timeEnd } from './benchmark';
import { WebpackCompilerHost, workaroundResolve } from './compiler_host';
import { resolveEntryModuleFromMain } from './entry_resolver';
import { DiagnosticMode, gatherDiagnostics, hasErrors } from './gather_diagnostics';
import { LazyRouteMap, findLazyRoutes } from './lazy_routes';
import { TypeScriptPathsPlugin } from './paths-plugin';
import { WebpackResourceLoader } from './resource_loader';
import {
LazyRouteMap,
exportLazyModuleMap,
exportNgFactory,
findResources,
@@ -135,7 +135,7 @@ export class AngularCompilerPlugin {
private _moduleResolutionCache: ts.ModuleResolutionCache;
private _resourceLoader?: WebpackResourceLoader;
// Contains `moduleImportPath#exportName` => `fullModulePath`.
private _lazyRoutes: LazyRouteMap = Object.create(null);
private _lazyRoutes: LazyRouteMap = {};
private _tsConfigPath: string;
private _entryModule: string | null;
private _mainPath: string | undefined;
@@ -421,6 +421,22 @@ export class AngularCompilerPlugin {
}
}

private _findLazyRoutesInAst(changedFilePaths: string[]): LazyRouteMap {
time('AngularCompilerPlugin._findLazyRoutesInAst');
const result: LazyRouteMap = {};
for (const filePath of changedFilePaths) {
const fileLazyRoutes = findLazyRoutes(filePath, this._compilerHost, undefined,
this._compilerOptions);
for (const routeKey of Object.keys(fileLazyRoutes)) {
const route = fileLazyRoutes[routeKey];
result[routeKey] = route;
}
}
timeEnd('AngularCompilerPlugin._findLazyRoutesInAst');

return result;
}

private _listLazyRoutesFromProgram(): LazyRouteMap {
let entryRoute: string | undefined;
let ngProgram: Program;
@@ -876,6 +892,12 @@ export class AngularCompilerPlugin {
}
}

private _getChangedTsFiles() {
return this._getChangedCompilationFiles()
.filter(k => (k.endsWith('.ts') || k.endsWith('.tsx')) && !k.endsWith('.d.ts'))
.filter(k => this._compilerHost.fileExists(k));
}

private async _update() {
time('AngularCompilerPlugin._update');
// We only want to update on TS and template changes, but all kinds of files are on this
@@ -890,11 +912,26 @@ export class AngularCompilerPlugin {
// Make a new program and load the Angular structure.
await this._createOrUpdateProgram();

// Try to find lazy routes if we have an entry module.
// We need to run the `listLazyRoutes` the first time because it also navigates libraries
// and other things that we might miss using the (faster) findLazyRoutesInAst.
// Lazy routes modules will be read with compilerHost and added to the changed files.
let lazyRouteMap: LazyRouteMap = {};
if (!this._JitMode || this._firstRun) {
lazyRouteMap = this._listLazyRoutesFromProgram();
} else {
const changedTsFiles = this._getChangedTsFiles();
if (changedTsFiles.length > 0) {
lazyRouteMap = this._findLazyRoutesInAst(changedTsFiles);
}
}

// Find lazy routes
const lazyRouteMap: LazyRouteMap = {
...this._listLazyRoutesFromProgram(),
lazyRouteMap = {
...lazyRouteMap,
...this._options.additionalLazyModules,
};

this._processLazyRoutes(lazyRouteMap);

// Emit files.
100 changes: 100 additions & 0 deletions packages/ngtools/webpack/src/lazy_routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/**
* @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 { dirname, join } from 'path';
import * as ts from 'typescript';
import { findAstNodes, resolve } from './refactor';


function _getContentOfKeyLiteral(_source: ts.SourceFile, node: ts.Node): string | null {
if (node.kind == ts.SyntaxKind.Identifier) {
return (node as ts.Identifier).text;
} else if (node.kind == ts.SyntaxKind.StringLiteral) {
return (node as ts.StringLiteral).text;
} else {
return null;
}
}


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


export function findLazyRoutes(
filePath: string,
host: ts.CompilerHost,
program?: ts.Program,
compilerOptions?: ts.CompilerOptions,
): LazyRouteMap {
if (!compilerOptions) {
if (!program) {
throw new Error('Must pass either program or compilerOptions to findLazyRoutes.');
}
compilerOptions = program.getCompilerOptions();
}
const fileName = resolve(filePath, host, compilerOptions).replace(/\\/g, '/');
let sourceFile: ts.SourceFile | undefined;
if (program) {
sourceFile = program.getSourceFile(fileName);
}

if (!sourceFile) {
const content = host.readFile(fileName);
if (content) {
sourceFile = ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true);
}
}

if (!sourceFile) {
throw new Error(`Source file not found: '${fileName}'.`);
}
const sf: ts.SourceFile = sourceFile;

return findAstNodes(null, sourceFile, ts.SyntaxKind.ObjectLiteralExpression, true)
// Get all their property assignments.
.map((node: ts.ObjectLiteralExpression) => {
return findAstNodes(node, sf, ts.SyntaxKind.PropertyAssignment, false);
})
// Take all `loadChildren` elements.
.reduce((acc: ts.PropertyAssignment[], props: ts.PropertyAssignment[]) => {
return acc.concat(props.filter(literal => {
return _getContentOfKeyLiteral(sf, literal.name) == 'loadChildren';
}));
}, [])
// Get only string values.
.filter((node: ts.PropertyAssignment) => node.initializer.kind == ts.SyntaxKind.StringLiteral)
// Get the string value.
.map((node: ts.PropertyAssignment) => (node.initializer as ts.StringLiteral).text)
// Map those to either [path, absoluteModulePath], or [path, null] if the module pointing to
// does not exist.
.map((routePath: string) => {
const moduleName = routePath.split('#')[0];
const compOptions = (program && program.getCompilerOptions()) || compilerOptions || {};
const resolvedModuleName: ts.ResolvedModuleWithFailedLookupLocations = moduleName[0] == '.'
? ({
resolvedModule: { resolvedFileName: join(dirname(filePath), moduleName) + '.ts' },
} as ts.ResolvedModuleWithFailedLookupLocations)
: ts.resolveModuleName(moduleName, filePath, compOptions, host);
if (resolvedModuleName.resolvedModule
&& resolvedModuleName.resolvedModule.resolvedFileName
&& host.fileExists(resolvedModuleName.resolvedModule.resolvedFileName)) {
return [routePath, resolvedModuleName.resolvedModule.resolvedFileName];
} else {
return [routePath, null];
}
})
// Reduce to the LazyRouteMap map.
.reduce((acc: LazyRouteMap, [routePath, resolvedModuleName]: [string, string | null]) => {
if (resolvedModuleName) {
acc[routePath] = resolvedModuleName;
}

return acc;
}, {});
}
Original file line number Diff line number Diff line change
@@ -7,14 +7,11 @@
*/
import * as path from 'path';
import * as ts from 'typescript';
import { LazyRouteMap } from '../lazy_routes';
import { getFirstNode, getLastNode } from './ast_helpers';
import { AddNodeOperation, StandardTransform, TransformOperation } from './interfaces';
import { makeTransform } from './make_transform';

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

export function exportLazyModuleMap(
shouldTransform: (fileName: string) => boolean,
lazyRoutesCb: () => LazyRouteMap,