@angular/core
Version: 
Angular - the core framework
756 lines (734 loc) • 29.2 kB
JavaScript
;
/**
 * @license Angular v19.2.8
 * (c) 2010-2025 Google LLC. https://angular.io/
 * License: MIT
 */
;
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;