diff --git a/.appveyor.yml b/.appveyor.yml index 5b1e589fbb17..eb3e089fd20b 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -18,4 +18,4 @@ test_script: build: off cache: - - node_modules + - node_modules -> package-lock.json diff --git a/package-lock.json b/package-lock.json index a7ce48cd5c33..3c6cc814194e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@angular/cli", - "version": "1.4.0", + "version": "1.4.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -12,6 +12,13 @@ "loader-utils": "1.1.0", "source-map": "0.5.6", "typescript": "2.4.2" + }, + "dependencies": { + "typescript": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.4.2.tgz", + "integrity": "sha1-+DlfhdRZJ2BnyYiqQYN6j4KHCEQ=" + } } }, "@angular-devkit/schematics": { @@ -33,38 +40,38 @@ } }, "@angular/compiler": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-4.3.5.tgz", - "integrity": "sha1-UNPJhmV77/H+9Pbdmj+ljiSr1Ug=", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-4.3.6.tgz", + "integrity": "sha1-vhcN8Ji3HoNczt8WjV+3sj5QRbg=", "dev": true, "requires": { "tslib": "1.7.1" } }, "@angular/compiler-cli": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-4.3.5.tgz", - "integrity": "sha1-JOmbNsCQk2P/gke/MxqLiert/mM=", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-4.3.6.tgz", + "integrity": "sha1-avpq72jdaB5hs5i+TWJw5choCxI=", "dev": true, "requires": { - "@angular/tsc-wrapped": "4.3.5", + "@angular/tsc-wrapped": "4.3.6", "minimist": "1.2.0", "reflect-metadata": "0.1.10" } }, "@angular/core": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-4.3.5.tgz", - "integrity": "sha1-vR79vx68+5wnojjiqkxIFZsIlbs=", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-4.3.6.tgz", + "integrity": "sha1-u6xj1o0Pe8s4nRKzQghlK+MofpY=", "dev": true, "requires": { "tslib": "1.7.1" } }, "@angular/tsc-wrapped": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/@angular/tsc-wrapped/-/tsc-wrapped-4.3.5.tgz", - "integrity": "sha1-lf2qgTz8VyYvx+9f6nJtYorvq6w=", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@angular/tsc-wrapped/-/tsc-wrapped-4.3.6.tgz", + "integrity": "sha1-GqZuCrLEeZpK0UtnXhOVOqX81DY=", "dev": true, "requires": { "tsickle": "0.21.6" diff --git a/packages/@angular/cli/commands/build.ts b/packages/@angular/cli/commands/build.ts index 6921108bfa2d..91d376e5a317 100644 --- a/packages/@angular/cli/commands/build.ts +++ b/packages/@angular/cli/commands/build.ts @@ -2,6 +2,7 @@ import { CliConfig } from '../models/config'; import { BuildOptions } from '../models/build-options'; import { Version } from '../upgrade/version'; import { oneLine } from 'common-tags'; +import { AngularCompilerPlugin } from '@ngtools/webpack'; const Command = require('../ember-cli/lib/models/command'); @@ -176,6 +177,13 @@ export const baseBuildCommandOptions: any = [ aliases: ['nc'], description: 'Use file name for lazy loaded chunks.', default: buildConfigDefaults['namedChunks'] + }, + { + name: 'experimental-angular-compiler', + type: Boolean, + // aliases: ['eac'], // We should not have shorthand aliases for experimental flags. + description: '(Experimental) Use new Angular Compiler (Angular version 5 and greater only).', + default: AngularCompilerPlugin.isSupported() } ]; diff --git a/packages/@angular/cli/models/build-options.ts b/packages/@angular/cli/models/build-options.ts index 3bc2706a10ce..948458b276ca 100644 --- a/packages/@angular/cli/models/build-options.ts +++ b/packages/@angular/cli/models/build-options.ts @@ -25,4 +25,5 @@ export interface BuildOptions { showCircularDependencies?: boolean; buildOptimizer?: boolean; namedChunks?: boolean; + experimentalAngularCompiler?: boolean; } diff --git a/packages/@angular/cli/models/webpack-config.ts b/packages/@angular/cli/models/webpack-config.ts index 35ed6d50042c..c6a78c74ddda 100644 --- a/packages/@angular/cli/models/webpack-config.ts +++ b/packages/@angular/cli/models/webpack-config.ts @@ -12,6 +12,7 @@ import { getAotConfig } from './webpack-configs'; import * as path from 'path'; +import { AngularCompilerPlugin } from '@ngtools/webpack'; export interface WebpackConfigOptions { projectRoot: string; @@ -78,6 +79,10 @@ export class NgCliWebpackConfig { && !(buildOptions.aot || buildOptions.target === 'production')) { throw new Error('The `--build-optimizer` option cannot be used without `--aot`.'); } + + if (buildOptions.experimentalAngularCompiler && !AngularCompilerPlugin.isSupported()) { + throw new Error('You need Angular 5 and up to use --experimental-angular-compiler.'); + } } // Fill in defaults for build targets diff --git a/packages/@angular/cli/models/webpack-configs/common.ts b/packages/@angular/cli/models/webpack-configs/common.ts index 82afcaab7cff..fc4ed0c1da31 100644 --- a/packages/@angular/cli/models/webpack-configs/common.ts +++ b/packages/@angular/cli/models/webpack-configs/common.ts @@ -168,7 +168,9 @@ export function getCommonConfig(wco: WebpackConfigOptions) { }, module: { rules: [ - { enforce: 'pre', test: /\.js$/, loader: 'source-map-loader', exclude: [nodeModules] }, + { enforce: 'pre', test: /\.js$/, loader: 'source-map-loader', exclude: [ + nodeModules, /\.ngfactory\.js$/, /\.ngstyle\.js$/ + ] }, { test: /\.html$/, loader: 'raw-loader' }, { test: /\.(eot|svg|cur)$/, loader: `file-loader?name=[name]${hashFormat.file}.[ext]` }, { diff --git a/packages/@angular/cli/models/webpack-configs/typescript.ts b/packages/@angular/cli/models/webpack-configs/typescript.ts index 348dc6369805..472df1139f11 100644 --- a/packages/@angular/cli/models/webpack-configs/typescript.ts +++ b/packages/@angular/cli/models/webpack-configs/typescript.ts @@ -1,6 +1,6 @@ import * as path from 'path'; import { stripIndent } from 'common-tags'; -import {AotPlugin} from '@ngtools/webpack'; +import { AotPlugin, AngularCompilerPlugin } from '@ngtools/webpack'; import { WebpackConfigOptions } from '../webpack-config'; const SilentError = require('silent-error'); @@ -63,17 +63,24 @@ function _createAotPlugin(wco: WebpackConfigOptions, options: any) { }; } - return new AotPlugin(Object.assign({}, { - mainPath: path.join(projectRoot, appConfig.root, appConfig.main), - i18nFile: buildOptions.i18nFile, - i18nFormat: buildOptions.i18nFormat, - locale: buildOptions.locale, - replaceExport: appConfig.platform === 'server', - missingTranslation: buildOptions.missingTranslation, - hostReplacementPaths, - // If we don't explicitely list excludes, it will default to `['**/*.spec.ts']`. - exclude: [] - }, options)); + const pluginOptions = Object.assign({}, { + mainPath: path.join(projectRoot, appConfig.root, appConfig.main), + i18nFile: buildOptions.i18nFile, + i18nFormat: buildOptions.i18nFormat, + locale: buildOptions.locale, + replaceExport: appConfig.platform === 'server', + missingTranslation: buildOptions.missingTranslation, + hostReplacementPaths, + sourceMap: buildOptions.sourcemaps, + // If we don't explicitely list excludes, it will default to `['**/*.spec.ts']`. + exclude: [] + }, options); + + if (wco.buildOptions.experimentalAngularCompiler && !options.skipCodeGeneration) { + return new AngularCompilerPlugin(pluginOptions); + } else { + return new AotPlugin(pluginOptions); + } } export const getNonAotConfig = function(wco: WebpackConfigOptions) { diff --git a/packages/@angular/cli/models/webpack-xi18n-config.ts b/packages/@angular/cli/models/webpack-xi18n-config.ts index 06e86ffc73a4..816d4091474e 100644 --- a/packages/@angular/cli/models/webpack-xi18n-config.ts +++ b/packages/@angular/cli/models/webpack-xi18n-config.ts @@ -24,7 +24,8 @@ export class XI18nWebpackConfig extends NgCliWebpackConfig { super({ target: 'development', verbose: extractOptions.verbose, - progress: extractOptions.progress + progress: extractOptions.progress, + experimentalAngularCompiler: false, }, appConfig); super.buildConfig(); } diff --git a/packages/@angular/cli/tasks/eject.ts b/packages/@angular/cli/tasks/eject.ts index 242404503843..b16876ba45f5 100644 --- a/packages/@angular/cli/tasks/eject.ts +++ b/packages/@angular/cli/tasks/eject.ts @@ -7,7 +7,7 @@ import { getAppFromConfig } from '../utilities/app-utils'; import { EjectTaskOptions } from '../commands/eject'; import { NgCliWebpackConfig } from '../models/webpack-config'; import { CliConfig } from '../models/config'; -import { AotPlugin } from '@ngtools/webpack'; +import { AotPlugin, AngularCompilerPlugin } from '@ngtools/webpack'; import { yellow } from 'chalk'; import { LicenseWebpackPlugin } from 'license-webpack-plugin'; @@ -213,6 +213,10 @@ class JsonWebpackSerializer { args = this._aotPluginSerialize(plugin); this._addImport('@ngtools/webpack', 'AotPlugin'); break; + case AngularCompilerPlugin: + args = this._aotPluginSerialize(plugin); + this._addImport('@ngtools/webpack', 'AngularCompilerPlugin'); + break; case HtmlWebpackPlugin: args = this._htmlWebpackPlugin(plugin); this.variableImports['html-webpack-plugin'] = 'HtmlWebpackPlugin'; diff --git a/packages/@ngtools/webpack/README.md b/packages/@ngtools/webpack/README.md index bc34b62ede74..8e634d118149 100644 --- a/packages/@ngtools/webpack/README.md +++ b/packages/@ngtools/webpack/README.md @@ -29,15 +29,18 @@ exports = { /* ... */ The loader works with the webpack plugin to compile your TypeScript. It's important to include both, and to not include any other TypeScript compiler loader. +For Angular version 5 and up, import `AngularCompilerPlugin` instead of `AotPlugin`. + ## Options * `tsConfigPath`. The path to the `tsconfig.json` file. This is required. In your `tsconfig.json`, you can pass options to the Angular Compiler with `angularCompilerOptions`. * `basePath`. Optional. The root to use by the compiler to resolve file paths. By default, use the `tsConfigPath` root. * `entryModule`. Optional if specified in `angularCompilerOptions`. The path and classname of the main application module. This follows the format `path/to/file#ClassName`. * `mainPath`. Optional if `entryModule` is specified. The `main.ts` file containing the bootstrap code. The plugin will use AST to determine the `entryModule`. -* `skipCodeGeneration`. Optional, defaults to false. Disable code generation and do not refactor the code to bootstrap. This replaces `templateUrl: "string"` with `template: require("string")` (and similar for styles) to allow for webpack to properly link the resources. -* `typeChecking`. Optional, defaults to true. Enable type checking through your application. This will slow down compilation, but show syntactic and semantic errors in webpack. +* `skipCodeGeneration`. Optional, defaults to false. Disable code generation and do not refactor the code to bootstrap. This replaces `templateUrl: "string"` with `template: require("string")` (and similar for styles) to allow for webpack to properly link the resources. Only available in `AotPlugin`. +* `typeChecking`. Optional, defaults to true. Enable type checking through your application. This will slow down compilation, but show syntactic and semantic errors in webpack. Only available in `AotPlugin`. * `exclude`. Optional. Extra files to exclude from TypeScript compilation. +* `sourceMap`. Optional. Include sourcemaps. * `compilerOptions`. Optional. Override options in `tsconfig.json`. ## Features diff --git a/packages/@ngtools/webpack/src/angular_compiler_plugin.ts b/packages/@ngtools/webpack/src/angular_compiler_plugin.ts new file mode 100644 index 000000000000..d20908904b48 --- /dev/null +++ b/packages/@ngtools/webpack/src/angular_compiler_plugin.ts @@ -0,0 +1,668 @@ +// @ignoreDep @angular/compiler-cli +// @ignoreDep @angular/compiler-cli/ngtools2 +import * as fs from 'fs'; +import * as path from 'path'; +import * as ts from 'typescript'; + +const { __NGTOOLS_PRIVATE_API_2, VERSION } = require('@angular/compiler-cli'); +const ContextElementDependency = require('webpack/lib/dependencies/ContextElementDependency'); +const NodeWatchFileSystem = require('webpack/lib/node/NodeWatchFileSystem'); + +import { WebpackResourceLoader } from './resource_loader'; +import { WebpackCompilerHost } from './compiler_host'; +import { Tapable } from './webpack'; +import { PathsPlugin } from './paths-plugin'; +import { findLazyRoutes, LazyRouteMap } from './lazy_routes'; +import { VirtualFileSystemDecorator } from './virtual_file_system_decorator'; +import { resolveEntryModuleFromMain } from './entry_resolver'; +import { + TransformOperation, + makeTransform, + replaceBootstrap, + exportNgFactory, + exportLazyModuleMap +} from './transformers'; + +// These imports do not exist on Angular versions lower than 5. +// The commented imports are for types that we use but have to replace with `any` for now. +// Types replaced this way have a comment on them. +// @ignoreDep @angular/compiler-cli/src/transformers/api +let compilerCliNgtools: any = {}; +try { + compilerCliNgtools = require('@angular/compiler-cli/ngtools2'); +} catch (e) { + // Don't throw an error if the private API does not exist. + // Instead, the `isSupported` method should return false and indicate the plugin cannot be used. +} + +const { + // Program, + // CompilerHost, + createProgram, + createCompilerHost, + // Diagnostic, + formatDiagnostics, + EmitFlags, + // CustomTransformers, +} = compilerCliNgtools; + +/** + * Option Constants + */ +export interface AngularCompilerPluginOptions { + sourceMap?: boolean; + tsConfigPath: string; + basePath?: string; + entryModule?: string; + mainPath?: string; + typeChecking?: boolean; + hostOverrideFileSystem?: { [path: string]: string }; + hostReplacementPaths?: { [path: string]: string }; + i18nFile?: string; + i18nFormat?: string; + locale?: string; + missingTranslation?: string; + replaceExport?: boolean; + + // Use tsconfig to include path globs. + exclude?: string | string[]; + include?: string[]; + compilerOptions?: ts.CompilerOptions; +} + +enum PLATFORM { + Browser, + Server +} + +export class AngularCompilerPlugin implements Tapable { + private _options: AngularCompilerPluginOptions; + + // TS compilation. + private _compilerOptions: ts.CompilerOptions; + private _angularCompilerOptions: any; + private _tsFilenames: string[]; + // Should be Program from @angular/compiler-cli instead of any. + private _program: any; + private _compilerHost: WebpackCompilerHost; + // Should be CompilerHost from @angular/compiler-cli instead of any. + private _angularCompilerHost: WebpackCompilerHost & any; + // Contains `factoryModuleImportPath#factoryExportName` => `fullFactoryModulePath`. + private _lazyRoutes: LazyRouteMap = Object.create(null); + private _tsConfigPath: string; + private _entryModule: string; + private _basePath: string; + private _transformMap: Map = new Map(); + private _platform: PLATFORM; + + // Webpack plugin. + private _firstRun = true; + private _donePromise: Promise | null; + private _compiler: any = null; + private _compilation: any = null; + private _failedCompilation = false; + + constructor(options: AngularCompilerPluginOptions) { + this._options = Object.assign({}, options); + this._setupOptions(this._options); + } + + get options() { return this._options; } + get done() { return this._donePromise; } + get failedCompilation() { return this._failedCompilation; } + get entryModule() { + const splitted = this._entryModule.split('#'); + const path = splitted[0]; + const className = splitted[1] || 'default'; + return { path, className }; + } + + static isSupported() { + return parseInt(VERSION.major) >= 5; + } + + private _setupOptions(options: AngularCompilerPluginOptions) { + // Fill in the missing options. + if (!options.hasOwnProperty('tsConfigPath')) { + throw new Error('Must specify "tsConfigPath" in the configuration of @ngtools/webpack.'); + } + // TS represents paths internally with '/' and expects the tsconfig path to be in this format + this._tsConfigPath = options.tsConfigPath.replace(/\\/g, '/'); + + // Check the base path. + const maybeBasePath = path.resolve(process.cwd(), this._tsConfigPath); + let basePath = maybeBasePath; + if (fs.statSync(maybeBasePath).isFile()) { + basePath = path.dirname(basePath); + } + if (options.hasOwnProperty('basePath')) { + basePath = path.resolve(process.cwd(), options.basePath); + } + + this._basePath = basePath; + + // Read the tsconfig. + const configResult = ts.readConfigFile(this._tsConfigPath, ts.sys.readFile); + if (configResult.error) { + const diagnostic = configResult.error; + const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'); + + if (diagnostic.file) { + const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start); + throw new Error(`${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message})`); + } else { + throw new Error(message); + } + } + + const tsConfigJson = configResult.config; + + // Extend compilerOptions. + if (options.hasOwnProperty('compilerOptions')) { + tsConfigJson.compilerOptions = Object.assign({}, + tsConfigJson.compilerOptions, + options.compilerOptions + ); + } + + // Default exclude to **/*.spec.ts files. + if (!options.hasOwnProperty('exclude')) { + options['exclude'] = ['**/*.spec.ts']; + } + + // Add custom excludes to default TypeScript excludes. + if (options.hasOwnProperty('exclude')) { + // If the tsconfig doesn't contain any excludes, we must add the default ones before adding + // any extra ones (otherwise we'd include all of these which can cause unexpected errors). + // This is the same logic as present in TypeScript. + if (!tsConfigJson.exclude) { + tsConfigJson['exclude'] = ['node_modules', 'bower_components', 'jspm_packages']; + if (tsConfigJson.compilerOptions && tsConfigJson.compilerOptions.outDir) { + tsConfigJson.exclude.push(tsConfigJson.compilerOptions.outDir); + } + } + + // Join our custom excludes with the existing ones. + tsConfigJson.exclude = tsConfigJson.exclude.concat(options.exclude); + } + + // Parse the tsconfig contents. + const tsConfig = ts.parseJsonConfigFileContent( + tsConfigJson, ts.sys, basePath, undefined, this._tsConfigPath); + + this._tsFilenames = tsConfig.fileNames; + this._compilerOptions = tsConfig.options; + + // Overwrite outDir so we can find generated files next to their .ts origin in compilerHost. + this._compilerOptions.outDir = ''; + + // Default plugin sourceMap to compiler options setting. + if (!options.hasOwnProperty('sourceMap')) { + options.sourceMap = this._compilerOptions.sourceMap || false; + } + + // Force the right sourcemap options. + if (options.sourceMap) { + this._compilerOptions.sourceMap = true; + this._compilerOptions.inlineSources = true; + this._compilerOptions.inlineSourceMap = false; + this._compilerOptions.sourceRoot = basePath; + } else { + this._compilerOptions.sourceMap = false; + this._compilerOptions.sourceRoot = undefined; + this._compilerOptions.inlineSources = undefined; + this._compilerOptions.inlineSourceMap = undefined; + } + + // Compose Angular Compiler Options. + this._angularCompilerOptions = Object.assign( + this._compilerOptions, + tsConfig.raw['angularCompilerOptions'], + { basePath } + ); + + // Process i18n options. + if (options.hasOwnProperty('i18nFile')) { + this._angularCompilerOptions.i18nInFile = options.i18nFile; + } + if (options.hasOwnProperty('i18nFormat')) { + this._angularCompilerOptions.i18nInFormat = options.i18nFormat; + } + if (options.hasOwnProperty('locale')) { + this._angularCompilerOptions.i18nInLocale = options.locale; + } + if (options.hasOwnProperty('missingTranslation')) { + this._angularCompilerOptions.i18nInMissingTranslations = options.missingTranslation; + } + + // Use entryModule if available in options, otherwise resolve it from mainPath after program + // creation. + if (this._options.entryModule) { + this._entryModule = this._options.entryModule; + } else if (this._angularCompilerOptions.entryModule) { + this._entryModule = path.resolve(this._basePath, + this._angularCompilerOptions.entryModule); + } + + // Create the webpack compiler host. + this._compilerHost = new WebpackCompilerHost(this._compilerOptions, this._basePath); + this._compilerHost.enableCaching(); + + // Override some files in the FileSystem. + if (this._options.hostOverrideFileSystem) { + for (const filePath of Object.keys(this._options.hostOverrideFileSystem)) { + this._compilerHost.writeFile(filePath, + this._options.hostOverrideFileSystem[filePath], false); + } + } + // Override some files in the FileSystem with paths from the actual file system. + if (this._options.hostReplacementPaths) { + for (const filePath of Object.keys(this._options.hostReplacementPaths)) { + const replacementFilePath = this._options.hostReplacementPaths[filePath]; + const content = this._compilerHost.readFile(replacementFilePath); + this._compilerHost.writeFile(filePath, content, false); + } + } + + // TODO: consider really using platform names in the plugin options. + this._platform = options.replaceExport ? PLATFORM.Server : PLATFORM.Browser; + } + + private _findLazyRoutesInAst(changedFilePaths: string[]): LazyRouteMap { + const result: LazyRouteMap = Object.create(null); + 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; + } + } + return result; + } + + private _getLazyRoutesFromNgtools() { + try { + return __NGTOOLS_PRIVATE_API_2.listLazyRoutes({ + program: this._program.getTsProgram(), + host: this._compilerHost, + angularCompilerOptions: Object.assign({}, this._angularCompilerOptions, { + // genDir seems to still be needed in @angular\compiler-cli\src\compiler_host.js:226. + genDir: '' + }), + entryModule: this._entryModule + }); + } catch (err) { + // We silence the error that the @angular/router could not be found. In that case, there is + // basically no route supported by the app itself. + if (err.message.startsWith('Could not resolve module @angular/router')) { + return {}; + } else { + throw err; + } + } + } + + // Process the lazy routes discovered, adding and removing them from _lazyRoutes. + // TODO: ensure these are correct. + private _processLazyRoutes(discoveredLazyRoutes: { [route: string]: string; }) { + Object.keys(discoveredLazyRoutes) + .forEach(lazyRouteKey => { + const [lazyRouteModule, moduleName] = lazyRouteKey.split('#'); + + if (!lazyRouteModule || !moduleName) { + return; + } + + const factoryPath = discoveredLazyRoutes[lazyRouteKey] + .replace(/(\.d)?\.ts$/, '.ngfactory.js'); + const factoryKey = `${lazyRouteModule}.ngfactory#${moduleName}NgFactory`; + + if (factoryKey in this._lazyRoutes) { + if (factoryPath === null) { + // This lazy route does not exist anymore, remove it from the list. + this._lazyRoutes[lazyRouteKey] = null; + } else if (this._lazyRoutes[factoryKey] !== factoryPath) { + // Found a duplicate, this is an error. + this._compilation.warnings.push( + new Error(`Duplicated path in loadChildren detected during a rebuild. ` + + `We will take the latest version detected and override it to save rebuild time. ` + + `You should perform a full build to validate that your routes don't overlap.`) + ); + } + } else { + // Found a new route, add it to the map and read it into the compiler host. + this._lazyRoutes[factoryKey] = factoryPath; + this._angularCompilerHost.readFile(lazyRouteModule); + this._angularCompilerHost.invalidate(lazyRouteModule); + } + }); + } + + + // Registration hook for webpack plugin. + apply(compiler: any) { + this._compiler = compiler; + + // Decorate inputFileSystem to serve contents of CompilerHost. + // Use decorated inputFileSystem in watchFileSystem. + compiler.plugin('environment', () => { + compiler.inputFileSystem = new VirtualFileSystemDecorator( + compiler.inputFileSystem, this._compilerHost); + compiler.watchFileSystem = new NodeWatchFileSystem(compiler.inputFileSystem); + }); + + // Add lazy modules to the context module for @angular/core + compiler.plugin('context-module-factory', (cmf: any) => { + const angularCorePackagePath = require.resolve('@angular/core/package.json'); + const angularCorePackageJson = require(angularCorePackagePath); + const angularCoreModulePath = path.resolve(path.dirname(angularCorePackagePath), + angularCorePackageJson['module']); + // Pick the last part after the last node_modules instance. We do this to let people have + // a linked @angular/core or cli which would not be under the same path as the project + // being built. + const angularCoreModuleDir = path.dirname(angularCoreModulePath).split(/node_modules/).pop(); + + cmf.plugin('after-resolve', (result: any, callback: (err?: any, request?: any) => void) => { + if (!result) { + return callback(); + } + + // Alter only request from Angular. + if (angularCoreModuleDir && !result.resource.endsWith(angularCoreModuleDir)) { + return callback(null, result); + } + + 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'); + result.dependencies.forEach((d: any) => d.critical = false); + result.resolveDependencies = (_fs: any, _resource: any, _recursive: any, + _regExp: RegExp, cb: any) => { + const dependencies = Object.keys(this._lazyRoutes) + .map((key) => { + const modulePath = this._lazyRoutes[key]; + const importPath = key.split('#')[0]; + if (modulePath !== null) { + return new ContextElementDependency(modulePath, importPath); + } else { + return null; + } + }) + .filter(x => !!x); + cb(null, dependencies); + }; + return callback(null, result); + }, () => callback(null)) + .catch(err => callback(err)); + }); + }); + + // Remake the plugin on each compilation. + compiler.plugin('make', (compilation: any, cb: any) => this._make(compilation, cb)); + compiler.plugin('invalid', () => this._firstRun = false); + compiler.plugin('after-emit', (compilation: any, cb: any) => { + compilation._ngToolsWebpackPluginInstance = null; + cb(); + }); + compiler.plugin('done', () => { + this._donePromise = null; + this._compilation = null; + this._failedCompilation = false; + }); + + // TODO: consider if it's better to remove this plugin and instead make it wait on the + // VirtualFileSystemDecorator. + compiler.plugin('after-resolvers', (compiler: any) => { + // Virtual file system. + // Wait for the plugin to be done when requesting `.ts` files directly (entry points), or + // when the issuer is a `.ts` or `.ngfactory.js` file. + compiler.resolvers.normal.plugin('before-resolve', (request: any, cb: () => void) => { + if (request.request.endsWith('.ts') + || (request.context.issuer && /\.ts|ngfactory\.js$/.test(request.context.issuer))) { + this.done!.then(() => cb(), () => cb()); + } else { + cb(); + } + }); + }); + + compiler.plugin('normal-module-factory', (nmf: any) => { + compiler.resolvers.normal.apply(new PathsPlugin({ + nmf, + tsConfigPath: this._tsConfigPath, + compilerOptions: this._compilerOptions, + compilerHost: this._compilerHost + })); + }); + } + + private _make(compilation: any, cb: (err?: any, request?: any) => void) { + this._compilation = compilation; + if (this._compilation._ngToolsWebpackPluginInstance) { + return cb(new Error('An @ngtools/webpack plugin already exist for this compilation.')); + } + + this._compilation._ngToolsWebpackPluginInstance = this; + + // Create the resource loader with the webpack compilation. + const resourceLoader = new WebpackResourceLoader(compilation); + this._compilerHost.setResourceLoader(resourceLoader); + + this._donePromise = Promise.resolve() + .then(() => { + if (this._firstRun) { + // Use the WebpackResourceLoaderwith a resource loader to create an AngularCompilerHost. + this._angularCompilerHost = createCompilerHost({ + options: this._angularCompilerOptions, + tsHost: this._compilerHost + // Should be CompilerHost from @angular/compiler-cli instead of any. + }) as any & WebpackCompilerHost; + + // Create the Angular program. + this._program = createProgram({ + rootNames: this._tsFilenames, + options: this._compilerOptions, + host: this._angularCompilerHost + }); + + return this._program.loadNgStructureAsync() + .then(() => { + // If there's still no entryModule try to resolve from mainPath. + if (!this._entryModule && this._options.mainPath) { + const mainPath = path.resolve(this._basePath, this._options.mainPath); + this._entryModule = resolveEntryModuleFromMain( + mainPath, this._compilerHost, this._program.getTsProgram()); + } + }); + } + }) + .then(() => this._update()) + .then(() => { + cb(); + }, (err: any) => { + this._failedCompilation = true; + compilation.errors.push(err.stack); + cb(); + }); + } + + private _update() { + let changedFiles: string[] = []; + + return Promise.resolve() + .then(() => { + // Try to find lazy routes. + // 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. + const changedTsFiles = this._compilerHost.getChangedFilePaths() + .filter(k => k.endsWith('.ts')); + if (this._firstRun) { + this._processLazyRoutes(this._getLazyRoutesFromNgtools()); + } else if (changedTsFiles.length > 0) { + this._processLazyRoutes(this._findLazyRoutesInAst(changedTsFiles)); + } + }) + .then(() => { + // We only want to update on TS and template changes, but all kinds of files are on this + // list, like package.json and .ngsummary.json files. + changedFiles = this._compilerHost.getChangedFilePaths() + .filter(k => /(ts|html|css|scss|sass|less|styl)/.test(k)); + }) + .then(() => { + // Make a new program and load the Angular structure if there are changes. + if (changedFiles.length > 0) { + this._tsFilenames = this._tsFilenames.concat(changedFiles) + .filter(k => k.endsWith('.ts')) + .filter(k => this._compilerHost.fileExists(k)); + + this._program = createProgram({ + rootNames: this._tsFilenames, + options: this._angularCompilerOptions, + host: this._angularCompilerHost, + oldProgram: this._program + }); + + return this._program.loadNgStructureAsync(); + } + }) + .then(() => { + // Build transforms, emit and report errors if there are changes or it's the first run. + if (changedFiles.length > 0 || this._firstRun) { + + // Go through each changed file and add transforms as needed. + const changedTsFiles = this._compilerHost.getChangedFilePaths() + .filter(k => k.endsWith('.ts')); + + changedTsFiles.forEach((fileName) => { + const sourceFile = this._program.getTsProgram().getSourceFile(fileName); + let transformOps; + if (this._platform === PLATFORM.Browser) { + transformOps = replaceBootstrap(sourceFile, this.entryModule); + } else if (this._platform === PLATFORM.Server) { + // export_module_map + transformOps = [ + ...exportNgFactory(sourceFile, this.entryModule), + ...exportLazyModuleMap(sourceFile, this._lazyRoutes) + ]; + } + + // We need to keep a map of transforms for each file, to reapply on each update. + this._transformMap.set(fileName, transformOps); + }); + + const transformOps: TransformOperation[] = []; + for (let fileTransformOps of this._transformMap.values()) { + transformOps.push(...fileTransformOps); + } + + // Should be CustomTransformers from @angular/compiler-cli instead of any. + const transformers: any = { + beforeTs: transformOps.length > 0 ? [makeTransform(transformOps)] : [] + }; + + // Emit files. + const { program, emitResult, diagnostics } = this._emit(this._program, transformers); + this._program = program; + + // Report diagnostics. + // TODO: check if the old _translateSourceMap function is needed. + const errors = diagnostics + .filter((diag) => diag.category === ts.DiagnosticCategory.Error); + const warnings = diagnostics + .filter((diag) => diag.category === ts.DiagnosticCategory.Warning); + + if (errors.length > 0) { + const message = formatDiagnostics(this._angularCompilerOptions, errors); + this._compilation.errors.push(message); + } + + if (warnings.length > 0) { + const message = formatDiagnostics(this._angularCompilerOptions, warnings); + this._compilation.warnings.push(message); + } + + // Reset changed files on successful compilation. + if (emitResult && !emitResult.emitSkipped && this._compilation.errors.length === 0) { + this._compilerHost.resetChangedFileTracker(); + } else { + this._failedCompilation = true; + } + } + }); + } + + + getFile(fileName: string) { + const outputFile = fileName.replace(/.ts$/, '.js'); + return { + outputText: this._compilerHost.readFile(outputFile), + sourceMap: this._compilerHost.readFile(outputFile + '.map') + }; + } + + // This code mostly comes from `performCompilation` in `@angular/compiler-cli`. + // It skips the program creation because we need to use `loadNgStructureAsync()`, + // and uses CustomTransformers. + // Should be Program and CustomTransformers from @angular/compiler-cli instead of any. + private _emit(program: any, customTransformers: any) { + // Should be Diagnostic from @angular/compiler-cli instead of any. + type Diagnostics = Array; + const allDiagnostics: Diagnostics = []; + + function checkDiagnostics(diags: Diagnostics | undefined) { + if (diags) { + allDiagnostics.push(...diags); + return diags.every(d => d.category !== ts.DiagnosticCategory.Error); + } + return true; + } + + let emitResult: ts.EmitResult | undefined; + try { + let shouldEmit = true; + // Check parameter diagnostics + shouldEmit = shouldEmit && checkDiagnostics([ + ...program.getTsOptionDiagnostics(), ...program.getNgOptionDiagnostics() + ]); + + // Check syntactic diagnostics + shouldEmit = shouldEmit && checkDiagnostics(program.getTsSyntacticDiagnostics()); + + // Check TypeScript semantic and Angular structure diagnostics + shouldEmit = shouldEmit && + checkDiagnostics( + [...program.getTsSemanticDiagnostics(), ...program.getNgStructuralDiagnostics()]); + + // Check Angular semantic diagnostics + shouldEmit = shouldEmit && checkDiagnostics(program.getNgSemanticDiagnostics()); + + if (shouldEmit) { + emitResult = program.emit({ emitFlags: EmitFlags.Default, customTransformers }); + allDiagnostics.push(...emitResult.diagnostics); + } + } catch (e) { + let errMsg: string; + + // This function is available in the import below, but this way we avoid the dependency. + // import { isSyntaxError } from '@angular/compiler'; + function isSyntaxError(error: Error): boolean { + return (error as any)['ngSyntaxError']; + } + + if (isSyntaxError(e)) { + // don't report the stack for syntax errors as they are well known errors. + errMsg = e.message; + } else { + errMsg = e.stack; + } + allDiagnostics.push({ + category: ts.DiagnosticCategory.Error, + message: errMsg, + }); + } + return { program, emitResult, diagnostics: allDiagnostics }; + } +} diff --git a/packages/@ngtools/webpack/src/compiler_host.ts b/packages/@ngtools/webpack/src/compiler_host.ts index e8f49d645bce..cbd90aad3686 100644 --- a/packages/@ngtools/webpack/src/compiler_host.ts +++ b/packages/@ngtools/webpack/src/compiler_host.ts @@ -1,6 +1,7 @@ import * as ts from 'typescript'; import {basename, dirname, join} from 'path'; import * as fs from 'fs'; +import {WebpackResourceLoader} from './resource_loader'; export interface OnErrorFn { @@ -103,6 +104,7 @@ export class WebpackCompilerHost implements ts.CompilerHost { private _setParentNodes: boolean; private _cache = false; + private _resourceLoader?: WebpackResourceLoader | undefined; constructor(private _options: ts.CompilerOptions, basePath: string) { this._setParentNodes = true; @@ -150,6 +152,7 @@ export class WebpackCompilerHost implements ts.CompilerHost { this._changedFiles = Object.create(null); this._changedDirs = Object.create(null); } + getChangedFilePaths(): string[] { return Object.keys(this._changedFiles); } @@ -267,4 +270,18 @@ export class WebpackCompilerHost implements ts.CompilerHost { getNewLine(): string { return this._delegate.getNewLine(); } + + setResourceLoader(resourceLoader: WebpackResourceLoader) { + this._resourceLoader = resourceLoader; + } + + readResource(fileName: string) { + if (this._resourceLoader) { + // We still read it to add it to the compiler host file list. + this.readFile(fileName); + return this._resourceLoader.get(fileName); + } else { + return this.readFile(fileName); + } + } } diff --git a/packages/@ngtools/webpack/src/index.ts b/packages/@ngtools/webpack/src/index.ts index b4f6bc8d5497..e80fc1d2ca58 100644 --- a/packages/@ngtools/webpack/src/index.ts +++ b/packages/@ngtools/webpack/src/index.ts @@ -28,6 +28,7 @@ if (!( version.major == '5' } export * from './plugin'; +export * from './angular_compiler_plugin'; export * from './extract_i18n_plugin'; export {ngcLoader as default} from './loader'; export {PathsPlugin} from './paths-plugin'; diff --git a/packages/@ngtools/webpack/src/lazy_routes.ts b/packages/@ngtools/webpack/src/lazy_routes.ts index 9f42aaf2bec9..74491fd7e6be 100644 --- a/packages/@ngtools/webpack/src/lazy_routes.ts +++ b/packages/@ngtools/webpack/src/lazy_routes.ts @@ -21,8 +21,9 @@ export interface LazyRouteMap { export function findLazyRoutes(filePath: string, - program: ts.Program, - host: ts.CompilerHost): LazyRouteMap { + host: ts.CompilerHost, + program?: ts.Program, + compilerOptions?: ts.CompilerOptions): LazyRouteMap { const refactor = new TypeScriptFileRefactor(filePath, host, program); return refactor @@ -46,11 +47,12 @@ export function findLazyRoutes(filePath: string, // 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 any) - : ts.resolveModuleName(moduleName, filePath, program.getCompilerOptions(), host); + : ts.resolveModuleName(moduleName, filePath, compOptions, host); if (resolvedModuleName.resolvedModule && resolvedModuleName.resolvedModule.resolvedFileName && host.fileExists(resolvedModuleName.resolvedModule.resolvedFileName)) { diff --git a/packages/@ngtools/webpack/src/loader.ts b/packages/@ngtools/webpack/src/loader.ts index 143eade4c435..da67fbfd555e 100644 --- a/packages/@ngtools/webpack/src/loader.ts +++ b/packages/@ngtools/webpack/src/loader.ts @@ -1,6 +1,7 @@ import * as path from 'path'; import * as ts from 'typescript'; import {AotPlugin} from './plugin'; +import {AngularCompilerPlugin} from './angular_compiler_plugin'; import {TypeScriptFileRefactor} from './refactor'; import {LoaderContext, ModuleReason} from './webpack'; @@ -223,6 +224,9 @@ function _replacePlatform( } +// TODO: remove platform server bootstrap replacement. +// It doesn't seem to be used anymore according to tests/e2e/tests/build/platform-server.ts and +// https://github.com/angular/angular-cli/wiki/stories-universal-rendering. function _replaceBootstrapOrRender(refactor: TypeScriptFileRefactor, call: ts.CallExpression) { // If neither bootstrapModule or renderModule can't be found, bail out early. let replacementTarget: string | undefined; @@ -270,7 +274,7 @@ function _replaceEntryModule(plugin: AotPlugin, refactor: TypeScriptFileRefactor .filter(identifier => identifier.parent && (identifier.parent.kind === ts.SyntaxKind.CallExpression || - identifier.parent.kind === ts.SyntaxKind.PropertyAssignment)) + identifier.parent.kind === ts.SyntaxKind.PropertyAssignment)) .filter(node => !!_getCaller(node)); if (modules.length == 0) { @@ -319,13 +323,13 @@ function _removeModuleId(refactor: TypeScriptFileRefactor) { .filter((node: ts.ObjectLiteralExpression) => { return node.properties.some(prop => { return prop.kind == ts.SyntaxKind.PropertyAssignment - && _getContentOfKeyLiteral(sourceFile, prop.name) == 'moduleId'; + && _getContentOfKeyLiteral(sourceFile, prop.name) == 'moduleId'; }); }) .forEach((node: ts.ObjectLiteralExpression) => { const moduleIdProp = node.properties.filter((prop: ts.ObjectLiteralElement, _idx: number) => { return prop.kind == ts.SyntaxKind.PropertyAssignment - && _getContentOfKeyLiteral(sourceFile, prop.name) == 'moduleId'; + && _getContentOfKeyLiteral(sourceFile, prop.name) == 'moduleId'; })[0]; // Get the trailing comma. const moduleIdCommaProp = moduleIdProp.parent @@ -377,7 +381,7 @@ function _getResourceNodes(refactor: TypeScriptFileRefactor) { // Find all object literals. return refactor.findAstNodes(sourceFile, ts.SyntaxKind.ObjectLiteralExpression, true) - // Get all their property assignments. + // Get all their property assignments. .map(node => refactor.findAstNodes(node, ts.SyntaxKind.PropertyAssignment)) // Flatten into a single array (from an array of array). .reduce((prev, curr) => curr ? prev.concat(curr) : prev, []) @@ -520,98 +524,113 @@ export function ngcLoader(this: LoaderContext & { _compilation: any }, source: s const cb = this.async(); const sourceFileName: string = this.resourcePath; - const plugin = this._compilation._ngToolsWebpackPluginInstance as AotPlugin; + const plugin = this._compilation._ngToolsWebpackPluginInstance; if (plugin) { - // We must verify that AotPlugin is an instance of the right class. + // We must verify that the plugin is an instance of the right class. // Throw an error if it isn't, that often means multiple @ngtools/webpack installs. - if (!(plugin instanceof AotPlugin)) { - throw new Error('AotPlugin was detected but it was an instance of the wrong class.\n' + if (!(plugin instanceof AotPlugin) && !(plugin instanceof AngularCompilerPlugin)) { + throw new Error('Angular Compiler was detected but it was an instance of the wrong class.\n' + 'This likely means you have several @ngtools/webpack packages installed. ' + 'You can check this with `npm ls @ngtools/webpack`, and then remove the extra copies.' ); } - if (plugin.compilerHost.readFile(sourceFileName) == source) { - // In the case where the source is the same as the one in compilerHost, we don't have - // extra TS loaders and there's no need to do any trickery. - source = null; - } - const refactor = new TypeScriptFileRefactor( - sourceFileName, plugin.compilerHost, plugin.program, source); - - Promise.resolve() - .then(() => { - if (!plugin.skipCodeGeneration) { - return Promise.resolve() - .then(() => _removeDecorators(refactor)) - .then(() => _refactorBootstrap(plugin, refactor)) - .then(() => _replaceExport(plugin, refactor)) - .then(() => _exportModuleMap(plugin, refactor)); - } else { - return Promise.resolve() - .then(() => _replaceResources(refactor)) - .then(() => _removeModuleId(refactor)) - .then(() => _exportModuleMap(plugin, refactor)); - } - }) - .then(() => { - if (plugin.typeCheck) { - // Check all diagnostics from this and reverse dependencies also. - if (!plugin.firstRun) { - _diagnoseDeps(this._module.reasons, plugin, new Set()); + if (plugin instanceof AngularCompilerPlugin) { + plugin.done + .then(() => { + const result = plugin.getFile(sourceFileName); + if (plugin.failedCompilation) { + // Return an empty string if there is no result to prevent extra loader errors. + // Plugin errors were already pushed to the compilation errors. + cb(null, result.outputText || '', result.sourceMap); + } else { + cb(null, result.outputText, result.sourceMap); } - // We do this here because it will throw on error, resulting in rebuilding this file - // the next time around if it changes. - plugin.diagnose(sourceFileName); - } - }) - .then(() => { - // Add resources as dependencies. - _getResourcesUrls(refactor).forEach((url: string) => { - this.addDependency(path.resolve(path.dirname(sourceFileName), url)); - }); - }) - .then(() => { - if (source) { - // We need to validate diagnostics. We ignore type checking though, to save time. - const diagnostics = refactor.getDiagnostics(false); - if (diagnostics.length) { - let message = ''; - - diagnostics.forEach(diagnostic => { - const messageText = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'); - - if (diagnostic.file) { - const position = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start!); - const fileName = diagnostic.file.fileName; - const {line, character} = position; - message += `${fileName} (${line + 1},${character + 1}): ${messageText}\n`; - } else { - message += `${messageText}\n`; - } - }); - throw new Error(message); + }) + .catch(err => cb(err)); + } else if (plugin instanceof AotPlugin) { + if (plugin.compilerHost.readFile(sourceFileName) == source) { + // In the case where the source is the same as the one in compilerHost, we don't have + // extra TS loaders and there's no need to do any trickery. + source = null; + } + const refactor = new TypeScriptFileRefactor( + sourceFileName, plugin.compilerHost, plugin.program, source); + + Promise.resolve() + .then(() => { + if (!plugin.skipCodeGeneration) { + return Promise.resolve() + .then(() => _removeDecorators(refactor)) + .then(() => _refactorBootstrap(plugin, refactor)) + .then(() => _replaceExport(plugin, refactor)) + .then(() => _exportModuleMap(plugin, refactor)); + } else { + return Promise.resolve() + .then(() => _replaceResources(refactor)) + .then(() => _removeModuleId(refactor)) + .then(() => _exportModuleMap(plugin, refactor)); + } + }) + .then(() => { + if (plugin.typeCheck) { + // Check all diagnostics from this and reverse dependencies also. + if (!plugin.firstRun) { + _diagnoseDeps(this._module.reasons, plugin, new Set()); + } + // We do this here because it will throw on error, resulting in rebuilding this file + // the next time around if it changes. + plugin.diagnose(sourceFileName); + } + }) + .then(() => { + // Add resources as dependencies. + _getResourcesUrls(refactor).forEach((url: string) => { + this.addDependency(path.resolve(path.dirname(sourceFileName), url)); + }); + }) + .then(() => { + if (source) { + // We need to validate diagnostics. We ignore type checking though, to save time. + const diagnostics = refactor.getDiagnostics(false); + if (diagnostics.length) { + let message = ''; + + diagnostics.forEach(diagnostic => { + const messageText = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'); + + if (diagnostic.file) { + const position = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start!); + const fileName = diagnostic.file.fileName; + const { line, character } = position; + message += `${fileName} (${line + 1},${character + 1}): ${messageText}\n`; + } else { + message += `${messageText}\n`; + } + }); + throw new Error(message); + } } - } - // Force a few compiler options to make sure we get the result we want. - const compilerOptions: ts.CompilerOptions = Object.assign({}, plugin.compilerOptions, { - inlineSources: true, - inlineSourceMap: false, - sourceRoot: plugin.basePath - }); + // Force a few compiler options to make sure we get the result we want. + const compilerOptions: ts.CompilerOptions = Object.assign({}, plugin.compilerOptions, { + inlineSources: true, + inlineSourceMap: false, + sourceRoot: plugin.basePath + }); - const result = refactor.transpile(compilerOptions); + const result = refactor.transpile(compilerOptions); - if (plugin.failedCompilation) { - // Return an empty string to prevent extra loader errors (missing imports etc). - // Plugin errors were already pushed to the compilation errors. - cb(null, ''); - } else { - cb(null, result.outputText, result.sourceMap); - } - }) - .catch(err => cb(err)); + if (plugin.failedCompilation) { + // Return an empty string to prevent extra loader errors (missing imports etc). + // Plugin errors were already pushed to the compilation errors. + cb(null, ''); + } else { + cb(null, result.outputText, result.sourceMap); + } + }) + .catch(err => cb(err)); + } } else { const options = loaderUtils.getOptions(this) || {}; const tsConfigPath = options.tsConfigPath; diff --git a/packages/@ngtools/webpack/src/plugin.ts b/packages/@ngtools/webpack/src/plugin.ts index 5fdf11462c02..77aa1f7b32b0 100644 --- a/packages/@ngtools/webpack/src/plugin.ts +++ b/packages/@ngtools/webpack/src/plugin.ts @@ -21,6 +21,7 @@ import {VirtualFileSystemDecorator} from './virtual_file_system_decorator'; * Option Constants */ export interface AotPluginOptions { + sourceMap?: boolean; tsConfigPath: string; basePath?: string; entryModule?: string; @@ -181,6 +182,26 @@ export class AotPlugin implements Tapable { let genDir = path.join(basePath, '$$_gendir'); this._compilerOptions = tsConfig.options; + + // Default plugin sourceMap to compiler options setting. + if (!options.hasOwnProperty('sourceMap')) { + options.sourceMap = this._compilerOptions.sourceMap || false; + } + + // Force the right sourcemap options. + if (options.sourceMap) { + this._compilerOptions.sourceMap = true; + this._compilerOptions.inlineSources = true; + this._compilerOptions.inlineSourceMap = false; + this._compilerOptions.sourceRoot = basePath; + } else { + this._compilerOptions.sourceMap = false; + this._compilerOptions.sourceRoot = undefined; + this._compilerOptions.inlineSources = undefined; + this._compilerOptions.inlineSourceMap = undefined; + } + + // Compose Angular Compiler Options. this._angularCompilerOptions = Object.assign( { genDir }, this._compilerOptions, @@ -268,7 +289,7 @@ export class AotPlugin implements Tapable { const result: LazyRouteMap = Object.create(null); const changedFilePaths = this._compilerHost.getChangedFilePaths(); for (const filePath of changedFilePaths) { - const fileLazyRoutes = findLazyRoutes(filePath, this._program, this._compilerHost); + const fileLazyRoutes = findLazyRoutes(filePath, this._compilerHost, this._program); for (const routeKey of Object.keys(fileLazyRoutes)) { const route = fileLazyRoutes[routeKey]; if (routeKey in this._lazyRoutes) { diff --git a/packages/@ngtools/webpack/src/refactor.ts b/packages/@ngtools/webpack/src/refactor.ts index 57fc1449fb21..98e816e7e0df 100644 --- a/packages/@ngtools/webpack/src/refactor.ts +++ b/packages/@ngtools/webpack/src/refactor.ts @@ -2,6 +2,7 @@ import * as path from 'path'; import * as ts from 'typescript'; import {SourceMapConsumer, SourceMapGenerator} from 'source-map'; +import { findAstNodes } from './transformers'; const MagicString = require('magic-string'); @@ -88,40 +89,7 @@ export class TypeScriptFileRefactor { kind: ts.SyntaxKind, recursive = false, max = Infinity): ts.Node[] { - if (max == 0) { - return []; - } - if (!node) { - node = this._sourceFile; - } - - let arr: ts.Node[] = []; - if (node.kind === kind) { - // If we're not recursively looking for children, stop here. - if (!recursive) { - return [node]; - } - - arr.push(node); - max--; - } - - if (max > 0) { - for (const child of node.getChildren(this._sourceFile)) { - this.findAstNodes(child, kind, recursive, max) - .forEach((node: ts.Node) => { - if (max > 0) { - arr.push(node); - } - max--; - }); - - if (max <= 0) { - break; - } - } - } - return arr; + return findAstNodes(node, this._sourceFile, kind, recursive, max); } findFirstAstNode(node: ts.Node | null, kind: ts.SyntaxKind): ts.Node | null { diff --git a/packages/@ngtools/webpack/src/transformers/ast_helpers.ts b/packages/@ngtools/webpack/src/transformers/ast_helpers.ts new file mode 100644 index 000000000000..ca56633aa1f3 --- /dev/null +++ b/packages/@ngtools/webpack/src/transformers/ast_helpers.ts @@ -0,0 +1,72 @@ +import * as ts from 'typescript'; + + +/** + * Find all nodes from the AST in the subtree of node of SyntaxKind kind. + * @param node The root node to check, or null if the whole tree should be searched. + * @param sourceFile The source file where the node is. + * @param kind The kind of nodes to find. + * @param recursive Whether to go in matched nodes to keep matching. + * @param max The maximum number of items to return. + * @return all nodes of kind, or [] if none is found + */ +export function findAstNodes( + node: ts.Node | null, + sourceFile: ts.SourceFile, + kind: ts.SyntaxKind, + recursive = false, + max = Infinity +): T[] { + // TODO: refactor operations that only need `refactor.findAstNodes()` to use this instead. + if (max == 0) { + return []; + } + if (!node) { + node = sourceFile; + } + + let arr: T[] = []; + if (node.kind === kind) { + // If we're not recursively looking for children, stop here. + if (!recursive) { + return [node as T]; + } + + arr.push(node as T); + max--; + } + + if (max > 0) { + for (const child of node.getChildren(sourceFile)) { + findAstNodes(child, sourceFile, kind, recursive, max) + .forEach((node: ts.Node) => { + if (max > 0) { + arr.push(node as T); + } + max--; + }); + + if (max <= 0) { + break; + } + } + } + return arr; +} + +export function getFirstNode(sourceFile: ts.SourceFile): ts.Node | null { + const syntaxList = findAstNodes(null, sourceFile, ts.SyntaxKind.SyntaxList, false, 1)[0] || null; + if (syntaxList) { + return (syntaxList && syntaxList.getChildCount() > 0) ? syntaxList.getChildAt(0) : null; + } + return null; +} + +export function getLastNode(sourceFile: ts.SourceFile): ts.Node | null { + const syntaxList = findAstNodes(null, sourceFile, ts.SyntaxKind.SyntaxList, false, 1)[0] || null; + if (syntaxList) { + const childCount = syntaxList.getChildCount(); + return childCount > 0 ? syntaxList.getChildAt(childCount - 1) : null; + } + return null; +} diff --git a/packages/@ngtools/webpack/src/transformers/export_lazy_module_map.ts b/packages/@ngtools/webpack/src/transformers/export_lazy_module_map.ts new file mode 100644 index 000000000000..cd5147e9be33 --- /dev/null +++ b/packages/@ngtools/webpack/src/transformers/export_lazy_module_map.ts @@ -0,0 +1,65 @@ +import * as path from 'path'; +import * as ts from 'typescript'; + +import { LazyRouteMap } from '../lazy_routes'; +import { getLastNode, getFirstNode } from './ast_helpers'; +import { + AddNodeOperation, + TransformOperation +} from './make_transform'; + +export function exportLazyModuleMap( + sourceFile: ts.SourceFile, + lazyRoutes: LazyRouteMap +): TransformOperation[] { + const ops: TransformOperation[] = []; + const dirName = path.normalize(path.dirname(sourceFile.fileName)); + + const modules = Object.keys(lazyRoutes) + .map((loadChildrenString) => { + const [, moduleName] = loadChildrenString.split('#'); + const modulePath = lazyRoutes[loadChildrenString]; + + return { + modulePath, + moduleName, + loadChildrenString + }; + }); + + modules.forEach((module, index) => { + const relativePath = path.relative(dirName, module.modulePath!).replace(/\\/g, '/'); + // Create the new namespace import node. + const namespaceImport = ts.createNamespaceImport(ts.createIdentifier(`__lazy_${index}__`)); + const importClause = ts.createImportClause(undefined, namespaceImport); + const newImport = ts.createImportDeclaration(undefined, undefined, importClause, + ts.createLiteral(relativePath)); + + ops.push(new AddNodeOperation( + sourceFile, + getFirstNode(sourceFile), + newImport + )); + }); + + const lazyModuleObjectLiteral = ts.createObjectLiteral( + modules.map((mod, idx) => ts.createPropertyAssignment( + ts.createLiteral(mod.loadChildrenString), + ts.createPropertyAccess(ts.createIdentifier(`__lazy_${idx}__`), mod.moduleName)) + ) + ); + + const lazyModuleVariableStmt = ts.createVariableStatement( + [ts.createToken(ts.SyntaxKind.ExportKeyword)], + [ts.createVariableDeclaration('LAZY_MODULE_MAP', undefined, lazyModuleObjectLiteral)] + ); + + ops.push(new AddNodeOperation( + sourceFile, + getLastNode(sourceFile), + undefined, + lazyModuleVariableStmt + )); + + return ops; +} diff --git a/packages/@ngtools/webpack/src/transformers/export_ngfactory.ts b/packages/@ngtools/webpack/src/transformers/export_ngfactory.ts new file mode 100644 index 000000000000..cb045fd58aae --- /dev/null +++ b/packages/@ngtools/webpack/src/transformers/export_ngfactory.ts @@ -0,0 +1,54 @@ +import * as ts from 'typescript'; + +import { findAstNodes, getFirstNode } from './ast_helpers'; +import { TransformOperation, AddNodeOperation } from './make_transform'; + +export function exportNgFactory( + sourceFile: ts.SourceFile, + entryModule: { path: string, className: string } +): TransformOperation[] { + const ops: TransformOperation[] = []; + + // Find all identifiers using the entry module class name. + const entryModuleIdentifiers = findAstNodes(null, sourceFile, + ts.SyntaxKind.Identifier, true) + .filter(identifier => identifier.getText() === entryModule.className); + + if (entryModuleIdentifiers.length === 0) { + return []; + } + + // Get the module path from the import. + let modulePath: string; + entryModuleIdentifiers.forEach((entryModuleIdentifier) => { + if (entryModuleIdentifier.parent.kind !== ts.SyntaxKind.ExportSpecifier) { + return; + } + + const exportSpec = entryModuleIdentifier.parent as ts.ExportSpecifier; + const moduleSpecifier = exportSpec.parent.parent.moduleSpecifier; + + if (moduleSpecifier.kind !== ts.SyntaxKind.StringLiteral) { + return; + } + + modulePath = (moduleSpecifier as ts.StringLiteral).text; + + // Add the transform operations. + const factoryClassName = entryModule.className + 'NgFactory'; + const factoryModulePath = modulePath + '.ngfactory'; + + const namedExports = ts.createNamedExports([ts.createExportSpecifier(undefined, + ts.createIdentifier(factoryClassName))]); + const newImport = ts.createExportDeclaration(undefined, undefined, namedExports, + ts.createLiteral(factoryModulePath)); + + ops.push(new AddNodeOperation( + sourceFile, + getFirstNode(sourceFile), + newImport + )); + }); + + return ops; +} diff --git a/packages/@ngtools/webpack/src/transformers/index.ts b/packages/@ngtools/webpack/src/transformers/index.ts new file mode 100644 index 000000000000..317b4328c454 --- /dev/null +++ b/packages/@ngtools/webpack/src/transformers/index.ts @@ -0,0 +1,7 @@ +export * from './ast_helpers'; +export * from './make_transform'; +export * from './insert_import'; +export * from './remove_import'; +export * from './replace_bootstrap'; +export * from './export_ngfactory'; +export * from './export_lazy_module_map'; diff --git a/packages/@ngtools/webpack/src/transformers/insert_import.ts b/packages/@ngtools/webpack/src/transformers/insert_import.ts new file mode 100644 index 000000000000..fa5ae08b0c26 --- /dev/null +++ b/packages/@ngtools/webpack/src/transformers/insert_import.ts @@ -0,0 +1,79 @@ +import * as ts from 'typescript'; + +import { findAstNodes, getFirstNode } from './ast_helpers'; +import { AddNodeOperation, TransformOperation } from './make_transform'; + + +export function insertImport( + sourceFile: ts.SourceFile, + symbolName: string, + modulePath: string +): TransformOperation[] { + const ops: TransformOperation[] = []; + // Find all imports. + const allImports = findAstNodes(null, sourceFile, ts.SyntaxKind.ImportDeclaration); + const maybeImports = allImports + .filter((node: ts.ImportDeclaration) => { + // Filter all imports that do not match the modulePath. + return node.moduleSpecifier.kind == ts.SyntaxKind.StringLiteral + && (node.moduleSpecifier as ts.StringLiteral).text == modulePath; + }) + .filter((node: ts.ImportDeclaration) => { + // Filter out import statements that are either `import 'XYZ'` or `import * as X from 'XYZ'`. + const clause = node.importClause as ts.ImportClause; + if (!clause || clause.name || !clause.namedBindings) { + return false; + } + return clause.namedBindings.kind == ts.SyntaxKind.NamedImports; + }) + .map((node: ts.ImportDeclaration) => { + // Return the `{ ... }` list of the named import. + return (node.importClause as ts.ImportClause).namedBindings as ts.NamedImports; + }); + + if (maybeImports.length) { + // There's an `import {A, B, C} from 'modulePath'`. + // Find if it's in either imports. If so, just return; nothing to do. + const hasImportAlready = maybeImports.some((node: ts.NamedImports) => { + return node.elements.some((element: ts.ImportSpecifier) => { + return element.name.text == symbolName; + }); + }); + if (hasImportAlready) { + return; + } + + // Just pick the first one and insert at the end of its identifier list. + ops.push(new AddNodeOperation( + sourceFile, + maybeImports[0].elements[maybeImports[0].elements.length - 1], + undefined, + ts.createImportSpecifier(undefined, ts.createIdentifier(symbolName)) + )); + } else { + // Create the new import node. + const namedImports = ts.createNamedImports([ts.createImportSpecifier(undefined, + ts.createIdentifier(symbolName))]); + const importClause = ts.createImportClause(undefined, namedImports); + const newImport = ts.createImportDeclaration(undefined, undefined, importClause, + ts.createLiteral(modulePath)); + + if (allImports.length > 0) { + // Find the last import and insert after. + ops.push(new AddNodeOperation( + sourceFile, + allImports[allImports.length - 1], + undefined, + newImport + )); + } else { + // Insert before the first node. + ops.push(new AddNodeOperation( + sourceFile, + getFirstNode(sourceFile), + newImport + )); + } + } + return ops; +} diff --git a/packages/@ngtools/webpack/src/transformers/make_transform.ts b/packages/@ngtools/webpack/src/transformers/make_transform.ts new file mode 100644 index 000000000000..eef96fbf9d49 --- /dev/null +++ b/packages/@ngtools/webpack/src/transformers/make_transform.ts @@ -0,0 +1,96 @@ +import * as ts from 'typescript'; + + +export enum OPERATION_KIND { + Remove, + Add, + Replace +} + +export abstract class TransformOperation { + constructor( + public kind: OPERATION_KIND, + public sourceFile: ts.SourceFile, + public target: ts.Node + ) { } +} + +export class RemoveNodeOperation extends TransformOperation { + constructor(sourceFile: ts.SourceFile, target: ts.Node) { + super(OPERATION_KIND.Remove, sourceFile, target); + } +} + +export class AddNodeOperation extends TransformOperation { + constructor(sourceFile: ts.SourceFile, target: ts.Node, + public before?: ts.Node, public after?: ts.Node) { + super(OPERATION_KIND.Add, sourceFile, target); + } +} + +export class ReplaceNodeOperation extends TransformOperation { + kind: OPERATION_KIND.Replace; + constructor(sourceFile: ts.SourceFile, target: ts.Node, public replacement: ts.Node) { + super(OPERATION_KIND.Replace, sourceFile, target); + } +} + +// TODO: add symbol workaround for https://github.com/Microsoft/TypeScript/issues/17551 and +// https://github.com/Microsoft/TypeScript/issues/17384 + +export function makeTransform(ops: TransformOperation[]): ts.TransformerFactory { + + const sourceFiles = ops.reduce((prev, curr) => + prev.includes(curr.sourceFile) ? prev : prev.concat(curr.sourceFile), []); + + const removeOps = ops.filter((op) => op.kind === OPERATION_KIND.Remove) as RemoveNodeOperation[]; + const addOps = ops.filter((op) => op.kind === OPERATION_KIND.Add) as AddNodeOperation[]; + const replaceOps = ops + .filter((op) => op.kind === OPERATION_KIND.Replace) as ReplaceNodeOperation[]; + + return (context: ts.TransformationContext): ts.Transformer => { + const transformer: ts.Transformer = (sf: ts.SourceFile) => { + + const visitor: ts.Visitor = (node) => { + let modified = false; + let modifiedNodes = [node]; + // Check if node should be dropped. + if (removeOps.find((op) => op.target === node)) { + modifiedNodes = []; + modified = true; + } + + // Check if node should be replaced (only replaces with first op found). + const replace = replaceOps.find((op) => op.target === node); + if (replace) { + modifiedNodes = [replace.replacement]; + modified = true; + } + + // Check if node should be added to. + const add = addOps.filter((op) => op.target === node); + if (add.length > 0) { + modifiedNodes = [ + ...add.filter((op) => op.before).map(((op) => op.before)), + ...modifiedNodes, + ...add.filter((op) => op.after).map(((op) => op.after)) + ]; + modified = true; + } + + // If we changed anything, return modified nodes without visiting further. + if (modified) { + return modifiedNodes; + } else { + // Otherwise return node as is and visit children. + return ts.visitEachChild(node, visitor, context); + } + }; + + // Only visit source files we have ops for. + return sourceFiles.includes(sf) ? ts.visitNode(sf, visitor) : sf; + }; + + return transformer; + }; +} diff --git a/packages/@ngtools/webpack/src/transformers/remove_import.ts b/packages/@ngtools/webpack/src/transformers/remove_import.ts new file mode 100644 index 000000000000..58cfdbcf6705 --- /dev/null +++ b/packages/@ngtools/webpack/src/transformers/remove_import.ts @@ -0,0 +1,70 @@ +import * as ts from 'typescript'; + +import { findAstNodes } from './ast_helpers'; +import { RemoveNodeOperation, TransformOperation } from './make_transform'; + +// Remove an import if we have removed all identifiers for it. +// Mainly workaround for https://github.com/Microsoft/TypeScript/issues/17552. +export function removeImport( + sourceFile: ts.SourceFile, + removedIdentifiers: ts.Identifier[] +): TransformOperation[] { + const ops: TransformOperation[] = []; + + if (removedIdentifiers.length === 0) { + return []; + } + + const identifierText = removedIdentifiers[0].text; + + // Find all imports that import `identifierText`. + const allImports = findAstNodes(null, sourceFile, ts.SyntaxKind.ImportDeclaration); + const identifierImports = allImports + .filter((node: ts.ImportDeclaration) => { + // TODO: try to support removing `import * as X from 'XYZ'`. + // Filter out import statements that are either `import 'XYZ'` or `import * as X from 'XYZ'`. + const clause = node.importClause as ts.ImportClause; + if (!clause || clause.name || !clause.namedBindings) { + return false; + } + return clause.namedBindings.kind == ts.SyntaxKind.NamedImports; + }) + .filter((node: ts.ImportDeclaration) => { + // Filter out imports that that don't have `identifierText`. + const namedImports = (node.importClause as ts.ImportClause).namedBindings as ts.NamedImports; + return namedImports.elements.some((element: ts.ImportSpecifier) => { + return element.name.text == identifierText; + }); + }); + + + // Find all identifiers with `identifierText` in the source file. + const allNodes = findAstNodes(null, sourceFile, ts.SyntaxKind.Identifier, true) + .filter(identifier => identifier.getText() === identifierText); + + // If there are more identifiers than the ones we already removed plus the ones we're going to + // remove from imports, don't do anything. + // This means that there's still a couple around that weren't removed and this would break code. + if (allNodes.length > removedIdentifiers.length + identifierImports.length) { + return []; + } + + // Go through the imports. + identifierImports.forEach((node: ts.ImportDeclaration) => { + const namedImports = (node.importClause as ts.ImportClause).namedBindings as ts.NamedImports; + // Only one import, remove the whole declaration. + if (namedImports.elements.length === 1) { + ops.push(new RemoveNodeOperation(sourceFile, node)); + } else { + namedImports.elements.forEach((element: ts.ImportSpecifier) => { + // Multiple imports, remove only the single identifier. + if (element.name.text == identifierText) { + ops.push(new RemoveNodeOperation(sourceFile, node)); + } + }); + } + + }); + + return ops; +} diff --git a/packages/@ngtools/webpack/src/transformers/replace_bootstrap.ts b/packages/@ngtools/webpack/src/transformers/replace_bootstrap.ts new file mode 100644 index 000000000000..3df2a1f0a5f0 --- /dev/null +++ b/packages/@ngtools/webpack/src/transformers/replace_bootstrap.ts @@ -0,0 +1,114 @@ +import * as ts from 'typescript'; + +import { findAstNodes } from './ast_helpers'; +import { insertImport } from './insert_import'; +import { removeImport } from './remove_import'; +import { + ReplaceNodeOperation, + TransformOperation +} from './make_transform'; + + +export function replaceBootstrap( + sourceFile: ts.SourceFile, + entryModule: { path: string, className: string } +): TransformOperation[] { + const ops: TransformOperation[] = []; + + // Find all identifiers using the entry module class name. + const entryModuleIdentifiers = findAstNodes(null, sourceFile, + ts.SyntaxKind.Identifier, true) + .filter(identifier => identifier.getText() === entryModule.className); + + if (entryModuleIdentifiers.length === 0) { + return []; + } + + // Get the module path from the import. + let modulePath: string; + entryModuleIdentifiers.forEach((entryModuleIdentifier) => { + // TODO: only supports `import {A, B, C} from 'modulePath'` atm, add other import support later. + if (entryModuleIdentifier.parent.kind !== ts.SyntaxKind.ImportSpecifier) { + return; + } + + const importSpec = entryModuleIdentifier.parent as ts.ImportSpecifier; + const moduleSpecifier = importSpec.parent.parent.parent.moduleSpecifier; + + if (moduleSpecifier.kind !== ts.SyntaxKind.StringLiteral) { + return; + } + + modulePath = (moduleSpecifier as ts.StringLiteral).text; + }); + + if (!modulePath) { + return []; + } + + // Find the bootstrap calls. + const removedEntryModuleIdentifiers: ts.Identifier[] = []; + const removedPlatformBrowserDynamicIdentifier: ts.Identifier[] = []; + entryModuleIdentifiers.forEach(entryModuleIdentifier => { + // Figure out if it's a `platformBrowserDynamic().bootstrapModule(AppModule)` call. + if (!( + entryModuleIdentifier.parent + && entryModuleIdentifier.parent.kind === ts.SyntaxKind.CallExpression + )) { + return; + } + + const callExpr = entryModuleIdentifier.parent as ts.CallExpression; + + if (callExpr.expression.kind !== ts.SyntaxKind.PropertyAccessExpression) { + return; + } + + const propAccessExpr = callExpr.expression as ts.PropertyAccessExpression; + + if (propAccessExpr.name.text !== 'bootstrapModule' + || propAccessExpr.expression.kind !== ts.SyntaxKind.CallExpression) { + return; + } + + const bootstrapModuleIdentifier = propAccessExpr.name; + const innerCallExpr = propAccessExpr.expression as ts.CallExpression; + + if (!( + innerCallExpr.expression.kind === ts.SyntaxKind.Identifier + && (innerCallExpr.expression as ts.Identifier).text === 'platformBrowserDynamic' + )) { + return; + } + + const platformBrowserDynamicIdentifier = innerCallExpr.expression as ts.Identifier; + + // Add the transform operations. + const factoryClassName = entryModule.className + 'NgFactory'; + const factoryModulePath = modulePath + '.ngfactory'; + ops.push( + // Replace the entry module import. + ...insertImport(sourceFile, factoryClassName, factoryModulePath), + new ReplaceNodeOperation(sourceFile, entryModuleIdentifier, + ts.createIdentifier(factoryClassName)), + // Replace the platformBrowserDynamic import. + ...insertImport(sourceFile, 'platformBrowser', '@angular/platform-browser'), + new ReplaceNodeOperation(sourceFile, platformBrowserDynamicIdentifier, + ts.createIdentifier('platformBrowser')), + new ReplaceNodeOperation(sourceFile, bootstrapModuleIdentifier, + ts.createIdentifier('bootstrapModuleFactory')), + ); + + // Save the import identifiers that we replaced for removal. + removedEntryModuleIdentifiers.push(entryModuleIdentifier); + removedPlatformBrowserDynamicIdentifier.push(platformBrowserDynamicIdentifier); + }); + + // Now that we know all the import identifiers we removed, we can remove the import. + ops.push( + ...removeImport(sourceFile, removedEntryModuleIdentifiers), + ...removeImport(sourceFile, removedPlatformBrowserDynamicIdentifier), + ); + + return ops; +} diff --git a/packages/@ngtools/webpack/src/virtual_file_system_decorator.ts b/packages/@ngtools/webpack/src/virtual_file_system_decorator.ts index e980df9df4a4..7a8b982ae801 100644 --- a/packages/@ngtools/webpack/src/virtual_file_system_decorator.ts +++ b/packages/@ngtools/webpack/src/virtual_file_system_decorator.ts @@ -9,6 +9,7 @@ export class VirtualFileSystemDecorator implements InputFileSystem { private _webpackCompilerHost: WebpackCompilerHost ) { } + // We only need to intercept calls to individual files that are present in WebpackCompilerHost. private _readFileSync(path: string): string | null { if (this._webpackCompilerHost.fileExists(path, false)) { return this._webpackCompilerHost.readFile(path); @@ -18,24 +19,13 @@ export class VirtualFileSystemDecorator implements InputFileSystem { } private _statSync(path: string): Stats | null { - if (this._webpackCompilerHost.fileExists(path, false) - || this._webpackCompilerHost.directoryExists(path, false)) { + if (this._webpackCompilerHost.fileExists(path, false)) { return this._webpackCompilerHost.stat(path); } return null; } - private _readDirSync(path: string): string[] | null { - if (this._webpackCompilerHost.directoryExists(path, false)) { - const dirs = this._webpackCompilerHost.getDirectories(path); - const files = this._webpackCompilerHost.getFiles(path); - return files.concat(dirs); - } - - return null; - } - stat(path: string, callback: Callback): void { const result = this._statSync(path); if (result) { @@ -46,12 +36,7 @@ export class VirtualFileSystemDecorator implements InputFileSystem { } readdir(path: string, callback: Callback): void { - const result = this._readDirSync(path); - if (result) { - callback(null, result); - } else { - this._inputFileSystem.readdir(path, callback); - } + this._inputFileSystem.readdir(path, callback); } readFile(path: string, callback: Callback): void { @@ -77,8 +62,7 @@ export class VirtualFileSystemDecorator implements InputFileSystem { } readdirSync(path: string): string[] { - const result = this._readDirSync(path); - return result || this._inputFileSystem.readdirSync(path); + return this._inputFileSystem.readdirSync(path); } readFileSync(path: string): string { diff --git a/tests/e2e/assets/webpack/test-app-ng5/app/app.component.html b/tests/e2e/assets/webpack/test-app-ng5/app/app.component.html new file mode 100644 index 000000000000..5a532db9308f --- /dev/null +++ b/tests/e2e/assets/webpack/test-app-ng5/app/app.component.html @@ -0,0 +1,5 @@ +
+

hello world

+ lazy + +
diff --git a/tests/e2e/assets/webpack/test-app-ng5/app/app.component.scss b/tests/e2e/assets/webpack/test-app-ng5/app/app.component.scss new file mode 100644 index 000000000000..5cde7b922336 --- /dev/null +++ b/tests/e2e/assets/webpack/test-app-ng5/app/app.component.scss @@ -0,0 +1,3 @@ +:host { + background-color: blue; +} diff --git a/tests/e2e/assets/webpack/test-app-ng5/app/app.component.ts b/tests/e2e/assets/webpack/test-app-ng5/app/app.component.ts new file mode 100644 index 000000000000..82a4059565d3 --- /dev/null +++ b/tests/e2e/assets/webpack/test-app-ng5/app/app.component.ts @@ -0,0 +1,15 @@ +import {Component, ViewEncapsulation} from '@angular/core'; +import {MyInjectable} from './injectable'; + + +@Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class AppComponent { + constructor(public inj: MyInjectable) { + console.log(inj); + } +} diff --git a/tests/e2e/assets/webpack/test-app-ng5/app/app.module.ts b/tests/e2e/assets/webpack/test-app-ng5/app/app.module.ts new file mode 100644 index 000000000000..ded686868a22 --- /dev/null +++ b/tests/e2e/assets/webpack/test-app-ng5/app/app.module.ts @@ -0,0 +1,27 @@ +import { NgModule, Component } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { RouterModule } from '@angular/router'; +import { AppComponent } from './app.component'; + +@Component({ + selector: 'home-view', + template: 'home!' +}) +export class HomeView {} + + +@NgModule({ + declarations: [ + AppComponent, + HomeView + ], + imports: [ + BrowserModule, + RouterModule.forRoot([ + {path: 'lazy', loadChildren: './lazy.module#LazyModule'}, + {path: '', component: HomeView} + ]) + ], + bootstrap: [AppComponent] +}) +export class AppModule { } diff --git a/tests/e2e/assets/webpack/test-app-ng5/app/feature/feature.module.ts b/tests/e2e/assets/webpack/test-app-ng5/app/feature/feature.module.ts new file mode 100644 index 000000000000..f464ca028b05 --- /dev/null +++ b/tests/e2e/assets/webpack/test-app-ng5/app/feature/feature.module.ts @@ -0,0 +1,20 @@ +import {NgModule, Component} from '@angular/core'; +import {RouterModule} from '@angular/router'; + +@Component({ + selector: 'feature-component', + template: 'foo.html' +}) +export class FeatureComponent {} + +@NgModule({ + declarations: [ + FeatureComponent + ], + imports: [ + RouterModule.forChild([ + { path: '', component: FeatureComponent} + ]) + ] +}) +export class FeatureModule {} diff --git a/tests/e2e/assets/webpack/test-app-ng5/app/feature/lazy-feature.module.ts b/tests/e2e/assets/webpack/test-app-ng5/app/feature/lazy-feature.module.ts new file mode 100644 index 000000000000..8fafca158b24 --- /dev/null +++ b/tests/e2e/assets/webpack/test-app-ng5/app/feature/lazy-feature.module.ts @@ -0,0 +1,23 @@ +import {NgModule, Component} from '@angular/core'; +import {RouterModule} from '@angular/router'; +import {HttpModule, Http} from '@angular/http'; + +@Component({ + selector: 'lazy-feature-comp', + template: 'lazy feature!' +}) +export class LazyFeatureComponent {} + +@NgModule({ + imports: [ + RouterModule.forChild([ + {path: '', component: LazyFeatureComponent, pathMatch: 'full'}, + {path: 'feature', loadChildren: './feature.module#FeatureModule'} + ]), + HttpModule + ], + declarations: [LazyFeatureComponent] +}) +export class LazyFeatureModule { + constructor(http: Http) {} +} diff --git a/tests/e2e/assets/webpack/test-app-ng5/app/injectable.ts b/tests/e2e/assets/webpack/test-app-ng5/app/injectable.ts new file mode 100644 index 000000000000..04d8486586c4 --- /dev/null +++ b/tests/e2e/assets/webpack/test-app-ng5/app/injectable.ts @@ -0,0 +1,8 @@ +import {Injectable, Inject, ViewContainerRef} from '@angular/core'; +import {DOCUMENT} from '@angular/platform-browser'; + + +@Injectable() +export class MyInjectable { + constructor(public viewContainer: ViewContainerRef, @Inject(DOCUMENT) public doc) {} +} diff --git a/tests/e2e/assets/webpack/test-app-ng5/app/lazy.module.ts b/tests/e2e/assets/webpack/test-app-ng5/app/lazy.module.ts new file mode 100644 index 000000000000..96da4de7515b --- /dev/null +++ b/tests/e2e/assets/webpack/test-app-ng5/app/lazy.module.ts @@ -0,0 +1,26 @@ +import {NgModule, Component} from '@angular/core'; +import {RouterModule} from '@angular/router'; +import {HttpModule, Http} from '@angular/http'; + +@Component({ + selector: 'lazy-comp', + template: 'lazy!' +}) +export class LazyComponent {} + +@NgModule({ + imports: [ + RouterModule.forChild([ + {path: '', component: LazyComponent, pathMatch: 'full'}, + {path: 'feature', loadChildren: './feature/feature.module#FeatureModule'}, + {path: 'lazy-feature', loadChildren: './feature/lazy-feature.module#LazyFeatureModule'} + ]), + HttpModule + ], + declarations: [LazyComponent] +}) +export class LazyModule { + constructor(http: Http) {} +} + +export class SecondModule {} diff --git a/tests/e2e/assets/webpack/test-app-ng5/app/main.ts b/tests/e2e/assets/webpack/test-app-ng5/app/main.ts new file mode 100644 index 000000000000..0a705a5daebb --- /dev/null +++ b/tests/e2e/assets/webpack/test-app-ng5/app/main.ts @@ -0,0 +1,5 @@ +import 'core-js/es7/reflect'; +import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; +import {AppModule} from './app.module'; + +platformBrowserDynamic().bootstrapModule(AppModule); diff --git a/tests/e2e/assets/webpack/test-app-ng5/index.html b/tests/e2e/assets/webpack/test-app-ng5/index.html new file mode 100644 index 000000000000..89fb0893c35d --- /dev/null +++ b/tests/e2e/assets/webpack/test-app-ng5/index.html @@ -0,0 +1,12 @@ + + + + Document + + + + + + + + diff --git a/tests/e2e/assets/webpack/test-app-ng5/package.json b/tests/e2e/assets/webpack/test-app-ng5/package.json new file mode 100644 index 000000000000..162363bc8c19 --- /dev/null +++ b/tests/e2e/assets/webpack/test-app-ng5/package.json @@ -0,0 +1,27 @@ +{ + "name": "test", + "license": "MIT", + "dependencies": { + "@angular/common": "^5.0.0-beta.5", + "@angular/compiler": "^5.0.0-beta.5", + "@angular/compiler-cli": "^5.0.0-beta.5", + "@angular/core": "^5.0.0-beta.5", + "@angular/http": "^5.0.0-beta.5", + "@angular/platform-browser": "^5.0.0-beta.5", + "@angular/platform-browser-dynamic": "^5.0.0-beta.5", + "@angular/platform-server": "^5.0.0-beta.5", + "@angular/router": "^5.0.0-beta.5", + "@ngtools/webpack": "0.0.0", + "core-js": "^2.4.1", + "rxjs": "^5.4.2", + "zone.js": "^0.8.14" + }, + "devDependencies": { + "node-sass": "^3.7.0", + "performance-now": "^0.2.0", + "raw-loader": "^0.5.1", + "sass-loader": "^3.2.0", + "typescript": "~2.4.2", + "webpack": "2.2.0" + } +} diff --git a/tests/e2e/assets/webpack/test-app-ng5/tsconfig.json b/tests/e2e/assets/webpack/test-app-ng5/tsconfig.json new file mode 100644 index 000000000000..c245563164ba --- /dev/null +++ b/tests/e2e/assets/webpack/test-app-ng5/tsconfig.json @@ -0,0 +1,25 @@ +{ + // Test comment + "compilerOptions": { + "baseUrl": "", + "module": "es2015", + "moduleResolution": "node", + "target": "es5", + "noImplicitAny": false, + "sourceMap": true, + "mapRoot": "", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "lib": [ + "es2017", + "dom" + ], + "outDir": "lib", + "skipLibCheck": true, + "rootDir": "." + }, + "angularCompilerOptions": { + "genDir": "./app/ngfactory", + "entryModule": "app/app.module#AppModule" + } +} diff --git a/tests/e2e/assets/webpack/test-app-ng5/webpack.config.js b/tests/e2e/assets/webpack/test-app-ng5/webpack.config.js new file mode 100644 index 000000000000..6d7776725a30 --- /dev/null +++ b/tests/e2e/assets/webpack/test-app-ng5/webpack.config.js @@ -0,0 +1,29 @@ +const ngToolsWebpack = require('@ngtools/webpack'); + +module.exports = { + resolve: { + extensions: ['.ts', '.js'] + }, + entry: './app/main.ts', + output: { + path: './dist', + publicPath: 'dist/', + filename: 'app.main.js' + }, + plugins: [ + new ngToolsWebpack.AngularCompilerPlugin({ + tsConfigPath: './tsconfig.json' + }) + ], + module: { + loaders: [ + { test: /\.scss$/, loaders: ['raw-loader', 'sass-loader'] }, + { test: /\.css$/, loader: 'raw-loader' }, + { test: /\.html$/, loader: 'raw-loader' }, + { test: /\.ts$/, loader: '@ngtools/webpack' } + ] + }, + devServer: { + historyApiFallback: true + } +}; diff --git a/tests/e2e/assets/webpack/test-app-weird-ng5/aotplugin.config.json b/tests/e2e/assets/webpack/test-app-weird-ng5/aotplugin.config.json new file mode 100644 index 000000000000..b6d2d7a7816d --- /dev/null +++ b/tests/e2e/assets/webpack/test-app-weird-ng5/aotplugin.config.json @@ -0,0 +1,4 @@ +{ + "tsConfigPath": "./not/so/source/tsconfig.json", + "mainPath": "app/main.jit.ts" +} \ No newline at end of file diff --git a/tests/e2e/assets/webpack/test-app-weird-ng5/not/so/source/app/app.component.html b/tests/e2e/assets/webpack/test-app-weird-ng5/not/so/source/app/app.component.html new file mode 100644 index 000000000000..3505c196ea74 --- /dev/null +++ b/tests/e2e/assets/webpack/test-app-weird-ng5/not/so/source/app/app.component.html @@ -0,0 +1,13 @@ +
+

hello world

+ lazy + +
+ + +

DEBUG_ONLY

+ + + +

PRODUCTION_ONLY

+ diff --git a/tests/e2e/assets/webpack/test-app-weird-ng5/not/so/source/app/app.component.scss b/tests/e2e/assets/webpack/test-app-weird-ng5/not/so/source/app/app.component.scss new file mode 100644 index 000000000000..ba143f5d5da1 --- /dev/null +++ b/tests/e2e/assets/webpack/test-app-weird-ng5/not/so/source/app/app.component.scss @@ -0,0 +1,15 @@ +:host { + background-color: blue; +} + +// @ifdef DEBUG +:host::before { + content: 'DEBUG_ONLY'; +} +// @endif + +// @ifndef DEBUG +:host::before { + content: 'PRODUCTION_ONLY'; +} +// @endif diff --git a/tests/e2e/assets/webpack/test-app-weird-ng5/not/so/source/app/app.component.ts b/tests/e2e/assets/webpack/test-app-weird-ng5/not/so/source/app/app.component.ts new file mode 100644 index 000000000000..09a19ad8f1ac --- /dev/null +++ b/tests/e2e/assets/webpack/test-app-weird-ng5/not/so/source/app/app.component.ts @@ -0,0 +1,10 @@ +import {Component, ViewEncapsulation} from '@angular/core'; + + +@Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class AppComponent { } diff --git a/tests/e2e/assets/webpack/test-app-weird-ng5/not/so/source/app/app.module.ts b/tests/e2e/assets/webpack/test-app-weird-ng5/not/so/source/app/app.module.ts new file mode 100644 index 000000000000..2bcd6c4883bd --- /dev/null +++ b/tests/e2e/assets/webpack/test-app-weird-ng5/not/so/source/app/app.module.ts @@ -0,0 +1,36 @@ +import { NgModule, Component } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { RouterModule } from '@angular/router'; +import { AppComponent } from './app.component'; + +@Component({ + selector: 'home-view', + template: 'home!' +}) +export class HomeView {} + + +// @ifdef DEBUG +console.log("DEBUG_ONLY"); +// @endif + +// @ifndef DEBUG +console.log("PRODUCTION_ONLY"); +// @endif + + +@NgModule({ + declarations: [ + AppComponent, + HomeView + ], + imports: [ + BrowserModule, + RouterModule.forRoot([ + {path: 'lazy', loadChildren: './lazy.module#LazyModule'}, + {path: '', component: HomeView} + ]) + ], + bootstrap: [AppComponent] +}) +export class AppModule { } diff --git a/tests/e2e/assets/webpack/test-app-weird-ng5/not/so/source/app/feature/feature.module.ts b/tests/e2e/assets/webpack/test-app-weird-ng5/not/so/source/app/feature/feature.module.ts new file mode 100644 index 000000000000..f464ca028b05 --- /dev/null +++ b/tests/e2e/assets/webpack/test-app-weird-ng5/not/so/source/app/feature/feature.module.ts @@ -0,0 +1,20 @@ +import {NgModule, Component} from '@angular/core'; +import {RouterModule} from '@angular/router'; + +@Component({ + selector: 'feature-component', + template: 'foo.html' +}) +export class FeatureComponent {} + +@NgModule({ + declarations: [ + FeatureComponent + ], + imports: [ + RouterModule.forChild([ + { path: '', component: FeatureComponent} + ]) + ] +}) +export class FeatureModule {} diff --git a/tests/e2e/assets/webpack/test-app-weird-ng5/not/so/source/app/feature/lazy-feature.module.ts b/tests/e2e/assets/webpack/test-app-weird-ng5/not/so/source/app/feature/lazy-feature.module.ts new file mode 100644 index 000000000000..8fafca158b24 --- /dev/null +++ b/tests/e2e/assets/webpack/test-app-weird-ng5/not/so/source/app/feature/lazy-feature.module.ts @@ -0,0 +1,23 @@ +import {NgModule, Component} from '@angular/core'; +import {RouterModule} from '@angular/router'; +import {HttpModule, Http} from '@angular/http'; + +@Component({ + selector: 'lazy-feature-comp', + template: 'lazy feature!' +}) +export class LazyFeatureComponent {} + +@NgModule({ + imports: [ + RouterModule.forChild([ + {path: '', component: LazyFeatureComponent, pathMatch: 'full'}, + {path: 'feature', loadChildren: './feature.module#FeatureModule'} + ]), + HttpModule + ], + declarations: [LazyFeatureComponent] +}) +export class LazyFeatureModule { + constructor(http: Http) {} +} diff --git a/tests/e2e/assets/webpack/test-app-weird-ng5/not/so/source/app/lazy.module.ts b/tests/e2e/assets/webpack/test-app-weird-ng5/not/so/source/app/lazy.module.ts new file mode 100644 index 000000000000..96da4de7515b --- /dev/null +++ b/tests/e2e/assets/webpack/test-app-weird-ng5/not/so/source/app/lazy.module.ts @@ -0,0 +1,26 @@ +import {NgModule, Component} from '@angular/core'; +import {RouterModule} from '@angular/router'; +import {HttpModule, Http} from '@angular/http'; + +@Component({ + selector: 'lazy-comp', + template: 'lazy!' +}) +export class LazyComponent {} + +@NgModule({ + imports: [ + RouterModule.forChild([ + {path: '', component: LazyComponent, pathMatch: 'full'}, + {path: 'feature', loadChildren: './feature/feature.module#FeatureModule'}, + {path: 'lazy-feature', loadChildren: './feature/lazy-feature.module#LazyFeatureModule'} + ]), + HttpModule + ], + declarations: [LazyComponent] +}) +export class LazyModule { + constructor(http: Http) {} +} + +export class SecondModule {} diff --git a/tests/e2e/assets/webpack/test-app-weird-ng5/not/so/source/app/main.jit.ts b/tests/e2e/assets/webpack/test-app-weird-ng5/not/so/source/app/main.jit.ts new file mode 100644 index 000000000000..0a705a5daebb --- /dev/null +++ b/tests/e2e/assets/webpack/test-app-weird-ng5/not/so/source/app/main.jit.ts @@ -0,0 +1,5 @@ +import 'core-js/es7/reflect'; +import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; +import {AppModule} from './app.module'; + +platformBrowserDynamic().bootstrapModule(AppModule); diff --git a/tests/e2e/assets/webpack/test-app-weird-ng5/not/so/source/index.html b/tests/e2e/assets/webpack/test-app-weird-ng5/not/so/source/index.html new file mode 100644 index 000000000000..89fb0893c35d --- /dev/null +++ b/tests/e2e/assets/webpack/test-app-weird-ng5/not/so/source/index.html @@ -0,0 +1,12 @@ + + + + Document + + + + + + + + diff --git a/tests/e2e/assets/webpack/test-app-weird-ng5/not/so/source/tsconfig.json b/tests/e2e/assets/webpack/test-app-weird-ng5/not/so/source/tsconfig.json new file mode 100644 index 000000000000..df35f5efe991 --- /dev/null +++ b/tests/e2e/assets/webpack/test-app-weird-ng5/not/so/source/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "module": "es2015", + "moduleResolution": "node", + "target": "es5", + "noImplicitAny": false, + "sourceMap": true, + "mapRoot": "", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "lib": [ + "es2017", + "dom" + ], + "outDir": "lib", + "skipLibCheck": true, + "rootDir": "." + }, + "angularCompilerOptions": { + "genDir": "app/generated/", + "entryModule": "app/app.module#AppModule" + } +} diff --git a/tests/e2e/assets/webpack/test-app-weird-ng5/package.json b/tests/e2e/assets/webpack/test-app-weird-ng5/package.json new file mode 100644 index 000000000000..b3ff99540a73 --- /dev/null +++ b/tests/e2e/assets/webpack/test-app-weird-ng5/package.json @@ -0,0 +1,28 @@ +{ + "name": "test", + "license": "MIT", + "dependencies": { + "@angular/common": "^5.0.0-beta.5", + "@angular/compiler": "^5.0.0-beta.5", + "@angular/compiler-cli": "^5.0.0-beta.5", + "@angular/core": "^5.0.0-beta.5", + "@angular/http": "^5.0.0-beta.5", + "@angular/platform-browser": "^5.0.0-beta.5", + "@angular/platform-browser-dynamic": "^5.0.0-beta.5", + "@angular/platform-server": "^5.0.0-beta.5", + "@angular/router": "^5.0.0-beta.5", + "@ngtools/webpack": "0.0.0", + "core-js": "^2.4.1", + "rxjs": "^5.4.2", + "zone.js": "^0.8.14" + }, + "devDependencies": { + "node-sass": "^3.7.0", + "performance-now": "^0.2.0", + "preprocess-loader": "^0.2.2", + "raw-loader": "^0.5.1", + "sass-loader": "^3.2.0", + "typescript": "~2.4.2", + "webpack": "2.2.0" + } +} diff --git a/tests/e2e/assets/webpack/test-app-weird-ng5/webpack.config.js b/tests/e2e/assets/webpack/test-app-weird-ng5/webpack.config.js new file mode 100644 index 000000000000..9c3925b6ea38 --- /dev/null +++ b/tests/e2e/assets/webpack/test-app-weird-ng5/webpack.config.js @@ -0,0 +1,37 @@ +const ngToolsWebpack = require('@ngtools/webpack'); + +const flags = require('./webpack.flags.json'); + +const preprocessLoader = 'preprocess-loader' + (flags.DEBUG ? '?+DEBUG' : ''); + + +module.exports = { + resolve: { + extensions: ['.ts', '.js'] + }, + entry: './not/so/source/app/main.jit.ts', + output: { + path: './dist', + publicPath: 'dist/', + filename: 'app.main.js' + }, + plugins: [ + new ngToolsWebpack.AngularCompilerPlugin(require('./aotplugin.config.json')) + ], + module: { + loaders: [ + { test: /\.scss$/, loaders: ['raw-loader', 'sass-loader', preprocessLoader] }, + { test: /\.css$/, loader: 'raw-loader' }, + { test: /\.html$/, loaders: ['raw-loader', preprocessLoader] }, + // Use preprocess to remove DEBUG only code. + // @ngtools/webpack must be the first (right most) loader. + { test: /\.ts$/, use: [ + { loader: preprocessLoader }, + { loader: '@ngtools/webpack' } + ] } + ] + }, + devServer: { + historyApiFallback: true + } +}; diff --git a/tests/e2e/assets/webpack/test-app-weird-ng5/webpack.flags.json b/tests/e2e/assets/webpack/test-app-weird-ng5/webpack.flags.json new file mode 100644 index 000000000000..aac05f738dc8 --- /dev/null +++ b/tests/e2e/assets/webpack/test-app-weird-ng5/webpack.flags.json @@ -0,0 +1,3 @@ +{ + "DEBUG": false +} diff --git a/tests/e2e/tests/build/aot/angular-compiler.ts b/tests/e2e/tests/build/aot/angular-compiler.ts new file mode 100644 index 000000000000..dffc07dbc50b --- /dev/null +++ b/tests/e2e/tests/build/aot/angular-compiler.ts @@ -0,0 +1,46 @@ +import { ng, npm } from '../../../utils/process'; +import { updateJsonFile } from '../../../utils/project'; +import { expectFileToMatch, rimraf, moveFile } from '../../../utils/fs'; +import { getGlobalVariable } from '../../../utils/env'; + + +// THIS TEST REQUIRES TO MOVE NODE_MODULES AND MOVE IT BACK. +export default function () { + // Skip this in ejected tests. + // Something goes wrong when installing node modules after eject. Remove this in ng5. + if (getGlobalVariable('argv').eject) { + return Promise.resolve(); + } + + return Promise.resolve() + .then(() => moveFile('node_modules', '../node_modules')) + .then(() => updateJsonFile('package.json', packageJson => { + const dependencies = packageJson['dependencies']; + dependencies['@angular/animations'] = '5.0.0-beta.6'; + dependencies['@angular/common'] = '5.0.0-beta.6'; + dependencies['@angular/compiler'] = '5.0.0-beta.6'; + dependencies['@angular/core'] = '5.0.0-beta.6'; + dependencies['@angular/forms'] = '5.0.0-beta.6'; + dependencies['@angular/http'] = '5.0.0-beta.6'; + dependencies['@angular/platform-browser'] = '5.0.0-beta.6'; + dependencies['@angular/platform-browser-dynamic'] = '5.0.0-beta.6'; + dependencies['@angular/router'] = '5.0.0-beta.6'; + const devDependencies = packageJson['devDependencies']; + devDependencies['@angular/compiler-cli'] = '5.0.0-beta.6'; + devDependencies['@angular/language-service'] = '5.0.0-beta.6'; + devDependencies['typescript'] = '2.4.2'; + })) + .then(() => npm('install')) + .then(() => ng('build', '--aot')) + .then(() => expectFileToMatch('dist/main.bundle.js', + /bootstrapModuleFactory.*\/\* AppModuleNgFactory \*\//)) + // Cleanup + .then(() => { + return rimraf('node_modules') + .then(() => moveFile('../node_modules', 'node_modules')); + }, (err: any) => { + return rimraf('node_modules') + .then(() => moveFile('../node_modules', 'node_modules')) + .then(() => { throw err; }); + }); +} diff --git a/tests/e2e/tests/build/aot/aot-decorators.ts b/tests/e2e/tests/build/aot/aot-decorators.ts index 86d15b6448bc..77bc1fb95a49 100644 --- a/tests/e2e/tests/build/aot/aot-decorators.ts +++ b/tests/e2e/tests/build/aot/aot-decorators.ts @@ -1,8 +1,16 @@ import {ng} from '../../../utils/process'; import {appendToFile, expectFileToMatch, prependToFile, replaceInFile} from '../../../utils/fs'; import {expectToFail} from '../../../utils/utils'; +import {getGlobalVariable} from '../../../utils/env'; export default function() { + + // TODO: re-enable this test for ng5 + // now we only remove decorators via --build-optimizer + if (getGlobalVariable('argv').nightly) { + return Promise.resolve(); + } + return ng('generate', 'component', 'test-component', '--module', 'app.module.ts') .then(() => prependToFile('src/app/test-component/test-component.component.ts', ` import { Optional, SkipSelf } from '@angular/core'; diff --git a/tests/e2e/tests/build/css-urls.ts b/tests/e2e/tests/build/css-urls.ts index ac86b2e3f008..9bc565a428d3 100644 --- a/tests/e2e/tests/build/css-urls.ts +++ b/tests/e2e/tests/build/css-urls.ts @@ -40,14 +40,13 @@ export default function () { })) .then(() => ng('build', '--extract-css', '--aot')) // Check paths are correctly generated. + .then(() => expectFileToMatch('dist/styles.bundle.css', '/assets/global-img-absolute.svg')) .then(() => expectFileToMatch('dist/styles.bundle.css', - /url\('\/assets\/global-img-absolute\.svg'\)/)) - .then(() => expectFileToMatch('dist/styles.bundle.css', - /url\(global-img-relative\.[0-9a-f]{20}\.svg\)/)) + /global-img-relative\.[0-9a-f]{20}\.svg/)) .then(() => expectFileToMatch('dist/main.bundle.js', - /url\(\\'\/assets\/component-img-absolute\.svg\\'\)/)) + '/assets/component-img-absolute.svg')) .then(() => expectFileToMatch('dist/main.bundle.js', - /url\(component-img-relative\.[0-9a-f]{20}\.svg\)/)) + /component-img-relative\.[0-9a-f]{20}\.svg/)) // Check files are correctly created. .then(() => expectToFail(() => expectFileToExist('dist/global-img-absolute.svg'))) .then(() => expectToFail(() => expectFileToExist('dist/component-img-absolute.svg'))) @@ -78,11 +77,11 @@ export default function () { .then(() => ng('build', '--base-href=/base/', '--deploy-url=deploy/', '--extract-css', '--aot')) .then(() => expectFileToMatch('dist/styles.bundle.css', - /url\('\/base\/deploy\/assets\/global-img-absolute\.svg'\)/)) + '/base/deploy/assets/global-img-absolute.svg')) .then(() => expectFileToMatch('dist/styles.bundle.css', - /url\(global-img-relative\.[0-9a-f]{20}\.svg\)/)) + /global-img-relative\.[0-9a-f]{20}\.svg/)) .then(() => expectFileToMatch('dist/main.bundle.js', - /url\(\\'\/base\/deploy\/assets\/component-img-absolute\.svg\\'\)/)) + '/base/deploy/assets/component-img-absolute.svg')) .then(() => expectFileToMatch('dist/main.bundle.js', - /url\(deploy\/component-img-relative\.[0-9a-f]{20}\.svg\)/)); + /deploy\/component-img-relative\.[0-9a-f]{20}\.svg/)); } diff --git a/tests/e2e/tests/build/platform-server.ts b/tests/e2e/tests/build/platform-server.ts index 314435b13dd3..e040242fd760 100644 --- a/tests/e2e/tests/build/platform-server.ts +++ b/tests/e2e/tests/build/platform-server.ts @@ -2,7 +2,7 @@ import { normalize } from 'path'; import { updateJsonFile, updateTsConfig } from '../../utils/project'; import { expectFileToMatch, writeFile, replaceInFile, prependToFile } from '../../utils/fs'; -import { ng, silentNpm, silentExec } from '../../utils/process'; +import { ng, silentNpm, exec } from '../../utils/process'; import { getGlobalVariable } from '../../utils/env'; export default function () { @@ -16,6 +16,12 @@ export default function () { return Promise.resolve(); } + let platformServerVersion = '^4.0.0'; + + if (getGlobalVariable('argv').nightly) { + platformServerVersion = 'github:angular/platform-server-builds'; + } + return Promise.resolve() .then(() => updateJsonFile('.angular-cli.json', configJson => { const app = configJson['apps'][0]; @@ -25,7 +31,7 @@ export default function () { })) .then(() => updateJsonFile('package.json', packageJson => { const dependencies = packageJson['dependencies']; - dependencies['@angular/platform-server'] = '^4.0.0'; + dependencies['@angular/platform-server'] = platformServerVersion; })) .then(() => updateTsConfig(tsConfig => { tsConfig['angularCompilerOptions'] = { @@ -56,7 +62,7 @@ export default function () { fs.writeFileSync('dist/index.html', html); \}); `)) - .then(() => silentExec(normalize('node'), 'index.js')) + .then(() => exec(normalize('node'), 'index.js')) .then(() => expectFileToMatch('dist/index.html', new RegExp('

Here are some links to help you start:

'))) .then(() => ng('build', '--aot')) @@ -65,7 +71,7 @@ export default function () { /__webpack_exports__, "AppModuleNgFactory"/)) .then(() => replaceInFile('./index.js', /AppModule/g, 'AppModuleNgFactory')) .then(() => replaceInFile('./index.js', /renderModule/g, 'renderModuleFactory')) - .then(() => silentExec(normalize('node'), 'index.js')) + .then(() => exec(normalize('node'), 'index.js')) .then(() => expectFileToMatch('dist/index.html', new RegExp('

Here are some links to help you start:

'))); } diff --git a/tests/e2e/tests/build/poll.ts b/tests/e2e/tests/build/poll.ts index 202eed096541..8240d87595bf 100644 --- a/tests/e2e/tests/build/poll.ts +++ b/tests/e2e/tests/build/poll.ts @@ -22,14 +22,15 @@ export default function() { return execAndWaitForOutputToMatch('ng', ['serve', '--poll=10000'], webpackGoodRegEx) // Wait before editing a file. // Editing too soon seems to trigger a rebuild and throw polling out of whack. - .then(() => wait(2000)) + .then(() => wait(3000)) .then(() => appendToFile('src/main.ts', 'console.log(1);')) - .then(() => waitForAnyProcessOutputToMatch(webpackGoodRegEx, 12000)) + // We have to wait poll time + rebuild build time for the regex match. + .then(() => waitForAnyProcessOutputToMatch(webpackGoodRegEx, 14000)) .then(() => appendToFile('src/main.ts', 'console.log(1);')) // No rebuilds should occur for a while - .then(() => expectToFail(() => waitForAnyProcessOutputToMatch(webpackGoodRegEx, 6000))) + .then(() => expectToFail(() => waitForAnyProcessOutputToMatch(webpackGoodRegEx, 7000))) // But a rebuild should happen roughly within the 10 second window. - .then(() => waitForAnyProcessOutputToMatch(webpackGoodRegEx, 12000)) + .then(() => waitForAnyProcessOutputToMatch(webpackGoodRegEx, 7000)) .then(() => killAllProcesses(), (err: any) => { killAllProcesses(); throw err; diff --git a/tests/e2e/tests/build/rebuild-deps-type-check.ts b/tests/e2e/tests/build/rebuild-deps-type-check.ts index b02c94184b99..f4ce937743e3 100644 --- a/tests/e2e/tests/build/rebuild-deps-type-check.ts +++ b/tests/e2e/tests/build/rebuild-deps-type-check.ts @@ -48,21 +48,21 @@ export default function() { } `)) // Should trigger a rebuild, this time an error is expected. - .then(() => waitForAnyProcessOutputToMatch(doneRe, 10000)) + .then(() => waitForAnyProcessOutputToMatch(doneRe, 20000)) .then(({ stderr }) => { - if (!/ERROR in .*\/src\/main\.ts \(/.test(stderr)) { + if (!/ERROR in (.*\/src\/)?main\.ts/.test(stderr)) { throw new Error('Expected an error but none happened.'); } }) // Change an UNRELATED file and the error should still happen. .then(() => wait(2000)) .then(() => appendToFile('src/app/app.module.ts', ` - function anything(): number {} + function anything(): number {} `)) // Should trigger a rebuild, this time an error is expected. - .then(() => waitForAnyProcessOutputToMatch(doneRe, 10000)) + .then(() => waitForAnyProcessOutputToMatch(doneRe, 20000)) .then(({ stderr }) => { - if (!/ERROR in .*\/src\/main\.ts \(/.test(stderr)) { + if (!/ERROR in (.*\/src\/)?main\.ts/.test(stderr)) { throw new Error('Expected an error but none happened.'); } }) @@ -73,9 +73,9 @@ export default function() { return value + 'hello'; } `)) - .then(() => waitForAnyProcessOutputToMatch(doneRe, 10000)) + .then(() => waitForAnyProcessOutputToMatch(doneRe, 20000)) .then(({ stderr }) => { - if (/ERROR in .*\/src\/main\.ts \(/.test(stderr)) { + if (/ERROR in (.*\/src\/)?main\.ts/.test(stderr)) { throw new Error('Expected no error but an error was shown.'); } }) diff --git a/tests/e2e/tests/build/rebuild.ts b/tests/e2e/tests/build/rebuild.ts index c69be2658a9a..ae8b870c839a 100644 --- a/tests/e2e/tests/build/rebuild.ts +++ b/tests/e2e/tests/build/rebuild.ts @@ -76,10 +76,39 @@ export default function() { // Change multiple files and check that all of them are invalidated and recompiled. .then(() => writeMultipleFiles({ 'src/app/app.module.ts': ` + import { BrowserModule } from '@angular/platform-browser'; + import { NgModule } from '@angular/core'; + + import { AppComponent } from './app.component'; + + @NgModule({ + declarations: [ + AppComponent + ], + imports: [ + BrowserModule + ], + providers: [], + bootstrap: [AppComponent] + }) + export class AppModule { } + console.log('$$_E2E_GOLDEN_VALUE_1'); export let X = '$$_E2E_GOLDEN_VALUE_2'; `, 'src/main.ts': ` + import { enableProdMode } from '@angular/core'; + import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + + import { AppModule } from './app/app.module'; + import { environment } from './environments/environment'; + + if (environment.production) { + enableProdMode(); + } + + platformBrowserDynamic().bootstrapModule(AppModule); + import * as m from './app/app.module'; console.log(m.X); console.log('$$_E2E_GOLDEN_VALUE_3'); diff --git a/tests/e2e/tests/build/watch.ts b/tests/e2e/tests/build/watch.ts index 850a32f0a0c3..3c97fcc8a6a3 100644 --- a/tests/e2e/tests/build/watch.ts +++ b/tests/e2e/tests/build/watch.ts @@ -17,7 +17,7 @@ export default function () { return execAndWaitForOutputToMatch('ng', ['serve'], webpackGoodRegEx) // Should trigger a rebuild. .then(() => exec('touch', 'src/main.ts')) - .then(() => waitForAnyProcessOutputToMatch(webpackGoodRegEx, 5000)) + .then(() => waitForAnyProcessOutputToMatch(webpackGoodRegEx, 10000)) .then(() => killAllProcesses(), (err: any) => { killAllProcesses(); throw err; @@ -25,7 +25,7 @@ export default function () { .then(() => execAndWaitForOutputToMatch('ng', ['serve', '--no-watch'], webpackGoodRegEx)) // Should not trigger a rebuild when not watching files. .then(() => exec('touch', 'src/main.ts')) - .then(() => expectToFail(() => waitForAnyProcessOutputToMatch(webpackGoodRegEx, 5000))) + .then(() => expectToFail(() => waitForAnyProcessOutputToMatch(webpackGoodRegEx, 10000))) .then(() => killAllProcesses(), (err: any) => { killAllProcesses(); throw err; diff --git a/tests/e2e/tests/misc/live-reload.ts b/tests/e2e/tests/misc/live-reload.ts index 159d6d6e00dc..294dedfc0a62 100644 --- a/tests/e2e/tests/misc/live-reload.ts +++ b/tests/e2e/tests/misc/live-reload.ts @@ -104,7 +104,7 @@ export default function () { // Let app run. .then(_ => wait(2000)) .then(_ => appendToFile('src/main.ts', 'console.log(1);')) - .then(_ => waitForAnyProcessOutputToMatch(webpackGoodRegEx, 5000)) + .then(_ => waitForAnyProcessOutputToMatch(webpackGoodRegEx, 10000)) .then(_ => wait(2000)) .then(_ => { if (liveReloadCount != 2) { @@ -123,7 +123,7 @@ export default function () { )) .then(_ => wait(2000)) .then(_ => appendToFile('src/main.ts', 'console.log(1);')) - .then(_ => waitForAnyProcessOutputToMatch(webpackGoodRegEx, 5000)) + .then(_ => waitForAnyProcessOutputToMatch(webpackGoodRegEx, 10000)) .then(_ => wait(2000)) .then(_ => { if (liveReloadCount != 1) { @@ -157,7 +157,7 @@ export default function () { )) .then(_ => wait(2000)) .then(_ => appendToFile('src/main.ts', 'console.log(1);')) - .then(_ => waitForAnyProcessOutputToMatch(webpackGoodRegEx, 5000)) + .then(_ => waitForAnyProcessOutputToMatch(webpackGoodRegEx, 10000)) .then(_ => wait(2000)) .then(_ => { if (liveReloadCount != 2) { diff --git a/tests/e2e/tests/packages/webpack/test-ng5.ts b/tests/e2e/tests/packages/webpack/test-ng5.ts new file mode 100644 index 000000000000..3b1ef8f14da6 --- /dev/null +++ b/tests/e2e/tests/packages/webpack/test-ng5.ts @@ -0,0 +1,23 @@ +import {normalize} from 'path'; +import {createProjectFromAsset} from '../../../utils/assets'; +import {exec} from '../../../utils/process'; +import {expectFileSizeToBeUnder, replaceInFile, expectFileToMatch} from '../../../utils/fs'; + + +export default function(skipCleaning: () => void) { + return Promise.resolve() + .then(() => createProjectFromAsset('webpack/test-app-ng5')) + .then(() => exec(normalize('node_modules/.bin/webpack'), '-p')) + .then(() => expectFileSizeToBeUnder('dist/app.main.js', 420000)) + .then(() => expectFileSizeToBeUnder('dist/0.app.main.js', 10000)) + // test resource urls without ./ + .then(() => replaceInFile('app/app.component.ts', + './app.component.html', 'app.component.html')) + .then(() => replaceInFile('app/app.component.ts', + './app.component.scss', 'app.component.scss')) + // test the inclusion of metadata + // This build also test resource URLs without ./ + .then(() => exec(normalize('node_modules/.bin/webpack'))) + .then(() => expectFileToMatch('dist/app.main.js', 'AppModuleNgFactory')) + .then(() => skipCleaning()); +} diff --git a/tests/e2e/tests/packages/webpack/weird-ng5.ts b/tests/e2e/tests/packages/webpack/weird-ng5.ts new file mode 100644 index 000000000000..c239d05fe75e --- /dev/null +++ b/tests/e2e/tests/packages/webpack/weird-ng5.ts @@ -0,0 +1,34 @@ +import {normalize} from 'path'; + +import {createProjectFromAsset} from '../../../utils/assets'; +import {exec} from '../../../utils/process'; +import {updateJsonFile} from '../../../utils/project'; +import {expectFileSizeToBeUnder, expectFileToExist, expectFileToMatch} from '../../../utils/fs'; +import {expectToFail} from '../../../utils/utils'; + + +export default function(skipCleaning: () => void) { + return Promise.resolve() + .then(() => createProjectFromAsset('webpack/test-app-weird-ng5')) + .then(() => exec(normalize('node_modules/.bin/webpack'), '-p')) + .then(() => expectFileToExist('dist/app.main.js')) + .then(() => expectFileToExist('dist/0.app.main.js')) + .then(() => expectFileToExist('dist/1.app.main.js')) + .then(() => expectFileToExist('dist/2.app.main.js')) + .then(() => expectFileSizeToBeUnder('dist/app.main.js', 410000)) + .then(() => expectFileSizeToBeUnder('dist/0.app.main.js', 40000)) + + // Verify that we're using the production environment. + .then(() => expectFileToMatch('dist/app.main.js', /PRODUCTION_ONLY/)) + .then(() => expectToFail(() => expectFileToMatch('dist/app.main.js', /DEBUG_ONLY/))) + + // Verify that we're using the debug environment now. + .then(() => updateJsonFile('webpack.flags.json', json => { + json['DEBUG'] = true; + })) + .then(() => exec(normalize('node_modules/.bin/webpack'), '-p')) + .then(() => expectFileToMatch('dist/app.main.js', /DEBUG_ONLY/)) + .then(() => expectToFail(() => expectFileToMatch('dist/app.main.js', /PRODUCTION_ONLY/))) + + .then(() => skipCleaning()); +} diff --git a/tests/e2e/utils/process.ts b/tests/e2e/utils/process.ts index 9b97300a8f9d..b6bc985fd664 100644 --- a/tests/e2e/utils/process.ts +++ b/tests/e2e/utils/process.ts @@ -162,6 +162,8 @@ export function ng(...args: string[]) { // Wait 1 second before running any end-to-end test. return new Promise(resolve => setTimeout(resolve, 1000)) .then(() => maybeSilentNg(...args)); + } else if (argv.nightly && args.includes('--aot')) { + args.push('--experimental-angular-compiler'); } return maybeSilentNg(...args); diff --git a/tests/e2e/utils/project.ts b/tests/e2e/utils/project.ts index 093dc49bd92c..c3eaf2cfb264 100644 --- a/tests/e2e/utils/project.ts +++ b/tests/e2e/utils/project.ts @@ -68,6 +68,7 @@ export function useBuiltPackages() { } export function useSha() { + const argv = getGlobalVariable('argv'); if (argv.nightly || argv['ng-sha']) { const label = argv['ng-sha'] ? `#2.0.0-${argv['ng-sha']}` : ''; return updateJsonFile('package.json', json => {