UNPKG

@angular/core

Version:

Angular - the core framework

756 lines (734 loc) • 29.2 kB
'use strict'; /** * @license Angular v19.2.8 * (c) 2010-2025 Google LLC. https://angular.io/ * License: MIT */ 'use strict'; var index = require('./index-CAJ-Rm56.js'); var schematics = require('@angular-devkit/schematics'); var core = require('@angular-devkit/core'); var posixPath = require('node:path/posix'); var os = require('os'); var ts = require('typescript'); var checker = require('./checker-BNmiXJIJ.js'); require('path'); var project_tsconfig_paths = require('./project_tsconfig_paths-CDVxT6Ov.js'); function _interopNamespaceDefault(e) { var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n.default = e; return Object.freeze(n); } var posixPath__namespace = /*#__PURE__*/_interopNamespaceDefault(posixPath); var os__namespace = /*#__PURE__*/_interopNamespaceDefault(os); /// <reference types="node" /> class NgtscCompilerHost { fs; options; constructor(fs, options = {}) { this.fs = fs; this.options = options; } getSourceFile(fileName, languageVersion) { const text = this.readFile(fileName); return text !== undefined ? ts.createSourceFile(fileName, text, languageVersion, true) : undefined; } getDefaultLibFileName(options) { return this.fs.join(this.getDefaultLibLocation(), ts.getDefaultLibFileName(options)); } getDefaultLibLocation() { return this.fs.getDefaultLibLocation(); } writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles) { const path = checker.absoluteFrom(fileName); this.fs.ensureDir(this.fs.dirname(path)); this.fs.writeFile(path, data); } getCurrentDirectory() { return this.fs.pwd(); } getCanonicalFileName(fileName) { return this.useCaseSensitiveFileNames() ? fileName : fileName.toLowerCase(); } useCaseSensitiveFileNames() { return this.fs.isCaseSensitive(); } getNewLine() { switch (this.options.newLine) { case ts.NewLineKind.CarriageReturnLineFeed: return '\r\n'; case ts.NewLineKind.LineFeed: return '\n'; default: return os__namespace.EOL; } } fileExists(fileName) { const absPath = this.fs.resolve(fileName); return this.fs.exists(absPath) && this.fs.stat(absPath).isFile(); } readFile(fileName) { const absPath = this.fs.resolve(fileName); if (!this.fileExists(absPath)) { return undefined; } return this.fs.readFile(absPath); } realpath(path) { return this.fs.realpath(this.fs.resolve(path)); } } // We use TypeScript's native `ts.matchFiles` utility for the virtual file systems // and their TypeScript compiler host `readDirectory` implementation. TypeScript's // function implements complex logic for matching files with respect to root // directory, extensions, excludes, includes etc. The function is currently // internal but we can use it as the API most likely will not change any time soon, // nor does it seem like this is being made public any time soon. // Related issue for tracking: https://github.com/microsoft/TypeScript/issues/13793. /** * Creates a {@link ts.CompilerHost#readDirectory} implementation function, * that leverages the specified file system (that may be e.g. virtual). */ function createFileSystemTsReadDirectoryFn(fs) { if (ts.matchFiles === undefined) { throw Error('Unable to read directory in configured file system. This means that ' + 'TypeScript changed its file matching internals.\n\nPlease consider downgrading your ' + 'TypeScript version, and report an issue in the Angular framework repository.'); } const matchFilesFn = ts.matchFiles.bind(ts); return (rootDir, extensions, excludes, includes, depth) => { const directoryExists = (p) => { const resolvedPath = fs.resolve(p); return fs.exists(resolvedPath) && fs.stat(resolvedPath).isDirectory(); }; return matchFilesFn(rootDir, extensions, excludes, includes, fs.isCaseSensitive(), fs.pwd(), depth, (p) => { const resolvedPath = fs.resolve(p); // TS also gracefully returns an empty file set. if (!directoryExists(resolvedPath)) { return { directories: [], files: [] }; } const children = fs.readdir(resolvedPath); const files = []; const directories = []; for (const child of children) { if (fs.stat(fs.join(resolvedPath, child))?.isDirectory()) { directories.push(child); } else { files.push(child); } } return { files, directories }; }, (p) => fs.resolve(p), (p) => directoryExists(p)); }; } function calcProjectFileAndBasePath(project, host = checker.getFileSystem()) { const absProject = host.resolve(project); const projectIsDir = host.lstat(absProject).isDirectory(); const projectFile = projectIsDir ? host.join(absProject, 'tsconfig.json') : absProject; const projectDir = projectIsDir ? absProject : host.dirname(absProject); const basePath = host.resolve(projectDir); return { projectFile, basePath }; } function readConfiguration(project, existingOptions, host = checker.getFileSystem()) { try { const fs = checker.getFileSystem(); const readConfigFile = (configFile) => ts.readConfigFile(configFile, (file) => host.readFile(host.resolve(file))); const readAngularCompilerOptions = (configFile, parentOptions = {}) => { const { config, error } = readConfigFile(configFile); if (error) { // Errors are handled later on by 'parseJsonConfigFileContent' return parentOptions; } // Note: In Google, `angularCompilerOptions` are stored in `bazelOptions`. // This function typically doesn't run for actual Angular compilations, but // tooling like Tsurge, or schematics may leverage this helper, so we account // for this here. const angularCompilerOptions = config.angularCompilerOptions ?? config.bazelOptions?.angularCompilerOptions; // we are only interested into merging 'angularCompilerOptions' as // other options like 'compilerOptions' are merged by TS let existingNgCompilerOptions = { ...angularCompilerOptions, ...parentOptions }; if (!config.extends) { return existingNgCompilerOptions; } const extendsPaths = typeof config.extends === 'string' ? [config.extends] : config.extends; // Call readAngularCompilerOptions recursively to merge NG Compiler options // Reverse the array so the overrides happen from right to left. return [...extendsPaths].reverse().reduce((prevOptions, extendsPath) => { const extendedConfigPath = getExtendedConfigPath(configFile, extendsPath, host, fs); return extendedConfigPath === null ? prevOptions : readAngularCompilerOptions(extendedConfigPath, prevOptions); }, existingNgCompilerOptions); }; const { projectFile, basePath } = calcProjectFileAndBasePath(project, host); const configFileName = host.resolve(host.pwd(), projectFile); const { config, error } = readConfigFile(projectFile); if (error) { return { project, errors: [error], rootNames: [], options: {}, emitFlags: index.EmitFlags.Default, }; } const existingCompilerOptions = { genDir: basePath, basePath, ...readAngularCompilerOptions(configFileName), ...existingOptions, }; const parseConfigHost = createParseConfigHost(host, fs); const { options, errors, fileNames: rootNames, projectReferences, } = ts.parseJsonConfigFileContent(config, parseConfigHost, basePath, existingCompilerOptions, configFileName); let emitFlags = index.EmitFlags.Default; if (!(options['skipMetadataEmit'] || options['flatModuleOutFile'])) { emitFlags |= index.EmitFlags.Metadata; } if (options['skipTemplateCodegen']) { emitFlags = emitFlags & ~index.EmitFlags.Codegen; } return { project: projectFile, rootNames, projectReferences, options, errors, emitFlags }; } catch (e) { const errors = [ { category: ts.DiagnosticCategory.Error, messageText: e.stack ?? e.message, file: undefined, start: undefined, length: undefined, source: 'angular', code: index.UNKNOWN_ERROR_CODE, }, ]; return { project: '', errors, rootNames: [], options: {}, emitFlags: index.EmitFlags.Default }; } } function createParseConfigHost(host, fs = checker.getFileSystem()) { return { fileExists: host.exists.bind(host), readDirectory: createFileSystemTsReadDirectoryFn(fs), readFile: host.readFile.bind(host), useCaseSensitiveFileNames: fs.isCaseSensitive(), }; } function getExtendedConfigPath(configFile, extendsValue, host, fs) { const result = getExtendedConfigPathWorker(configFile, extendsValue, host, fs); if (result !== null) { return result; } // Try to resolve the paths with a json extension append a json extension to the file in case if // it is missing and the resolution failed. This is to replicate TypeScript behaviour, see: // https://github.com/microsoft/TypeScript/blob/294a5a7d784a5a95a8048ee990400979a6bc3a1c/src/compiler/commandLineParser.ts#L2806 return getExtendedConfigPathWorker(configFile, `${extendsValue}.json`, host, fs); } function getExtendedConfigPathWorker(configFile, extendsValue, host, fs) { if (extendsValue.startsWith('.') || fs.isRooted(extendsValue)) { const extendedConfigPath = host.resolve(host.dirname(configFile), extendsValue); if (host.exists(extendedConfigPath)) { return extendedConfigPath; } } else { const parseConfigHost = createParseConfigHost(host, fs); // Path isn't a rooted or relative path, resolve like a module. const { resolvedModule } = ts.nodeModuleNameResolver(extendsValue, configFile, { moduleResolution: ts.ModuleResolutionKind.Node10, resolveJsonModule: true }, parseConfigHost); if (resolvedModule) { return checker.absoluteFrom(resolvedModule.resolvedFileName); } } return null; } /** * Angular compiler file system implementation that leverages an * CLI schematic virtual file tree. */ class DevkitMigrationFilesystem { tree; constructor(tree) { this.tree = tree; } extname(path) { return core.extname(path); } isRoot(path) { return path === core.normalize('/'); } isRooted(path) { return this.normalize(path).startsWith('/'); } dirname(file) { return this.normalize(core.dirname(file)); } join(basePath, ...paths) { return this.normalize(core.join(basePath, ...paths)); } relative(from, to) { return this.normalize(core.relative(from, to)); } basename(filePath, extension) { return posixPath__namespace.basename(filePath, extension); } normalize(path) { return core.normalize(path); } resolve(...paths) { const normalizedPaths = paths.map((p) => core.normalize(p)); // In dev-kit, the NodeJS working directory should never be // considered, so `/` is the last resort over `cwd`. return this.normalize(posixPath__namespace.resolve(core.normalize('/'), ...normalizedPaths)); } pwd() { return '/'; } isCaseSensitive() { return true; } exists(path) { return statPath(this.tree, path) !== null; } readFile(path) { return this.tree.readText(path); } readFileBuffer(path) { const buffer = this.tree.read(path); if (buffer === null) { throw new Error(`File does not exist: ${path}`); } return buffer; } readdir(path) { const dir = this.tree.getDir(path); return [ ...dir.subdirs, ...dir.subfiles, ]; } lstat(path) { const stat = statPath(this.tree, path); if (stat === null) { throw new Error(`File does not exist for "lstat": ${path}`); } return stat; } stat(path) { const stat = statPath(this.tree, path); if (stat === null) { throw new Error(`File does not exist for "stat": ${path}`); } return stat; } realpath(filePath) { return filePath; } getDefaultLibLocation() { return 'node_modules/typescript/lib'; } ensureDir(path) { // Migrations should compute replacements and not write directly. throw new Error('DevkitFilesystem#ensureDir is not supported.'); } writeFile(path, data) { // Migrations should compute replacements and not write directly. throw new Error('DevkitFilesystem#writeFile is not supported.'); } removeFile(path) { // Migrations should compute replacements and not write directly. throw new Error('DevkitFilesystem#removeFile is not supported.'); } copyFile(from, to) { // Migrations should compute replacements and not write directly. throw new Error('DevkitFilesystem#copyFile is not supported.'); } moveFile(from, to) { // Migrations should compute replacements and not write directly. throw new Error('DevkitFilesystem#moveFile is not supported.'); } removeDeep(path) { // Migrations should compute replacements and not write directly. throw new Error('DevkitFilesystem#removeDeep is not supported.'); } chdir(_path) { throw new Error('FileSystem#chdir is not supported.'); } symlink() { throw new Error('FileSystem#symlink is not supported.'); } } /** Stats the given path in the virtual tree. */ function statPath(tree, path) { let fileInfo = null; let dirInfo = null; try { fileInfo = tree.get(path); } catch (e) { if (e.constructor.name === 'PathIsDirectoryException') { dirInfo = tree.getDir(path); } else { throw e; } } if (fileInfo !== null || dirInfo !== null) { return { isDirectory: () => dirInfo !== null, isFile: () => fileInfo !== null, isSymbolicLink: () => false, }; } return null; } /** * Groups the given replacements per project relative * file path. * * This allows for simple execution of the replacements * against a given file. E.g. via {@link applyTextUpdates}. */ function groupReplacementsByFile(replacements) { const result = new Map(); for (const { projectFile, update } of replacements) { if (!result.has(projectFile.rootRelativePath)) { result.set(projectFile.rootRelativePath, []); } result.get(projectFile.rootRelativePath).push(update); } return result; } /** * Synchronously combines unit data for the given migration. * * Note: This helper is useful for testing and execution of * Tsurge migrations in non-batchable environments. In general, * prefer parallel execution of combining via e.g. Beam combiners. */ async function synchronouslyCombineUnitData(migration, unitDatas) { if (unitDatas.length === 0) { return null; } if (unitDatas.length === 1) { return unitDatas[0]; } let combined = unitDatas[0]; for (let i = 1; i < unitDatas.length; i++) { const other = unitDatas[i]; combined = await migration.combine(combined, other); } return combined; } /** * By default, Tsurge will always create an Angular compiler program * for projects analyzed and migrated. This works perfectly fine in * third-party where Tsurge migrations run in Angular CLI projects. * * In first party, when running against full Google3, creating an Angular * program for e.g. plain `ts_library` targets is overly expensive and * can result in out of memory issues for large TS targets. In 1P we can * reliably distinguish between TS and Angular targets via the `angularCompilerOptions`. */ function google3UsePlainTsProgramIfNoKnownAngularOption() { return process.env['GOOGLE3_TSURGE'] === '1'; } /** Options that are good defaults for Tsurge migrations. */ const defaultMigrationTsOptions = { // Avoid checking libraries to speed up migrations. skipLibCheck: true, skipDefaultLibCheck: true, noEmit: true, // Does not apply to g3 and externally is enforced when the app is built by the compiler. disableTypeScriptVersionCheck: true, }; /** * Creates an instance of a TypeScript program for the given project. */ function createPlainTsProgram(tsHost, tsconfig, optionOverrides) { const program = ts.createProgram({ rootNames: tsconfig.rootNames, options: { ...tsconfig.options, ...defaultMigrationTsOptions, ...optionOverrides, }, }); return { ngCompiler: null, program, userOptions: tsconfig.options, programAbsoluteRootFileNames: tsconfig.rootNames, host: tsHost, }; } /** * Parses the configuration of the given TypeScript project and creates * an instance of the Angular compiler for the project. */ function createNgtscProgram(tsHost, tsconfig, optionOverrides) { const ngtscProgram = new index.NgtscProgram(tsconfig.rootNames, { ...tsconfig.options, ...defaultMigrationTsOptions, ...optionOverrides, }, tsHost); // Expose an easy way to debug-print ng semantic diagnostics. if (process.env['DEBUG_NG_SEMANTIC_DIAGNOSTICS'] === '1') { console.error(ts.formatDiagnosticsWithColorAndContext(ngtscProgram.getNgSemanticDiagnostics(), tsHost)); } return { ngCompiler: ngtscProgram.compiler, program: ngtscProgram.getTsProgram(), userOptions: tsconfig.options, programAbsoluteRootFileNames: tsconfig.rootNames, host: tsHost, }; } /** Code of the error raised by TypeScript when a tsconfig doesn't match any files. */ const NO_INPUTS_ERROR_CODE = 18003; /** Parses the given tsconfig file, supporting Angular compiler options. */ function parseTsconfigOrDie(absoluteTsconfigPath, fs) { const tsconfig = readConfiguration(absoluteTsconfigPath, {}, fs); // Skip the "No inputs found..." error since we don't want to interrupt the migration if a // tsconfig doesn't match a file. This will result in an empty `Program` which is still valid. const errors = tsconfig.errors.filter((diag) => diag.code !== NO_INPUTS_ERROR_CODE); if (errors.length) { throw new Error(`Tsconfig could not be parsed or is invalid:\n\n` + `${errors.map((e) => e.messageText)}`); } return tsconfig; } /** Creates the base program info for the given tsconfig path. */ function createBaseProgramInfo(absoluteTsconfigPath, fs, optionOverrides = {}) { if (fs === undefined) { fs = new checker.NodeJSFileSystem(); checker.setFileSystem(fs); } const tsconfig = parseTsconfigOrDie(absoluteTsconfigPath, fs); const tsHost = new NgtscCompilerHost(fs, tsconfig.options); // When enabled, use a plain TS program if we are sure it's not // an Angular project based on the `tsconfig.json`. if (google3UsePlainTsProgramIfNoKnownAngularOption() && tsconfig.options['_useHostForImportGeneration'] === undefined) { return createPlainTsProgram(tsHost, tsconfig, optionOverrides); } return createNgtscProgram(tsHost, tsconfig, optionOverrides); } /** * @private * * Base class for the possible Tsurge migration variants. * * For example, this class exposes methods to conveniently create * TypeScript programs, while also allowing migration authors to override. */ class TsurgeBaseMigration { /** * Advanced Tsurge users can override this method, but most of the time, * overriding {@link prepareProgram} is more desirable. * * By default: * - In 3P: Ngtsc programs are being created. * - In 1P: Ngtsc or TS programs are created based on the Blaze target. */ createProgram(tsconfigAbsPath, fs, optionOverrides) { return createBaseProgramInfo(tsconfigAbsPath, fs, optionOverrides); } // Optional function to prepare the base `ProgramInfo` even further, // for the analyze and migrate phases. E.g. determining source files. prepareProgram(info) { const fullProgramSourceFiles = [...info.program.getSourceFiles()]; const sourceFiles = fullProgramSourceFiles.filter((f) => !f.isDeclarationFile && // Note `isShim` will work for the initial program, but for TCB programs, the shims are no longer annotated. !checker.isShim(f) && !f.fileName.endsWith('.ngtypecheck.ts')); // Sort it by length in reverse order (longest first). This speeds up lookups, // since there's no need to keep going through the array once a match is found. const sortedRootDirs = checker.getRootDirs(info.host, info.userOptions).sort((a, b) => b.length - a.length); // TODO: Consider also following TS's logic here, finding the common source root. // See: Program#getCommonSourceDirectory. const primaryRoot = checker.absoluteFrom(info.userOptions.rootDir ?? sortedRootDirs.at(-1) ?? info.program.getCurrentDirectory()); return { ...info, sourceFiles, fullProgramSourceFiles, sortedRootDirs, projectRoot: primaryRoot, }; } } /** * A simpler variant of a {@link TsurgeComplexMigration} that does not * fan-out into multiple workers per compilation unit to compute * the final migration replacements. * * This is faster and less resource intensive as workers and TS programs * are only ever created once. * * This is commonly the case when migrations are refactored to eagerly * compute replacements in the analyze stage, and then leverage the * global unit data to filter replacements that turned out to be "invalid". */ class TsurgeFunnelMigration extends TsurgeBaseMigration { } /** * Complex variant of a `Tsurge` migration. * * For example, every analyze worker may contribute to a list of TS * references that are later combined. The migrate phase can then compute actual * file updates for all individual compilation units, leveraging the global metadata * to e.g. see if there are any references from other compilation units that may be * problematic and prevent migration of a given file. */ class TsurgeComplexMigration extends TsurgeBaseMigration { } /*! * @license * Copyright Google LLC 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.dev/license */ exports.MigrationStage = void 0; (function (MigrationStage) { /** The migration is analyzing an entrypoint */ MigrationStage[MigrationStage["Analysis"] = 0] = "Analysis"; /** The migration is about to migrate an entrypoint */ MigrationStage[MigrationStage["Migrate"] = 1] = "Migrate"; })(exports.MigrationStage || (exports.MigrationStage = {})); /** Runs a Tsurge within an Angular Devkit context. */ async function runMigrationInDevkit(config) { const { buildPaths, testPaths } = await project_tsconfig_paths.getProjectTsConfigPaths(config.tree); if (!buildPaths.length && !testPaths.length) { throw new schematics.SchematicsException('Could not find any tsconfig file. Cannot run the migration.'); } const tsconfigPaths = [...buildPaths, ...testPaths]; const fs = new DevkitMigrationFilesystem(config.tree); checker.setFileSystem(fs); const migration = config.getMigration(fs); const unitResults = []; const isFunnelMigration = migration instanceof TsurgeFunnelMigration; for (const tsconfigPath of tsconfigPaths) { config.beforeProgramCreation?.(tsconfigPath, exports.MigrationStage.Analysis); const baseInfo = migration.createProgram(tsconfigPath, fs); const info = migration.prepareProgram(baseInfo); config.afterProgramCreation?.(info, fs, exports.MigrationStage.Analysis); config.beforeUnitAnalysis?.(tsconfigPath); unitResults.push(await migration.analyze(info)); } config.afterAllAnalyzed?.(); const combined = await synchronouslyCombineUnitData(migration, unitResults); if (combined === null) { config.afterAnalysisFailure?.(); return; } const globalMeta = await migration.globalMeta(combined); let replacements; if (isFunnelMigration) { replacements = (await migration.migrate(globalMeta)).replacements; } else { replacements = []; for (const tsconfigPath of tsconfigPaths) { config.beforeProgramCreation?.(tsconfigPath, exports.MigrationStage.Migrate); const baseInfo = migration.createProgram(tsconfigPath, fs); const info = migration.prepareProgram(baseInfo); config.afterProgramCreation?.(info, fs, exports.MigrationStage.Migrate); const result = await migration.migrate(globalMeta, info); replacements.push(...result.replacements); } } const replacementsPerFile = new Map(); const changesPerFile = groupReplacementsByFile(replacements); for (const [file, changes] of changesPerFile) { if (!replacementsPerFile.has(file)) { replacementsPerFile.set(file, changes); } } for (const [file, changes] of replacementsPerFile) { const recorder = config.tree.beginUpdate(file); for (const c of changes) { recorder .remove(c.data.position, c.data.end - c.data.position) .insertRight(c.data.position, c.data.toInsert); } config.tree.commitUpdate(recorder); } config.whenDone?.(await migration.stats(globalMeta)); } /** A text replacement for the given file. */ class Replacement { projectFile; update; constructor(projectFile, update) { this.projectFile = projectFile; this.update = update; } } /** An isolated text update that may be applied to a file. */ class TextUpdate { data; constructor(data) { this.data = data; } } /** Confirms that the given data `T` is serializable. */ function confirmAsSerializable(data) { return data; } /** * Gets a project file instance for the given file. * * Use this helper for dealing with project paths throughout your * migration. The return type is serializable. * * See {@link ProjectFile}. */ function projectFile(file, { sortedRootDirs, projectRoot }) { const fs = checker.getFileSystem(); const filePath = fs.resolve(typeof file === 'string' ? file : file.fileName); // Sorted root directories are sorted longest to shortest. First match // is the appropriate root directory for ID computation. for (const rootDir of sortedRootDirs) { if (!isWithinBasePath(fs, rootDir, filePath)) { continue; } return { id: fs.relative(rootDir, filePath), rootRelativePath: fs.relative(projectRoot, filePath), }; } // E.g. project directory may be `src/`, but files may be looked up // from `node_modules/`. This is fine, but in those cases, no root // directory matches. const rootRelativePath = fs.relative(projectRoot, filePath); return { id: rootRelativePath, rootRelativePath: rootRelativePath, }; } /** * Whether `path` is a descendant of the `base`? * E.g. `a/b/c` is within `a/b` but not within `a/x`. */ function isWithinBasePath(fs, base, path) { return checker.isLocalRelativePath(fs.relative(base, path)); } exports.Replacement = Replacement; exports.TextUpdate = TextUpdate; exports.TsurgeComplexMigration = TsurgeComplexMigration; exports.TsurgeFunnelMigration = TsurgeFunnelMigration; exports.confirmAsSerializable = confirmAsSerializable; exports.createBaseProgramInfo = createBaseProgramInfo; exports.projectFile = projectFile; exports.runMigrationInDevkit = runMigrationInDevkit;