UNPKG

@angular/core

Version:

Angular - the core framework

587 lines (567 loc) • 21.7 kB
'use strict'; /** * @license Angular v21.0.5 * (c) 2010-2025 Google LLC. https://angular.io/ * License: MIT */ 'use strict'; var compilerCli = require('@angular/compiler-cli'); var schematics = require('@angular-devkit/schematics'); var core = require('@angular-devkit/core'); var posixPath = require('node:path/posix'); var migrations = require('@angular/compiler-cli/private/migrations'); var ts = require('typescript'); var path = require('node:path'); var project_tsconfig_paths = require('./project_tsconfig_paths-CDVxT6Ov.cjs'); 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 path__namespace = /*#__PURE__*/_interopNamespaceDefault(path); /** * 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; } /** Whether we are executing inside Google */ function isGoogle3() { return process.env['GOOGLE3_TSURGE'] === '1'; } /** * 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 compilerCli.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 = compilerCli.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; } // Note: Try to keep mostly in sync with // //depot/google3/javascript/angular2/tools/ngc_wrapped/tsc_plugin.ts // TODO: Consider moving this logic into the 1P launcher. const EXT = /(\.ts|\.d\.ts|\.js|\.jsx|\.tsx)$/; function fileNameToModuleNameFactory(rootDirs, workspaceName) { return (importedFilePath) => { let relativePath = ''; for (const rootDir of rootDirs) { const rel = path__namespace.posix.relative(rootDir, importedFilePath); if (!rel.startsWith('.')) { relativePath = rel; break; } } if (relativePath) { return `${workspaceName}/${relativePath.replace(EXT, '')}`; } else { return importedFilePath.replace(EXT, ''); } }; } /** Creates the base program info for the given tsconfig path. */ function createBaseProgramInfo(absoluteTsconfigPath, fs, optionOverrides = {}) { // Make sure the FS becomes globally available. Some code paths // of the Angular compiler, or tsconfig parsing aren't leveraging // the specified file system. compilerCli.setFileSystem(fs); const tsconfig = parseTsconfigOrDie(absoluteTsconfigPath, fs); const tsHost = new compilerCli.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); } // The Angular program may try to emit references during analysis or migration. // To replicate the Google3 import emission here, ensure the unified module resolution // can be enabled by the compiler. if (isGoogle3() && tsconfig.options.rootDirs) { tsHost.fileNameToModuleName = fileNameToModuleNameFactory(tsconfig.options.rootDirs, /* workspaceName*/ 'google3'); } return createNgtscProgram(tsHost, tsconfig, optionOverrides); } /** * Creates the {@link ProgramInfo} from the given base information. * * This function purely exists to support custom programs that are * intended to be injected into Tsurge migrations. e.g. for language * service refactorings. */ function getProgramInfoFromBaseInfo(baseInfo) { const fullProgramSourceFiles = [...baseInfo.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. !migrations.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 = migrations.getRootDirs(baseInfo.host, baseInfo.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 = compilerCli.absoluteFrom(baseInfo.userOptions.rootDir ?? sortedRootDirs.at(-1) ?? baseInfo.program.getCurrentDirectory()); return { ...baseInfo, sourceFiles, fullProgramSourceFiles, sortedRootDirs, projectRoot: primaryRoot, }; } /** * @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 { /** * Creates the TypeScript program for a given compilation unit. * * 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, optionsOverride) { return getProgramInfoFromBaseInfo(createBaseProgramInfo(tsconfigAbsPath, fs, optionsOverride)); } } /** * 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 { } 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); compilerCli.setFileSystem(fs); const migration = config.getMigration(fs); const unitResults = []; const isFunnelMigration = migration instanceof TsurgeFunnelMigration; const compilationUnitAssignments = new Map(); for (const tsconfigPath of tsconfigPaths) { config.beforeProgramCreation?.(tsconfigPath, exports.MigrationStage.Analysis); const info = migration.createProgram(tsconfigPath, fs); modifyProgramInfoToEnsureNonOverlappingFiles(tsconfigPath, info, compilationUnitAssignments); 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 info = migration.createProgram(tsconfigPath, fs); modifyProgramInfoToEnsureNonOverlappingFiles(tsconfigPath, info, compilationUnitAssignments); 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)); } /** * Special logic for devkit migrations. In the Angular CLI, or in 3P precisely, * projects can have tsconfigs with overlapping source files. i.e. two tsconfigs * like e.g. build or test include the same `ts.SourceFile` (`.ts`). Migrations * should never have 2+ compilation units with overlapping source files as this * can result in duplicated replacements or analysis— hence we only ever assign a * source file to a compilation unit *once*. * * Note that this is fine as we expect Tsurge migrations to work together as * isolated compilation units— so it shouldn't matter if worst case a `.ts` * file ends up in the e.g. test program. */ function modifyProgramInfoToEnsureNonOverlappingFiles(tsconfigPath, info, compilationUnitAssignments) { const sourceFiles = []; for (const sf of info.sourceFiles) { const assignment = compilationUnitAssignments.get(sf.fileName); // File is already assigned to a different compilation unit. if (assignment !== undefined && assignment !== tsconfigPath) { continue; } compilationUnitAssignments.set(sf.fileName, tsconfigPath); sourceFiles.push(sf); } info.sourceFiles = sourceFiles; } /** 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 = compilerCli.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 compilerCli.isLocalRelativePath(fs.relative(base, path)); } exports.Replacement = Replacement; exports.TextUpdate = TextUpdate; exports.TsurgeComplexMigration = TsurgeComplexMigration; exports.TsurgeFunnelMigration = TsurgeFunnelMigration; exports.confirmAsSerializable = confirmAsSerializable; exports.projectFile = projectFile; exports.runMigrationInDevkit = runMigrationInDevkit;