diff --git a/packages/ngtools/webpack/BUILD.bazel b/packages/ngtools/webpack/BUILD.bazel index c947a09c9133..87e40d56bda5 100644 --- a/packages/ngtools/webpack/BUILD.bazel +++ b/packages/ngtools/webpack/BUILD.bazel @@ -35,8 +35,6 @@ ts_library( module_name = "@ngtools/webpack", module_root = "src/index.d.ts", deps = [ - "//packages/angular_devkit/core", - "//packages/angular_devkit/core/node", "@npm//@angular/compiler-cli", "@npm//@types/node", "@npm//@types/webpack", diff --git a/packages/ngtools/webpack/README.md b/packages/ngtools/webpack/README.md index 79e5dec34c72..54be0bc51bd4 100644 --- a/packages/ngtools/webpack/README.md +++ b/packages/ngtools/webpack/README.md @@ -1,77 +1,38 @@ -# Angular Ahead-of-Time Webpack Plugin +# Angular Compiler Webpack Plugin -Webpack 4.0 plugin that AoT compiles your Angular components and modules. +Webpack 4.x/5.x plugin for the Angular Ahead-of-Time compiler. The plugin also supports Angular JIT mode. ## Usage In your webpack config, add the following plugin and loader. -Angular version 5 and up, use `AngularCompilerPlugin`: - ```typescript -import { AngularCompilerPlugin } from '@ngtools/webpack'; +import { AngularWebpackPlugin } from '@ngtools/webpack'; exports = { /* ... */ module: { rules: [ { - test: /(?:\.ngfactory\.js|\.ngstyle\.js|\.ts)$/, + test: /\.[jt]sx?$/, loader: '@ngtools/webpack' } ] }, plugins: [ - new AngularCompilerPlugin({ - tsConfigPath: 'path/to/tsconfig.json', - entryModule: 'path/to/app.module#AppModule', - sourceMap: true, - i18nInFile: 'path/to/translations.en.xlf', - i18nInFormat: 'xlf', - i18nOutFile: 'path/to/translations.xlf', - i18nOutFormat: 'xlf', - locale: 'en', - hostReplacementPaths: { - 'path/to/config.development.ts': 'path/to/config.production.ts' - } + new AngularWebpackPlugin({ + tsconfig: 'path/to/tsconfig.json', }) ] }; ``` -The loader works with webpack plugin to compile your TypeScript. It's important to include both, and to not include any other TypeScript compiler loader. +The loader works with webpack plugin to compile the application's TypeScript. It is important to include both, and to not include any other TypeScript loader. ## 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 class name 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. -* `sourceMap`. Optional. Include sourcemaps. -* `compilerOptions`. Optional. Override options in `tsconfig.json`. -* `contextElementDependencyConstructor`. Optional. Set to `require('webpack/lib/dependencies/ContextElementDependency')` if you are having `No module factory available for dependency type: ContextElementDependency` errors. -* `directTemplateLoading`. Optional. It causes the plugin to load component templates (HTML) directly from the filesystem. This is more efficient if only using the `raw-loader` to load component templates. Do not enable this option if additional loaders are configured for component templates. -* `forkTypeChecker`. Optional, defaults to `true`. Run the TypeScript type checker in a forked process. -* `hostReplacementPaths`. Optional. It allows replacing resources with other resources in the build. -* `platform`. Optional, defaults to `0`. Possible values are `0` and `1`. `0` stands for browser and `1` for server. -* `logger`. Optional. A custom logger that sends information to STDOUT and STDERR. -* `nameLazyFiles`. Optional. If `true` then uses the `[request]` placeholder to set dynamic chunk names. -* `missingTranslation`. Optional and only used for View Engine compilations. defaults to `warning`. Possible values are `warning`, `error` or `ignore`. Determines how to handle missing translations for i18n. -* `i18nInFile`. Optional and only used for View Engine compilations. Localization file to use for i18n. -* `i18nInFormat`. Optional and only used for View Engine compilations. The format of the localization file. -* `i18nOutFile`. Optional and only used for View Engine compilations. The name of the file to write extractions to. -* `i18nOutFormat`. Optional and only used for View Engine compilations. The format of the localization file where extractions will be written to. -* `locale`. Optional and only used for View Engine compilations. Locale to use for i18n. -## Features -The benefits and ability of using [`@ngtools/webpack`](https://www.npmjs.com/~ngtools) standalone from the Angular CLI as presented in [Stephen Fluin's Angular CLI talk](https://youtu.be/uBRK6cTr4Vk?t=6m45s) at Angular Connect 2016: - -* Compiles Sass/Less into CSS -* TypeScript transpilation -* Bundles JavaScript, CSS -* Asset optimization -* Virtual filesystem for assets -* For serving local assets and compile versions. -* Live-reload via websockets -* Code splitting -* Recognizing the use of `loadChildren` in the router, and bundling those modules separately so that any dependencies of those modules are not going to be loaded as part of your main bundle. These separate bundles will be pulled out of the critical path of your application, making your total application bundle much smaller and loading it much more performant. +* `tsconfig` [default: `tsconfig.json`] - The path to the application's TypeScript Configuration file. In the `tsconfig.json`, you can pass options to the Angular Compiler with `angularCompilerOptions`. Relative paths will be resolved from the Webpack compilation's context. +* `compilerOptions` [default: none] - Overrides options in the application's TypeScript Configuration file (`tsconfig.json`). +* `jitMode` [default: `false`] - Enables JIT compilation 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. +* `directTemplateLoading` [default: `true`] - Causes the plugin to load component templates (HTML) directly from the filesystem. This is more efficient if only using the `raw-loader` to load component templates. Do not enable this option if additional loaders are configured for component templates. +* `fileReplacements` [default: none] - Allows replacing TypeScript files with other TypeScript files in the build. This option acts on fully resolved file paths. diff --git a/packages/ngtools/webpack/package.json b/packages/ngtools/webpack/package.json index ffcc2c671449..73ccefe661dc 100644 --- a/packages/ngtools/webpack/package.json +++ b/packages/ngtools/webpack/package.json @@ -21,7 +21,6 @@ }, "homepage": "https://github.com/angular/angular-cli/tree/master/packages/@ngtools/webpack", "dependencies": { - "@angular-devkit/core": "0.0.0", "enhanced-resolve": "5.7.0", "webpack-sources": "2.2.0" }, @@ -31,6 +30,7 @@ "webpack": "^4.0.0 || ^5.20.0" }, "devDependencies": { + "@angular-devkit/core": "0.0.0", "@angular/compiler": "12.0.0-next.7", "@angular/compiler-cli": "12.0.0-next.7", "typescript": "4.2.3", diff --git a/packages/ngtools/webpack/src/angular_compiler_plugin.ts b/packages/ngtools/webpack/src/angular_compiler_plugin.ts deleted file mode 100644 index d44b8a4e341d..000000000000 --- a/packages/ngtools/webpack/src/angular_compiler_plugin.ts +++ /dev/null @@ -1,1243 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { - Path, - dirname, - getSystemPath, - logging, - normalize, - resolve, - virtualFs, -} from '@angular-devkit/core'; -import { createConsoleLogger } from '@angular-devkit/core/node'; -import { - CompilerHost, - CompilerOptions, - DEFAULT_ERROR_CODE, - Diagnostic, - EmitFlags, - Program, - SOURCE, - UNKNOWN_ERROR_CODE, - createCompilerHost, - createProgram, - formatDiagnostics, - readConfiguration, -} from '@angular/compiler-cli'; -import { constructorParametersDownlevelTransform } from '@angular/compiler-cli/src/tooling'; -import { ChildProcess, ForkOptions, fork } from 'child_process'; -import * as fs from 'fs'; -import * as path from 'path'; -import * as ts from 'typescript'; -import { Compiler, WebpackFourCompiler, compilation } from 'webpack'; -import { time, timeEnd } from './benchmark'; -import { WebpackCompilerHost } from './compiler_host'; -import { DiagnosticMode, gatherDiagnostics, hasErrors, reportDiagnostics } from './diagnostics'; -import { resolveEntryModuleFromMain } from './entry_resolver'; -import { - AngularCompilerPluginOptions, - PLATFORM, -} from './interfaces'; -import { NgccProcessor } from './ngcc_processor'; -import { TypeScriptPathsPlugin } from './paths-plugin'; -import { WebpackResourceLoader } from './resource_loader'; -import { - exportNgFactory, - findResources, - importFactory, - registerLocaleData, - removeDecorators, - replaceBootstrap, - replaceResources, - replaceServerBootstrap, -} from './transformers'; -import { collectDeepNodes } from './transformers/ast_helpers'; -import { removeIvyJitSupportCalls } from './transformers/remove-ivy-jit-support-calls'; -import { - AUTO_START_ARG, -} from './type_checker'; -import { - InitMessage, - LogMessage, - MESSAGE_KIND, - UpdateMessage, -} from './type_checker_messages'; -import { flattenArray, forwardSlashPath, workaroundResolve } from './utils'; -import { - VirtualFileSystemDecorator, - VirtualWatchFileSystemDecorator, -} from './virtual_file_system_decorator'; -import { addError, addWarning } from './webpack-diagnostics'; -import { createWebpackInputHost } from './webpack-input-host'; -import { isWebpackFiveOrHigher, mergeResolverMainFields } from './webpack-version'; - -export class AngularCompilerPlugin { - private _options: AngularCompilerPluginOptions; - - // TS compilation. - // The majority of these are initialized in _setupOptions which is called from the constructor. - private _compilerOptions!: CompilerOptions; - private _rootNames!: string[]; - private _program: (ts.Program | Program) | undefined; - private _compilerHost!: WebpackCompilerHost & CompilerHost; - private _moduleResolutionCache!: ts.ModuleResolutionCache; - private _resourceLoader?: WebpackResourceLoader; - private _useFactories = false; - private _tsConfigPath!: string; - private _entryModule: string | null = null; - private _mainPath: string | undefined; - private _basePath!: string; - private _transformers: ts.TransformerFactory[] = []; - private _platformTransformers: ts.TransformerFactory[] | null = null; - private _platform!: PLATFORM; - private _JitMode = false; - private _emitSkipped = true; - // This is needed because if the first build fails we need to do a full emit - // even whe only a single file gets updated. - private _hadFullJitEmit: boolean | undefined; - private _unusedFiles = new Set(); - private _typeDeps = new Set(); - private _changedFileExtensions = new Set(['ts', 'tsx', 'html', 'css', 'js', 'json']); - private _nodeModulesRegExp = /[\\\/]node_modules[\\\/]/; - - // Webpack plugin. - private _firstRun = true; - private _donePromise: Promise | null = null; - private _normalizedLocale: string | null = null; - private _warnings: string[] = []; - private _errors: string[] = []; - - // TypeChecker process. - private _forkTypeChecker = true; - private _typeCheckerProcess: ChildProcess | null = null; - private _forkedTypeCheckerInitialized = false; - - // Logging. - private _logger: logging.Logger; - - private _mainFields: string[] = []; - - constructor(options: AngularCompilerPluginOptions) { - this._options = Object.assign({}, options); - this._logger = options.logger || createConsoleLogger(); - this._setupOptions(this._options); - } - - get options() { return this._options; } - get done() { return this._donePromise; } - get entryModule() { - if (!this._entryModule) { - return null; - } - const splitted = this._entryModule.split(/(#[a-zA-Z_]([\w]+))$/); - const path = splitted[0]; - const className = !!splitted[1] ? splitted[1].substring(1) : 'default'; - - return { path, className }; - } - - get typeChecker(): ts.TypeChecker | null { - const tsProgram = this._getTsProgram(); - - return tsProgram ? tsProgram.getTypeChecker() : null; - } - - private _setupOptions(options: AngularCompilerPluginOptions) { - time('AngularCompilerPlugin._setupOptions'); - - // 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 = forwardSlashPath(options.tsConfigPath); - - // 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.basePath !== undefined) { - basePath = path.resolve(process.cwd(), options.basePath); - } - - // Parse the tsconfig contents. - const { errors, rootNames, options: compilerOptions } = readConfiguration(this._tsConfigPath, options.compilerOptions); - if (errors && errors.length) { - throw new Error(formatDiagnostics(errors)); - } - - this._rootNames = rootNames; - this._compilerOptions = compilerOptions; - this._basePath = compilerOptions.basePath || basePath || ''; - - // Overwrite outDir so we can find generated files next to their .ts origin in compilerHost. - this._compilerOptions.outDir = ''; - this._compilerOptions.suppressOutputPathCheck = true; - - // 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.mapRoot = undefined; - // We will set the source to the full path of the file in the loader, so we don't - // need sourceRoot here. - this._compilerOptions.sourceRoot = undefined; - } else { - this._compilerOptions.sourceMap = false; - this._compilerOptions.sourceRoot = undefined; - this._compilerOptions.inlineSources = undefined; - this._compilerOptions.inlineSourceMap = undefined; - this._compilerOptions.mapRoot = undefined; - this._compilerOptions.sourceRoot = undefined; - } - - // We want to allow emitting with errors so that imports can be added - // to the webpack dependency tree and rebuilds triggered by file edits. - this._compilerOptions.noEmitOnError = false; - - // Set JIT (no code generation) or AOT mode. - if (options.skipCodeGeneration !== undefined) { - this._JitMode = options.skipCodeGeneration; - } - - // Process i18n options. - if (options.i18nInFile !== undefined) { - this._compilerOptions.i18nInFile = options.i18nInFile; - } - if (options.i18nInFormat !== undefined) { - this._compilerOptions.i18nInFormat = options.i18nInFormat; - } - if (options.i18nOutFile !== undefined) { - this._compilerOptions.i18nOutFile = options.i18nOutFile; - } - if (options.i18nOutFormat !== undefined) { - this._compilerOptions.i18nOutFormat = options.i18nOutFormat; - } - if (options.locale !== undefined) { - this._compilerOptions.i18nInLocale = options.locale; - this._compilerOptions.i18nOutLocale = options.locale; - this._normalizedLocale = this._validateLocale(options.locale); - } - if (options.missingTranslation !== undefined) { - this._compilerOptions.i18nInMissingTranslations = - options.missingTranslation as 'error' | 'warning' | 'ignore'; - } - - // For performance, disable AOT decorator downleveling transformer for applications in the CLI. - // The transformer is not needed for VE or Ivy in this plugin since Angular decorators are removed. - // While the transformer would make no changes, it would still need to walk each source file AST. - this._compilerOptions.annotationsAs = 'decorators' as 'decorators'; - - // Process forked type checker options. - if (options.forkTypeChecker !== undefined) { - this._forkTypeChecker = options.forkTypeChecker; - } - // this._forkTypeChecker = false; - - // Add custom platform transformers. - if (options.platformTransformers !== undefined) { - this._platformTransformers = options.platformTransformers; - } - - if (this._compilerOptions.strictMetadataEmit) { - this._warnings.push( - `Using Angular compiler option 'strictMetadataEmit' for applications might cause undefined behavior.`, - ); - } - - if (!this._JitMode && !this._compilerOptions.enableIvy) { - // Only attempt to use factories when AOT and not Ivy. - this._useFactories = true; - } - - // 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._compilerOptions.entryModule) { - this._entryModule = path.resolve(this._basePath, - this._compilerOptions.entryModule as string); // temporary cast for type issue - } - - // Set platform. - this._platform = options.platform || PLATFORM.Browser; - - // Make transformers. - this._makeTransformers(); - - timeEnd('AngularCompilerPlugin._setupOptions'); - } - - private _getTsProgram() { - if (!this._program) { - return undefined; - } - - return this._JitMode ? this._program as ts.Program : (this._program as Program).getTsProgram(); - } - - updateChangedFileExtensions(extension: string) { - if (extension) { - this._changedFileExtensions.add(extension); - } - } - - private _getChangedCompilationFiles() { - return this._compilerHost.getChangedFilePaths() - .filter(k => { - for (const ext of this._changedFileExtensions) { - if (k.endsWith(ext)) { - return true; - } - } - - return false; - }); - } - - private async _createOrUpdateProgram() { - // Get the root files from the ts config. - // When a new root name (like a lazy route) is added, it won't be available from - // following imports on the existing files, so we need to get the new list of root files. - const config = readConfiguration(this._tsConfigPath); - this._rootNames = config.rootNames; - - // Update the forked type checker with all changed compilation files. - // This includes templates, that also need to be reloaded on the type checker. - if (this._forkTypeChecker && this._typeCheckerProcess && !this._firstRun) { - this._updateForkedTypeChecker(this._rootNames, this._getChangedCompilationFiles()); - } - - const oldTsProgram = this._getTsProgram(); - - if (this._JitMode) { - // Create the TypeScript program. - time('AngularCompilerPlugin._createOrUpdateProgram.ts.createProgram'); - this._program = ts.createProgram( - this._rootNames, - this._compilerOptions, - this._compilerHost, - oldTsProgram, - ); - timeEnd('AngularCompilerPlugin._createOrUpdateProgram.ts.createProgram'); - } else { - time('AngularCompilerPlugin._createOrUpdateProgram.ng.createProgram'); - // Create the Angular program. - this._program = createProgram({ - rootNames: this._rootNames, - options: this._compilerOptions, - host: this._compilerHost, - oldProgram: this._program as Program, - }); - timeEnd('AngularCompilerPlugin._createOrUpdateProgram.ng.createProgram'); - - time('AngularCompilerPlugin._createOrUpdateProgram.ng.loadNgStructureAsync'); - await this._program.loadNgStructureAsync(); - timeEnd('AngularCompilerPlugin._createOrUpdateProgram.ng.loadNgStructureAsync'); - } - - const newTsProgram = this._getTsProgram(); - const newProgramSourceFiles = newTsProgram?.getSourceFiles(); - const localDtsFiles = new Set( - newProgramSourceFiles?.filter( - f => f.isDeclarationFile && !this._nodeModulesRegExp.test(f.fileName), - ) - .map(f => this._compilerHost.denormalizePath(f.fileName)), - ); - - if (!oldTsProgram) { - // Add all non node package dts files as depedencies when not having an old program - for (const dts of localDtsFiles) { - this._typeDeps.add(dts); - } - } else if (oldTsProgram && newProgramSourceFiles) { - // The invalidation should only happen if we have an old program - // as otherwise we will invalidate all the sourcefiles. - const oldFiles = new Set(oldTsProgram.getSourceFiles().map(sf => sf.fileName)); - const newProgramFiles = new Set(newProgramSourceFiles.map(sf => sf.fileName)); - - for (const dependency of this._typeDeps) { - // Remove type dependencies of no longer existing files - if (!newProgramFiles.has(forwardSlashPath(dependency))) { - this._typeDeps.delete(dependency); - } - } - - for (const fileName of newProgramFiles) { - if (oldFiles.has(fileName)) { - continue; - } - - this._compilerHost.invalidate(fileName); - - const denormalizedFileName = this._compilerHost.denormalizePath(fileName); - if (localDtsFiles.has(denormalizedFileName)) { - // Add new dts file as a type depedency - this._typeDeps.add(denormalizedFileName); - } - } - } - - // If there's still no entryModule try to resolve from mainPath. - if (!this._entryModule && this._mainPath) { - time('AngularCompilerPlugin._make.resolveEntryModuleFromMain'); - this._entryModule = resolveEntryModuleFromMain( - this._mainPath, this._compilerHost, this._getTsProgram() as ts.Program); - timeEnd('AngularCompilerPlugin._make.resolveEntryModuleFromMain'); - } - } - - private _createForkedTypeChecker() { - const typeCheckerFile = './type_checker_worker.js'; - - const debugArgRegex = /--inspect(?:-brk|-port)?|--debug(?:-brk|-port)/; - - const execArgv = process.execArgv.filter((arg) => { - // Remove debug args. - // Workaround for https://github.com/nodejs/node/issues/9435 - return !debugArgRegex.test(arg); - }); - // Signal the process to start listening for messages - // Solves https://github.com/angular/angular-cli/issues/9071 - const forkArgs = [AUTO_START_ARG]; - const forkOptions: ForkOptions = { execArgv }; - - this._typeCheckerProcess = fork( - path.resolve(__dirname, typeCheckerFile), - forkArgs, - forkOptions); - - // Handle child messages. - this._typeCheckerProcess.on('message', message => { - switch (message.kind) { - case MESSAGE_KIND.Log: - const logMessage = message as LogMessage; - this._logger.log(logMessage.level, `\n${logMessage.message}`); - break; - default: - throw new Error(`TypeChecker: Unexpected message received: ${message}.`); - } - }); - - // Handle child process exit. - this._typeCheckerProcess.once('exit', (_, signal) => { - this._typeCheckerProcess = null; - - // If process exited not because of SIGTERM (see _killForkedTypeChecker), than something - // went wrong and it should fallback to type checking on the main thread. - if (signal !== 'SIGTERM') { - this._forkTypeChecker = false; - const msg = 'AngularCompilerPlugin: Forked Type Checker exited unexpectedly. ' + - 'Falling back to type checking on main thread.'; - this._warnings.push(msg); - } - }); - } - - private _killForkedTypeChecker() { - if (this._typeCheckerProcess && !this._typeCheckerProcess.killed) { - try { - this._typeCheckerProcess.kill(); - } catch {} - this._typeCheckerProcess = null; - } - } - - private _updateForkedTypeChecker(rootNames: string[], changedCompilationFiles: string[]) { - if (this._typeCheckerProcess) { - if (!this._forkedTypeCheckerInitialized) { - let hostReplacementPaths = {}; - if (this._options.hostReplacementPaths - && typeof this._options.hostReplacementPaths != 'function') { - hostReplacementPaths = this._options.hostReplacementPaths; - } - this._typeCheckerProcess.send(new InitMessage(this._compilerOptions, this._basePath, - this._JitMode, this._rootNames, hostReplacementPaths)); - this._forkedTypeCheckerInitialized = true; - } - this._typeCheckerProcess.send(new UpdateMessage(rootNames, changedCompilationFiles)); - } - } - - private _checkUnusedFiles(compilation: compilation.Compilation) { - // Only do the unused TS files checks when under Ivy - // since previously we did include unused files in the compilation - // See: https://github.com/angular/angular-cli/pull/15030 - // Don't do checks for compilations with errors, since that can result in a partial compilation. - if (!this._compilerOptions.enableIvy || compilation.errors.length > 0) { - return; - } - - // Bail if there's no TS program. Nothing to do in that case. - const program = this._getTsProgram(); - if (!program) { - return; - } - - // Exclude the following files from unused checks - // - ngfactories & ngstyle might not have a correspondent - // JS file example `@angular/core/core.ngfactory.ts`. - // - ngtypecheck.ts and __ng_typecheck__.ts are used for type-checking in Ivy. - const fileExcludeRegExp = /(\.(ngfactory|ngstyle|ngsummary|ngtypecheck)\.ts|ng_typecheck__\.ts)$/; - - // Start all the source file names we care about. - // Ignore matches to the regexp above, files we've already reported once before, and - // node_modules. - const sourceFiles = program.getSourceFiles() - .map(x => this._compilerHost.denormalizePath(x.fileName)) - .filter(f => !(fileExcludeRegExp.test(f) || this._unusedFiles.has(f) - || this._nodeModulesRegExp.test(f))); - - // Make a set with the sources, but exclude .d.ts files since those are type-only. - const unusedSourceFileNames = new Set(sourceFiles.filter(f => !f.endsWith('.d.ts'))); - // Separately keep track of type-only deps. - const typeDepFileNames = new Set(sourceFiles); - - // This function removes a source file name and all its dependencies from the set. - const removeSourceFile = (fileName: string, originalModule = false) => { - if (unusedSourceFileNames.has(fileName) || (originalModule && typeDepFileNames.has(fileName))) { - unusedSourceFileNames.delete(fileName); - if (originalModule) { - typeDepFileNames.delete(fileName); - } - this.getDependencies(fileName, false).forEach(f => removeSourceFile(f)); - } - }; - - // Go over all the modules in the webpack compilation and remove them from the sets. - // tslint:disable-next-line: no-any - compilation.modules.forEach((m: compilation.Module & { resource?: string }) => - m.resource ? removeSourceFile(m.resource, true) : null, - ); - - // Anything that remains is unused, because it wasn't referenced directly or transitively - // on the files in the compilation. - for (const fileName of unusedSourceFileNames) { - addWarning( - compilation, - `${fileName} is part of the TypeScript compilation but it's unused.\n` + - `Add only entry points to the 'files' or 'include' properties in your tsconfig.`, - ); - this._unusedFiles.add(fileName); - // Remove the truly unused from the type dep list. - typeDepFileNames.delete(fileName); - } - - // At this point we know what the type deps are. - // These are the TS files that weren't part of the compilation modules, aren't unused, but were - // part of the TS original source list. - // Next build we add them to the TS entry points so that they trigger rebuilds. - for (const fileName of typeDepFileNames) { - this._typeDeps.add(fileName); - } - } - - // Registration hook for webpack plugin. - // tslint:disable-next-line:no-big-function - apply(webpackCompiler: Compiler | WebpackFourCompiler) { - const compiler = webpackCompiler as Compiler & { - watchMode?: boolean; - parentCompilation?: compilation.Compilation; - watchFileSystem?: unknown; - }; - // The below is require by NGCC processor - // since we need to know which fields we need to process - compiler.hooks.environment.tap('angular-compiler', () => { - const { options } = compiler; - const mainFields = options.resolve && options.resolve.mainFields; - if (mainFields) { - this._mainFields = flattenArray(mainFields); - } - }); - - // cleanup if not watching - compiler.hooks.thisCompilation.tap('angular-compiler', compilation => { - compilation.hooks.finishModules.tap('angular-compiler', () => { - this._checkUnusedFiles(compilation); - - let rootCompiler = compiler; - while (rootCompiler.parentCompilation) { - // tslint:disable-next-line:no-any - rootCompiler = compiler.parentCompilation as any; - } - - // only present for webpack 4.23.0+, assume true otherwise - const watchMode = rootCompiler.watchMode === undefined ? true : rootCompiler.watchMode; - if (!watchMode) { - this._program = undefined; - this._transformers = []; - this._resourceLoader = undefined; - this._compilerHost.reset(); - } - }); - }); - - // Decorate inputFileSystem to serve contents of CompilerHost. - // Use decorated inputFileSystem in watchFileSystem. - compiler.hooks.environment.tap('angular-compiler', () => { - let host: virtualFs.Host = this._options.host || createWebpackInputHost( - compiler.inputFileSystem, - ); - - let replacements: Map | ((path: Path) => Path) | undefined; - if (this._options.hostReplacementPaths) { - if (typeof this._options.hostReplacementPaths == 'function') { - const replacementResolver = this._options.hostReplacementPaths; - replacements = path => normalize(replacementResolver(getSystemPath(path))); - host = new class extends virtualFs.ResolverHost { - _resolve(path: Path) { - return normalize(replacementResolver(getSystemPath(path))); - } - }(host); - } else { - replacements = new Map(); - const aliasHost = new virtualFs.AliasHost(host); - for (const from in this._options.hostReplacementPaths) { - const normalizedFrom = resolve(normalize(this._basePath), normalize(from)); - const normalizedWith = resolve( - normalize(this._basePath), - normalize(this._options.hostReplacementPaths[from]), - ); - aliasHost.aliases.set(normalizedFrom, normalizedWith); - replacements.set(normalizedFrom, normalizedWith); - } - host = aliasHost; - } - } - - let ngccProcessor: NgccProcessor | undefined; - if (this._compilerOptions.enableIvy) { - ngccProcessor = new NgccProcessor( - this._mainFields, - this._warnings, - this._errors, - this._basePath, - this._tsConfigPath, - compiler.inputFileSystem, - compiler.options.resolve?.symlinks, - ); - - ngccProcessor.process(); - } - - // Use an identity function as all our paths are absolute already. - this._moduleResolutionCache = ts.createModuleResolutionCache(this._basePath, x => x); - - // Create the webpack compiler host. - const webpackCompilerHost = new WebpackCompilerHost( - this._compilerOptions, - this._basePath, - host, - true, - this._options.directTemplateLoading, - ngccProcessor, - this._moduleResolutionCache, - ); - - // Create and set a new WebpackResourceLoader in AOT - if (!this._JitMode) { - this._resourceLoader = new WebpackResourceLoader(); - webpackCompilerHost.setResourceLoader(this._resourceLoader); - } - - // Use the WebpackCompilerHost with a resource loader to create an AngularCompilerHost. - this._compilerHost = createCompilerHost({ - options: this._compilerOptions, - tsHost: webpackCompilerHost, - }) as CompilerHost & WebpackCompilerHost; - - // Resolve mainPath if provided. - if (this._options.mainPath) { - this._mainPath = this._compilerHost.resolve(this._options.mainPath); - } - - const inputDecorator = new VirtualFileSystemDecorator( - compiler.inputFileSystem, - this._compilerHost, - ); - compiler.inputFileSystem = inputDecorator; - compiler.watchFileSystem = new VirtualWatchFileSystemDecorator( - inputDecorator, - replacements, - ); - }); - - // Create and destroy forked type checker on watch mode. - compiler.hooks.watchRun.tap('angular-compiler', () => { - if (this._forkTypeChecker && !this._typeCheckerProcess) { - this._createForkedTypeChecker(); - } - }); - compiler.hooks.watchClose.tap('angular-compiler', () => this._killForkedTypeChecker()); - - // Remake the plugin on each compilation. - compiler.hooks.make.tapPromise( - 'angular-compiler', - compilation => this._donePromise = this._make(compilation), - ); - compiler.hooks.invalid.tap('angular-compiler', () => this._firstRun = false); - compiler.hooks.afterEmit.tap('angular-compiler', compilation => { - // tslint:disable-next-line:no-any - (compilation as any)._ngToolsWebpackPluginInstance = null; - }); - compiler.hooks.done.tap('angular-compiler', () => { - this._donePromise = null; - }); - - compiler.hooks.afterResolvers.tap('angular-compiler', compiler => { - if (this._compilerOptions.enableIvy) { - // When Ivy is enabled we need to add the fields added by NGCC - // to take precedence over the provided mainFields. - // NGCC adds fields in package.json suffixed with '_ivy_ngcc' - // Example: module -> module_ivy_ngcc - // tslint:disable-next-line:no-any - (compiler as any).resolverFactory.hooks.resolveOptions - .for('normal') - // tslint:disable-next-line:no-any - .tap('WebpackOptionsApply', (resolveOptions: any) => { - const originalMainFields: string[] = resolveOptions.mainFields; - const ivyMainFields = originalMainFields.map(f => `${f}_ivy_ngcc`); - - return mergeResolverMainFields(resolveOptions, originalMainFields, ivyMainFields); - }); - } - - // tslint:disable-next-line: no-any - (compiler as any).resolverFactory.hooks.resolveOptions - .for('normal') - .tap('angular-compiler', (resolveOptions: { plugins: unknown[] }) => { - if (!resolveOptions.plugins) { - resolveOptions.plugins = []; - } - resolveOptions.plugins.push(new TypeScriptPathsPlugin(this._compilerOptions)); - - return resolveOptions; - }); - - compiler.hooks.normalModuleFactory.tap('angular-compiler', nmf => { - // Virtual file system. - // TODO: consider if it's better to remove this plugin and instead make it wait on the - // VirtualFileSystemDecorator. - // 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. - nmf.hooks.beforeResolve.tapPromise( - 'angular-compiler', - async (request) => { - if (this.done && request) { - const name = request.request; - const issuer = request.contextInfo.issuer; - if (name.endsWith('.ts') || name.endsWith('.tsx') - || (issuer && /\.ts|ngfactory\.js$/.test(issuer))) { - try { - await this.done; - } catch { } - } - } - - if (!isWebpackFiveOrHigher()) { - return request; - } - }, - ); - }); - }); - } - - private async _make(compilation: compilation.Compilation) { - time('AngularCompilerPlugin._make'); - // tslint:disable-next-line:no-any - if ((compilation as any)._ngToolsWebpackPluginInstance) { - throw new Error('An @ngtools/webpack plugin already exist for this compilation.'); - } - - // If there is no compiler host at this point, it means that the environment hook did not run. - // This happens in child compilations that inherit the parent compilation file system. - // Node: child compilations also do not run most webpack compiler hooks, including almost all - // we use here. The child compiler will always run as if it was the first build. - if (this._compilerHost === undefined) { - const inputFs = compilation.compiler.inputFileSystem as VirtualFileSystemDecorator; - if (!inputFs.getWebpackCompilerHost) { - throw new Error('AngularCompilerPlugin is running in a child compilation, but could' + - 'not find a WebpackCompilerHost in the parent compilation.'); - } - - // Use the existing WebpackCompilerHost to ensure builds and rebuilds work. - this._compilerHost = createCompilerHost({ - options: this._compilerOptions, - tsHost: inputFs.getWebpackCompilerHost(), - }) as CompilerHost & WebpackCompilerHost; - } - - // Set a private variable for this plugin instance. - // tslint:disable-next-line:no-any - (compilation as any)._ngToolsWebpackPluginInstance = this; - - // Update the resource loader with the new webpack compilation. - if (this._resourceLoader) { - this._resourceLoader.update(compilation); - } - - try { - await this._update(); - this.pushCompilationErrors(compilation); - } catch (err) { - addError(compilation, err.message || err); - this.pushCompilationErrors(compilation); - } - - timeEnd('AngularCompilerPlugin._make'); - } - - private pushCompilationErrors(compilation: compilation.Compilation) { - this._errors.forEach((error) => addError(compilation, error)); - this._warnings.forEach((warning) => addWarning(compilation, warning)); - this._errors = []; - this._warnings = []; - } - - private _makeTransformers() { - const isAppPath = (fileName: string) => - !fileName.endsWith('.ngfactory.ts') && !fileName.endsWith('.ngstyle.ts'); - const isMainPath = (fileName: string) => fileName === ( - this._mainPath ? workaroundResolve(this._mainPath) : this._mainPath - ); - const getEntryModule = () => this.entryModule - ? { path: workaroundResolve(this.entryModule.path), className: this.entryModule.className } - : this.entryModule; - const getTypeChecker = () => (this._getTsProgram() as ts.Program).getTypeChecker(); - - if (this._JitMode) { - // Replace resources in JIT. - this._transformers.push( - replaceResources(isAppPath, getTypeChecker, this._options.directTemplateLoading)); - // Downlevel constructor parameters for DI support - // This is required to support forwardRef in ES2015 due to TDZ issues - // This wrapper is needed here due to the program not being available until after the transformers are created. - const downlevelFactory: ts.TransformerFactory = (context) => { - const factory = constructorParametersDownlevelTransform(this._getTsProgram() as ts.Program); - - return factory(context); - }; - this._transformers.push(downlevelFactory); - } else { - if (!this._compilerOptions.enableIvy) { - // Remove unneeded angular decorators in VE. - // In Ivy they are removed in ngc directly. - this._transformers.push(removeDecorators(isAppPath, getTypeChecker)); - } else { - // Default for both options is to emit (undefined means true) - const removeClassMetadata = this._options.emitClassMetadata === false; - const removeNgModuleScope = this._options.emitNgModuleScope === false; - if (removeClassMetadata || removeNgModuleScope) { - this._transformers.push( - removeIvyJitSupportCalls(removeClassMetadata, removeNgModuleScope, getTypeChecker), - ); - } - } - // Import ngfactory in loadChildren import syntax - if (this._useFactories) { - // Only transform imports to use factories with View Engine. - this._transformers.push(importFactory(msg => this._warnings.push(msg), getTypeChecker)); - } - } - - if (this._platformTransformers !== null) { - this._transformers.push(...this._platformTransformers); - } else { - if (this._platform === PLATFORM.Browser) { - // If we have a locale, auto import the locale data file. - // This transform must go before replaceBootstrap because it looks for the entry module - // import, which will be replaced. - if (this._normalizedLocale) { - this._transformers.push(registerLocaleData(isAppPath, getEntryModule, - this._normalizedLocale)); - } - - if (!this._JitMode) { - // Replace bootstrap in browser non JIT Mode. - this._transformers.push(replaceBootstrap( - isAppPath, - getEntryModule, - getTypeChecker, - this._useFactories, - )); - } - } else if (this._platform === PLATFORM.Server && this._useFactories) { - this._transformers.push( - exportNgFactory(isMainPath, getEntryModule), - replaceServerBootstrap(isMainPath, getEntryModule, getTypeChecker)); - } - } - } - - private async _update() { - time('AngularCompilerPlugin._update'); - // 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. - const changedFiles = this._getChangedCompilationFiles(); - - // If nothing we care about changed and it isn't the first run, don't do anything. - if (changedFiles.length === 0 && !this._firstRun) { - return; - } - - // Make a new program and load the Angular structure. - await this._createOrUpdateProgram(); - - // Emit files. - time('AngularCompilerPlugin._update._emit'); - const { emitResult, diagnostics } = this._emit(); - timeEnd('AngularCompilerPlugin._update._emit'); - - // Report any diagnostics. - reportDiagnostics( - diagnostics, - msg => this._errors.push(msg), - msg => this._warnings.push(msg), - ); - - this._emitSkipped = !emitResult || emitResult.emitSkipped; - - // Reset changed files on successful compilation. - if (!this._emitSkipped && this._errors.length === 0) { - this._compilerHost.resetChangedFileTracker(); - } - timeEnd('AngularCompilerPlugin._update'); - } - - writeI18nOutFile() { - function _recursiveMkDir(p: string) { - if (!fs.existsSync(p)) { - _recursiveMkDir(path.dirname(p)); - fs.mkdirSync(p); - } - } - - // Write the extracted messages to disk. - if (this._compilerOptions.i18nOutFile) { - const i18nOutFilePath = path.resolve(this._basePath, this._compilerOptions.i18nOutFile); - const i18nOutFileContent = this._compilerHost.readFile(i18nOutFilePath); - if (i18nOutFileContent) { - _recursiveMkDir(path.dirname(i18nOutFilePath)); - fs.writeFileSync(i18nOutFilePath, i18nOutFileContent); - } - } - } - - getCompiledFile(fileName: string) { - const outputFile = fileName.replace(/.tsx?$/, '.js'); - let outputText: string; - let sourceMap: string | undefined; - let errorDependencies: string[] = []; - - if (this._emitSkipped) { - const text = this._compilerHost.readFile(outputFile); - if (text) { - // If the compilation didn't emit files this time, try to return the cached files from the - // last compilation and let the compilation errors show what's wrong. - outputText = text; - sourceMap = this._compilerHost.readFile(outputFile + '.map'); - } else { - // There's nothing we can serve. Return an empty string to prevent lenghty webpack errors, - // add the rebuild warning if it's not there yet. - // We also need to all changed files as dependencies of this file, so that all of them - // will be watched and trigger a rebuild next time. - outputText = ''; - const program = this._getTsProgram(); - errorDependencies = (program ? program.getSourceFiles().map(x => x.fileName) : []) - // These paths are used by the loader so we must denormalize them. - .map((p) => this._compilerHost.denormalizePath(p)); - } - } else { - // Check if the TS input file and the JS output file exist. - if (((fileName.endsWith('.ts') || fileName.endsWith('.tsx')) - && !this._compilerHost.fileExists(fileName)) - || !this._compilerHost.fileExists(outputFile, false)) { - let msg = `${fileName} is missing from the TypeScript compilation. ` - + `Please make sure it is in your tsconfig via the 'files' or 'include' property.`; - - if (this._nodeModulesRegExp.test(fileName)) { - msg += '\nThe missing file seems to be part of a third party library. ' - + 'TS files in published libraries are often a sign of a badly packaged library. ' - + 'Please open an issue in the library repository to alert its author and ask them ' - + 'to package the library using the Angular Package Format (https://goo.gl/jB3GVv).'; - } - - throw new Error(msg); - } - - outputText = this._compilerHost.readFile(outputFile) || ''; - sourceMap = this._compilerHost.readFile(outputFile + '.map'); - } - - return { outputText, sourceMap, errorDependencies }; - } - - getDependencies(fileName: string, includeResources = true): string[] { - const resolvedFileName = this._compilerHost.resolve(fileName); - const sourceFile = this._compilerHost.getSourceFile(resolvedFileName, ts.ScriptTarget.Latest); - if (!sourceFile) { - return []; - } - - const options = this._compilerOptions; - const host = this._compilerHost; - const cache = this._moduleResolutionCache; - - const esImports = collectDeepNodes( - sourceFile, - [ - ts.SyntaxKind.ImportDeclaration, - ts.SyntaxKind.ExportDeclaration, - ], - ) - .map(decl => { - if (!decl.moduleSpecifier) { - return null; - } - - const moduleName = (decl.moduleSpecifier as ts.StringLiteral).text; - const resolved = ts.resolveModuleName(moduleName, resolvedFileName, options, host, cache); - - if (resolved.resolvedModule) { - return resolved.resolvedModule.resolvedFileName; - } else { - return null; - } - }) - .filter(x => x) as string[]; - - let resourceImports: string[] = []; - const resourceDependencies: string[] = []; - if (includeResources) { - resourceImports = findResources(sourceFile) - .map(resourcePath => resolve(dirname(resolvedFileName), normalize(resourcePath))); - - for (const resource of resourceImports) { - for (const dep of this.getResourceDependencies( - this._compilerHost.denormalizePath(resource))) { - resourceDependencies.push(dep); - } - } - } - - // These paths are meant to be used by the loader so we must denormalize them. - const uniqueDependencies = new Set([ - ...esImports, - ...resourceImports, - ...resourceDependencies, - ].map((p) => p && this._compilerHost.denormalizePath(p))); - - return [...uniqueDependencies]; - } - - getResourceDependencies(fileName: string) { - if (!this._resourceLoader) { - return []; - } - // The source loader uses TS-style forward slash paths for all platforms. - const resolvedFileName = forwardSlashPath(fileName); - - return this._resourceLoader.getResourceDependencies(resolvedFileName); - } - - getTypeDependencies(fileName: string): string[] { - // We currently add all type deps directly to the main path. - // If there's no main path or the lookup isn't the main path, bail. - if (!this._mainPath || this._compilerHost.resolve(fileName) != this._mainPath) { - return []; - } - - // Note: this set is always for the previous build, not the current build. - // It should be better than not having rebuilds on type deps but isn't 100% correct. - return Array.from(this._typeDeps); - } - - // This code mostly comes from `performCompilation` in `@angular/compiler-cli`. - // It skips the program creation because we need to use `loadNgStructureAsync()`, - // and uses CustomTransformers. - private _emit() { - time('AngularCompilerPlugin._emit'); - const program = this._program; - const allDiagnostics: Array = []; - const diagMode = (this._firstRun || !this._forkTypeChecker) ? - DiagnosticMode.All : DiagnosticMode.Syntactic; - - let emitResult: ts.EmitResult | undefined; - try { - if (this._JitMode) { - const tsProgram = program as ts.Program; - const changedTsFiles = new Set(); - - if (this._firstRun) { - // Check parameter diagnostics. - time('AngularCompilerPlugin._emit.ts.getOptionsDiagnostics'); - allDiagnostics.push(...tsProgram.getOptionsDiagnostics()); - timeEnd('AngularCompilerPlugin._emit.ts.getOptionsDiagnostics'); - } else { - // generate a list of changed files for emit - // not needed on first run since a full program emit is required - for (const changedFile of this._compilerHost.getChangedFilePaths()) { - if (!/.(tsx|ts|json|js)$/.test(changedFile)) { - continue; - } - // existing type definitions are not emitted - if (changedFile.endsWith('.d.ts')) { - continue; - } - changedTsFiles.add(changedFile); - } - } - - allDiagnostics.push(...gatherDiagnostics(tsProgram, this._JitMode, - 'AngularCompilerPlugin._emit.ts', diagMode)); - - if (!hasErrors(allDiagnostics)) { - if (this._firstRun || changedTsFiles.size > 20 || !this._hadFullJitEmit) { - emitResult = tsProgram.emit( - undefined, - undefined, - undefined, - undefined, - { before: this._transformers }, - ); - this._hadFullJitEmit = !emitResult.emitSkipped; - allDiagnostics.push(...emitResult.diagnostics); - } else { - for (const changedFile of changedTsFiles) { - const sourceFile = tsProgram.getSourceFile(changedFile); - if (!sourceFile) { - continue; - } - - const timeLabel = `AngularCompilerPlugin._emit.ts+${sourceFile.fileName}+.emit`; - time(timeLabel); - emitResult = tsProgram.emit(sourceFile, undefined, undefined, undefined, - { before: this._transformers }, - ); - allDiagnostics.push(...emitResult.diagnostics); - timeEnd(timeLabel); - } - } - } - } else { - const angularProgram = program as Program; - - // Check Angular structural diagnostics. - time('AngularCompilerPlugin._emit.ng.getNgStructuralDiagnostics'); - allDiagnostics.push(...angularProgram.getNgStructuralDiagnostics()); - timeEnd('AngularCompilerPlugin._emit.ng.getNgStructuralDiagnostics'); - - if (this._firstRun) { - // Check TypeScript parameter diagnostics. - time('AngularCompilerPlugin._emit.ng.getTsOptionDiagnostics'); - allDiagnostics.push(...angularProgram.getTsOptionDiagnostics()); - timeEnd('AngularCompilerPlugin._emit.ng.getTsOptionDiagnostics'); - - // Check Angular parameter diagnostics. - time('AngularCompilerPlugin._emit.ng.getNgOptionDiagnostics'); - allDiagnostics.push(...angularProgram.getNgOptionDiagnostics()); - timeEnd('AngularCompilerPlugin._emit.ng.getNgOptionDiagnostics'); - } - - allDiagnostics.push(...gatherDiagnostics(angularProgram, this._JitMode, - 'AngularCompilerPlugin._emit.ng', diagMode)); - - if (!hasErrors(allDiagnostics)) { - time('AngularCompilerPlugin._emit.ng.emit'); - const extractI18n = !!this._compilerOptions.i18nOutFile; - const emitFlags = extractI18n ? EmitFlags.I18nBundle : EmitFlags.Default; - emitResult = angularProgram.emit({ - emitFlags, customTransformers: { - beforeTs: this._transformers, - }, - }); - allDiagnostics.push(...emitResult.diagnostics); - if (extractI18n) { - this.writeI18nOutFile(); - } - timeEnd('AngularCompilerPlugin._emit.ng.emit'); - } - } - } catch (e) { - time('AngularCompilerPlugin._emit.catch'); - // 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']; // tslint:disable-line:no-any - } - - let errMsg: string; - let code: number; - if (isSyntaxError(e)) { - // don't report the stack for syntax errors as they are well known errors. - errMsg = e.message; - code = DEFAULT_ERROR_CODE; - } else { - errMsg = e.stack; - // It is not a syntax error we might have a program with unknown state, discard it. - this._program = undefined; - code = UNKNOWN_ERROR_CODE; - } - allDiagnostics.push( - { category: ts.DiagnosticCategory.Error, messageText: errMsg, code, source: SOURCE }); - timeEnd('AngularCompilerPlugin._emit.catch'); - } - timeEnd('AngularCompilerPlugin._emit'); - - return { program, emitResult, diagnostics: allDiagnostics }; - } - - private _validateLocale(locale: string): string | null { - // Get the path of the common module. - const commonPath = path.dirname(require.resolve('@angular/common/package.json')); - // Check if the locale file exists - if (!fs.existsSync(path.resolve(commonPath, 'locales', `${locale}.js`))) { - // Check for an alternative locale (if the locale id was badly formatted). - const locales = fs.readdirSync(path.resolve(commonPath, 'locales')) - .filter(file => file.endsWith('.js')) - .map(file => file.replace('.js', '')); - - let newLocale; - const normalizedLocale = locale.toLowerCase().replace(/_/g, '-'); - for (const l of locales) { - if (l.toLowerCase() === normalizedLocale) { - newLocale = l; - break; - } - } - - if (newLocale) { - locale = newLocale; - } else { - // Check for a parent locale - const parentLocale = normalizedLocale.split('-')[0]; - if (locales.indexOf(parentLocale) !== -1) { - locale = parentLocale; - } else { - this._warnings.push(`AngularCompilerPlugin: Unable to load the locale data file ` + - `"@angular/common/locales/${locale}", ` + - `please check that "${locale}" is a valid locale id. - If needed, you can use "registerLocaleData" manually.`); - - return null; - } - } - } - - return locale; - } -} diff --git a/packages/ngtools/webpack/src/compiler_host.ts b/packages/ngtools/webpack/src/compiler_host.ts deleted file mode 100644 index 302670628b79..000000000000 --- a/packages/ngtools/webpack/src/compiler_host.ts +++ /dev/null @@ -1,462 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { Path, getSystemPath, isAbsolute, join, normalize, virtualFs } from '@angular-devkit/core'; -import { Stats } from 'fs'; -import * as ts from 'typescript'; -import { NgccProcessor } from './ngcc_processor'; -import { WebpackResourceLoader } from './resource_loader'; -import { forwardSlashPath, workaroundResolve } from './utils'; - -export interface OnErrorFn { - (message: string): void; -} - -const dev = Math.floor(Math.random() * 10000); - -export class WebpackCompilerHost implements ts.CompilerHost { - private _syncHost: virtualFs.SyncDelegateHost; - private _innerMemoryHost: virtualFs.SimpleMemoryHost; - private _memoryHost: virtualFs.SyncDelegateHost; - private _changedFiles = new Set(); - private _readResourceFiles = new Set(); - private _basePath: Path; - private _resourceLoader?: WebpackResourceLoader; - private _sourceFileCache = new Map(); - private _virtualFileExtensions = [ - '.js', - '.js.map', - '.ngfactory.js', - '.ngfactory.js.map', - '.ngsummary.json', - ]; - - private _virtualStyleFileExtensions = [ - '.shim.ngstyle.js', - '.shim.ngstyle.js.map', - ]; - - constructor( - private _options: ts.CompilerOptions, - basePath: string, - host: virtualFs.Host, - private readonly cacheSourceFiles: boolean, - private readonly directTemplateLoading = false, - private readonly ngccProcessor?: NgccProcessor, - private readonly moduleResolutionCache?: ts.ModuleResolutionCache, - ) { - this._syncHost = new virtualFs.SyncDelegateHost(host); - this._innerMemoryHost = new virtualFs.SimpleMemoryHost(); - this._memoryHost = new virtualFs.SyncDelegateHost(this._innerMemoryHost); - this._basePath = normalize(basePath); - } - - private get virtualFiles(): Path[] { - return [...((this._memoryHost.delegate as {}) as { _cache: Map })._cache.keys()]; - } - - reset() { - this._innerMemoryHost.reset(); - this._changedFiles.clear(); - this._readResourceFiles.clear(); - this._sourceFileCache.clear(); - this._resourceLoader = undefined; - } - - denormalizePath(path: string) { - return getSystemPath(normalize(path)); - } - - resolve(path: string): Path { - const p = normalize(path); - if (isAbsolute(p)) { - return p; - } else { - return join(this._basePath, p); - } - } - - resetChangedFileTracker() { - this._changedFiles.clear(); - } - - getChangedFilePaths(): string[] { - return [...this._changedFiles]; - } - - getNgFactoryPaths(): string[] { - return ( - this.virtualFiles - .filter(fileName => fileName.endsWith('.ngfactory.js') || fileName.endsWith('.ngstyle.js')) - // These paths are used by the virtual file system decorator so we must denormalize them. - .map(path => this.denormalizePath(path)) - ); - } - - invalidate(fileName: string): void { - const fullPath = this.resolve(fileName); - this._sourceFileCache.delete(fullPath); - - if (this.ngccProcessor) { - // Delete the ngcc processor cache using the TS-format file names. - this.ngccProcessor.invalidate(forwardSlashPath(fileName)); - } - - let exists = false; - try { - exists = this._syncHost.isFile(fullPath); - if (exists) { - this._changedFiles.add(workaroundResolve(fullPath)); - } - } catch {} - - // File doesn't exist anymore and is not a factory, so we should delete the related - // virtual files. - if ( - !exists && - fullPath.endsWith('.ts') && - !(fullPath.endsWith('.ngfactory.ts') || fullPath.endsWith('.shim.ngstyle.ts')) - ) { - this._virtualFileExtensions.forEach(ext => { - const virtualFile = (fullPath.slice(0, -3) + ext) as Path; - if (this._memoryHost.exists(virtualFile)) { - this._memoryHost.delete(virtualFile); - } - }); - } - - if (fullPath.endsWith('.ts')) { - return; - } - - // In case resolveJsonModule and allowJs we also need to remove virtual emitted files - // both if they exists or not. - if ( - (fullPath.endsWith('.js') || fullPath.endsWith('.json')) && - !/(\.(ngfactory|ngstyle)\.js|ngsummary\.json)$/.test(fullPath) - ) { - if (this._memoryHost.exists(fullPath)) { - this._memoryHost.delete(fullPath); - } - - return; - } - - if (!exists) { - // At this point we're only looking at resource files (html/css/scss/etc). - // If the original was deleted, we should delete the virtual files too. - // If the original wasn't deleted we should leave them to be overwritten, because webpack - // might begin the loading process before our plugin has re-emitted them. - for (const ext of this._virtualStyleFileExtensions) { - const virtualFile = (fullPath + ext) as Path; - if (this._memoryHost.exists(virtualFile)) { - this._memoryHost.delete(virtualFile); - } - } - } - } - - fileExists(fileName: string, delegate = true): boolean { - const p = this.resolve(fileName); - - if (this._memoryHost.isFile(p)) { - return true; - } - - if (!delegate) { - return false; - } - - let exists = false; - try { - exists = this._syncHost.isFile(p); - } catch {} - - return exists; - } - - readFile(fileName: string): string | undefined { - const filePath = this.resolve(fileName); - - try { - let content: ArrayBuffer; - if (this._memoryHost.isFile(filePath)) { - content = this._memoryHost.read(filePath); - } else { - content = this._syncHost.read(filePath); - } - - // strip BOM - return virtualFs.fileBufferToString(content).replace(/^\uFEFF/, ''); - } catch { - return undefined; - } - } - - readFileBuffer(fileName: string): Buffer { - const filePath = this.resolve(fileName); - - if (this._memoryHost.isFile(filePath)) { - return Buffer.from(this._memoryHost.read(filePath)); - } else { - const content = this._syncHost.read(filePath); - - return Buffer.from(content); - } - } - - private _makeStats(stats: virtualFs.Stats>): Stats { - return { - isFile: () => stats.isFile(), - isDirectory: () => stats.isDirectory(), - isBlockDevice: () => (stats.isBlockDevice && stats.isBlockDevice()) || false, - isCharacterDevice: () => (stats.isCharacterDevice && stats.isCharacterDevice()) || false, - isFIFO: () => (stats.isFIFO && stats.isFIFO()) || false, - isSymbolicLink: () => (stats.isSymbolicLink && stats.isSymbolicLink()) || false, - isSocket: () => (stats.isSocket && stats.isSocket()) || false, - dev: stats.dev === undefined ? dev : stats.dev, - ino: stats.ino === undefined ? Math.floor(Math.random() * 100000) : stats.ino, - mode: stats.mode === undefined ? parseInt('777', 8) : stats.mode, - nlink: stats.nlink === undefined ? 1 : stats.nlink, - uid: stats.uid || 0, - gid: stats.gid || 0, - rdev: stats.rdev || 0, - size: stats.size, - blksize: stats.blksize === undefined ? 512 : stats.blksize, - blocks: stats.blocks === undefined ? Math.ceil(stats.size / 512) : stats.blocks, - atime: stats.atime, - atimeMs: stats.atime.getTime(), - mtime: stats.mtime, - mtimeMs: stats.mtime.getTime(), - ctime: stats.ctime, - ctimeMs: stats.ctime.getTime(), - birthtime: stats.birthtime, - birthtimeMs: stats.birthtime.getTime(), - }; - } - - stat(path: string): Stats | null { - const p = this.resolve(path); - - let stats: virtualFs.Stats> | Stats | null = null; - try { - stats = this._memoryHost.stat(p) || this._syncHost.stat(p); - } catch {} - - if (!stats) { - return null; - } - - if (stats instanceof Stats) { - return stats; - } - - return this._makeStats(stats); - } - - directoryExists(directoryName: string): boolean { - const p = this.resolve(directoryName); - - try { - return this._memoryHost.isDirectory(p) || this._syncHost.isDirectory(p); - } catch { - return false; - } - } - - getDirectories(path: string): string[] { - const p = this.resolve(path); - - let delegated: string[]; - try { - delegated = this._syncHost.list(p).filter(x => { - try { - return this._syncHost.isDirectory(join(p, x)); - } catch { - return false; - } - }); - } catch { - delegated = []; - } - - let memory: string[]; - try { - memory = this._memoryHost.list(p).filter(x => { - try { - return this._memoryHost.isDirectory(join(p, x)); - } catch { - return false; - } - }); - } catch { - memory = []; - } - - return [...new Set([...delegated, ...memory])]; - } - - getSourceFile(fileName: string, languageVersion: ts.ScriptTarget, onError?: OnErrorFn) { - const p = this.resolve(fileName); - - try { - const cached = this._sourceFileCache.get(p); - if (cached) { - return cached; - } - - const content = this.readFile(fileName); - if (content !== undefined) { - const sf = ts.createSourceFile(workaroundResolve(fileName), content, languageVersion, true); - - if (this.cacheSourceFiles) { - this._sourceFileCache.set(p, sf); - } - - return sf; - } - } catch (e) { - if (onError) { - onError(e.message); - } - } - - return undefined; - } - - getDefaultLibFileName(options: ts.CompilerOptions) { - return ts.createCompilerHost(options).getDefaultLibFileName(options); - } - - // This is due to typescript CompilerHost interface being weird on writeFile. This shuts down - // typings in WebStorm. - get writeFile() { - return ( - fileName: string, - data: string, - _writeByteOrderMark: boolean, - onError?: (message: string) => void, - _sourceFiles?: ReadonlyArray, - ): void => { - const p = this.resolve(fileName); - - try { - this._memoryHost.write(p, virtualFs.stringToFileBuffer(data)); - } catch (e) { - if (onError) { - onError(e.message); - } - } - }; - } - - getCurrentDirectory(): string { - return workaroundResolve(this._basePath); - } - - getCanonicalFileName(fileName: string): string { - const path = workaroundResolve(this.resolve(fileName)); - - return this.useCaseSensitiveFileNames() ? path : path.toLowerCase(); - } - - useCaseSensitiveFileNames(): boolean { - return !process.platform.startsWith('win32'); - } - - getNewLine(): string { - return '\n'; - } - - setResourceLoader(resourceLoader: WebpackResourceLoader) { - this._resourceLoader = resourceLoader; - } - - readResource(fileName: string) { - // These paths are meant to be used by the loader so we must denormalize them - const denormalizedFileName = workaroundResolve(fileName); - this._readResourceFiles.add(denormalizedFileName); - - if (this.directTemplateLoading && (fileName.endsWith('.html') || fileName.endsWith('.svg'))) { - return this.readFile(fileName); - } - - if (this._resourceLoader) { - return this._resourceLoader.get(denormalizedFileName); - } else { - return this.readFile(fileName); - } - } - - getModifiedResourceFiles(): Set { - const modifiedFiles = new Set(); - - for (const changedFile of this._changedFiles) { - const denormalizedFileName = workaroundResolve(changedFile); - if (this._readResourceFiles.has(denormalizedFileName)) { - modifiedFiles.add(denormalizedFileName); - } - - if (!this._resourceLoader) { - continue; - } - for (const resourcePath of this._resourceLoader.getAffectedResources(denormalizedFileName)) { - modifiedFiles.add(resourcePath); - } - } - - return modifiedFiles; - } - - trace(message: string) { - // tslint:disable-next-line: no-console - console.log(message); - } - - resolveModuleNames( - moduleNames: string[], - containingFile: string, - ): (ts.ResolvedModule | undefined)[] { - return moduleNames.map(moduleName => { - const { resolvedModule } = ts.resolveModuleName( - moduleName, - workaroundResolve(containingFile), - this._options, - this, - this.moduleResolutionCache, - ); - - if (this._options.enableIvy && resolvedModule && this.ngccProcessor) { - this.ngccProcessor.processModule(moduleName, resolvedModule); - } - - return resolvedModule; - }); - } - - resolveTypeReferenceDirectives( - typeReferenceDirectiveNames: string[], - containingFile: string, - redirectedReference?: ts.ResolvedProjectReference, - ): (ts.ResolvedTypeReferenceDirective | undefined)[] { - return typeReferenceDirectiveNames.map(moduleName => { - const { resolvedTypeReferenceDirective } = ts.resolveTypeReferenceDirective( - moduleName, - workaroundResolve(containingFile), - this._options, - this, - redirectedReference, - ); - - if (this._options.enableIvy && resolvedTypeReferenceDirective && this.ngccProcessor) { - this.ngccProcessor.processModule(moduleName, resolvedTypeReferenceDirective); - } - - return resolvedTypeReferenceDirective; - }); - } -} diff --git a/packages/ngtools/webpack/src/diagnostics.ts b/packages/ngtools/webpack/src/diagnostics.ts deleted file mode 100644 index 674aecb7a657..000000000000 --- a/packages/ngtools/webpack/src/diagnostics.ts +++ /dev/null @@ -1,160 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { - Diagnostic, Diagnostics, - Program, formatDiagnostics, isNgDiagnostic, -} from '@angular/compiler-cli'; -import * as ts from 'typescript'; -import { time, timeEnd } from './benchmark'; - -export enum DiagnosticMode { - Syntactic = 1 << 0, - Semantic = 1 << 1, - - All = Syntactic | Semantic, - Default = All, -} - -export class CancellationToken implements ts.CancellationToken { - private _isCancelled = false; - - requestCancellation() { - this._isCancelled = true; - } - - isCancellationRequested() { - return this._isCancelled; - } - - throwIfCancellationRequested() { - if (this.isCancellationRequested()) { - throw new ts.OperationCanceledException(); - } - } -} - -export function hasErrors(diags: Diagnostics) { - return diags.some(d => d.category === ts.DiagnosticCategory.Error); -} - -export function gatherDiagnostics( - program: ts.Program | Program, - jitMode: boolean, - benchmarkLabel: string, - mode = DiagnosticMode.All, - cancellationToken?: CancellationToken, -): Diagnostics { - const allDiagnostics: Array = []; - let checkOtherDiagnostics = true; - - function checkDiagnostics(fn: T) { - if (checkOtherDiagnostics) { - const diags = fn(undefined, cancellationToken); - if (diags) { - allDiagnostics.push(...diags); - - checkOtherDiagnostics = !hasErrors(diags); - } - } - } - - const gatherSyntacticDiagnostics = (mode & DiagnosticMode.Syntactic) != 0; - const gatherSemanticDiagnostics = (mode & DiagnosticMode.Semantic) != 0; - - if (jitMode) { - const tsProgram = program as ts.Program; - if (gatherSyntacticDiagnostics) { - // Check syntactic diagnostics. - time(`${benchmarkLabel}.gatherDiagnostics.ts.getSyntacticDiagnostics`); - checkDiagnostics(tsProgram.getSyntacticDiagnostics.bind(tsProgram)); - timeEnd(`${benchmarkLabel}.gatherDiagnostics.ts.getSyntacticDiagnostics`); - } - - if (gatherSemanticDiagnostics) { - // Check semantic diagnostics. - time(`${benchmarkLabel}.gatherDiagnostics.ts.getSemanticDiagnostics`); - checkDiagnostics(tsProgram.getSemanticDiagnostics.bind(tsProgram)); - timeEnd(`${benchmarkLabel}.gatherDiagnostics.ts.getSemanticDiagnostics`); - } - } else { - const angularProgram = program as Program; - if (gatherSyntacticDiagnostics) { - // Check TypeScript syntactic diagnostics. - time(`${benchmarkLabel}.gatherDiagnostics.ng.getTsSyntacticDiagnostics`); - checkDiagnostics(angularProgram.getTsSyntacticDiagnostics.bind(angularProgram)); - timeEnd(`${benchmarkLabel}.gatherDiagnostics.ng.getTsSyntacticDiagnostics`); - } - - if (gatherSemanticDiagnostics) { - // Check TypeScript semantic and Angular structure diagnostics. - time(`${benchmarkLabel}.gatherDiagnostics.ng.getTsSemanticDiagnostics`); - checkDiagnostics(angularProgram.getTsSemanticDiagnostics.bind(angularProgram)); - timeEnd(`${benchmarkLabel}.gatherDiagnostics.ng.getTsSemanticDiagnostics`); - - // Check Angular semantic diagnostics - time(`${benchmarkLabel}.gatherDiagnostics.ng.getNgSemanticDiagnostics`); - checkDiagnostics(angularProgram.getNgSemanticDiagnostics.bind(angularProgram)); - timeEnd(`${benchmarkLabel}.gatherDiagnostics.ng.getNgSemanticDiagnostics`); - } - } - - return allDiagnostics; -} - -export function reportDiagnostics( - diagnostics: Diagnostics, - reportError: (msg: string) => void, - reportWarning: (msg: string) => void, -) { - const tsErrors = []; - const tsWarnings = []; - const ngErrors = []; - const ngWarnings = []; - - for (const diagnostic of diagnostics) { - switch (diagnostic.category) { - case ts.DiagnosticCategory.Error: - if (isNgDiagnostic(diagnostic)) { - ngErrors.push(diagnostic); - } else { - tsErrors.push(diagnostic); - } - break; - case ts.DiagnosticCategory.Message: - case ts.DiagnosticCategory.Suggestion: - // Warnings? - case ts.DiagnosticCategory.Warning: - if (isNgDiagnostic(diagnostic)) { - ngWarnings.push(diagnostic); - } else { - tsWarnings.push(diagnostic); - } - break; - } - } - - if (tsErrors.length > 0) { - const message = formatDiagnostics(tsErrors); - reportError(message); - } - - if (tsWarnings.length > 0) { - const message = formatDiagnostics(tsWarnings); - reportWarning(message); - } - - if (ngErrors.length > 0) { - const message = formatDiagnostics(ngErrors); - reportError(message); - } - - if (ngWarnings.length > 0) { - const message = formatDiagnostics(ngWarnings); - reportWarning(message); - } -} diff --git a/packages/ngtools/webpack/src/entry_resolver.ts b/packages/ngtools/webpack/src/entry_resolver.ts deleted file mode 100644 index 0f0fa51051a9..000000000000 --- a/packages/ngtools/webpack/src/entry_resolver.ts +++ /dev/null @@ -1,165 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -import { Path, join } from '@angular-devkit/core'; -import * as ts from 'typescript'; -import { TypeScriptFileRefactor, findAstNodes } from './refactor'; - - -function _recursiveSymbolExportLookup(refactor: TypeScriptFileRefactor, - symbolName: string, - host: ts.CompilerHost, - program: ts.Program): string | null { - // Check this file. - const hasSymbol = findAstNodes(null, refactor.sourceFile, ts.isClassDeclaration) - .some((cd) => { - return cd.name != undefined && cd.name.text == symbolName; - }); - if (hasSymbol) { - return refactor.fileName; - } - - // We found the bootstrap variable, now we just need to get where it's imported. - const exports = refactor.findAstNodes(null, ts.SyntaxKind.ExportDeclaration) - .map(node => node as ts.ExportDeclaration); - - for (const decl of exports) { - if (!decl.moduleSpecifier || decl.moduleSpecifier.kind !== ts.SyntaxKind.StringLiteral) { - continue; - } - - const modulePath = (decl.moduleSpecifier as ts.StringLiteral).text; - const resolvedModule = ts.resolveModuleName( - modulePath, refactor.fileName, program.getCompilerOptions(), host); - if (!resolvedModule.resolvedModule || !resolvedModule.resolvedModule.resolvedFileName) { - return null; - } - - const module = resolvedModule.resolvedModule.resolvedFileName; - if (!decl.exportClause) { - const moduleRefactor = new TypeScriptFileRefactor(module, host, program); - const maybeModule = _recursiveSymbolExportLookup(moduleRefactor, symbolName, host, program); - if (maybeModule) { - return maybeModule; - } - continue; - } - - const binding = decl.exportClause as ts.NamedExports; - for (const specifier of binding.elements) { - if (specifier.name.text == symbolName) { - // If it's a directory, load its index and recursively lookup. - // If it's a file it will return false - if (host.directoryExists && host.directoryExists(module)) { - const indexModule = join(module as Path, 'index.ts'); - if (host.fileExists(indexModule)) { - const indexRefactor = new TypeScriptFileRefactor(indexModule, host, program); - const maybeModule = _recursiveSymbolExportLookup( - indexRefactor, symbolName, host, program); - if (maybeModule) { - return maybeModule; - } - } - } - - // Create the source and verify that the symbol is at least a class. - const source = new TypeScriptFileRefactor(module, host, program); - const hasSymbol = findAstNodes(null, source.sourceFile, ts.isClassDeclaration) - .some((cd) => { - return cd.name != undefined && cd.name.text == symbolName; - }); - - if (hasSymbol) { - return module; - } - } - } - } - - return null; -} - -function _symbolImportLookup(refactor: TypeScriptFileRefactor, - symbolName: string, - host: ts.CompilerHost, - program: ts.Program): string | null { - // We found the bootstrap variable, now we just need to get where it's imported. - const imports = refactor.findAstNodes(null, ts.SyntaxKind.ImportDeclaration) - .map(node => node as ts.ImportDeclaration); - - for (const decl of imports) { - if (!decl.importClause || !decl.moduleSpecifier) { - continue; - } - if (decl.moduleSpecifier.kind !== ts.SyntaxKind.StringLiteral) { - continue; - } - - const resolvedModule = ts.resolveModuleName( - (decl.moduleSpecifier as ts.StringLiteral).text, - refactor.fileName, program.getCompilerOptions(), host); - if (!resolvedModule.resolvedModule || !resolvedModule.resolvedModule.resolvedFileName) { - continue; - } - - const module = resolvedModule.resolvedModule.resolvedFileName; - if (decl.importClause.namedBindings - && decl.importClause.namedBindings.kind == ts.SyntaxKind.NamespaceImport) { - const binding = decl.importClause.namedBindings as ts.NamespaceImport; - if (binding.name.text == symbolName) { - // This is a default export. - return module; - } - } else if (decl.importClause.namedBindings - && decl.importClause.namedBindings.kind == ts.SyntaxKind.NamedImports) { - const binding = decl.importClause.namedBindings as ts.NamedImports; - for (const specifier of binding.elements) { - if (specifier.name.text == symbolName) { - // Create the source and recursively lookup the import. - const source = new TypeScriptFileRefactor(module, host, program); - const maybeModule = _recursiveSymbolExportLookup(source, symbolName, host, program); - if (maybeModule) { - return maybeModule; - } - } - } - } - } - - return null; -} - - -export function resolveEntryModuleFromMain(mainPath: string, - host: ts.CompilerHost, - program: ts.Program): string | null { - const source = new TypeScriptFileRefactor(mainPath, host, program); - - const bootstrap = source.findAstNodes(source.sourceFile, ts.SyntaxKind.CallExpression, true) - .map(node => node as ts.CallExpression) - .filter(call => { - const access = call.expression as ts.PropertyAccessExpression; - - return access.kind == ts.SyntaxKind.PropertyAccessExpression - && access.name.kind == ts.SyntaxKind.Identifier - && (access.name.text == 'bootstrapModule' - || access.name.text == 'bootstrapModuleFactory'); - }) - .map(node => node.arguments[0] as ts.Identifier) - .filter(node => node.kind == ts.SyntaxKind.Identifier); - - if (bootstrap.length === 1) { - const bootstrapSymbolName = bootstrap[0].text; - const module = _symbolImportLookup(source, bootstrapSymbolName, host, program); - if (module) { - return `${module.replace(/\.ts$/, '')}#${bootstrapSymbolName}`; - } - } - - return null; -} diff --git a/packages/ngtools/webpack/src/index.ts b/packages/ngtools/webpack/src/index.ts index 8b90d4eff002..9d7da50295de 100644 --- a/packages/ngtools/webpack/src/index.ts +++ b/packages/ngtools/webpack/src/index.ts @@ -5,11 +5,20 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ +import * as ivyInternal from './ivy'; +export { + AngularWebpackLoaderPath, + AngularWebpackPlugin, + AngularWebpackPluginOptions, + default, +} from './ivy'; -export * from './angular_compiler_plugin'; -export * from './interfaces'; -export { ngcLoader as default } from './loader'; - -export const NgToolsLoader = __filename; - -export * as ivy from './ivy'; +/** @deprecated Deprecated as of v12, please use the direct exports + * (`AngularWebpackPlugin` instead of `ivy.AngularWebpackPlugin`) + */ +export namespace ivy { + export const AngularWebpackLoaderPath = ivyInternal.AngularWebpackLoaderPath; + export const AngularWebpackPlugin = ivyInternal.AngularWebpackPlugin; + export type AngularWebpackPlugin = ivyInternal.AngularWebpackPlugin; + export type AngularPluginOptions = ivyInternal.AngularWebpackPluginOptions; +} diff --git a/packages/ngtools/webpack/src/interfaces.ts b/packages/ngtools/webpack/src/interfaces.ts deleted file mode 100644 index 47a8eac046a9..000000000000 --- a/packages/ngtools/webpack/src/interfaces.ts +++ /dev/null @@ -1,58 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -import { logging, virtualFs } from '@angular-devkit/core'; -import { CompilerOptions } from '@angular/compiler-cli'; -import * as fs from 'fs'; -import * as ts from 'typescript'; - -export enum PLATFORM { - Browser, - Server, -} - -/** - * Option Constants - */ -export interface AngularCompilerPluginOptions { - sourceMap?: boolean; - tsConfigPath: string; - basePath?: string; - entryModule?: string; - mainPath?: string; - skipCodeGeneration?: boolean; - hostReplacementPaths?: { [path: string]: string } | ((path: string) => string); - forkTypeChecker?: boolean; - - /** @deprecated since version 9 - When using Ivy this option has no effect as i18n is no longer part of the TypeScript compilation. */ - i18nInFile?: string; - /** @deprecated since version 9 - When using Ivy this option has no effect as i18n is no longer part of the TypeScript compilation. */ - i18nInFormat?: string; - /** @deprecated since version 9 - When using Ivy this option has no effect as i18n is no longer part of the TypeScript compilation. */ - i18nOutFile?: string; - /** @deprecated since version 9 - When using Ivy this option has no effect as i18n is no longer part of the TypeScript compilation. */ - i18nOutFormat?: string; - /** @deprecated since version 9 - When using Ivy this option has no effect as i18n is no longer part of the TypeScript compilation. */ - locale?: string; - /** @deprecated since version 9 - When using Ivy this option has no effect as i18n is no longer part of the TypeScript compilation. */ - missingTranslation?: string; - - platform?: PLATFORM; - nameLazyFiles?: boolean; - logger?: logging.Logger; - directTemplateLoading?: boolean; - - emitClassMetadata?: boolean; - emitNgModuleScope?: boolean; - - // Use tsconfig to include path globs. - compilerOptions?: CompilerOptions; - - host?: virtualFs.Host; - platformTransformers?: ts.TransformerFactory[]; -} diff --git a/packages/ngtools/webpack/src/ivy/index.ts b/packages/ngtools/webpack/src/ivy/index.ts index b5e2985745ad..132e9f84c75d 100644 --- a/packages/ngtools/webpack/src/ivy/index.ts +++ b/packages/ngtools/webpack/src/ivy/index.ts @@ -6,6 +6,6 @@ * found in the LICENSE file at https://angular.io/license */ export { angularWebpackLoader as default } from './loader'; -export { AngularPluginOptions, AngularWebpackPlugin } from './plugin'; +export { AngularWebpackPluginOptions, AngularWebpackPlugin } from './plugin'; export const AngularWebpackLoaderPath = __filename; diff --git a/packages/ngtools/webpack/src/ivy/plugin.ts b/packages/ngtools/webpack/src/ivy/plugin.ts index 5144a69a1084..83d02beed92f 100644 --- a/packages/ngtools/webpack/src/ivy/plugin.ts +++ b/packages/ngtools/webpack/src/ivy/plugin.ts @@ -38,7 +38,7 @@ import { createAotTransformers, createJitTransformers, mergeTransformers } from */ const DIAGNOSTICS_AFFECTED_THRESHOLD = 1; -export interface AngularPluginOptions { +export interface AngularWebpackPluginOptions { tsconfig: string; compilerOptions?: CompilerOptions; fileReplacements: Record; @@ -86,7 +86,7 @@ function hashContent(content: string): Uint8Array { const PLUGIN_NAME = 'angular-compiler'; export class AngularWebpackPlugin { - private readonly pluginOptions: AngularPluginOptions; + private readonly pluginOptions: AngularWebpackPluginOptions; private watchMode?: boolean; private ngtscNextProgram?: NgtscProgram; private builder?: ts.EmitAndSemanticDiagnosticsBuilderProgram; @@ -97,7 +97,7 @@ export class AngularWebpackPlugin { private readonly requiredFilesToEmitCache = new Map(); private readonly fileEmitHistory = new Map(); - constructor(options: Partial = {}) { + constructor(options: Partial = {}) { this.pluginOptions = { emitClassMetadata: false, emitNgModuleScope: false, @@ -110,7 +110,7 @@ export class AngularWebpackPlugin { }; } - get options(): AngularPluginOptions { + get options(): AngularWebpackPluginOptions { return this.pluginOptions; } diff --git a/packages/ngtools/webpack/src/loader.ts b/packages/ngtools/webpack/src/loader.ts deleted file mode 100644 index f3a94b41449a..000000000000 --- a/packages/ngtools/webpack/src/loader.ts +++ /dev/null @@ -1,116 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -// TODO: fix typings. -// tslint:disable-next-line:no-global-tslint-disable -// tslint:disable:no-any -import * as path from 'path'; -import { loader } from 'webpack'; -import { AngularCompilerPlugin } from './angular_compiler_plugin'; -import { time, timeEnd } from './benchmark'; - - -const sourceMappingUrlRe = /^\/\/# sourceMappingURL=[^\r\n]*/gm; - -export function ngcLoader(this: loader.LoaderContext) { - const cb = this.async(); - const sourceFileName: string = this.resourcePath; - const timeLabel = `ngcLoader+${sourceFileName}+`; - - if (!cb) { - throw new Error('This loader needs to support asynchronous webpack compilations.'); - } - - time(timeLabel); - - const plugin = (this._compilation as { _ngToolsWebpackPluginInstance?: AngularCompilerPlugin }) - ._ngToolsWebpackPluginInstance; - if (!plugin) { - throw new Error('The AngularCompilerPlugin was not found. ' - + 'The @ngtools/webpack loader requires the plugin.'); - } - - // 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 AngularCompilerPlugin) || !plugin.done) { - 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.', - ); - } - - time(timeLabel + '.ngcLoader.AngularCompilerPlugin'); - plugin.done - .then(() => { - timeEnd(timeLabel + '.ngcLoader.AngularCompilerPlugin'); - const result = plugin.getCompiledFile(sourceFileName); - - if (result.sourceMap) { - // Process sourcemaps for Webpack. - // Remove the sourceMappingURL. - result.outputText = result.outputText.replace(sourceMappingUrlRe, ''); - // Set the map source to use the full path of the file. - const sourceMap = JSON.parse(result.sourceMap); - sourceMap.sources = sourceMap.sources.map((fileName: string) => { - return path.join(path.dirname(sourceFileName), fileName); - }); - result.sourceMap = sourceMap; - } - - // Manually add the dependencies for TS files. - // Type only imports will be stripped out by compilation so we need to add them as - // as dependencies. - // Component resources files (html and css templates) also need to be added manually for - // AOT, so that this file is reloaded when they change. - if (sourceFileName.endsWith('.ts')) { - result.errorDependencies.forEach(dep => this.addDependency(dep)); - const dependencies = plugin.getDependencies(sourceFileName); - dependencies - .filter(d => d.endsWith('index.ts')) - .forEach(d => dependencies.push(...plugin.getDependencies(d))); - - [...new Set(dependencies)].forEach(dep => { - plugin.updateChangedFileExtensions(path.extname(dep)); - this.addDependency(dep); - }); - } - - // NgFactory files depend on the component template, but we can't know what that file - // is (if any). So we add all the dependencies that the original component file has - // to the factory as well, which includes html and css templates, and the component - // itself (for inline html/templates templates). - const ngFactoryRe = /\.ngfactory.js$/; - if (ngFactoryRe.test(sourceFileName)) { - const originalFile = sourceFileName.replace(ngFactoryRe, '.ts'); - this.addDependency(originalFile); - const origDependencies = plugin.getDependencies(originalFile); - origDependencies.forEach(dep => this.addDependency(dep)); - } - - // NgStyle files depend on the style file they represent. - // E.g. `some-style.less.shim.ngstyle.js` depends on `some-style.less`. - // Those files can in turn depend on others, so we have to add them all. - const ngStyleRe = /(?:\.shim)?\.ngstyle\.js$/; - if (ngStyleRe.test(sourceFileName)) { - const styleFile = sourceFileName.replace(ngStyleRe, ''); - for (const dep of plugin.getResourceDependencies(styleFile)) { - this.addDependency(dep); - } - } - - // Add type-only dependencies that should trigger a rebuild when they change. - const typeDependencies = plugin.getTypeDependencies(sourceFileName); - typeDependencies.forEach(dep => this.addDependency(dep)); - - timeEnd(timeLabel); - cb(null, result.outputText, result.sourceMap as any); - }) - .catch(err => { - timeEnd(timeLabel); - cb(err); - }); -} diff --git a/packages/ngtools/webpack/src/refactor.ts b/packages/ngtools/webpack/src/refactor.ts deleted file mode 100644 index 3aa5a6f001a0..000000000000 --- a/packages/ngtools/webpack/src/refactor.ts +++ /dev/null @@ -1,170 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import * as path from 'path'; -import * as ts from 'typescript'; -import { forwardSlashPath } from './utils'; - - -/** - * 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?: boolean, - max?: number, -): ts.Node[]; - -/** - * 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 guard - * @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, - guard: (node: ts.Node) => node is T, - recursive?: boolean, - max?: number, -): T[]; - -export function findAstNodes( - node: ts.Node | null, - sourceFile: ts.SourceFile, - kindOrGuard: ts.SyntaxKind | ((node: ts.Node) => node is T), - 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; - } - - const test = - typeof kindOrGuard === 'function' - ? kindOrGuard - : (node: ts.Node): node is T => node.kind === kindOrGuard; - - const arr: T[] = []; - if (test(node)) { - // If we're not recursively looking for children, stop here. - if (!recursive) { - return [node as T]; - } - - arr.push(node); - max--; - } - - if (max > 0) { - for (const child of node.getChildren(sourceFile)) { - findAstNodes(child, sourceFile, test, recursive, max) - .forEach((node) => { - if (max > 0) { - arr.push(node); - } - max--; - }); - - if (max <= 0) { - break; - } - } - } - - return arr; -} - -export function resolve( - filePath: string, - _host: ts.CompilerHost, - compilerOptions: ts.CompilerOptions, -): string { - if (path.isAbsolute(filePath)) { - return filePath; - } - const basePath = compilerOptions.baseUrl || compilerOptions.rootDir; - if (!basePath) { - throw new Error(`Trying to resolve '${filePath}' without a basePath.`); - } - - return path.join(basePath, filePath); -} - - -export class TypeScriptFileRefactor { - private _fileName: string; - private _sourceFile: ts.SourceFile; - - get fileName() { return this._fileName; } - get sourceFile() { return this._sourceFile; } - - constructor(fileName: string, - _host: ts.CompilerHost, - _program?: ts.Program, - source?: string | null) { - let sourceFile: ts.SourceFile | null = null; - - if (_program) { - fileName = forwardSlashPath(resolve(fileName, _host, _program.getCompilerOptions())); - - if (source) { - sourceFile = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true); - } else { - sourceFile = _program.getSourceFile(fileName) || null; - } - } - if (!sourceFile) { - const maybeContent = source || _host.readFile(fileName); - if (maybeContent) { - sourceFile = ts.createSourceFile( - fileName, - maybeContent, - ts.ScriptTarget.Latest, - true, - ); - } - } - if (!sourceFile) { - throw new Error('Must have a source file to refactor.'); - } - - this._fileName = fileName; - this._sourceFile = sourceFile; - } - - /** - * 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 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 - */ - findAstNodes(node: ts.Node | null, - kind: ts.SyntaxKind, - recursive = false, - max = Infinity): ts.Node[] { - return findAstNodes(node, this._sourceFile, kind, recursive, max); - } - -} diff --git a/packages/ngtools/webpack/src/transformers/export_ngfactory.ts b/packages/ngtools/webpack/src/transformers/export_ngfactory.ts deleted file mode 100644 index 49627e39394f..000000000000 --- a/packages/ngtools/webpack/src/transformers/export_ngfactory.ts +++ /dev/null @@ -1,80 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { dirname, relative } from 'path'; -import * as ts from 'typescript'; -import { forwardSlashPath } from '../utils'; -import { collectDeepNodes, getFirstNode } from './ast_helpers'; -import { AddNodeOperation, StandardTransform, TransformOperation } from './interfaces'; -import { makeTransform } from './make_transform'; - -export function exportNgFactory( - shouldTransform: (fileName: string) => boolean, - getEntryModule: () => { path: string, className: string } | null, -): ts.TransformerFactory { - - const standardTransform: StandardTransform = function (sourceFile: ts.SourceFile) { - const ops: TransformOperation[] = []; - - const entryModule = getEntryModule(); - - if (!shouldTransform(sourceFile.fileName) || !entryModule) { - return ops; - } - - // Find all identifiers using the entry module class name. - const entryModuleIdentifiers = collectDeepNodes(sourceFile, - ts.SyntaxKind.Identifier) - .filter(identifier => identifier.text === entryModule.className); - - if (entryModuleIdentifiers.length === 0) { - return []; - } - - const relativeEntryModulePath = relative(dirname(sourceFile.fileName), entryModule.path); - const normalizedEntryModulePath = forwardSlashPath(`./${relativeEntryModulePath}`); - - // Get the module path from the import. - entryModuleIdentifiers.forEach((entryModuleIdentifier) => { - if (!entryModuleIdentifier.parent - || entryModuleIdentifier.parent.kind !== ts.SyntaxKind.ExportSpecifier) { - return; - } - - const exportSpec = entryModuleIdentifier.parent as ts.ExportSpecifier; - const moduleSpecifier = exportSpec.parent - && exportSpec.parent.parent - && exportSpec.parent.parent.moduleSpecifier; - - if (!moduleSpecifier || moduleSpecifier.kind !== ts.SyntaxKind.StringLiteral) { - return; - } - - // Add the transform operations. - const factoryClassName = entryModule.className + 'NgFactory'; - const factoryModulePath = normalizedEntryModulePath + '.ngfactory'; - - const namedExports = ts.factory.createNamedExports([ts.factory.createExportSpecifier(undefined, - ts.factory.createIdentifier(factoryClassName))]); - const newImport = ts.factory.createExportDeclaration(undefined, undefined, false, namedExports, - ts.factory.createStringLiteral(factoryModulePath)); - - const firstNode = getFirstNode(sourceFile); - if (firstNode) { - ops.push(new AddNodeOperation( - sourceFile, - firstNode, - newImport, - )); - } - }); - - return ops; - }; - - return makeTransform(standardTransform); -} diff --git a/packages/ngtools/webpack/src/transformers/export_ngfactory_spec.ts b/packages/ngtools/webpack/src/transformers/export_ngfactory_spec.ts deleted file mode 100644 index 1a92d4851dac..000000000000 --- a/packages/ngtools/webpack/src/transformers/export_ngfactory_spec.ts +++ /dev/null @@ -1,64 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { tags } from '@angular-devkit/core'; // tslint:disable-line:no-implicit-dependencies -import { exportNgFactory } from './export_ngfactory'; -import { transformTypescript } from './spec_helpers'; - -describe('@ngtools/webpack transformers', () => { - describe('export_ngfactory', () => { - it('should export the ngfactory', () => { - const input = tags.stripIndent` - export { AppModule } from './app/app.module'; - `; - const output = tags.stripIndent` - export { AppModuleNgFactory } from "./app/app.module.ngfactory"; - export { AppModule } from './app/app.module'; - `; - - const transformer = exportNgFactory( - () => true, - () => ({ path: '/project/src/app/app.module', className: 'AppModule' }), - ); - const result = transformTypescript(input, [transformer]); - - expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); - }); - - it('should export the ngfactory when there is a barrel file', () => { - const input = tags.stripIndent` - export { AppModule } from './app'; - `; - const output = tags.stripIndent` - export { AppModuleNgFactory } from "./app/app.module.ngfactory"; - export { AppModule } from './app'; - `; - - const transformer = exportNgFactory( - () => true, - () => ({ path: '/project/src/app/app.module', className: 'AppModule' }), - ); - const result = transformTypescript(input, [transformer]); - - expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); - }); - - it('should not do anything if shouldTransform returns false', () => { - const input = tags.stripIndent` - export { AppModule } from './app/app.module'; - `; - - const transformer = exportNgFactory( - () => false, - () => ({ path: '/project/src/app/app.module', className: 'AppModule' }), - ); - const result = transformTypescript(input, [transformer]); - - expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${input}`); - }); - }); -}); diff --git a/packages/ngtools/webpack/src/transformers/find_resources.ts b/packages/ngtools/webpack/src/transformers/find_resources.ts deleted file mode 100644 index 11ceeb92c7fd..000000000000 --- a/packages/ngtools/webpack/src/transformers/find_resources.ts +++ /dev/null @@ -1,68 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import * as ts from 'typescript'; -import { collectDeepNodes } from './ast_helpers'; -import { getResourceUrl } from './replace_resources'; - -export function findResources(sourceFile: ts.SourceFile): string[] { - const resources: string[] = []; - const decorators = collectDeepNodes(sourceFile, ts.SyntaxKind.Decorator); - - for (const node of decorators) { - if (!ts.isCallExpression(node.expression)) { - continue; - } - - const decoratorFactory = node.expression; - const args = decoratorFactory.arguments; - if (args.length !== 1 || !ts.isObjectLiteralExpression(args[0])) { - // Unsupported component metadata - continue; - } - - ts.visitNodes( - (args[0] as ts.ObjectLiteralExpression).properties, - (node) => { - if (!ts.isPropertyAssignment(node) || ts.isComputedPropertyName(node.name)) { - return node; - } - - const name = node.name.text; - switch (name) { - case 'templateUrl': - const url = getResourceUrl(node.initializer); - - if (url) { - resources.push(url); - } - break; - - case 'styleUrls': - if (!ts.isArrayLiteralExpression(node.initializer)) { - return node; - } - - ts.visitNodes(node.initializer.elements, (node) => { - const url = getResourceUrl(node); - - if (url) { - resources.push(url); - } - - return node; - }); - break; - } - - return node; - }, - ); - } - - return resources; -} diff --git a/packages/ngtools/webpack/src/transformers/find_resources_spec.ts b/packages/ngtools/webpack/src/transformers/find_resources_spec.ts deleted file mode 100644 index 0e0cf9cbaaa8..000000000000 --- a/packages/ngtools/webpack/src/transformers/find_resources_spec.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { tags } from '@angular-devkit/core'; // tslint:disable-line:no-implicit-dependencies -import * as ts from 'typescript'; -import { findResources } from './find_resources'; - - -describe('@ngtools/webpack transformers', () => { - describe('find_resources', () => { - it('should return resources', () => { - const input = tags.stripIndent` - import { Component } from '@angular/core'; - - @Component({ - selector: 'app-root', - templateUrl: './app.component.html', - styleUrls: ['./app.component.css', './app.component.2.css'] - }) - export class AppComponent { - title = 'app'; - } - `; - - const result = findResources(ts.createSourceFile('temp.ts', input, ts.ScriptTarget.ES2015)); - expect(result).toEqual([ - './app.component.html', - './app.component.css', - './app.component.2.css', - ]); - }); - - it('should not return resources if they are not in decorator', () => { - const input = tags.stripIndent` - const foo = { - templateUrl: './app.component.html', - styleUrls: ['./app.component.css', './app.component.2.css'] - } - `; - - const result = findResources(ts.createSourceFile('temp.ts', input, ts.ScriptTarget.ES2015)); - expect(result).toEqual([]); - }); - }); -}); diff --git a/packages/ngtools/webpack/src/transformers/import_factory.ts b/packages/ngtools/webpack/src/transformers/import_factory.ts deleted file mode 100644 index bef697a2ce4f..000000000000 --- a/packages/ngtools/webpack/src/transformers/import_factory.ts +++ /dev/null @@ -1,240 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { dirname, relative } from 'path'; -import * as ts from 'typescript'; -import { forwardSlashPath } from '../utils'; - - -// Check if a ts.Symbol is an alias. -const isAlias = (symbol: ts.Symbol) => symbol.flags & ts.SymbolFlags.Alias; - -/** - * Given this original source code: - * - * import { NgModule } from '@angular/core'; - * import { Routes, RouterModule } from '@angular/router'; - * - * const routes: Routes = [{ - * path: 'lazy', - * loadChildren: () => import('./lazy/lazy.module').then(m => m.LazyModule). - * }]; - * - * @NgModule({ - * imports: [RouterModule.forRoot(routes)], - * exports: [RouterModule] - * }) - * export class AppRoutingModule { } - * - * NGC (View Engine) will process it into: - * - * import { Routes } from '@angular/router'; - * const ɵ0 = () => import('./lazy/lazy.module').then(m => m.LazyModule); - * const routes: Routes = [{ - * path: 'lazy', - * loadChildren: ɵ0 - * }]; - * export class AppRoutingModule { - * } - * export { ɵ0 }; - * - * The importFactory transformation will only see the AST after it is process by NGC. - * You can confirm this with the code below: - * - * const res = ts.createPrinter().printNode(ts.EmitHint.Unspecified, sourceFile, sourceFile); - * console.log(`### Original source: \n${sourceFile.text}\n###`); - * console.log(`### Current source: \n${currentText}\n###`); - * - * At this point it doesn't yet matter what the target (ES5/ES2015/etc) is, so the original - * constructs, like `class` and arrow functions, still remain. - * - */ - -export function importFactory( - warningCb: (warning: string) => void, - getTypeChecker: () => ts.TypeChecker, -): ts.TransformerFactory { - return (context: ts.TransformationContext) => { - // TODO(filipesilva): change the link to https://angular.io/guide/ivy once it is out. - return (sourceFile: ts.SourceFile) => { - const warning = ` -Found 'loadChildren' with a non-string syntax in ${sourceFile.fileName} but could not transform it. -Make sure it matches the format below: - -loadChildren: () => import('IMPORT_STRING').then(M => M.EXPORT_NAME) - -Please note that only IMPORT_STRING, M, and EXPORT_NAME can be replaced in this format. - -Visit https://v8.angular.io/guide/ivy for more information on using Ivy. -`; - - const emitWarning = () => warningCb(warning); - const visitVariableStatement: ts.Visitor = (node: ts.Node) => { - if (ts.isVariableDeclaration(node)) { - return replaceImport(node, context, emitWarning, sourceFile.fileName, getTypeChecker()); - } - - return ts.visitEachChild(node, visitVariableStatement, context); - }; - - const visitToplevelNodes: ts.Visitor = (node: ts.Node) => { - // We only care about finding variable declarations, which are found in this structure: - // VariableStatement -> VariableDeclarationList -> VariableDeclaration - if (ts.isVariableStatement(node)) { - return ts.visitEachChild(node, visitVariableStatement, context); - } - - // There's no point in recursing into anything but variable statements, so return the node. - return node; - }; - - return ts.visitEachChild(sourceFile, visitToplevelNodes, context); - }; - }; -} - -function replaceImport( - node: ts.VariableDeclaration, - context: ts.TransformationContext, - emitWarning: () => void, - fileName: string, - typeChecker: ts.TypeChecker, -): ts.Node { - // This ONLY matches the original source code format below: - // loadChildren: () => import('IMPORT_STRING').then(M => M.EXPORT_NAME) - // And expects that source code to be transformed by NGC (see comment for importFactory). - // It will not match nor alter variations, for instance: - // - not using arrow functions - // - using `await` instead of `then` - // - using a default export (https://github.com/angular/angular/issues/11402) - // The only parts that can change are the ones in caps: IMPORT_STRING, M and EXPORT_NAME. - - // Exit early if the structure is not what we expect. - - // ɵ0 = something - const name = node.name; - if (!( - ts.isIdentifier(name) - && /ɵ\d+/.test(name.text) - )) { - return node; - } - - const initializer = node.initializer; - if (initializer === undefined) { - return node; - } - - // ɵ0 = () => something - if (!( - ts.isArrowFunction(initializer) - && initializer.parameters.length === 0 - )) { - return node; - } - - // ɵ0 = () => something.then(something) - const topArrowFnBody = initializer.body; - if (!ts.isCallExpression(topArrowFnBody)) { - return node; - } - - const topArrowFnBodyExpr = topArrowFnBody.expression; - if (!( - ts.isPropertyAccessExpression(topArrowFnBodyExpr) - && ts.isIdentifier(topArrowFnBodyExpr.name) - )) { - return node; - } - if (topArrowFnBodyExpr.name.text != 'then') { - return node; - } - - // ɵ0 = () => import('IMPORT_STRING').then(something) - const importCall = topArrowFnBodyExpr.expression; - if (!( - ts.isCallExpression(importCall) - && importCall.expression.kind === ts.SyntaxKind.ImportKeyword - && importCall.arguments.length === 1 - && ts.isStringLiteral(importCall.arguments[0]) - )) { - return node; - } - - // Now that we know it's both `ɵ0` (generated by NGC) and a `import()`, start emitting a warning - // if the structure isn't as expected to help users identify unusable syntax. - const warnAndBail = () => { - emitWarning(); - - return node; - }; - - // ɵ0 = () => import('IMPORT_STRING').then(m => m.EXPORT_NAME) - if (!( - topArrowFnBody.arguments.length === 1 - && ts.isArrowFunction(topArrowFnBody.arguments[0]) - )) { - return warnAndBail(); - } - - const thenArrowFn = topArrowFnBody.arguments[0] as ts.ArrowFunction; - if (!( - thenArrowFn.parameters.length === 1 - && ts.isPropertyAccessExpression(thenArrowFn.body) - && ts.isIdentifier(thenArrowFn.body.name) - )) { - return warnAndBail(); - } - - // At this point we know what are the nodes we need to replace. - const exportNameId = thenArrowFn.body.name; - const importStringLit = importCall.arguments[0] as ts.StringLiteral; - - // Try to resolve the import. It might be a reexport from somewhere and the ngfactory will only - // be present next to the original module. - let exportedSymbol = typeChecker.getSymbolAtLocation(exportNameId); - if (!exportedSymbol) { - return warnAndBail(); - } - // Named exports are also a declaration in the re-exporting module so we have to follow the - // re-exports to find the original symbol. - if (isAlias(exportedSymbol)) { - exportedSymbol = typeChecker.getAliasedSymbol(exportedSymbol); - if (!exportedSymbol) { - return warnAndBail(); - } - } - - // Find declarations of the original symbol so we can get their source file name. - const exportedSymbolDecl = exportedSymbol.getDeclarations(); - if (!exportedSymbolDecl || exportedSymbolDecl.length === 0) { - return warnAndBail(); - } - - // Let's guess the first declaration is the one we want, because we don't have a better criteria. - // Get the relative path from the containing module to the imported module. - const relativePath = relative(dirname(fileName), exportedSymbolDecl[0].getSourceFile().fileName); - - // node's `relative` call doesn't actually add `./` so we add it here. - // Also replace the 'ts' extension with just 'ngfactory'. - const newImportString = `./${forwardSlashPath(relativePath)}`.replace(/ts$/, 'ngfactory'); - - // The easiest way to alter them is with a simple visitor. - const replacementVisitor: ts.Visitor = (node: ts.Node) => { - if (node === importStringLit) { - // Transform the import string. - return ts.factory.createStringLiteral(newImportString); - } else if (node === exportNameId) { - // Transform the export name. - return ts.factory.createIdentifier(exportNameId.text + 'NgFactory'); - } - - return ts.visitEachChild(node, replacementVisitor, context); - }; - - return ts.visitEachChild(node, replacementVisitor, context); -} diff --git a/packages/ngtools/webpack/src/transformers/import_factory_spec.ts b/packages/ngtools/webpack/src/transformers/import_factory_spec.ts deleted file mode 100644 index 9a21797f0132..000000000000 --- a/packages/ngtools/webpack/src/transformers/import_factory_spec.ts +++ /dev/null @@ -1,173 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { tags } from '@angular-devkit/core'; -import { importFactory } from './import_factory'; -import { createTypescriptContext, transformTypescript } from './spec_helpers'; - -describe('@ngtools/webpack transformers', () => { - describe('import_factory', () => { - it('should support arrow functions', () => { - const additionalFiles: Record = { - 'lazy/lazy.module.ts': ` - export const LazyModule = {}; - `, - }; - const input = tags.stripIndent` - const ɵ0 = () => import('./lazy/lazy.module').then(m => m.LazyModule); - const routes = [{ - path: 'lazy', - loadChildren: ɵ0 - }]; - `; - const output = tags.stripIndent` - const ɵ0 = () => import("./lazy/lazy.module.ngfactory").then(m => m.LazyModuleNgFactory); - const routes = [{ - path: 'lazy', - loadChildren: ɵ0 - }]; - `; - - let warningCalled = false; - const { program, compilerHost } = createTypescriptContext(input, additionalFiles, true); - const transformer = importFactory(() => warningCalled = true, () => program.getTypeChecker()); - const result = transformTypescript(undefined, [transformer], program, compilerHost); - - expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); - expect(warningCalled).toBeFalsy(); - }); - - it('should not transform if the format is different than expected', () => { - const additionalFiles: Record = { - 'lazy/lazy.module.ts': ` - export const LazyModule = {}; - `, - }; - const input = tags.stripIndent` - const ɵ0 = () => import('./lazy/lazy.module').then(function (m) { return m.LazyModule; }); - const routes = [{ - path: 'lazy', - loadChildren: ɵ0 - }]; - `; - - let warningCalled = false; - const { program, compilerHost } = createTypescriptContext(input, additionalFiles, true); - const transformer = importFactory(() => warningCalled = true, () => program.getTypeChecker()); - const result = transformTypescript(undefined, [transformer], program, compilerHost); - - expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${input}`); - expect(warningCalled).toBeTruthy(); - }); - - it('should support resolving * re-exports', () => { - const additionalFiles: Record = { - 'shared/index.ts': ` - export * from './path/to/lazy/lazy.module'; - `, - 'shared/path/to/lazy/lazy.module.ts': ` - export const LazyModule = {}; - `, - }; - const input = tags.stripIndent` - const ɵ0 = () => import('./shared').then(m => m.LazyModule); - const routes = [{ - path: 'lazy', - loadChildren: ɵ0 - }]; - `; - - const output = tags.stripIndent` - const ɵ0 = () => import("./shared/path/to/lazy/lazy.module.ngfactory").then(m => m.LazyModuleNgFactory); - const routes = [{ - path: 'lazy', - loadChildren: ɵ0 - }]; - `; - - const { program, compilerHost } = createTypescriptContext(input, additionalFiles, true); - const transformer = importFactory(() => { }, () => program.getTypeChecker()); - const result = transformTypescript(undefined, [transformer], program, compilerHost); - - expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); - }); - - it('should support resolving named re-exports', () => { - const additionalFiles: Record = { - 'shared/index.ts': ` - export { LazyModule } from './path/to/lazy/lazy.module'; - `, - 'shared/path/to/lazy/lazy.module.ts': ` - export const LazyModule = {}; - `, - }; - const input = tags.stripIndent` - const ɵ0 = () => import('./shared').then(m => m.LazyModule); - const routes = [{ - path: 'lazy', - loadChildren: ɵ0 - }]; - `; - - const output = tags.stripIndent` - const ɵ0 = () => import("./shared/path/to/lazy/lazy.module.ngfactory").then(m => m.LazyModuleNgFactory); - const routes = [{ - path: 'lazy', - loadChildren: ɵ0 - }]; - `; - - const { program, compilerHost } = createTypescriptContext(input, additionalFiles, true); - const transformer = importFactory(() => { }, () => program.getTypeChecker()); - const result = transformTypescript(undefined, [transformer], program, compilerHost); - - expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); - }); - - - it('should support resolving re-export chains', () => { - const additionalFiles: Record = { - 'shared/index.ts': ` - export { LazyModule } from './index2'; - `, - 'shared/index2.ts': ` - export * from './index3'; - `, - 'shared/index3.ts': ` - export { LazyModule } from './index4'; - `, - 'shared/index4.ts': ` - export * from './path/to/lazy/lazy.module'; - `, - 'shared/path/to/lazy/lazy.module.ts': ` - export const LazyModule = {}; - `, - }; - const input = tags.stripIndent` - const ɵ0 = () => import('./shared').then(m => m.LazyModule); - const routes = [{ - path: 'lazy', - loadChildren: ɵ0 - }]; - `; - - const output = tags.stripIndent` - const ɵ0 = () => import("./shared/path/to/lazy/lazy.module.ngfactory").then(m => m.LazyModuleNgFactory); - const routes = [{ - path: 'lazy', - loadChildren: ɵ0 - }]; - `; - - const { program, compilerHost } = createTypescriptContext(input, additionalFiles, true); - const transformer = importFactory(() => { }, () => program.getTypeChecker()); - const result = transformTypescript(undefined, [transformer], program, compilerHost); - - expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); - }); - }); -}); diff --git a/packages/ngtools/webpack/src/transformers/index.ts b/packages/ngtools/webpack/src/transformers/index.ts index a3e9ffe038b8..7531574fff7e 100644 --- a/packages/ngtools/webpack/src/transformers/index.ts +++ b/packages/ngtools/webpack/src/transformers/index.ts @@ -8,13 +8,5 @@ export * from './interfaces'; export * from './ast_helpers'; export * from './make_transform'; -export * from './insert_import'; export * from './elide_imports'; -export * from './replace_bootstrap'; -export * from './replace_server_bootstrap'; -export * from './export_ngfactory'; -export * from './register_locale_data'; export * from './replace_resources'; -export * from './remove_decorators'; -export * from './find_resources'; -export * from './import_factory'; diff --git a/packages/ngtools/webpack/src/transformers/insert_import.ts b/packages/ngtools/webpack/src/transformers/insert_import.ts deleted file mode 100644 index a45d98cfbaf0..000000000000 --- a/packages/ngtools/webpack/src/transformers/insert_import.ts +++ /dev/null @@ -1,142 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import * as ts from 'typescript'; -import { collectDeepNodes, getFirstNode } from './ast_helpers'; -import { AddNodeOperation, TransformOperation } from './interfaces'; - - -export function insertStarImport( - sourceFile: ts.SourceFile, - identifier: ts.Identifier, - modulePath: string, - target?: ts.Node, - before = false, -): TransformOperation[] { - const ops: TransformOperation[] = []; - const allImports = collectDeepNodes(sourceFile, ts.SyntaxKind.ImportDeclaration); - - // We don't need to verify if the symbol is already imported, star imports should be unique. - - // Create the new import node. - const namespaceImport = ts.factory.createNamespaceImport(identifier); - const importClause = ts.factory.createImportClause(false, undefined, namespaceImport); - const newImport = ts.factory.createImportDeclaration(undefined, undefined, importClause, - ts.factory.createStringLiteral(modulePath)); - - if (target) { - ops.push(new AddNodeOperation( - sourceFile, - target, - before ? newImport : undefined, - before ? undefined : newImport, - )); - } else if (allImports.length > 0) { - // Find the last import and insert after. - ops.push(new AddNodeOperation( - sourceFile, - allImports[allImports.length - 1], - undefined, - newImport, - )); - } else { - const firstNode = getFirstNode(sourceFile); - - if (firstNode) { - // Insert before the first node. - ops.push(new AddNodeOperation( - sourceFile, - firstNode, - newImport, - )); - } - } - - return ops; -} - - -export function insertImport( - sourceFile: ts.SourceFile, - symbolName: string, - modulePath: string, -): TransformOperation[] { - const ops: TransformOperation[] = []; - // Find all imports. - const allImports = collectDeepNodes(sourceFile, ts.SyntaxKind.ImportDeclaration); - const maybeImports = allImports - .filter(ts.isImportDeclaration) - .filter((node) => { - // 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) => { - // 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) => { - // 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 ops; - } - - // 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.factory.createImportSpecifier(undefined, ts.factory.createIdentifier(symbolName)), - )); - } else { - // Create the new import node. - const namedImports = ts.factory.createNamedImports([ts.factory.createImportSpecifier(undefined, - ts.factory.createIdentifier(symbolName))]); - const importClause = ts.factory.createImportClause(false, undefined, namedImports); - const newImport = ts.factory.createImportDeclaration(undefined, undefined, importClause, - ts.factory.createStringLiteral(modulePath)); - - if (allImports.length > 0) { - // Find the last import and insert after. - ops.push(new AddNodeOperation( - sourceFile, - allImports[allImports.length - 1], - undefined, - newImport, - )); - } else { - const firstNode = getFirstNode(sourceFile); - - if (firstNode) { - // Insert before the first node. - ops.push(new AddNodeOperation( - sourceFile, - firstNode, - newImport, - )); - } - } - } - - return ops; -} diff --git a/packages/ngtools/webpack/src/transformers/multiple_transformers_spec.ts b/packages/ngtools/webpack/src/transformers/multiple_transformers_spec.ts deleted file mode 100644 index 5c8a06754872..000000000000 --- a/packages/ngtools/webpack/src/transformers/multiple_transformers_spec.ts +++ /dev/null @@ -1,80 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { tags } from '@angular-devkit/core'; // tslint:disable-line:no-implicit-dependencies -import { exportNgFactory } from './export_ngfactory'; -import { removeDecorators } from './remove_decorators'; -import { replaceBootstrap } from './replace_bootstrap'; -import { createTypescriptContext, transformTypescript } from './spec_helpers'; - - -describe('@ngtools/webpack transformers', () => { - describe('multiple_transformers', () => { - it('should apply multiple transformers on the same file', () => { - const input = tags.stripIndent` - import { enableProdMode } from '@angular/core'; - import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; - import { Component } from '@angular/core'; - - import { AppModule } from './app/app.module'; - import { environment } from './environments/environment'; - - @Component({ - selector: 'app-root', - templateUrl: './app.component.html', - styleUrls: ['./app.component.css'] - }) - class AppComponent { - title = 'app'; - } - - if (environment.production) { - enableProdMode(); - } - - platformBrowserDynamic().bootstrapModule(AppModule); - `; - - // tslint:disable:max-line-length - const output = tags.stripIndent` - import { enableProdMode } from '@angular/core'; - import { environment } from './environments/environment'; - - import * as __NgCli_bootstrap_1 from "./app/app.module.ngfactory"; - import * as __NgCli_bootstrap_2 from "@angular/platform-browser"; - - class AppComponent { - constructor() { this.title = 'app'; } - } - - if (environment.production) { - enableProdMode(); - } - __NgCli_bootstrap_2.platformBrowser().bootstrapModuleFactory(__NgCli_bootstrap_1.AppModuleNgFactory); - `; - // tslint:enable:max-line-length - - const { program, compilerHost } = createTypescriptContext(input); - - const shouldTransform = () => true; - const getEntryModule = () => - ({ path: '/project/src/app/app.module', className: 'AppModule' }); - const getTypeChecker = () => program.getTypeChecker(); - - - const transformers = [ - replaceBootstrap(shouldTransform, getEntryModule, getTypeChecker), - exportNgFactory(shouldTransform, getEntryModule), - removeDecorators(shouldTransform, getTypeChecker), - ]; - - const result = transformTypescript(undefined, transformers, program, compilerHost); - - expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); - }); - }); -}); diff --git a/packages/ngtools/webpack/src/transformers/register_locale_data.ts b/packages/ngtools/webpack/src/transformers/register_locale_data.ts deleted file mode 100644 index c7479f7fbe0a..000000000000 --- a/packages/ngtools/webpack/src/transformers/register_locale_data.ts +++ /dev/null @@ -1,104 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import * as ts from 'typescript'; -import { collectDeepNodes, getFirstNode } from './ast_helpers'; -import { insertStarImport } from './insert_import'; -import { AddNodeOperation, StandardTransform, TransformOperation } from './interfaces'; -import { makeTransform } from './make_transform'; - - -export function registerLocaleData( - shouldTransform: (fileName: string) => boolean, - getEntryModule: () => { path: string, className: string } | null, - locale: string, -): ts.TransformerFactory { - - const standardTransform: StandardTransform = function (sourceFile: ts.SourceFile) { - const ops: TransformOperation[] = []; - - const entryModule = getEntryModule(); - - if (!shouldTransform(sourceFile.fileName) || !entryModule || !locale) { - return ops; - } - - // Find all identifiers using the entry module class name. - const entryModuleIdentifiers = collectDeepNodes(sourceFile, - ts.SyntaxKind.Identifier) - .filter(identifier => identifier.text === entryModule.className); - - if (entryModuleIdentifiers.length === 0) { - return []; - } - - // Find the bootstrap call - 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 firstNode = getFirstNode(sourceFile); - - if (!firstNode) { - return; - } - - // Create the import node for the locale. - const localeNamespaceId = ts.factory.createUniqueName('__NgCli_locale_'); - ops.push(...insertStarImport( - sourceFile, - localeNamespaceId, - `@angular/common/locales/${locale}`, - firstNode, - true, - )); - - // Create the import node for the registerLocaleData function. - const regIdentifier = ts.factory.createIdentifier(`registerLocaleData`); - const regNamespaceId = ts.factory.createUniqueName('__NgCli_locale_'); - ops.push( - ...insertStarImport(sourceFile, regNamespaceId, '@angular/common', firstNode, true), - ); - - // Create the register function call - const registerFunctionCall = ts.factory.createCallExpression( - ts.factory.createPropertyAccessExpression(regNamespaceId, regIdentifier), - undefined, - [ts.factory.createPropertyAccessExpression(localeNamespaceId, 'default')], - ); - const registerFunctionStatement = ts.factory.createExpressionStatement(registerFunctionCall); - - ops.push(new AddNodeOperation( - sourceFile, - firstNode, - registerFunctionStatement, - )); - }); - - return ops; - }; - - return makeTransform(standardTransform); -} diff --git a/packages/ngtools/webpack/src/transformers/register_locale_data_spec.ts b/packages/ngtools/webpack/src/transformers/register_locale_data_spec.ts deleted file mode 100644 index 3765bfcdb599..000000000000 --- a/packages/ngtools/webpack/src/transformers/register_locale_data_spec.ts +++ /dev/null @@ -1,77 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { tags } from '@angular-devkit/core'; // tslint:disable-line:no-implicit-dependencies -import { registerLocaleData } from './register_locale_data'; -import { transformTypescript } from './spec_helpers'; - -describe('@ngtools/webpack transformers', () => { - describe('register_locale_data', () => { - it('should add locale imports', () => { - const input = tags.stripIndent` - 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); - `; - const output = tags.stripIndent` - import * as __NgCli_locale_1 from "@angular/common/locales/fr"; - import * as __NgCli_locale_2 from "@angular/common"; - __NgCli_locale_2.registerLocaleData(__NgCli_locale_1.default); - - 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); - `; - - const transformer = registerLocaleData( - () => true, - () => ({ path: '/project/src/app/app.module', className: 'AppModule' }), - 'fr', - ); - const result = transformTypescript(input, [transformer]); - - expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); - }); - - it('should not add locale imports when there is no entry module', () => { - const input = tags.stripIndent` - 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); - `; - - const transformer = registerLocaleData(() => true, () => null, 'fr'); - const result = transformTypescript(input, [transformer]); - - expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${input}`); - }); - }); -}); diff --git a/packages/ngtools/webpack/src/transformers/remove_decorators.ts b/packages/ngtools/webpack/src/transformers/remove_decorators.ts deleted file mode 100644 index 7c0c72bdaf7c..000000000000 --- a/packages/ngtools/webpack/src/transformers/remove_decorators.ts +++ /dev/null @@ -1,98 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import * as ts from 'typescript'; -import { collectDeepNodes } from './ast_helpers'; -import { RemoveNodeOperation, StandardTransform, TransformOperation } from './interfaces'; -import { makeTransform } from './make_transform'; - - -export function removeDecorators( - shouldTransform: (fileName: string) => boolean, - getTypeChecker: () => ts.TypeChecker, -): ts.TransformerFactory { - - const standardTransform: StandardTransform = function (sourceFile: ts.SourceFile) { - const ops: TransformOperation[] = []; - - if (!shouldTransform(sourceFile.fileName)) { - return ops; - } - - collectDeepNodes(sourceFile, ts.SyntaxKind.Decorator) - .filter((decorator) => shouldRemove(decorator, getTypeChecker())) - .forEach((decorator) => { - // Remove the decorator node. - ops.push(new RemoveNodeOperation(sourceFile, decorator)); - }); - - return ops; - }; - - return makeTransform(standardTransform, getTypeChecker); -} - -function shouldRemove(decorator: ts.Decorator, typeChecker: ts.TypeChecker): boolean { - const origin = getDecoratorOrigin(decorator, typeChecker); - - return origin ? origin.module === '@angular/core' : false; -} - -// Decorator helpers. -interface DecoratorOrigin { - name: string; - module: string; -} - -function getDecoratorOrigin( - decorator: ts.Decorator, - typeChecker: ts.TypeChecker, -): DecoratorOrigin | null { - if (!ts.isCallExpression(decorator.expression)) { - return null; - } - - let identifier: ts.Node; - let name: string | undefined = undefined; - if (ts.isPropertyAccessExpression(decorator.expression.expression)) { - identifier = decorator.expression.expression.expression; - name = decorator.expression.expression.name.text; - } else if (ts.isIdentifier(decorator.expression.expression)) { - identifier = decorator.expression.expression; - } else { - return null; - } - - // NOTE: resolver.getReferencedImportDeclaration would work as well but is internal - const symbol = typeChecker.getSymbolAtLocation(identifier); - if (symbol && symbol.declarations && symbol.declarations.length > 0) { - const declaration = symbol.declarations[0]; - let module: string | undefined; - if (ts.isImportSpecifier(declaration)) { - name = (declaration.propertyName || declaration.name).text; - module = declaration.parent - && declaration.parent.parent - && declaration.parent.parent.parent - && (declaration.parent.parent.parent.moduleSpecifier as ts.StringLiteral).text - || ''; - } else if (ts.isNamespaceImport(declaration)) { - // Use the name from the decorator namespace property access - module = declaration.parent - && declaration.parent.parent - && (declaration.parent.parent.moduleSpecifier as ts.StringLiteral).text; - } else if (ts.isImportClause(declaration)) { - name = declaration.name && declaration.name.text; - module = declaration.parent && (declaration.parent.moduleSpecifier as ts.StringLiteral).text; - } else { - return null; - } - - return { name: name || '', module: module || '' }; - } - - return null; -} diff --git a/packages/ngtools/webpack/src/transformers/remove_decorators_spec.ts b/packages/ngtools/webpack/src/transformers/remove_decorators_spec.ts deleted file mode 100644 index 9cd037c23048..000000000000 --- a/packages/ngtools/webpack/src/transformers/remove_decorators_spec.ts +++ /dev/null @@ -1,294 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -// tslint:disable:no-big-function -import { tags } from '@angular-devkit/core'; // tslint:disable-line:no-implicit-dependencies -import { removeDecorators } from './remove_decorators'; -import { createTypescriptContext, transformTypescript } from './spec_helpers'; - -describe('@ngtools/webpack transformers', () => { - describe('remove_decorators', () => { - it('should remove Angular decorators', () => { - const input = tags.stripIndent` - import { Component } from '@angular/core'; - - @Component({ - selector: 'app-root', - templateUrl: './app.component.html', - styleUrls: ['./app.component.css'] - }) - export class AppComponent { - title = 'app'; - } - `; - const output = tags.stripIndent` - export class AppComponent { - constructor() { - this.title = 'app'; - } - } - `; - - const { program, compilerHost } = createTypescriptContext(input); - const transformer = removeDecorators( - () => true, - () => program.getTypeChecker(), - ); - const result = transformTypescript(undefined, [transformer], program, compilerHost); - - expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); - }); - - it('should not remove non-Angular decorators', () => { - const input = tags.stripIndent` - import { Component } from 'another-lib'; - - @Component({ - selector: 'app-root', - templateUrl: './app.component.html', - styleUrls: ['./app.component.css'] - }) - export class AppComponent { - title = 'app'; - } - `; - const output = ` - import { __decorate } from "tslib"; - import { Component } from 'another-lib'; - let AppComponent = class AppComponent { - constructor() { - this.title = 'app'; - } - }; - AppComponent = __decorate([ - Component({ - selector: 'app-root', - templateUrl: './app.component.html', - styleUrls: ['./app.component.css'] - }) - ], AppComponent); - export { AppComponent }; - `; - - const { program, compilerHost } = createTypescriptContext(input); - const transformer = removeDecorators( - () => true, - () => program.getTypeChecker(), - ); - const result = transformTypescript(undefined, [transformer], program, compilerHost); - - expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); - }); - - it('should keep other decorators on class member', () => { - const input = tags.stripIndent` - import { Component, HostListener } from '@angular/core'; - import { AnotherDecorator } from 'another-lib'; - - @Component({ - selector: 'app-root', - templateUrl: './app.component.html', - styleUrls: ['./app.component.css'] - }) - export class AppComponent { - title = 'app'; - - @HostListener('document:keydown.escape') - @AnotherDecorator() - onEscape() { - console.log('run'); - } - } - `; - const output = tags.stripIndent` - import { __decorate } from "tslib"; - import { AnotherDecorator } from 'another-lib'; - - export class AppComponent { - constructor() { - this.title = 'app'; - } - - onEscape() { - console.log('run'); - } - } - __decorate([ - AnotherDecorator() - ], AppComponent.prototype, "onEscape", null); - `; - - const { program, compilerHost } = createTypescriptContext(input); - const transformer = removeDecorators( - () => true, - () => program.getTypeChecker(), - ); - const result = transformTypescript(undefined, [transformer], program, compilerHost); - - expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); - }); - - it('should keep other decorators on class declaration', () => { - const input = tags.stripIndent` - import { Component } from '@angular/core'; - import { AnotherDecorator } from 'another-lib'; - - @AnotherDecorator() - @Component({ - selector: 'app-root', - templateUrl: './app.component.html', - styleUrls: ['./app.component.css'] - }) - export class AppComponent { - title = 'app'; - } - `; - const output = tags.stripIndent` - import { __decorate } from "tslib"; - import { AnotherDecorator } from 'another-lib'; - - let AppComponent = class AppComponent { - constructor() { - this.title = 'app'; - } - }; - AppComponent = __decorate([ - AnotherDecorator() - ], AppComponent); - export { AppComponent }; - `; - - const { program, compilerHost } = createTypescriptContext(input); - const transformer = removeDecorators( - () => true, - () => program.getTypeChecker(), - ); - const result = transformTypescript(undefined, [transformer], program, compilerHost); - - expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); - }); - - it('should remove imports for identifiers within the decorator', () => { - const input = tags.stripIndent` - import { Component } from '@angular/core'; - import { ChangeDetectionStrategy } from '@angular/core'; - - @Component({ - selector: 'app-root', - changeDetection: ChangeDetectionStrategy.OnPush, - templateUrl: './app.component.html', - styleUrls: ['./app.component.css'] - }) - export class AppComponent { - title = 'app'; - } - `; - const output = tags.stripIndent` - export class AppComponent { - constructor() { - this.title = 'app'; - } - } - `; - - const { program, compilerHost } = createTypescriptContext(input); - const transformer = removeDecorators( - () => true, - () => program.getTypeChecker(), - ); - const result = transformTypescript(undefined, [transformer], program, compilerHost); - - expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); - }); - - it('should not remove imports from types that are still used', () => { - const input = tags.stripIndent` - import { Component, ChangeDetectionStrategy, EventEmitter } from '@angular/core'; - import { abc } from 'xyz'; - - @Component({ - selector: 'app-root', - changeDetection: ChangeDetectionStrategy.OnPush, - templateUrl: './app.component.html', - styleUrls: ['./app.component.css'] - }) - export class AppComponent { - notify: EventEmitter = new EventEmitter(); - title = 'app'; - example = { abc }; - } - - export { ChangeDetectionStrategy }; - `; - const output = tags.stripIndent` - import { ChangeDetectionStrategy, EventEmitter } from '@angular/core'; - import { abc } from 'xyz'; - - export class AppComponent { - constructor() { - this.notify = new EventEmitter(); - this.title = 'app'; - this.example = { abc }; - } - } - - export { ChangeDetectionStrategy }; - `; - - const { program, compilerHost } = createTypescriptContext(input); - const transformer = removeDecorators( - () => true, - () => program.getTypeChecker(), - ); - const result = transformTypescript(undefined, [transformer], program, compilerHost); - - expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); - }); - - it('should not prevent removal of unused imports', () => { - // Note: this is actually testing the behaviour of `./elide_imports.ts`, but the problem - // only came up when used together with `./remove_decorators.ts` so we test it here. - // Originally reported as https://github.com/angular/angular-cli/issues/14617 - // The bug happened because `elideImports` would attempt to remove `BigDependency` and - // in the process modify the import statement, which then prevented TS from removing it - // the interface too. - const input = tags.stripIndent` - import {BigDependency, BigDependencyOptions} from 'src/app/lorem/big-dependency'; - import {Injectable} from '@angular/core'; - - const bigDependencyLoader = () => import('../lorem/big-dependency'); - - @Injectable({providedIn: 'root'}) - export class LoremService { - load(options: BigDependencyOptions): Promise { - return bigDependencyLoader() - .then((m: {BigDependency}) => new m.BigDependency().setOptions(options)); - } - } - - `; - const output = tags.stripIndent` - const bigDependencyLoader = () => import('../lorem/big-dependency'); - export class LoremService { - load(options) { - return bigDependencyLoader() - .then((m) => new m.BigDependency().setOptions(options)); - } - } - `; - - const { program, compilerHost } = createTypescriptContext(input); - const transformer = removeDecorators( - () => true, - () => program.getTypeChecker(), - ); - const result = transformTypescript(undefined, [transformer], program, compilerHost); - - expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); - }); - }); -}); diff --git a/packages/ngtools/webpack/src/transformers/replace_bootstrap.ts b/packages/ngtools/webpack/src/transformers/replace_bootstrap.ts deleted file mode 100644 index 23db9e237c61..000000000000 --- a/packages/ngtools/webpack/src/transformers/replace_bootstrap.ts +++ /dev/null @@ -1,110 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { dirname, relative } from 'path'; -import * as ts from 'typescript'; -import { forwardSlashPath } from '../utils'; -import { collectDeepNodes } from './ast_helpers'; -import { insertStarImport } from './insert_import'; -import { ReplaceNodeOperation, StandardTransform, TransformOperation } from './interfaces'; -import { makeTransform } from './make_transform'; - - -export function replaceBootstrap( - shouldTransform: (fileName: string) => boolean, - getEntryModule: () => { path: string, className: string } | null, - getTypeChecker: () => ts.TypeChecker, - useFactories = true, -): ts.TransformerFactory { - - const standardTransform: StandardTransform = function (sourceFile: ts.SourceFile) { - const ops: TransformOperation[] = []; - - const entryModule = getEntryModule(); - - if (!shouldTransform(sourceFile.fileName) || !entryModule) { - return ops; - } - - // Find all identifiers. - const entryModuleIdentifiers = collectDeepNodes(sourceFile, - ts.SyntaxKind.Identifier) - .filter(identifier => identifier.text === entryModule.className); - - if (entryModuleIdentifiers.length === 0) { - return []; - } - - // Find the bootstrap calls. - 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; - - const idPlatformBrowser = ts.factory.createUniqueName('__NgCli_bootstrap_'); - const idNgFactory = ts.factory.createUniqueName('__NgCli_bootstrap_'); - - // Add the transform operations. - const relativeEntryModulePath = relative(dirname(sourceFile.fileName), entryModule.path); - let className = entryModule.className; - let modulePath = forwardSlashPath(`./${relativeEntryModulePath}`); - let bootstrapIdentifier = 'bootstrapModule'; - - if (useFactories) { - className += 'NgFactory'; - modulePath += '.ngfactory'; - bootstrapIdentifier = 'bootstrapModuleFactory'; - } - - ops.push( - // Replace the entry module import. - ...insertStarImport(sourceFile, idNgFactory, modulePath), - new ReplaceNodeOperation(sourceFile, entryModuleIdentifier, - ts.factory.createPropertyAccessExpression(idNgFactory, ts.factory.createIdentifier(className))), - // Replace the platformBrowserDynamic import. - ...insertStarImport(sourceFile, idPlatformBrowser, '@angular/platform-browser'), - new ReplaceNodeOperation(sourceFile, platformBrowserDynamicIdentifier, - ts.factory.createPropertyAccessExpression(idPlatformBrowser, 'platformBrowser')), - new ReplaceNodeOperation(sourceFile, bootstrapModuleIdentifier, - ts.factory.createIdentifier(bootstrapIdentifier)), - ); - }); - - return ops; - }; - - return makeTransform(standardTransform, getTypeChecker); -} diff --git a/packages/ngtools/webpack/src/transformers/replace_bootstrap_spec.ts b/packages/ngtools/webpack/src/transformers/replace_bootstrap_spec.ts deleted file mode 100644 index 2a9afa19952c..000000000000 --- a/packages/ngtools/webpack/src/transformers/replace_bootstrap_spec.ts +++ /dev/null @@ -1,162 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { tags } from '@angular-devkit/core'; // tslint:disable-line:no-implicit-dependencies -import { replaceBootstrap } from './replace_bootstrap'; -import { createTypescriptContext, transformTypescript } from './spec_helpers'; - -describe('@ngtools/webpack transformers', () => { - describe('replace_bootstrap', () => { - it('should replace bootstrap', () => { - const input = tags.stripIndent` - 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); - `; - - const output = tags.stripIndent` - import { enableProdMode } from '@angular/core'; - import { environment } from './environments/environment'; - - import * as __NgCli_bootstrap_1 from "./app/app.module.ngfactory"; - import * as __NgCli_bootstrap_2 from "@angular/platform-browser"; - - if (environment.production) { - enableProdMode(); - } - __NgCli_bootstrap_2.platformBrowser().bootstrapModuleFactory(__NgCli_bootstrap_1.AppModuleNgFactory); - `; - - const { program, compilerHost } = createTypescriptContext(input); - const transformer = replaceBootstrap( - () => true, - () => ({ path: '/project/src/app/app.module', className: 'AppModule' }), - () => program.getTypeChecker(), - ); - const result = transformTypescript(undefined, [transformer], program, compilerHost); - - expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); - }); - - it('should replace bootstrap without referencing ngFactory when useFactories is false', () => { - const input = tags.stripIndent` - 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); - `; - - // tslint:disable:max-line-length - const output = tags.stripIndent` - import { enableProdMode } from '@angular/core'; - import { environment } from './environments/environment'; - - import * as __NgCli_bootstrap_1 from "./app/app.module"; - import * as __NgCli_bootstrap_2 from "@angular/platform-browser"; - - if (environment.production) { - enableProdMode(); - } - __NgCli_bootstrap_2.platformBrowser().bootstrapModule(__NgCli_bootstrap_1.AppModule); - `; - // tslint:enable:max-line-length - - const { program, compilerHost } = createTypescriptContext(input); - const transformer = replaceBootstrap( - () => true, - () => ({ path: '/project/src/app/app.module', className: 'AppModule' }), - () => program.getTypeChecker(), - false, - ); - const result = transformTypescript(undefined, [transformer], program, compilerHost); - - expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); - }); - - it('should replace bootstrap when barrel files are used', () => { - const input = tags.stripIndent` - import { enableProdMode } from '@angular/core'; - import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; - - import { AppModule } from './app'; - import { environment } from './environments/environment'; - - if (environment.production) { - enableProdMode(); - } - - platformBrowserDynamic().bootstrapModule(AppModule); - `; - - // tslint:disable:max-line-length - const output = tags.stripIndent` - import { enableProdMode } from '@angular/core'; - import { environment } from './environments/environment'; - - import * as __NgCli_bootstrap_1 from "./app/app.module.ngfactory"; - import * as __NgCli_bootstrap_2 from "@angular/platform-browser"; - - if (environment.production) { - enableProdMode(); - } - __NgCli_bootstrap_2.platformBrowser().bootstrapModuleFactory(__NgCli_bootstrap_1.AppModuleNgFactory); - `; - // tslint:enable:max-line-length - - const { program, compilerHost } = createTypescriptContext(input); - const transformer = replaceBootstrap( - () => true, - () => ({ path: '/project/src/app/app.module', className: 'AppModule' }), - () => program.getTypeChecker(), - ); - const result = transformTypescript(undefined, [transformer], program, compilerHost); - - expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); - }); - - it('should not replace bootstrap when there is no entry module', () => { - const input = tags.stripIndent` - 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); - `; - - const { program, compilerHost } = createTypescriptContext(input); - const transformer = replaceBootstrap( - () => true, - () => null, - () => program.getTypeChecker(), - ); - const result = transformTypescript(undefined, [transformer], program, compilerHost); - - expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${input}`); - }); - }); -}); diff --git a/packages/ngtools/webpack/src/transformers/replace_server_bootstrap.ts b/packages/ngtools/webpack/src/transformers/replace_server_bootstrap.ts deleted file mode 100644 index 910775cf1dff..000000000000 --- a/packages/ngtools/webpack/src/transformers/replace_server_bootstrap.ts +++ /dev/null @@ -1,141 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { dirname, relative } from 'path'; -import * as ts from 'typescript'; -import { forwardSlashPath } from '../utils'; -import { collectDeepNodes } from './ast_helpers'; -import { insertStarImport } from './insert_import'; -import { ReplaceNodeOperation, StandardTransform, TransformOperation } from './interfaces'; -import { makeTransform } from './make_transform'; - -export function replaceServerBootstrap( - shouldTransform: (fileName: string) => boolean, - getEntryModule: () => { path: string, className: string } | null, - getTypeChecker: () => ts.TypeChecker, -): ts.TransformerFactory { - - const standardTransform: StandardTransform = function (sourceFile: ts.SourceFile) { - const ops: TransformOperation[] = []; - - const entryModule = getEntryModule(); - - if (!shouldTransform(sourceFile.fileName) || !entryModule) { - return ops; - } - - // Find all identifiers. - const entryModuleIdentifiers = collectDeepNodes(sourceFile, - ts.SyntaxKind.Identifier) - .filter(identifier => identifier.text === entryModule.className); - - if (entryModuleIdentifiers.length === 0) { - return []; - } - - const relativeEntryModulePath = relative(dirname(sourceFile.fileName), entryModule.path); - const normalizedEntryModulePath = forwardSlashPath(`./${relativeEntryModulePath}`); - const factoryClassName = entryModule.className + 'NgFactory'; - const factoryModulePath = normalizedEntryModulePath + '.ngfactory'; - - // Find the bootstrap calls. - entryModuleIdentifiers.forEach(entryModuleIdentifier => { - if (!entryModuleIdentifier.parent) { - return; - } - - if (entryModuleIdentifier.parent.kind !== ts.SyntaxKind.CallExpression && - entryModuleIdentifier.parent.kind !== ts.SyntaxKind.PropertyAssignment) { - return; - } - - if (entryModuleIdentifier.parent.kind === ts.SyntaxKind.CallExpression) { - // Figure out if it's a `platformDynamicServer().bootstrapModule(AppModule)` call. - - const callExpr = entryModuleIdentifier.parent as ts.CallExpression; - - if (callExpr.expression.kind === ts.SyntaxKind.PropertyAccessExpression) { - - 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 === 'platformDynamicServer' - )) { - return; - } - - const platformDynamicServerIdentifier = innerCallExpr.expression as ts.Identifier; - - const idPlatformServer = ts.factory.createUniqueName('__NgCli_bootstrap_'); - const idNgFactory = ts.factory.createUniqueName('__NgCli_bootstrap_'); - - // Add the transform operations. - ops.push( - // Replace the entry module import. - ...insertStarImport(sourceFile, idNgFactory, factoryModulePath), - new ReplaceNodeOperation(sourceFile, entryModuleIdentifier, - ts.factory.createPropertyAccessExpression(idNgFactory, ts.factory.createIdentifier(factoryClassName))), - // Replace the platformBrowserDynamic import. - ...insertStarImport(sourceFile, idPlatformServer, '@angular/platform-server'), - new ReplaceNodeOperation(sourceFile, platformDynamicServerIdentifier, - ts.factory.createPropertyAccessExpression(idPlatformServer, 'platformServer')), - new ReplaceNodeOperation(sourceFile, bootstrapModuleIdentifier, - ts.factory.createIdentifier('bootstrapModuleFactory')), - ); - } else if (callExpr.expression.kind === ts.SyntaxKind.Identifier) { - // Figure out if it is renderModule - - const identifierExpr = callExpr.expression as ts.Identifier; - - if (identifierExpr.text !== 'renderModule') { - return; - } - - const renderModuleIdentifier = identifierExpr as ts.Identifier; - - const idPlatformServer = ts.factory.createUniqueName('__NgCli_bootstrap_'); - const idNgFactory = ts.factory.createUniqueName('__NgCli_bootstrap_'); - - ops.push( - // Replace the entry module import. - ...insertStarImport(sourceFile, idNgFactory, factoryModulePath), - new ReplaceNodeOperation(sourceFile, entryModuleIdentifier, - ts.factory.createPropertyAccessExpression(idNgFactory, ts.factory.createIdentifier(factoryClassName))), - // Replace the renderModule import. - ...insertStarImport(sourceFile, idPlatformServer, '@angular/platform-server'), - new ReplaceNodeOperation(sourceFile, renderModuleIdentifier, - ts.factory.createPropertyAccessExpression(idPlatformServer, 'renderModuleFactory')), - ); - } - } else if (entryModuleIdentifier.parent.kind === ts.SyntaxKind.PropertyAssignment) { - // This is for things that accept a module as a property in a config object - // .ie the express engine - - const idNgFactory = ts.factory.createUniqueName('__NgCli_bootstrap_'); - - ops.push( - ...insertStarImport(sourceFile, idNgFactory, factoryModulePath), - new ReplaceNodeOperation(sourceFile, entryModuleIdentifier, - ts.factory.createPropertyAccessExpression(idNgFactory, ts.factory.createIdentifier(factoryClassName))), - ); - } - }); - - return ops; - }; - - return makeTransform(standardTransform, getTypeChecker); -} diff --git a/packages/ngtools/webpack/src/transformers/replace_server_bootstrap_spec.ts b/packages/ngtools/webpack/src/transformers/replace_server_bootstrap_spec.ts deleted file mode 100644 index bd7b51fdcc9d..000000000000 --- a/packages/ngtools/webpack/src/transformers/replace_server_bootstrap_spec.ts +++ /dev/null @@ -1,215 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -// tslint:disable:no-big-function -import { tags } from '@angular-devkit/core'; // tslint:disable-line:no-implicit-dependencies -import { replaceServerBootstrap } from './replace_server_bootstrap'; -import { createTypescriptContext, transformTypescript } from './spec_helpers'; - -describe('@ngtools/webpack transformers', () => { - describe('replace_server_bootstrap', () => { - it('should replace bootstrap', () => { - const input = tags.stripIndent` - import { enableProdMode } from '@angular/core'; - import { platformDynamicServer } from '@angular/platform-server'; - - import { AppModule } from './app/app.module'; - import { environment } from './environments/environment'; - - if (environment.production) { - enableProdMode(); - } - - platformDynamicServer().bootstrapModule(AppModule); - `; - - const output = tags.stripIndent` - import { enableProdMode } from '@angular/core'; - import { environment } from './environments/environment'; - - import * as __NgCli_bootstrap_1 from "./app/app.module.ngfactory"; - import * as __NgCli_bootstrap_2 from "@angular/platform-server"; - - if (environment.production) { - enableProdMode(); - } - __NgCli_bootstrap_2.platformServer().bootstrapModuleFactory(__NgCli_bootstrap_1.AppModuleNgFactory); - `; - - const { program, compilerHost } = createTypescriptContext(input); - const transformer = replaceServerBootstrap( - () => true, - () => ({ path: '/project/src/app/app.module', className: 'AppModule' }), - () => program.getTypeChecker(), - ); - const result = transformTypescript(undefined, [transformer], program, compilerHost); - - expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); - }); - - it('should replace renderModule', () => { - const input = tags.stripIndent` - import { enableProdMode } from '@angular/core'; - import { renderModule } from '@angular/platform-server'; - - import { AppModule } from './app/app.module'; - import { environment } from './environments/environment'; - - if (environment.production) { - enableProdMode(); - } - - renderModule(AppModule, { - document: '', - url: '/' - }); - `; - - const output = tags.stripIndent` - import { enableProdMode } from '@angular/core'; - import { environment } from './environments/environment'; - - import * as __NgCli_bootstrap_1 from "./app/app.module.ngfactory"; - import * as __NgCli_bootstrap_2 from "@angular/platform-server"; - - if (environment.production) { - enableProdMode(); - } - __NgCli_bootstrap_2.renderModuleFactory(__NgCli_bootstrap_1.AppModuleNgFactory, { - document: '', - url: '/' - }); - `; - - const { program, compilerHost } = createTypescriptContext(input); - const transformer = replaceServerBootstrap( - () => true, - () => ({ path: '/project/src/app/app.module', className: 'AppModule' }), - () => program.getTypeChecker(), - ); - const result = transformTypescript(undefined, [transformer], program, compilerHost); - - expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); - }); - - it('should replace when the module is used in a config object', () => { - const input = tags.stripIndent` - import * as express from 'express'; - - import { enableProdMode } from '@angular/core'; - import { ngExpressEngine } from '@nguniversal/express-engine'; - - import { AppModule } from './app/app.module'; - import { environment } from './environments/environment'; - - if (environment.production) { - enableProdMode(); - } - - const server = express(); - server.engine('html', ngExpressEngine({ - bootstrap: AppModule - })); - `; - - const output = tags.stripIndent` - import * as express from 'express'; - - import { enableProdMode } from '@angular/core'; - import { ngExpressEngine } from '@nguniversal/express-engine'; - - import { environment } from './environments/environment'; - - import * as __NgCli_bootstrap_1 from "./app/app.module.ngfactory"; - - if (environment.production) { - enableProdMode(); - } - - const server = express(); - server.engine('html', ngExpressEngine({ - bootstrap: __NgCli_bootstrap_1.AppModuleNgFactory - })); - `; - - const { program, compilerHost } = createTypescriptContext(input); - const transformer = replaceServerBootstrap( - () => true, - () => ({ path: '/project/src/app/app.module', className: 'AppModule' }), - () => program.getTypeChecker(), - ); - const result = transformTypescript(undefined, [transformer], program, compilerHost); - - expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); - }); - - it('should replace bootstrap when barrel files are used', () => { - const input = tags.stripIndent` - import { enableProdMode } from '@angular/core'; - import { platformDynamicServer } from '@angular/platform-browser-dynamic'; - - import { AppModule } from './app'; - import { environment } from './environments/environment'; - - if (environment.production) { - enableProdMode(); - } - - platformDynamicServer().bootstrapModule(AppModule); - `; - - const output = tags.stripIndent` - import { enableProdMode } from '@angular/core'; - import { environment } from './environments/environment'; - - import * as __NgCli_bootstrap_1 from "./app/app.module.ngfactory"; - import * as __NgCli_bootstrap_2 from "@angular/platform-server"; - - if (environment.production) { - enableProdMode(); - } - __NgCli_bootstrap_2.platformServer().bootstrapModuleFactory(__NgCli_bootstrap_1.AppModuleNgFactory); - `; - - const { program, compilerHost } = createTypescriptContext(input); - const transformer = replaceServerBootstrap( - () => true, - () => ({ path: '/project/src/app/app.module', className: 'AppModule' }), - () => program.getTypeChecker(), - ); - const result = transformTypescript(undefined, [transformer], program, compilerHost); - - expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); - }); - - it('should not replace bootstrap when there is no entry module', () => { - const input = tags.stripIndent` - import { enableProdMode } from '@angular/core'; - import { platformDynamicServer } from '@angular/platform-browser-dynamic'; - - import { AppModule } from './app/app.module'; - import { environment } from './environments/environment'; - - if (environment.production) { - enableProdMode(); - } - - platformDynamicServer().bootstrapModule(AppModule); - `; - - const { program, compilerHost } = createTypescriptContext(input); - const transformer = replaceServerBootstrap( - () => true, - () => null, - () => program.getTypeChecker(), - ); - const result = transformTypescript(undefined, [transformer], program, compilerHost); - - expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${input}`); - }); - }); -}); diff --git a/packages/ngtools/webpack/src/transformers/spec_helpers.ts b/packages/ngtools/webpack/src/transformers/spec_helpers.ts index e4e7ca04f3b4..674d809c068e 100644 --- a/packages/ngtools/webpack/src/transformers/spec_helpers.ts +++ b/packages/ngtools/webpack/src/transformers/spec_helpers.ts @@ -5,18 +5,11 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import { virtualFs } from '@angular-devkit/core'; -import { readFileSync, readdirSync, statSync } from 'fs'; -import { dirname, join } from 'path'; +import { basename } from 'path'; import * as ts from 'typescript'; -import { WebpackCompilerHost } from '../compiler_host'; - // Test transform helpers. -const basePath = '/project/src/'; -const basefileName = basePath + 'test-file.ts'; -const typeScriptLibFiles = loadTypeScriptLibFiles(); -const tsLibFiles = loadTsLibFiles(); +const basefileName = 'test-file.ts'; export function createTypescriptContext( content: string, @@ -38,40 +31,35 @@ export function createTypescriptContext( sourceMap: false, importHelpers: true, experimentalDecorators: true, + types: [], ...extraCompilerOptions, }; // Create compiler host. - const compilerHost = new WebpackCompilerHost( + const compilerHost = ts.createCompilerHost( compilerOptions, - basePath, - new virtualFs.SimpleMemoryHost(), - false, + true, ); - // Add a dummy file to host content. - compilerHost.writeFile(fileName, content, false); - - if (useLibs) { - // Write the default libs. - // These are needed for tests that use import(), because it relies on a Promise being there. - const compilerLibFolder = dirname(compilerHost.getDefaultLibFileName(compilerOptions)); - for (const [k, v] of Object.entries(typeScriptLibFiles)) { - compilerHost.writeFile(join(compilerLibFolder, k), v, false); - } - } - - if (compilerOptions.importHelpers) { - for (const [k, v] of Object.entries(tsLibFiles)) { - compilerHost.writeFile(k, v, false); - } - } + const baseFileExists = compilerHost.fileExists; + compilerHost.fileExists = function (compilerFileName: string) { + return ( + compilerFileName === fileName || + !!additionalFiles?.[basename(compilerFileName)] || + baseFileExists(compilerFileName) + ); + }; - if (additionalFiles) { - for (const key in additionalFiles) { - compilerHost.writeFile(basePath + key, additionalFiles[key], false); + const baseReadFile = compilerHost.readFile; + compilerHost.readFile = function(compilerFileName: string) { + if (compilerFileName === fileName) { + return content; + } else if (additionalFiles?.[basename(compilerFileName)]) { + return additionalFiles[basename(compilerFileName)]; + } else { + return baseReadFile(compilerFileName); } - } + }; // Create the TypeScript program. const program = ts.createProgram([fileName], compilerOptions, compilerHost); @@ -83,7 +71,7 @@ export function transformTypescript( content: string | undefined, transformers: ts.TransformerFactory[], program?: ts.Program, - compilerHost?: WebpackCompilerHost, + compilerHost?: ts.CompilerHost, ): string | undefined { // Use given context or create a new one. if (content !== undefined) { @@ -98,9 +86,15 @@ export function transformTypescript( throw new Error('transformTypescript needs either `content` or a `program` and `compilerHost'); } + const outputFileName = basefileName.replace(/\.tsx?$/, '.js'); + let outputContent; // Emit. const { emitSkipped, diagnostics } = program.emit( - undefined, undefined, undefined, undefined, { before: transformers }, + undefined, (filename, data) => { + if (filename === outputFileName) { + outputContent = data; + } + }, undefined, undefined, { before: transformers }, ); // Throw error with diagnostics if emit wasn't successfull. @@ -109,33 +103,5 @@ export function transformTypescript( } // Return the transpiled js. - return compilerHost.readFile(basefileName.replace(/\.tsx?$/, '.js')); -} - -function loadTypeScriptLibFiles(): Record { - const libFolderPath = dirname(require.resolve('typescript/lib/lib.d.ts')); - const libFolderFiles = readdirSync(libFolderPath); - const libFileNames = libFolderFiles.filter(f => f.startsWith('lib.') && f.endsWith('.d.ts')); - - // Return a map of the lib names to their content. - const libs: Record = {}; - for (const f of libFileNames) { - libs[f] = readFileSync(join(libFolderPath, f), 'utf-8'); - } - - return libs; -} - -function loadTsLibFiles(): Record { - const libFolderPath = dirname(require.resolve('tslib/package.json')); - const libFolderFiles = readdirSync(libFolderPath) - .filter(p => statSync(join(libFolderPath, p)).isFile()); - - // Return a map of the lib names to their content. - const libs: Record = {}; - for (const f of libFolderFiles) { - libs[join('node_modules/tslib', f)] = readFileSync(join(libFolderPath, f), 'utf-8'); - } - - return libs; + return outputContent; } diff --git a/packages/ngtools/webpack/src/type_checker.ts b/packages/ngtools/webpack/src/type_checker.ts deleted file mode 100644 index ea50676bc10c..000000000000 --- a/packages/ngtools/webpack/src/type_checker.ts +++ /dev/null @@ -1,135 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { normalize, resolve, virtualFs } from '@angular-devkit/core'; -import { NodeJsSyncHost } from '@angular-devkit/core/node'; -import { - CompilerHost, - CompilerOptions, - Program, - createCompilerHost, - createProgram, -} from '@angular/compiler-cli'; -import * as ts from 'typescript'; -import { time, timeEnd } from './benchmark'; -import { WebpackCompilerHost } from './compiler_host'; -import { - CancellationToken, DiagnosticMode, - gatherDiagnostics, reportDiagnostics, -} from './diagnostics'; -import { LogMessage, TypeCheckerMessage } from './type_checker_messages'; - - -// This file should run in a child process with the AUTO_START_ARG argument -export const AUTO_START_ARG = '9d93e901-158a-4cf9-ba1b-2f0582ffcfeb'; - -export class TypeChecker { - private _program?: ts.Program | Program; - private _compilerHost: WebpackCompilerHost & CompilerHost; - - constructor( - private _compilerOptions: CompilerOptions, - _basePath: string, - private _JitMode: boolean, - private _rootNames: string[], - hostReplacementPaths: { [path: string]: string }, - ) { - time('TypeChecker.constructor'); - const host = new virtualFs.AliasHost(new NodeJsSyncHost()); - - // Add file replacements. - for (const from in hostReplacementPaths) { - const normalizedFrom = resolve(normalize(_basePath), normalize(from)); - const normalizedWith = resolve( - normalize(_basePath), - normalize(hostReplacementPaths[from]), - ); - host.aliases.set(normalizedFrom, normalizedWith); - } - - const compilerHost = new WebpackCompilerHost( - _compilerOptions, - _basePath, - host, - true, - ); - // We don't set a async resource loader on the compiler host because we only support - // html templates, which are the only ones that can throw errors, and those can be loaded - // synchronously. - // If we need to also report errors on styles then we'll need to ask the main thread - // for these resources. - this._compilerHost = createCompilerHost({ - options: this._compilerOptions, - tsHost: compilerHost, - }) as CompilerHost & WebpackCompilerHost; - timeEnd('TypeChecker.constructor'); - } - - private _update(rootNames: string[], changedCompilationFiles: string[]) { - time('TypeChecker._update'); - this._rootNames = rootNames; - changedCompilationFiles.forEach((fileName) => { - this._compilerHost.invalidate(fileName); - }); - timeEnd('TypeChecker._update'); - } - - private _createOrUpdateProgram() { - if (this._JitMode) { - // Create the TypeScript program. - time('TypeChecker._createOrUpdateProgram.ts.createProgram'); - this._program = ts.createProgram( - this._rootNames, - this._compilerOptions, - this._compilerHost, - this._program as ts.Program, - ) as ts.Program; - timeEnd('TypeChecker._createOrUpdateProgram.ts.createProgram'); - } else { - time('TypeChecker._createOrUpdateProgram.ng.createProgram'); - // Create the Angular program. - this._program = createProgram({ - rootNames: this._rootNames, - options: this._compilerOptions, - host: this._compilerHost, - oldProgram: this._program as Program, - }) as Program; - timeEnd('TypeChecker._createOrUpdateProgram.ng.createProgram'); - } - } - - private _diagnose(cancellationToken: CancellationToken) { - if (!this._program) { - return; - } - - const allDiagnostics = gatherDiagnostics( - this._program, this._JitMode, 'TypeChecker', DiagnosticMode.Semantic, cancellationToken); - - // Report diagnostics. - if (!cancellationToken.isCancellationRequested()) { - reportDiagnostics( - allDiagnostics, - msg => this.sendMessage(new LogMessage('error', 'Error: ' + msg)), - msg => this.sendMessage(new LogMessage('warn', 'Warning: ' + msg)), - ); - } - } - - private sendMessage(msg: TypeCheckerMessage) { - if (process.send) { - process.send(msg); - } - } - - public update(rootNames: string[], changedCompilationFiles: string[], - cancellationToken: CancellationToken) { - this._update(rootNames, changedCompilationFiles); - this._createOrUpdateProgram(); - this._diagnose(cancellationToken); - } -} diff --git a/packages/ngtools/webpack/src/type_checker_bootstrap.js b/packages/ngtools/webpack/src/type_checker_bootstrap.js deleted file mode 100644 index da95c57aaca5..000000000000 --- a/packages/ngtools/webpack/src/type_checker_bootstrap.js +++ /dev/null @@ -1,9 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -require('../../../../lib/bootstrap-local'); -require('./type_checker_worker.ts'); diff --git a/packages/ngtools/webpack/src/type_checker_messages.ts b/packages/ngtools/webpack/src/type_checker_messages.ts deleted file mode 100644 index d42c0492d6c0..000000000000 --- a/packages/ngtools/webpack/src/type_checker_messages.ts +++ /dev/null @@ -1,44 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { logging } from '@angular-devkit/core'; -import * as ts from 'typescript'; - - -export enum MESSAGE_KIND { - Init, - Update, - Log, -} - -export abstract class TypeCheckerMessage { - constructor(public kind: MESSAGE_KIND) { } -} - -export class InitMessage extends TypeCheckerMessage { - constructor( - public compilerOptions: ts.CompilerOptions, - public basePath: string, - public jitMode: boolean, - public rootNames: string[], - public hostReplacementPaths: { [path: string]: string }, - ) { - super(MESSAGE_KIND.Init); - } -} - -export class UpdateMessage extends TypeCheckerMessage { - constructor(public rootNames: string[], public changedCompilationFiles: string[]) { - super(MESSAGE_KIND.Update); - } -} - -export class LogMessage extends TypeCheckerMessage { - constructor(public level: logging.LogLevel, public message: string) { - super(MESSAGE_KIND.Log); - } -} diff --git a/packages/ngtools/webpack/src/type_checker_worker.ts b/packages/ngtools/webpack/src/type_checker_worker.ts deleted file mode 100644 index 488f34b24e84..000000000000 --- a/packages/ngtools/webpack/src/type_checker_worker.ts +++ /dev/null @@ -1,59 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import * as process from 'process'; -import { time, timeEnd } from './benchmark'; -import { CancellationToken } from './diagnostics'; -import { - AUTO_START_ARG, - TypeChecker, -} from './type_checker'; -import { - InitMessage, - MESSAGE_KIND, - TypeCheckerMessage, - UpdateMessage, -} from './type_checker_messages'; - -let typeChecker: TypeChecker; -let lastCancellationToken: CancellationToken; - -// only listen to messages if started from the AngularCompilerPlugin -if (process.argv.indexOf(AUTO_START_ARG) >= 0) { - process.on('message', (message: TypeCheckerMessage) => { - time('TypeChecker.message'); - switch (message.kind) { - case MESSAGE_KIND.Init: - const initMessage = message as InitMessage; - typeChecker = new TypeChecker( - initMessage.compilerOptions, - initMessage.basePath, - initMessage.jitMode, - initMessage.rootNames, - initMessage.hostReplacementPaths, - ); - break; - case MESSAGE_KIND.Update: - if (!typeChecker) { - throw new Error('TypeChecker: update message received before initialization'); - } - if (lastCancellationToken) { - // This cancellation token doesn't seem to do much, messages don't seem to be processed - // before the diagnostics finish. - lastCancellationToken.requestCancellation(); - } - const updateMessage = message as UpdateMessage; - lastCancellationToken = new CancellationToken(); - typeChecker.update(updateMessage.rootNames, updateMessage.changedCompilationFiles, - lastCancellationToken); - break; - default: - throw new Error(`TypeChecker: Unexpected message received: ${message}.`); - } - timeEnd('TypeChecker.message'); - }); -} diff --git a/packages/ngtools/webpack/src/utils.ts b/packages/ngtools/webpack/src/utils.ts deleted file mode 100644 index 3b21d591ed36..000000000000 --- a/packages/ngtools/webpack/src/utils.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { Path, getSystemPath, normalize } from '@angular-devkit/core'; - -// `TsCompilerAotCompilerTypeCheckHostAdapter` in @angular/compiler-cli seems to resolve module -// names directly via `resolveModuleName`, which prevents full Path usage. -// NSTSC also uses Node.JS `path.resolve` which will result in incorrect paths in Windows -// Example: `/D/MyPath/MyProject` -> `D:/d/mypath/myproject` -// To work around this we must provide the same path format as TS internally uses in -// the SourceFile paths. -export function workaroundResolve(path: Path | string): string { - return forwardSlashPath(getSystemPath(normalize(path))); -} - -export function flattenArray(value: Array): T[] { - return ([] as T[]).concat.apply([], value); -} - -// TS represents paths internally with '/' and expects paths to be in this format. -export function forwardSlashPath(path: string): string { - return path.replace(/\\/g, '/'); -} diff --git a/packages/ngtools/webpack/src/utils_spec.ts b/packages/ngtools/webpack/src/utils_spec.ts deleted file mode 100644 index 6da7940a0eb7..000000000000 --- a/packages/ngtools/webpack/src/utils_spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -import { flattenArray } from './utils'; - -describe('@ngtools/webpack utils', () => { - describe('flattenArray', () => { - it('should flatten an array', () => { - const arr = flattenArray(['module', ['browser', 'main']]); - expect(arr).toEqual(['module', 'browser', 'main']); - }); - }); -}); diff --git a/packages/ngtools/webpack/src/virtual_file_system_decorator.ts b/packages/ngtools/webpack/src/virtual_file_system_decorator.ts deleted file mode 100644 index 438dec15906a..000000000000 --- a/packages/ngtools/webpack/src/virtual_file_system_decorator.ts +++ /dev/null @@ -1,315 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { FileDoesNotExistException, Path, getSystemPath, normalize } from '@angular-devkit/core'; -import { Stats } from 'fs'; -import { InputFileSystem } from 'webpack'; -import { WebpackCompilerHost } from './compiler_host'; -import { isWebpackFiveOrHigher } from './webpack-version'; - -interface NodeWatchFileSystemInterface { - inputFileSystem: InputFileSystem; - new(inputFileSystem: InputFileSystem): NodeWatchFileSystemInterface; - // tslint:disable-next-line:no-any - watch(files: any, dirs: any, missing: any, startTime: any, options: any, callback: any, - // tslint:disable-next-line:no-any - callbackUndelayed: any): any; -} - -export const NodeWatchFileSystem: NodeWatchFileSystemInterface = require( - 'webpack/lib/node/NodeWatchFileSystem'); - -// NOTE: @types/webpack InputFileSystem is missing some methods -export class VirtualFileSystemDecorator implements InputFileSystem { - constructor( - private _inputFileSystem: InputFileSystem, - private _webpackCompilerHost: WebpackCompilerHost, - ) { } - - getWebpackCompilerHost() { - return this._webpackCompilerHost; - } - - getVirtualFilesPaths() { - return this._webpackCompilerHost.getNgFactoryPaths(); - } - - stat(path: string, callback: (err: Error, result: Stats) => void): void { - const result = this._webpackCompilerHost.stat(path); - if (result) { - // tslint:disable-next-line:no-any - callback(null as any, result); - } else { - // tslint:disable-next-line:no-any - callback(new FileDoesNotExistException(path), undefined as any); - } - } - - readdir(path: string, callback: (err: Error, result: string[]) => void): void { - // tslint:disable-next-line: no-any - (this._inputFileSystem as any).readdir(path, callback); - } - - readFile(path: string, callback: (err: Error, contents: Buffer) => void): void { - try { - // tslint:disable-next-line:no-any - callback(null as any, this._webpackCompilerHost.readFileBuffer(path)); - } catch (e) { - // tslint:disable-next-line:no-any - callback(e, undefined as any); - } - } - - readJson(path: string, callback: (err: Error, result: unknown) => void): void { - // tslint:disable-next-line:no-any - (this._inputFileSystem as any).readJson(path, callback); - } - - readlink( - path: string, - // Callback types differ between Webpack 4 and 5 - // tslint:disable-next-line: no-any - callback: (err: any, linkString: any) => void, - ): void { - this._inputFileSystem.readlink(path, callback); - } - - statSync(path: string): Stats { - const stats = this._webpackCompilerHost.stat(path); - if (stats === null) { - throw new FileDoesNotExistException(path); - } - - return stats; - } - - readdirSync(path: string): string[] { - // tslint:disable-next-line:no-any - return (this._inputFileSystem as any).readdirSync(path); - } - - readFileSync(path: string): Buffer { - return this._webpackCompilerHost.readFileBuffer(path); - } - - readJsonSync(path: string): string { - // tslint:disable-next-line:no-any - return (this._inputFileSystem as any).readJsonSync(path); - } - - readlinkSync(path: string): string { - // Synchronous functions are missing from the Webpack typings - // tslint:disable-next-line: no-any - return (this._inputFileSystem as any).readlinkSync(path); - } - - purge(changes?: string[] | string): void { - if (typeof changes === 'string') { - this._webpackCompilerHost.invalidate(changes); - } else if (Array.isArray(changes)) { - changes.forEach((fileName: string) => this._webpackCompilerHost.invalidate(fileName)); - } - if (this._inputFileSystem.purge) { - // tslint:disable-next-line:no-any - (this._inputFileSystem as any).purge(changes); - } - } -} - -export class VirtualWatchFileSystemDecorator extends NodeWatchFileSystem { - constructor( - private _virtualInputFileSystem: VirtualFileSystemDecorator, - private _replacements?: Map | ((path: Path) => Path), - ) { - super(_virtualInputFileSystem); - } - - mapReplacements( - original: Iterable, - reverseReplacements: Map, - ): Iterable { - if (!this._replacements) { - return original; - } - const replacements = this._replacements; - - return [...original].map(file => { - if (typeof replacements === 'function') { - const replacement = getSystemPath(replacements(normalize(file))); - if (replacement !== file) { - reverseReplacements.set(replacement, file); - } - - return replacement; - } else { - const replacement = replacements.get(normalize(file)); - if (replacement) { - const fullReplacement = getSystemPath(replacement); - reverseReplacements.set(fullReplacement, file); - - return fullReplacement; - } else { - return file; - } - } - }); - } - - reverseTimestamps( - map: Map, - reverseReplacements: Map, - ): Map { - for (const entry of Array.from(map.entries())) { - const original = reverseReplacements.get(entry[0]); - if (original) { - map.set(original, entry[1]); - map.delete(entry[0]); - } - } - - return map; - } - - createWebpack4Watch() { - return ( - files: Iterable, - dirs: Iterable, - missing: Iterable, - startTime: number, - options: {}, - callback: Parameters[5], - callbackUndelayed: (filename: string, timestamp: number) => void, - ): ReturnType => { - const reverseReplacements = new Map(); - - const newCallbackUndelayed = (filename: string, timestamp: number) => { - const original = reverseReplacements.get(filename); - if (original) { - this._virtualInputFileSystem.purge(original); - callbackUndelayed(original, timestamp); - } else { - callbackUndelayed(filename, timestamp); - } - }; - - const newCallback: Parameters[5] = ( - err: Error | null, - filesModified: string[], - contextModified: string[], - missingModified: string[], - fileTimestamps: Map, - contextTimestamps: Map, - ) => { - // Update fileTimestamps with timestamps from virtual files. - const virtualFilesStats = this._virtualInputFileSystem.getVirtualFilesPaths() - .map((fileName) => ({ - path: fileName, - mtime: +this._virtualInputFileSystem.statSync(fileName).mtime, - })); - virtualFilesStats.forEach(stats => fileTimestamps.set(stats.path, +stats.mtime)); - callback( - err, - filesModified.map(value => reverseReplacements.get(value) || value), - contextModified.map(value => reverseReplacements.get(value) || value), - missingModified.map(value => reverseReplacements.get(value) || value), - this.reverseTimestamps(fileTimestamps, reverseReplacements), - this.reverseTimestamps(contextTimestamps, reverseReplacements), - ); - }; - - const watcher = super.watch( - this.mapReplacements(files, reverseReplacements), - this.mapReplacements(dirs, reverseReplacements), - this.mapReplacements(missing, reverseReplacements), - startTime, - options, - newCallback, - newCallbackUndelayed, - ); - - return { - close: () => watcher.close(), - pause: () => watcher.pause(), - getFileTimestamps: () => - this.reverseTimestamps(watcher.getFileTimestamps(), reverseReplacements), - getContextTimestamps: () => - this.reverseTimestamps(watcher.getContextTimestamps(), reverseReplacements), - }; - }; - } - - createWebpack5Watch() { - return ( - files: Iterable, - dirs: Iterable, - missing: Iterable, - startTime: number, - options: {}, - callback: Parameters[5], - callbackUndelayed: (filename: string, timestamp: number) => void, - ): ReturnType => { - const reverseReplacements = new Map(); - - const newCallbackUndelayed = (filename: string, timestamp: number) => { - const original = reverseReplacements.get(filename); - if (original) { - this._virtualInputFileSystem.purge(original); - callbackUndelayed(original, timestamp); - } else { - callbackUndelayed(filename, timestamp); - } - }; - - const newCallback = ( - err: Error, - // tslint:disable-next-line: no-any - fileTimeInfoEntries: Map, - // tslint:disable-next-line: no-any - contextTimeInfoEntries: Map, - missing: Set, - removals: Set, - ) => { - // Update fileTimestamps with timestamps from virtual files. - const virtualFilesStats = this._virtualInputFileSystem.getVirtualFilesPaths() - .map((fileName) => ({ - path: fileName, - mtime: +this._virtualInputFileSystem.statSync(fileName).mtime, - })); - virtualFilesStats.forEach(stats => fileTimeInfoEntries.set(stats.path, +stats.mtime)); - callback( - err, - this.reverseTimestamps(fileTimeInfoEntries, reverseReplacements), - this.reverseTimestamps(contextTimeInfoEntries, reverseReplacements), - new Set([...missing].map(value => reverseReplacements.get(value) || value)), - new Set([...removals].map(value => reverseReplacements.get(value) || value)), - ); - }; - - const watcher = super.watch( - this.mapReplacements(files, reverseReplacements), - this.mapReplacements(dirs, reverseReplacements), - this.mapReplacements(missing, reverseReplacements), - startTime, - options, - newCallback, - newCallbackUndelayed, - ); - - return { - close: () => watcher.close(), - pause: () => watcher.pause(), - getFileTimeInfoEntries: () => - this.reverseTimestamps(watcher.getFileTimeInfoEntries(), reverseReplacements), - getContextTimeInfoEntries: () => - this.reverseTimestamps(watcher.getContextTimeInfoEntries(), reverseReplacements), - }; - }; - } - - // tslint:disable-next-line: no-any - watch = isWebpackFiveOrHigher() ? this.createWebpack5Watch() : this.createWebpack4Watch() as any; -} diff --git a/packages/ngtools/webpack/src/webpack-input-host.ts b/packages/ngtools/webpack/src/webpack-input-host.ts deleted file mode 100644 index 292c9de8e1ce..000000000000 --- a/packages/ngtools/webpack/src/webpack-input-host.ts +++ /dev/null @@ -1,69 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { PathFragment, fragment, getSystemPath, virtualFs } from '@angular-devkit/core'; -import { Stats } from 'fs'; -import { InputFileSystem } from 'webpack'; - -// Host is used instead of ReadonlyHost due to most decorators only supporting Hosts -export function createWebpackInputHost(inputFileSystem: InputFileSystem) { - return virtualFs.createSyncHost({ - write() { - throw new Error('Not supported.'); - }, - - delete() { - throw new Error('Not supported.'); - }, - - rename() { - throw new Error('Not supported.'); - }, - - read(path): virtualFs.FileBuffer { - // Synchronous functions are missing from the Webpack typings - // tslint:disable-next-line: no-any - const data = (inputFileSystem as any).readFileSync(getSystemPath(path)); - - return new Uint8Array(data).buffer as ArrayBuffer; - }, - - list(path): PathFragment[] { - // readdirSync exists but is not in the Webpack typings - const names: string[] = ((inputFileSystem as unknown) as { - readdirSync: (path: string) => string[]; - }).readdirSync(getSystemPath(path)); - - return names.map((name) => fragment(name)); - }, - - exists(path): boolean { - return !!this.stat(path); - }, - - isDirectory(path): boolean { - return this.stat(path)?.isDirectory() ?? false; - }, - - isFile(path): boolean { - return this.stat(path)?.isFile() ?? false; - }, - - stat(path): Stats | null { - try { - // Synchronous functions are missing from the Webpack typings - // tslint:disable-next-line: no-any - return (inputFileSystem as any).statSync(getSystemPath(path)); - } catch (e) { - if (e.code === 'ENOENT') { - return null; - } - throw e; - } - }, - }); -}