"use strict";
/**
 * @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
 */
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AotCompilation = void 0;
const node_assert_1 = __importDefault(require("node:assert"));
const node_path_1 = require("node:path");
const typescript_1 = __importDefault(require("typescript"));
const profiling_1 = require("../../esbuild/profiling");
const angular_host_1 = require("../angular-host");
const jit_bootstrap_transformer_1 = require("../transformers/jit-bootstrap-transformer");
const lazy_routes_transformer_1 = require("../transformers/lazy-routes-transformer");
const web_worker_transformer_1 = require("../transformers/web-worker-transformer");
const angular_compilation_1 = require("./angular-compilation");
const hmr_candidates_1 = require("./hmr-candidates");
/**
 * The modified files count limit for performing component HMR analysis.
 * Performing content analysis for a large amount of files can result in longer rebuild times
 * than a full rebuild would entail.
 */
const HMR_MODIFIED_FILE_LIMIT = 32;
class AngularCompilationState {
    angularProgram;
    compilerHost;
    typeScriptProgram;
    affectedFiles;
    templateDiagnosticsOptimization;
    webWorkerTransform;
    diagnosticCache;
    constructor(angularProgram, compilerHost, typeScriptProgram, affectedFiles, templateDiagnosticsOptimization, webWorkerTransform, diagnosticCache = new WeakMap()) {
        this.angularProgram = angularProgram;
        this.compilerHost = compilerHost;
        this.typeScriptProgram = typeScriptProgram;
        this.affectedFiles = affectedFiles;
        this.templateDiagnosticsOptimization = templateDiagnosticsOptimization;
        this.webWorkerTransform = webWorkerTransform;
        this.diagnosticCache = diagnosticCache;
    }
    get angularCompiler() {
        return this.angularProgram.compiler;
    }
}
class AotCompilation extends angular_compilation_1.AngularCompilation {
    browserOnlyBuild;
    #state;
    constructor(browserOnlyBuild) {
        super();
        this.browserOnlyBuild = browserOnlyBuild;
    }
    async initialize(tsconfig, hostOptions, compilerOptionsTransformer) {
        // Dynamically load the Angular compiler CLI package
        const { NgtscProgram, OptimizeFor } = await angular_compilation_1.AngularCompilation.loadCompilerCli();
        // Load the compiler configuration and transform as needed
        const { options: originalCompilerOptions, rootNames, errors: configurationDiagnostics, } = await this.loadConfiguration(tsconfig);
        const compilerOptions = compilerOptionsTransformer?.(originalCompilerOptions) ?? originalCompilerOptions;
        if (compilerOptions.externalRuntimeStyles) {
            hostOptions.externalStylesheets ??= new Map();
        }
        // Reuse the package.json cache from the previous compilation
        const packageJsonCache = this.#state?.compilerHost
            .getModuleResolutionCache?.()
            ?.getPackageJsonInfoCache();
        const useHmr = compilerOptions['_enableHmr'] &&
            hostOptions.modifiedFiles &&
            hostOptions.modifiedFiles.size <= HMR_MODIFIED_FILE_LIMIT;
        let staleSourceFiles;
        let clearPackageJsonCache = false;
        if (hostOptions.modifiedFiles && this.#state) {
            for (const modifiedFile of hostOptions.modifiedFiles) {
                // Clear package.json cache if a node modules file was modified
                if (!clearPackageJsonCache && modifiedFile.includes('node_modules')) {
                    clearPackageJsonCache = true;
                    packageJsonCache?.clear();
                }
                // Collect stale source files for HMR analysis of inline component resources
                if (useHmr) {
                    const sourceFile = this.#state.typeScriptProgram.getSourceFile(modifiedFile);
                    if (sourceFile) {
                        staleSourceFiles ??= new Map();
                        staleSourceFiles.set(modifiedFile, sourceFile);
                    }
                }
            }
        }
        // Create Angular compiler host
        const host = (0, angular_host_1.createAngularCompilerHost)(typescript_1.default, compilerOptions, hostOptions, packageJsonCache);
        // Create the Angular specific program that contains the Angular compiler
        const angularProgram = (0, profiling_1.profileSync)('NG_CREATE_PROGRAM', () => new NgtscProgram(rootNames, compilerOptions, host, this.#state?.angularProgram));
        const angularCompiler = angularProgram.compiler;
        const angularTypeScriptProgram = angularProgram.getTsProgram();
        (0, angular_host_1.ensureSourceFileVersions)(angularTypeScriptProgram);
        let oldProgram = this.#state?.typeScriptProgram;
        let usingBuildInfo = false;
        if (!oldProgram) {
            oldProgram = typescript_1.default.readBuilderProgram(compilerOptions, host);
            usingBuildInfo = !!oldProgram;
        }
        const typeScriptProgram = typescript_1.default.createEmitAndSemanticDiagnosticsBuilderProgram(angularTypeScriptProgram, host, oldProgram, configurationDiagnostics);
        await (0, profiling_1.profileAsync)('NG_ANALYZE_PROGRAM', () => angularCompiler.analyzeAsync());
        let templateUpdates;
        if (useHmr && hostOptions.modifiedFiles && this.#state) {
            const componentNodes = (0, hmr_candidates_1.collectHmrCandidates)(hostOptions.modifiedFiles, angularProgram, staleSourceFiles);
            for (const node of componentNodes) {
                if (!typescript_1.default.isClassDeclaration(node)) {
                    continue;
                }
                const componentFilename = node.getSourceFile().fileName;
                let relativePath = (0, node_path_1.relative)(host.getCurrentDirectory(), componentFilename);
                if (relativePath.startsWith('..')) {
                    relativePath = componentFilename;
                }
                relativePath = relativePath.replaceAll('\\', '/');
                const updateId = encodeURIComponent(`${host.getCanonicalFileName(relativePath)}@${node.name?.text}`);
                const updateText = angularCompiler.emitHmrUpdateModule(node);
                // If compiler cannot generate an update for the component, prevent template updates.
                if (updateText === null) {
                    // Build is needed if a template cannot be updated
                    templateUpdates = undefined;
                    break;
                }
                templateUpdates ??= new Map();
                templateUpdates.set(updateId, updateText);
            }
        }
        const affectedFiles = (0, profiling_1.profileSync)('NG_FIND_AFFECTED', () => findAffectedFiles(typeScriptProgram, angularCompiler, usingBuildInfo));
        const componentResourcesDependencies = new Map();
        // Get all files referenced in the TypeScript/Angular program including component resources
        const referencedFiles = typeScriptProgram
            .getSourceFiles()
            .filter((sourceFile) => !angularCompiler.ignoreForEmit.has(sourceFile))
            .flatMap((sourceFile) => {
            const resourceDependencies = angularCompiler.getResourceDependencies(sourceFile);
            componentResourcesDependencies.set(sourceFile.fileName, resourceDependencies);
            // Also invalidate Angular diagnostics for a source file if component resources are modified
            if (this.#state && hostOptions.modifiedFiles?.size) {
                for (const resourceDependency of resourceDependencies) {
                    if (hostOptions.modifiedFiles.has(resourceDependency)) {
                        this.#state.diagnosticCache.delete(sourceFile);
                        // Also mark as affected in case changed template affects diagnostics
                        affectedFiles.add(sourceFile);
                    }
                }
            }
            return [sourceFile.fileName, ...resourceDependencies];
        });
        this.#state = new AngularCompilationState(angularProgram, host, typeScriptProgram, affectedFiles, affectedFiles.size === 1 ? OptimizeFor.SingleFile : OptimizeFor.WholeProgram, (0, web_worker_transformer_1.createWorkerTransformer)(hostOptions.processWebWorker.bind(hostOptions)), this.#state?.diagnosticCache);
        return {
            affectedFiles,
            compilerOptions,
            referencedFiles,
            externalStylesheets: hostOptions.externalStylesheets,
            templateUpdates,
            componentResourcesDependencies,
        };
    }
    *collectDiagnostics(modes) {
        (0, node_assert_1.default)(this.#state, 'Angular compilation must be initialized prior to collecting diagnostics.');
        const { affectedFiles, angularCompiler, diagnosticCache, templateDiagnosticsOptimization, typeScriptProgram, } = this.#state;
        const syntactic = modes & angular_compilation_1.DiagnosticModes.Syntactic;
        const semantic = modes & angular_compilation_1.DiagnosticModes.Semantic;
        // Collect program level diagnostics
        if (modes & angular_compilation_1.DiagnosticModes.Option) {
            yield* typeScriptProgram.getConfigFileParsingDiagnostics();
            yield* angularCompiler.getOptionDiagnostics();
            yield* typeScriptProgram.getOptionsDiagnostics();
        }
        if (syntactic) {
            yield* typeScriptProgram.getGlobalDiagnostics();
        }
        // Collect source file specific diagnostics
        for (const sourceFile of typeScriptProgram.getSourceFiles()) {
            if (angularCompiler.ignoreForDiagnostics.has(sourceFile)) {
                continue;
            }
            if (syntactic) {
                // TypeScript will use cached diagnostics for files that have not been
                // changed or affected for this build when using incremental building.
                yield* (0, profiling_1.profileSync)('NG_DIAGNOSTICS_SYNTACTIC', () => typeScriptProgram.getSyntacticDiagnostics(sourceFile), true);
            }
            if (!semantic) {
                continue;
            }
            yield* (0, profiling_1.profileSync)('NG_DIAGNOSTICS_SEMANTIC', () => typeScriptProgram.getSemanticDiagnostics(sourceFile), true);
            // Declaration files cannot have template diagnostics
            if (sourceFile.isDeclarationFile) {
                continue;
            }
            // Only request Angular template diagnostics for affected files to avoid
            // overhead of template diagnostics for unchanged files.
            if (affectedFiles.has(sourceFile)) {
                const angularDiagnostics = (0, profiling_1.profileSync)('NG_DIAGNOSTICS_TEMPLATE', () => angularCompiler.getDiagnosticsForFile(sourceFile, templateDiagnosticsOptimization), true);
                diagnosticCache.set(sourceFile, angularDiagnostics);
                yield* angularDiagnostics;
            }
            else {
                const angularDiagnostics = diagnosticCache.get(sourceFile);
                if (angularDiagnostics) {
                    yield* angularDiagnostics;
                }
            }
        }
    }
    emitAffectedFiles() {
        (0, node_assert_1.default)(this.#state, 'Angular compilation must be initialized prior to emitting files.');
        const { affectedFiles, angularCompiler, compilerHost, typeScriptProgram, webWorkerTransform } = this.#state;
        const compilerOptions = typeScriptProgram.getCompilerOptions();
        const buildInfoFilename = compilerOptions.tsBuildInfoFile ?? '.tsbuildinfo';
        const useTypeScriptTranspilation = !compilerOptions.isolatedModules ||
            !!compilerOptions.sourceMap ||
            !!compilerOptions.inlineSourceMap;
        const emittedFiles = new Map();
        const writeFileCallback = (filename, contents, _a, _b, sourceFiles) => {
            if (!sourceFiles?.length && filename.endsWith(buildInfoFilename)) {
                // Save builder info contents to specified location
                compilerHost.writeFile(filename, contents, false);
                return;
            }
            (0, node_assert_1.default)(sourceFiles?.length === 1, 'Invalid TypeScript program emit for ' + filename);
            const sourceFile = typescript_1.default.getOriginalNode(sourceFiles[0], typescript_1.default.isSourceFile);
            if (angularCompiler.ignoreForEmit.has(sourceFile)) {
                return;
            }
            angularCompiler.incrementalCompilation.recordSuccessfulEmit(sourceFile);
            emittedFiles.set(sourceFile, { filename: sourceFile.fileName, contents });
        };
        const transformers = angularCompiler.prepareEmit().transformers;
        transformers.before ??= [];
        transformers.before.push((0, jit_bootstrap_transformer_1.replaceBootstrap)(() => typeScriptProgram.getProgram().getTypeChecker()), webWorkerTransform);
        if (!this.browserOnlyBuild) {
            transformers.before.push((0, lazy_routes_transformer_1.lazyRoutesTransformer)(compilerOptions, compilerHost));
        }
        // Emit is handled in write file callback when using TypeScript
        if (useTypeScriptTranspilation) {
            // TypeScript will loop until there are no more affected files in the program
            while (typeScriptProgram.emitNextAffectedFile(writeFileCallback, undefined, undefined, transformers)) {
                /* empty */
            }
        }
        else if (compilerOptions.tsBuildInfoFile) {
            // Manually get the builder state for the persistent cache
            // The TypeScript API currently embeds this behavior inside the program emit
            // via emitNextAffectedFile but that also applies all internal transforms.
            const programWithGetState = typeScriptProgram.getProgram();
            (0, node_assert_1.default)(typeof programWithGetState.emitBuildInfo === 'function', 'TypeScript program emitBuildInfo is missing.');
            programWithGetState.emitBuildInfo();
        }
        // Angular may have files that must be emitted but TypeScript does not consider affected
        for (const sourceFile of typeScriptProgram.getSourceFiles()) {
            if (emittedFiles.has(sourceFile) || angularCompiler.ignoreForEmit.has(sourceFile)) {
                continue;
            }
            if (sourceFile.isDeclarationFile) {
                continue;
            }
            if (angularCompiler.incrementalCompilation.safeToSkipEmit(sourceFile) &&
                !affectedFiles.has(sourceFile)) {
                continue;
            }
            if (useTypeScriptTranspilation) {
                typeScriptProgram.emit(sourceFile, writeFileCallback, undefined, undefined, transformers);
                continue;
            }
            // When not using TypeScript transpilation, directly apply only Angular specific transformations
            const transformResult = typescript_1.default.transform(sourceFile, [
                ...(transformers.before ?? []),
                ...(transformers.after ?? []),
            ], compilerOptions);
            (0, node_assert_1.default)(transformResult.transformed.length === 1, 'TypeScript transforms should not produce multiple outputs for ' + sourceFile.fileName);
            let contents;
            if (sourceFile === transformResult.transformed[0]) {
                // Use original content if no changes were made
                contents = sourceFile.text;
            }
            else {
                // Otherwise, print the transformed source file
                const printer = typescript_1.default.createPrinter(compilerOptions, transformResult);
                contents = printer.printFile(transformResult.transformed[0]);
            }
            angularCompiler.incrementalCompilation.recordSuccessfulEmit(sourceFile);
            emittedFiles.set(sourceFile, { filename: sourceFile.fileName, contents });
        }
        return emittedFiles.values();
    }
}
exports.AotCompilation = AotCompilation;
function findAffectedFiles(builder, { ignoreForDiagnostics }, includeTTC) {
    const affectedFiles = new Set();
    // eslint-disable-next-line no-constant-condition
    while (true) {
        const result = builder.getSemanticDiagnosticsOfNextAffectedFile(undefined, (sourceFile) => {
            // If the affected file is a TTC shim, add the shim's original source file.
            // This ensures that changes that affect TTC are typechecked even when the changes
            // are otherwise unrelated from a TS perspective and do not result in Ivy codegen changes.
            // For example, changing @Input property types of a directive used in another component's
            // template.
            // A TTC shim is a file that has been ignored for diagnostics and has a filename ending in `.ngtypecheck.ts`.
            if (ignoreForDiagnostics.has(sourceFile) && sourceFile.fileName.endsWith('.ngtypecheck.ts')) {
                // This file name conversion relies on internal compiler logic and should be converted
                // to an official method when available. 15 is length of `.ngtypecheck.ts`
                const originalFilename = sourceFile.fileName.slice(0, -15) + '.ts';
                const originalSourceFile = builder.getSourceFile(originalFilename);
                if (originalSourceFile) {
                    affectedFiles.add(originalSourceFile);
                }
                return true;
            }
            return false;
        });
        if (!result) {
            break;
        }
        affectedFiles.add(result.affected);
    }
    // Add all files with associated template type checking files.
    // Stored TS build info does not have knowledge of the AOT compiler or the typechecking state of the templates.
    // To ensure that errors are reported correctly, all AOT component diagnostics need to be analyzed even if build
    // info is present.
    if (includeTTC) {
        for (const sourceFile of builder.getSourceFiles()) {
            if (ignoreForDiagnostics.has(sourceFile) && sourceFile.fileName.endsWith('.ngtypecheck.ts')) {
                // This file name conversion relies on internal compiler logic and should be converted
                // to an official method when available. 15 is length of `.ngtypecheck.ts`
                const originalFilename = sourceFile.fileName.slice(0, -15) + '.ts';
                const originalSourceFile = builder.getSourceFile(originalFilename);
                if (originalSourceFile) {
                    affectedFiles.add(originalSourceFile);
                }
            }
        }
    }
    return affectedFiles;
}
//# sourceMappingURL=aot-compilation.js.map