"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.INDEX_HTML_SERVER = exports.INDEX_HTML_CSR = void 0;
exports.normalizeOptions = normalizeOptions;
exports.getLocaleBaseHref = getLocaleBaseHref;
const node_fs_1 = require("node:fs");
const promises_1 = require("node:fs/promises");
const node_module_1 = require("node:module");
const node_path_1 = __importDefault(require("node:path"));
const utils_1 = require("../../utils");
const color_1 = require("../../utils/color");
const environment_options_1 = require("../../utils/environment-options");
const i18n_options_1 = require("../../utils/i18n-options");
const normalize_cache_1 = require("../../utils/normalize-cache");
const postcss_configuration_1 = require("../../utils/postcss-configuration");
const project_metadata_1 = require("../../utils/project-metadata");
const url_1 = require("../../utils/url");
const schema_1 = require("./schema");
/**
 * The filename for the client-side rendered HTML template.
 * This template is used for client-side rendering (CSR) in a web application.
 */
exports.INDEX_HTML_CSR = 'index.csr.html';
/**
 * The filename for the server-side rendered HTML template.
 * This template is used for server-side rendering (SSR) in a web application.
 */
exports.INDEX_HTML_SERVER = 'index.server.html';
/**
 * Normalize the user provided options by creating full paths for all path based options
 * and converting multi-form options into a single form that can be directly used
 * by the build process.
 *
 * @param context The context for current builder execution.
 * @param projectName The name of the project for the current execution.
 * @param options An object containing the options to use for the build.
 * @param plugins An optional array of programmatically supplied build plugins.
 * @returns An object containing normalized options required to perform the build.
 */
// eslint-disable-next-line max-lines-per-function
async function normalizeOptions(context, projectName, options, extensions) {
    // If not explicitly set, default to the Node.js process argument
    const preserveSymlinks = options.preserveSymlinks ?? process.execArgv.includes('--preserve-symlinks');
    // Setup base paths based on workspace root and project information
    const workspaceRoot = preserveSymlinks
        ? context.workspaceRoot
        : // NOTE: promises.realpath should not be used here since it uses realpath.native which
            // can cause case conversion and other undesirable behavior on Windows systems.
            // ref: https://github.com/nodejs/node/issues/7726
            (0, node_fs_1.realpathSync)(context.workspaceRoot);
    const projectMetadata = await context.getProjectMetadata(projectName);
    const { projectRoot, projectSourceRoot } = (0, project_metadata_1.getProjectRootPaths)(workspaceRoot, projectMetadata);
    // Gather persistent caching option and provide a project specific cache location
    const cacheOptions = (0, normalize_cache_1.normalizeCacheOptions)(projectMetadata, workspaceRoot);
    cacheOptions.path = node_path_1.default.join(cacheOptions.path, projectName);
    const i18nOptions = (0, i18n_options_1.createI18nOptions)(projectMetadata, options.localize, context.logger, !!options.ssr);
    i18nOptions.duplicateTranslationBehavior = options.i18nDuplicateTranslation;
    i18nOptions.missingTranslationBehavior = options.i18nMissingTranslation;
    if (options.forceI18nFlatOutput) {
        i18nOptions.flatOutput = true;
    }
    const entryPoints = normalizeEntryPoints(workspaceRoot, projectSourceRoot, options.browser, options.entryPoints);
    const tsconfig = node_path_1.default.join(workspaceRoot, options.tsConfig);
    const optimizationOptions = (0, utils_1.normalizeOptimization)(options.optimization);
    const sourcemapOptions = (0, utils_1.normalizeSourceMaps)(options.sourceMap ?? false);
    const assets = options.assets?.length
        ? (0, utils_1.normalizeAssetPatterns)(options.assets, workspaceRoot, projectRoot, projectSourceRoot)
        : undefined;
    let fileReplacements;
    if (options.fileReplacements) {
        for (const replacement of options.fileReplacements) {
            const fileReplaceWith = node_path_1.default.join(workspaceRoot, replacement.with);
            try {
                await (0, promises_1.access)(fileReplaceWith, promises_1.constants.F_OK);
            }
            catch {
                throw new Error(`The ${fileReplaceWith} path in file replacements does not exist.`);
            }
            fileReplacements ??= {};
            fileReplacements[node_path_1.default.join(workspaceRoot, replacement.replace)] = fileReplaceWith;
        }
    }
    let loaderExtensions;
    if (options.loader) {
        for (const [extension, value] of Object.entries(options.loader)) {
            if (extension[0] !== '.' || /\.[cm]?[jt]sx?$/.test(extension)) {
                continue;
            }
            if (value !== 'text' &&
                value !== 'binary' &&
                value !== 'file' &&
                value !== 'dataurl' &&
                value !== 'base64' &&
                value !== 'empty') {
                continue;
            }
            loaderExtensions ??= {};
            loaderExtensions[extension] = value;
        }
    }
    // Validate prerender and ssr options when using the outputMode
    if (options.outputMode === schema_1.OutputMode.Server) {
        if (!options.server) {
            throw new Error('The "server" option is required when "outputMode" is set to "server".');
        }
        if (typeof options.ssr === 'boolean' || !options.ssr?.entry) {
            throw new Error('The "ssr.entry" option is required when "outputMode" is set to "server".');
        }
    }
    if (options.outputMode) {
        if (!options.server) {
            options.ssr = false;
        }
        if (options.prerender !== undefined) {
            context.logger.warn('The "prerender" option is not considered when "outputMode" is specified.');
        }
        options.prerender = !!options.server;
        if (options.appShell !== undefined) {
            context.logger.warn('The "appShell" option is not considered when "outputMode" is specified.');
        }
    }
    // A configuration file can exist in the project or workspace root
    const searchDirectories = await (0, postcss_configuration_1.generateSearchDirectories)([projectRoot, workspaceRoot]);
    const postcssConfiguration = await (0, postcss_configuration_1.loadPostcssConfiguration)(searchDirectories);
    // Skip tailwind configuration if postcss is customized
    const tailwindConfiguration = postcssConfiguration
        ? undefined
        : await getTailwindConfig(searchDirectories, workspaceRoot, context);
    let serverEntryPoint;
    if (typeof options.server === 'string') {
        if (options.server === '') {
            throw new Error('The "server" option cannot be an empty string.');
        }
        serverEntryPoint = node_path_1.default.join(workspaceRoot, options.server);
    }
    let prerenderOptions;
    if (options.prerender) {
        const { discoverRoutes = true, routesFile = undefined } = options.prerender === true ? {} : options.prerender;
        prerenderOptions = {
            discoverRoutes,
            routesFile: routesFile && node_path_1.default.join(workspaceRoot, routesFile),
        };
    }
    let ssrOptions;
    if (options.ssr === true) {
        ssrOptions = {};
    }
    else if (typeof options.ssr === 'object') {
        const { entry, experimentalPlatform = schema_1.ExperimentalPlatform.Node } = options.ssr;
        ssrOptions = {
            entry: entry && node_path_1.default.join(workspaceRoot, entry),
            platform: experimentalPlatform,
        };
    }
    let appShellOptions;
    if (options.appShell) {
        appShellOptions = {
            route: 'shell',
        };
    }
    const outputPath = options.outputPath ?? node_path_1.default.join(workspaceRoot, 'dist', projectName);
    const outputOptions = {
        browser: 'browser',
        server: 'server',
        media: 'media',
        ...(typeof outputPath === 'string' ? undefined : outputPath),
        base: (0, project_metadata_1.normalizeDirectoryPath)(node_path_1.default.resolve(workspaceRoot, typeof outputPath === 'string' ? outputPath : outputPath.base)),
        clean: options.deleteOutputPath ?? true,
        // For app-shell and SSG server files are not required by users.
        // Omit these when SSR is not enabled.
        ignoreServer: ((ssrOptions === undefined || serverEntryPoint === undefined) &&
            options.outputMode === undefined) ||
            options.outputMode === schema_1.OutputMode.Static,
    };
    const outputNames = {
        bundles: options.outputHashing === schema_1.OutputHashing.All || options.outputHashing === schema_1.OutputHashing.Bundles
            ? '[name]-[hash]'
            : '[name]',
        media: outputOptions.media +
            (options.outputHashing === schema_1.OutputHashing.All || options.outputHashing === schema_1.OutputHashing.Media
                ? '/[name]-[hash]'
                : '/[name]'),
    };
    const globalStyles = normalizeGlobalEntries(options.styles, 'styles');
    const globalScripts = normalizeGlobalEntries(options.scripts, 'scripts');
    let indexHtmlOptions;
    // index can never have a value of `true` but in the schema it's of type `boolean`.
    if (typeof options.index !== 'boolean') {
        let indexInput;
        let indexOutput;
        // The output file will be created within the configured output path
        if (typeof options.index === 'string') {
            indexInput = indexOutput = node_path_1.default.join(workspaceRoot, options.index);
        }
        else if (typeof options.index === 'undefined') {
            indexInput = node_path_1.default.join(projectSourceRoot, 'index.html');
            indexOutput = 'index.html';
        }
        else {
            indexInput = node_path_1.default.join(workspaceRoot, options.index.input);
            indexOutput = options.index.output || 'index.html';
        }
        /**
         * If SSR is activated, create a distinct entry file for the `index.html`.
         * This is necessary because numerous server/cloud providers automatically serve the `index.html` as a static file
         * if it exists (handling SSG).
         *
         * For instance, accessing `foo.com/` would lead to `foo.com/index.html` being served instead of hitting the server.
         *
         * This approach can also be applied to service workers, where the `index.csr.html` is served instead of the prerendered `index.html`.
         */
        const indexBaseName = node_path_1.default.basename(indexOutput);
        indexOutput =
            (ssrOptions || prerenderOptions) && indexBaseName === 'index.html'
                ? exports.INDEX_HTML_CSR
                : indexBaseName;
        indexHtmlOptions = {
            input: indexInput,
            output: indexOutput,
            insertionOrder: [
                ['polyfills', true],
                ...globalStyles.filter((s) => s.initial).map((s) => [s.name, false]),
                ...globalScripts.filter((s) => s.initial).map((s) => [s.name, false]),
                ['main', true],
                // [name, esm]
            ],
            transformer: extensions?.indexHtmlTransformer,
            // Preload initial defaults to true
            preloadInitial: typeof options.index !== 'object' || (options.index.preloadInitial ?? true),
        };
    }
    if (appShellOptions || ssrOptions || prerenderOptions) {
        if (!serverEntryPoint) {
            throw new Error('The "server" option is required when enabling "ssr", "prerender" or "app-shell".');
        }
        if (!indexHtmlOptions) {
            throw new Error('The "index" option cannot be set to false when enabling "ssr", "prerender" or "app-shell".');
        }
    }
    const { autoCsp, allowedHosts = [] } = options.security ?? {};
    const security = {
        allowedHosts,
        autoCsp: autoCsp
            ? {
                unsafeEval: autoCsp === true ? false : !!autoCsp.unsafeEval,
            }
            : undefined,
    };
    // Initial options to keep
    const { allowedCommonJsDependencies, aot = true, baseHref, crossOrigin, externalDependencies, extractLicenses, inlineStyleLanguage = 'css', outExtension, serviceWorker, poll, polyfills, statsJson, outputMode, stylePreprocessorOptions, subresourceIntegrity, verbose, watch, progress = true, externalPackages, namedChunks, budgets, deployUrl, clearScreen, define, partialSSRBuild = false, externalRuntimeStyles, instrumentForCoverage, } = options;
    // Return all the normalized options
    return {
        advancedOptimizations: !!aot && optimizationOptions.scripts,
        allowedCommonJsDependencies,
        baseHref,
        cacheOptions,
        crossOrigin,
        externalDependencies: normalizeExternals(externalDependencies),
        externalPackages: typeof externalPackages === 'object'
            ? {
                ...externalPackages,
                exclude: normalizeExternals(externalPackages.exclude),
            }
            : externalPackages,
        extractLicenses,
        inlineStyleLanguage,
        jit: !aot,
        stats: !!statsJson,
        polyfills: polyfills === undefined || Array.isArray(polyfills) ? polyfills : [polyfills],
        poll,
        progress,
        preserveSymlinks,
        stylePreprocessorOptions,
        subresourceIntegrity,
        serverEntryPoint,
        prerenderOptions,
        appShellOptions,
        outputMode,
        ssrOptions,
        verbose,
        watch,
        workspaceRoot,
        entryPoints,
        optimizationOptions,
        outputOptions,
        outExtension,
        sourcemapOptions,
        tsconfig,
        projectRoot,
        assets,
        outputNames,
        fileReplacements,
        globalStyles,
        globalScripts,
        serviceWorker: serviceWorker
            ? node_path_1.default.join(workspaceRoot, typeof serviceWorker === 'string' ? serviceWorker : 'src/ngsw-config.json')
            : undefined,
        indexHtmlOptions,
        tailwindConfiguration,
        postcssConfiguration,
        i18nOptions,
        namedChunks,
        budgets: budgets?.length ? budgets : undefined,
        publicPath: deployUrl,
        plugins: extensions?.codePlugins?.length ? extensions?.codePlugins : undefined,
        loaderExtensions,
        jsonLogs: environment_options_1.useJSONBuildLogs,
        colors: (0, color_1.supportColor)(),
        clearScreen,
        define,
        partialSSRBuild: environment_options_1.usePartialSsrBuild || partialSSRBuild,
        externalRuntimeStyles: aot && externalRuntimeStyles,
        instrumentForCoverage,
        security,
        templateUpdates: !!options.templateUpdates,
        incrementalResults: !!options.incrementalResults,
        customConditions: options.conditions,
        frameworkVersion: await findFrameworkVersion(projectRoot),
    };
}
async function getTailwindConfig(searchDirectories, workspaceRoot, context) {
    const tailwindConfigurationPath = (0, postcss_configuration_1.findTailwindConfiguration)(searchDirectories);
    if (!tailwindConfigurationPath) {
        return undefined;
    }
    // Create a node resolver from the configuration file
    const resolver = (0, node_module_1.createRequire)(tailwindConfigurationPath);
    try {
        return {
            file: tailwindConfigurationPath,
            package: resolver.resolve('tailwindcss'),
        };
    }
    catch {
        const relativeTailwindConfigPath = node_path_1.default.relative(workspaceRoot, tailwindConfigurationPath);
        context.logger.warn(`Tailwind CSS configuration file found (${relativeTailwindConfigPath})` +
            ` but the 'tailwindcss' package is not installed.` +
            ` To enable Tailwind CSS, please install the 'tailwindcss' package.`);
    }
    return undefined;
}
/**
 * Normalize entry point options. To maintain compatibility with the legacy browser builder, we need a single `browser`
 * option which defines a single entry point. However, we also want to support multiple entry points as an internal option.
 * The two options are mutually exclusive and if `browser` is provided it will be used as the sole entry point.
 * If `entryPoints` are provided, they will be used as the set of entry points.
 *
 * @param workspaceRoot Path to the root of the Angular workspace.
 * @param browser The `browser` option pointing at the application entry point. While required per the schema file, it may be omitted by
 *     programmatic usages of `browser-esbuild`.
 * @param entryPoints Set of entry points to use if provided.
 * @returns An object mapping entry point names to their file paths.
 */
function normalizeEntryPoints(workspaceRoot, projectSourceRoot, browser, entryPoints) {
    if (browser === '') {
        throw new Error('`browser` option cannot be an empty string.');
    }
    // `browser` and `entryPoints` are mutually exclusive.
    if (browser && entryPoints) {
        throw new Error('Only one of `browser` or `entryPoints` may be provided.');
    }
    if (browser) {
        // Use `browser` alone.
        return { 'main': node_path_1.default.join(workspaceRoot, browser) };
    }
    else if (!entryPoints) {
        // Default browser entry if no explicit entry points
        return { 'main': node_path_1.default.join(projectSourceRoot, 'main.ts') };
    }
    else if (entryPoints instanceof Map) {
        return Object.fromEntries(Array.from(entryPoints.entries(), ([name, entryPoint]) => {
            // Get the full file path to a relative entry point input. Leave bare specifiers alone so they are resolved as modules.
            const isRelativePath = entryPoint.startsWith('.');
            const entryPointPath = isRelativePath ? node_path_1.default.join(workspaceRoot, entryPoint) : entryPoint;
            return [name, entryPointPath];
        }));
    }
    else {
        // Use `entryPoints` alone.
        const entryPointPaths = {};
        for (const entryPoint of entryPoints) {
            const parsedEntryPoint = node_path_1.default.parse(entryPoint);
            // Use the input file path without an extension as the "name" of the entry point dictating its output location.
            // Relative entry points are generated at the same relative path in the output directory.
            // Absolute entry points are always generated with the same file name in the root of the output directory. This includes absolute
            // paths pointing at files actually within the workspace root.
            const entryPointName = node_path_1.default.isAbsolute(entryPoint)
                ? parsedEntryPoint.name
                : node_path_1.default.join(parsedEntryPoint.dir, parsedEntryPoint.name);
            // Get the full file path to a relative entry point input. Leave bare specifiers alone so they are resolved as modules.
            const isRelativePath = entryPoint.startsWith('.');
            const entryPointPath = isRelativePath ? node_path_1.default.join(workspaceRoot, entryPoint) : entryPoint;
            // Check for conflicts with previous entry points.
            const existingEntryPointPath = entryPointPaths[entryPointName];
            if (existingEntryPointPath) {
                throw new Error(`\`${existingEntryPointPath}\` and \`${entryPointPath}\` both output to the same location \`${entryPointName}\`.` +
                    ' Rename or move one of the files to fix the conflict.');
            }
            entryPointPaths[entryPointName] = entryPointPath;
        }
        return entryPointPaths;
    }
}
function normalizeGlobalEntries(rawEntries, defaultName) {
    if (!rawEntries?.length) {
        return [];
    }
    const bundles = new Map();
    for (const rawEntry of rawEntries) {
        let entry;
        if (typeof rawEntry === 'string') {
            // string entries use default bundle name and inject values
            entry = { input: rawEntry };
        }
        else {
            entry = rawEntry;
        }
        const { bundleName, input, inject = true } = entry;
        // Non-injected entries default to the file name
        const name = bundleName || (inject ? defaultName : node_path_1.default.basename(input, node_path_1.default.extname(input)));
        const existing = bundles.get(name);
        if (!existing) {
            bundles.set(name, { name, files: [input], initial: inject });
            continue;
        }
        if (existing.initial !== inject) {
            throw new Error(`The "${name}" bundle is mixing injected and non-injected entries. ` +
                'Verify that the project options are correct.');
        }
        existing.files.push(input);
    }
    return [...bundles.values()];
}
function getLocaleBaseHref(baseHref = '', i18n, locale) {
    if (i18n.flatOutput) {
        return undefined;
    }
    const localeData = i18n.locales[locale];
    if (!localeData) {
        return undefined;
    }
    const baseHrefSuffix = localeData.baseHref ?? localeData.subPath + '/';
    let joinedBaseHref;
    if (baseHrefSuffix !== '') {
        joinedBaseHref = (0, url_1.addTrailingSlash)((0, url_1.joinUrlParts)(baseHref, baseHrefSuffix));
        if (baseHref && baseHref[0] !== '/') {
            joinedBaseHref = (0, url_1.stripLeadingSlash)(joinedBaseHref);
        }
    }
    return joinedBaseHref;
}
/**
 * Normalizes an array of external dependency paths by ensuring that
 * wildcard patterns (`/*`) are removed from package names.
 *
 * This avoids the need to handle this normalization repeatedly in our plugins,
 * as esbuild already treats `--external:@foo/bar` as implicitly including
 * `--external:@foo/bar/*`. By standardizing the input, we ensure consistency
 * and reduce redundant checks across our plugins.
 *
 * @param value - An optional array of dependency paths to normalize.
 * @returns A new array with wildcard patterns removed from package names, or `undefined` if input is `undefined`.
 */
function normalizeExternals(value) {
    if (!value) {
        return undefined;
    }
    return [
        ...new Set(value.map((d) => 
        // remove "/*" wildcard in the end if provided string is not path-like
        d.endsWith('/*') && !/^\.{0,2}\//.test(d) ? d.slice(0, -2) : d)),
    ];
}
async function findFrameworkVersion(projectRoot) {
    // Create a custom require function for ESM compliance.
    // NOTE: The trailing slash is significant.
    const projectResolve = (0, node_module_1.createRequire)(projectRoot + '/').resolve;
    try {
        const manifestPath = projectResolve('@angular/core/package.json');
        const manifestData = await (0, promises_1.readFile)(manifestPath, 'utf-8');
        const manifestObject = JSON.parse(manifestData);
        const version = manifestObject.version;
        return version;
    }
    catch {
        throw new Error('Error: It appears that "@angular/core" is missing as a dependency. Please ensure it is included in your project.');
    }
}
//# sourceMappingURL=options.js.map