Skip to content

Support import() syntax for loadChildren in ViewEngine #13948

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

Merged
merged 6 commits into from
Mar 29, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
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
21 changes: 7 additions & 14 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,24 +106,17 @@ jobs:
path: /tmp/dist
destination: cli/new-production

e2e-cli-ng-snapshots:
e2e-cli-ivy:
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: why switch the order of these? makes the delta harder to review

Copy link
Contributor Author

Choose a reason for hiding this comment

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

To keep similar e2e jobs (e2e-cli and e2e-cli-ivy) together.

The old e2e-cli-ng-snapshots was after e2e-cli-ivy-snapshots because they shared the snapshot flag and only ran on some builds. Now e2e-cli-ivy-snapshots is instead e2e-cli-ivy and should instead be next to e2e-cli.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added.

<<: *defaults
environment:
BASH_ENV: ~/.profile
resource_class: xlarge
parallelism: 4
steps:
- run:
name: Don't run expensive e2e tests for forks other than renovate-bot and angular
command: >
if [[ "$CIRCLE_PR_USERNAME" != "renovate-bot" ]] &&
[[ "$CIRCLE_PROJECT_USERNAME" != "angular" || $CIRCLE_BRANCH != "master" ]]; then
circleci step halt
fi
- attach_workspace: *attach_options
- run: xvfb-run -a node ./tests/legacy-cli/run_e2e --nb-shards=${CIRCLE_NODE_TOTAL} --shard=${CIRCLE_NODE_INDEX} --ng-snapshots
- run: xvfb-run -a node ./tests/legacy-cli/run_e2e --nb-shards=${CIRCLE_NODE_TOTAL} --shard=${CIRCLE_NODE_INDEX} --ivy

e2e-cli-ivy-snapshots:
e2e-cli-ng-snapshots:
<<: *defaults
environment:
BASH_ENV: ~/.profile
Expand All @@ -138,7 +131,7 @@ jobs:
circleci step halt
fi
- attach_workspace: *attach_options
- run: xvfb-run -a node ./tests/legacy-cli/run_e2e --nb-shards=${CIRCLE_NODE_TOTAL} --shard=${CIRCLE_NODE_INDEX} --ng-snapshots --ivy
- run: xvfb-run -a node ./tests/legacy-cli/run_e2e --nb-shards=${CIRCLE_NODE_TOTAL} --shard=${CIRCLE_NODE_INDEX} --ng-snapshots

build:
<<: *defaults
Expand Down Expand Up @@ -210,6 +203,9 @@ workflows:
- e2e-cli:
requires:
- build
- e2e-cli-ivy:
requires:
- build
- snapshot_publish_docs:
requires:
- install
Expand All @@ -220,9 +216,6 @@ workflows:
- e2e-cli-ng-snapshots:
requires:
- build
- e2e-cli-ivy-snapshots:
requires:
- build
- snapshot_publish:
requires:
- test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export interface BuildOptions {
forkTypeChecker: boolean;
profile?: boolean;
es5BrowserSupport?: boolean;
experimentalImportFactories?: boolean;

main: string;
index: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@
import { tags } from '@angular-devkit/core';
import * as CopyWebpackPlugin from 'copy-webpack-plugin';
import * as path from 'path';
import { Configuration, HashedModuleIdsPlugin, Output, debug } from 'webpack';
import {
Configuration,
ContextReplacementPlugin,
HashedModuleIdsPlugin,
Output,
debug,
} from 'webpack';
import { AssetPatternClass } from '../../../browser/schema';
import { BundleBudgetPlugin } from '../../plugins/bundle-budget';
import { CleanCssWebpackPlugin } from '../../plugins/cleancss-webpack-plugin';
Expand Down Expand Up @@ -345,6 +351,12 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration {
...extraMinimizers,
],
},
plugins: extraPlugins,
plugins: [
// Always replace the context for the System.import in angular/core to prevent warnings.
// https://github.com/angular/angular/issues/11580
// With VE the correct context is added in @ngtools/webpack, but Ivy doesn't need it at all.
new ContextReplacementPlugin(/\@angular(\\|\/)core(\\|\/)/),
...extraPlugins,
],
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ function _createAotPlugin(
contextElementDependencyConstructor: require('webpack/lib/dependencies/ContextElementDependency'),
logger: wco.logger,
directTemplateLoading: true,
importFactories: buildOptions.experimentalImportFactories,
...options,
};
return new AngularCompilerPlugin(pluginOptions);
Expand Down
5 changes: 5 additions & 0 deletions packages/angular_devkit/build_angular/src/browser/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,11 @@
"type": "boolean",
"default": false,
"x-deprecated": true
},
"experimentalImportFactories": {
"description": "**EXPERIMENTAL** Transform import statements for lazy routes to import factories when using View Engine. Should only be used when switching back and forth between View Engine and Ivy. See https://angular.io/guide/ivy for usage information.",
"type": "boolean",
"default": false
}
},
"additionalProperties": false,
Expand Down
190 changes: 117 additions & 73 deletions packages/ngtools/webpack/src/angular_compiler_plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import {
exportLazyModuleMap,
exportNgFactory,
findResources,
importFactory,
registerLocaleData,
removeDecorators,
replaceBootstrap,
Expand Down Expand Up @@ -103,6 +104,12 @@ export interface AngularCompilerPluginOptions {
nameLazyFiles?: boolean;
logger?: logging.Logger;
directTemplateLoading?: boolean;
// When using the loadChildren string syntax, @ngtools/webpack must query @angular/compiler-cli
// via a private API to know which lazy routes exist. This increases build and rebuild time.
// When using Ivy, the string syntax is not supported at all. Thus we shouldn't attempt that.
// This option is also used for when the compilation doesn't need this sort of processing at all.
discoverLazyRoutes?: boolean;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you kindly explain the reasoning behind this option?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

When using the loadChildren string syntax, @ngtools/webpack must query @angular/compiler-cli via a private API to know which lazy routes exist. This increases build and rebuild time.

When using Ivy, the string syntax is not supported at all. Thus we shouldn't attempt that.

This commit also exists in #13700 for slightly different reasons. There, it is meant to disable this sort of processing for compilations that do not need it.

Copy link
Collaborator

@alan-agius4 alan-agius4 Mar 26, 2019

Choose a reason for hiding this comment

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

Thanks for the clarification, it makes more sense now :) As to disable lazy route discovery under ivy we can use enableIvy, but seeing that other PR. I can now see the use case.

Copy link
Member

Choose a reason for hiding this comment

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

Should we warn (or probably error) in the transformer (or somewhere else) if the string form is found when this option is off?

Users may not know why their applications are not working.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think not because that means we need to always check, which incurs a performance penalty on every AOT build.

The "right way" of doing that probably a Ivy migration when it's stable.

Copy link
Contributor

Choose a reason for hiding this comment

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

let's try to generally answer questions like this in the form of a comment in the code - no one later will find this thread

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

importFactories?: boolean;

// added to the list of lazy routes
additionalLazyModules?: { [module: string]: string };
Expand Down Expand Up @@ -134,6 +141,8 @@ export class AngularCompilerPlugin {
private _compilerHost: WebpackCompilerHost & CompilerHost;
private _moduleResolutionCache: ts.ModuleResolutionCache;
private _resourceLoader?: WebpackResourceLoader;
private _discoverLazyRoutes = true;
private _importFactories = false;
// Contains `moduleImportPath#exportName` => `fullModulePath`.
private _lazyRoutes: LazyRouteMap = {};
private _tsConfigPath: string;
Expand Down Expand Up @@ -292,6 +301,36 @@ export class AngularCompilerPlugin {
this._platformTransformers = options.platformTransformers;
}

// Determine if lazy route discovery via Compiler CLI private API should be attempted.
if (this._compilerOptions.enableIvy) {
// Never try to discover lazy routes with Ivy.
this._discoverLazyRoutes = false;
} else if (options.discoverLazyRoutes !== undefined) {
// The default is to discover routes, but it can be overriden.
this._discoverLazyRoutes = options.discoverLazyRoutes;
}

if (this._discoverLazyRoutes === false && this.options.additionalLazyModuleResources
&& this.options.additionalLazyModuleResources.length > 0) {
this._warnings.push(
new Error(`Lazy route discovery is disabled but additional Lazy Module Resources were`
+ ` provided. These will be ignored.`),
);
}

if (this._discoverLazyRoutes === false && this.options.additionalLazyModules
&& Object.keys(this.options.additionalLazyModules).length > 0) {
this._warnings.push(
new Error(`Lazy route discovery is disabled but additional lazy modules were provided.`
+ `These will be ignored.`),
);
}

if (!this._compilerOptions.enableIvy && options.importFactories === true) {
// Only transform imports to use factories with View Engine.
this._importFactories = true;
}

// Default ContextElementDependency to the one we can import from here.
// Failing to use the right ContextElementDependency will throw the error below:
// "No module factory available for dependency type: ContextElementDependency"
Expand Down Expand Up @@ -411,7 +450,7 @@ export class AngularCompilerPlugin {
this._entryModule = resolveEntryModuleFromMain(
this._mainPath, this._compilerHost, this._getTsProgram() as ts.Program);

if (!this.entryModule && !this._compilerOptions.enableIvy) {
if (this._discoverLazyRoutes && !this.entryModule && !this._compilerOptions.enableIvy) {
this._warnings.push('Lazy routes discovery is not enabled. '
+ 'Because there is neither an entryModule nor a '
+ 'statically analyzable bootstrap code in the main file.',
Expand Down Expand Up @@ -697,64 +736,63 @@ export class AngularCompilerPlugin {
);
});

// Add lazy modules to the context module for @angular/core
compiler.hooks.contextModuleFactory.tap('angular-compiler', cmf => {
const angularCorePackagePath = require.resolve('@angular/core/package.json');

// APFv6 does not have single FESM anymore. Instead of verifying if we're pointing to
// FESMs, we resolve the `@angular/core` path and verify that the path for the
// module starts with it.
// This may be slower but it will be compatible with both APF5, 6 and potential future
// versions (until the dynamic import appears outside of core I suppose).
// We resolve any symbolic links in order to get the real path that would be used in webpack.
const angularCoreResourceRoot = fs.realpathSync(path.dirname(angularCorePackagePath));

cmf.hooks.afterResolve.tapPromise('angular-compiler', async result => {
// Alter only existing request from Angular or one of the additional lazy module resources.
const isLazyModuleResource = (resource: string) =>
resource.startsWith(angularCoreResourceRoot) ||
( this.options.additionalLazyModuleResources &&
this.options.additionalLazyModuleResources.includes(resource));

if (!result || !this.done || !isLazyModuleResource(result.resource)) {
return result;
}
if (this._discoverLazyRoutes) {
// Add lazy modules to the context module for @angular/core
compiler.hooks.contextModuleFactory.tap('angular-compiler', cmf => {
const angularCorePackagePath = require.resolve('@angular/core/package.json');

// APFv6 does not have single FESM anymore. Instead of verifying if we're pointing to
// FESMs, we resolve the `@angular/core` path and verify that the path for the
// module starts with it.
// This may be slower but it will be compatible with both APF5, 6 and potential future
// versions (until the dynamic import appears outside of core I suppose).
// We resolve symbolic links in order to get the real path that would be used in webpack.
const angularCoreResourceRoot = fs.realpathSync(path.dirname(angularCorePackagePath));

cmf.hooks.afterResolve.tapPromise('angular-compiler', async result => {
// Alter only existing request from Angular or the additional lazy module resources.
const isLazyModuleResource = (resource: string) =>
resource.startsWith(angularCoreResourceRoot) ||
(this.options.additionalLazyModuleResources &&
this.options.additionalLazyModuleResources.includes(resource));

if (!result || !this.done || !isLazyModuleResource(result.resource)) {
return result;
}

return this.done.then(
() => {
// This folder does not exist, but we need to give webpack a resource.
// TODO: check if we can't just leave it as is (angularCoreModuleDir).
result.resource = path.join(this._basePath, '$$_lazy_route_resource');
// tslint:disable-next-line:no-any
result.dependencies.forEach((d: any) => d.critical = false);
// tslint:disable-next-line:no-any
result.resolveDependencies = (_fs: any, options: any, callback: Callback) => {
const dependencies = Object.keys(this._lazyRoutes)
.map((key) => {
const modulePath = this._lazyRoutes[key];
if (modulePath !== null) {
const name = key.split('#')[0];

return new this._contextElementDependencyConstructor(modulePath, name);
} else {
return null;
}
})
.filter(x => !!x);

if (this._options.nameLazyFiles) {
options.chunkName = '[request]';
}
await this.done;

// This folder does not exist, but we need to give webpack a resource.
// TODO: check if we can't just leave it as is (angularCoreModuleDir).
result.resource = path.join(this._basePath, '$$_lazy_route_resource');
// tslint:disable-next-line:no-any
result.dependencies.forEach((d: any) => d.critical = false);
// tslint:disable-next-line:no-any
result.resolveDependencies = (_fs: any, options: any, callback: Callback) => {
const dependencies = Object.keys(this._lazyRoutes)
.map((key) => {
const modulePath = this._lazyRoutes[key];
if (modulePath !== null) {
const name = key.split('#')[0];

return new this._contextElementDependencyConstructor(modulePath, name);
} else {
return null;
}
})
.filter(x => !!x);

if (this._options.nameLazyFiles) {
options.chunkName = '[request]';
}

callback(null, dependencies);
};
callback(null, dependencies);
};

return result;
},
() => undefined,
);
return result;
});
});
});
}

// Create and destroy forked type checker on watch mode.
compiler.hooks.watchRun.tap('angular-compiler', () => {
Expand Down Expand Up @@ -868,6 +906,10 @@ export class AngularCompilerPlugin {
} else {
// Remove unneeded angular decorators.
this._transformers.push(removeDecorators(isAppPath, getTypeChecker));
// Import ngfactory in loadChildren import syntax
if (this._importFactories) {
this._transformers.push(importFactory(msg => this._warnings.push(msg)));
}
}

if (this._platformTransformers !== null) {
Expand Down Expand Up @@ -922,27 +964,29 @@ 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);
if (this._discoverLazyRoutes) {
// 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
lazyRouteMap = {
...lazyRouteMap,
...this._options.additionalLazyModules,
};
// Find lazy routes
lazyRouteMap = {
...lazyRouteMap,
...this._options.additionalLazyModules,
};

this._processLazyRoutes(lazyRouteMap);
this._processLazyRoutes(lazyRouteMap);
}

// Emit files.
time('AngularCompilerPlugin._update._emit');
Expand Down
Loading