"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.buildApplicationInternal = buildApplicationInternal;
exports.buildApplication = buildApplication;
const architect_1 = require("@angular-devkit/architect");
const node_assert_1 = __importDefault(require("node:assert"));
const promises_1 = __importDefault(require("node:fs/promises"));
const node_path_1 = __importDefault(require("node:path"));
const bundler_context_1 = require("../../tools/esbuild/bundler-context");
const utils_1 = require("../../tools/esbuild/utils");
const color_1 = require("../../utils/color");
const delete_output_dir_1 = require("../../utils/delete-output-dir");
const environment_options_1 = require("../../utils/environment-options");
const purge_cache_1 = require("../../utils/purge-cache");
const version_1 = require("../../utils/version");
const build_action_1 = require("./build-action");
const execute_build_1 = require("./execute-build");
const options_1 = require("./options");
const results_1 = require("./results");
async function* buildApplicationInternal(options, 
// TODO: Integrate abort signal support into builder system
context, extensions) {
    const { workspaceRoot, logger, target } = context;
    // Check Angular version.
    (0, version_1.assertCompatibleAngularVersion)(workspaceRoot);
    // Purge old build disk cache.
    await (0, purge_cache_1.purgeStaleBuildCache)(context);
    // Determine project name from builder context target
    const projectName = target?.project;
    if (!projectName) {
        context.logger.error(`The 'application' builder requires a target to be specified.`);
        // Only the vite-based dev server current uses the errors value
        yield { kind: results_1.ResultKind.Failure, errors: [] };
        return;
    }
    const normalizedOptions = await (0, options_1.normalizeOptions)(context, projectName, options, extensions);
    if (!normalizedOptions.outputOptions.ignoreServer) {
        const { browser, server } = normalizedOptions.outputOptions;
        if (browser === '') {
            context.logger.error(`'outputPath.browser' cannot be configured to an empty string when SSR is enabled.`);
            yield { kind: results_1.ResultKind.Failure, errors: [] };
            return;
        }
        if (browser === server) {
            context.logger.error(`'outputPath.browser' and 'outputPath.server' cannot be configured to the same value.`);
            yield { kind: results_1.ResultKind.Failure, errors: [] };
            return;
        }
    }
    // Setup an abort controller with a builder teardown if no signal is present
    let signal = context.signal;
    if (!signal) {
        const controller = new AbortController();
        signal = controller.signal;
        context.addTeardown(() => controller.abort('builder-teardown'));
    }
    yield* (0, build_action_1.runEsBuildBuildAction)(async (rebuildState) => {
        const { prerenderOptions, jsonLogs } = normalizedOptions;
        const startTime = process.hrtime.bigint();
        const result = await (0, execute_build_1.executeBuild)(normalizedOptions, context, rebuildState);
        if (jsonLogs) {
            result.addLog(await (0, utils_1.createJsonBuildManifest)(result, normalizedOptions));
        }
        else {
            if (prerenderOptions) {
                const prerenderedRoutesLength = result.prerenderedRoutes.length;
                let prerenderMsg = `Prerendered ${prerenderedRoutesLength} static route`;
                prerenderMsg += prerenderedRoutesLength !== 1 ? 's.' : '.';
                result.addLog(color_1.colors.magenta(prerenderMsg));
            }
            const buildTime = Number(process.hrtime.bigint() - startTime) / 10 ** 9;
            const hasError = result.errors.length > 0;
            result.addLog(`Application bundle generation ${hasError ? 'failed' : 'complete'}. [${buildTime.toFixed(3)} seconds]\n`);
        }
        return result;
    }, {
        watch: normalizedOptions.watch,
        preserveSymlinks: normalizedOptions.preserveSymlinks,
        poll: normalizedOptions.poll,
        cacheOptions: normalizedOptions.cacheOptions,
        outputOptions: normalizedOptions.outputOptions,
        verbose: normalizedOptions.verbose,
        projectRoot: normalizedOptions.projectRoot,
        workspaceRoot: normalizedOptions.workspaceRoot,
        progress: normalizedOptions.progress,
        clearScreen: normalizedOptions.clearScreen,
        colors: normalizedOptions.colors,
        jsonLogs: normalizedOptions.jsonLogs,
        logger,
        signal,
    });
}
async function* buildApplication(options, context, pluginsOrExtensions) {
    let extensions;
    if (pluginsOrExtensions && Array.isArray(pluginsOrExtensions)) {
        extensions = {
            codePlugins: pluginsOrExtensions,
        };
    }
    else {
        extensions = pluginsOrExtensions;
    }
    let initial = true;
    for await (const result of buildApplicationInternal(options, context, extensions)) {
        const outputOptions = result.detail?.['outputOptions'];
        if (initial) {
            initial = false;
            // Clean the output location if requested.
            // Output options may not be present if the build failed.
            if (outputOptions?.clean) {
                await (0, delete_output_dir_1.deleteOutputDir)(context.workspaceRoot, outputOptions.base, [
                    outputOptions.browser,
                    outputOptions.server,
                ]);
            }
        }
        if (result.kind === results_1.ResultKind.Failure) {
            yield { success: false };
            continue;
        }
        (0, node_assert_1.default)(outputOptions, 'Application output options are required for builder usage.');
        (0, node_assert_1.default)(result.kind === results_1.ResultKind.Full, 'Application build did not provide a full output.');
        // TODO: Restructure output logging to better handle stdout JSON piping
        if (!environment_options_1.useJSONBuildLogs) {
            context.logger.info(`Output location: ${outputOptions.base}\n`);
        }
        // Writes the output files to disk and ensures the containing directories are present
        const directoryExists = new Set();
        await (0, utils_1.emitFilesToDisk)(Object.entries(result.files), async ([filePath, file]) => {
            if (outputOptions.ignoreServer && file.type === bundler_context_1.BuildOutputFileType.Server) {
                return;
            }
            let typeDirectory;
            switch (file.type) {
                case bundler_context_1.BuildOutputFileType.Browser:
                case bundler_context_1.BuildOutputFileType.Media:
                    typeDirectory = outputOptions.browser;
                    break;
                case bundler_context_1.BuildOutputFileType.Server:
                    typeDirectory = outputOptions.server;
                    break;
                case bundler_context_1.BuildOutputFileType.Root:
                    typeDirectory = '';
                    break;
                default:
                    throw new Error(`Unhandled write for file "${filePath}" with type "${bundler_context_1.BuildOutputFileType[file.type]}".`);
            }
            // NOTE: 'base' is a fully resolved path at this point
            const fullFilePath = node_path_1.default.join(outputOptions.base, typeDirectory, filePath);
            // Ensure output subdirectories exist
            const fileBasePath = node_path_1.default.dirname(fullFilePath);
            if (fileBasePath && !directoryExists.has(fileBasePath)) {
                await promises_1.default.mkdir(fileBasePath, { recursive: true });
                directoryExists.add(fileBasePath);
            }
            if (file.origin === 'memory') {
                // Write file contents
                await promises_1.default.writeFile(fullFilePath, file.contents);
            }
            else {
                // Copy file contents
                await promises_1.default.copyFile(file.inputPath, fullFilePath, promises_1.default.constants.COPYFILE_FICLONE);
            }
        });
        yield { success: true };
    }
}
exports.default = (0, architect_1.createBuilder)(buildApplication);
