"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.createBrowserCodeBundleOptions = createBrowserCodeBundleOptions;
exports.createBrowserPolyfillBundleOptions = createBrowserPolyfillBundleOptions;
exports.createServerCodeBundleOptions = createServerCodeBundleOptions;
exports.createServerPolyfillBundleOptions = createServerPolyfillBundleOptions;
const node_assert_1 = __importDefault(require("node:assert"));
const node_crypto_1 = require("node:crypto");
const promises_1 = require("node:fs/promises");
const node_path_1 = require("node:path");
const environment_options_1 = require("../../utils/environment-options");
const compiler_plugin_1 = require("./angular/compiler-plugin");
const compiler_plugin_options_1 = require("./compiler-plugin-options");
const external_packages_plugin_1 = require("./external-packages-plugin");
const i18n_locale_plugin_1 = require("./i18n-locale-plugin");
const loader_import_attribute_plugin_1 = require("./loader-import-attribute-plugin");
const rxjs_esm_resolution_plugin_1 = require("./rxjs-esm-resolution-plugin");
const sourcemap_ignorelist_plugin_1 = require("./sourcemap-ignorelist-plugin");
const utils_1 = require("./utils");
const virtual_module_plugin_1 = require("./virtual-module-plugin");
const wasm_plugin_1 = require("./wasm-plugin");
function createBrowserCodeBundleOptions(options, target, sourceFileCache) {
    const { entryPoints, outputNames, polyfills } = options;
    const { pluginOptions, styleOptions } = (0, compiler_plugin_options_1.createCompilerPluginOptions)(options, target, sourceFileCache);
    const zoneless = (0, utils_1.isZonelessApp)(polyfills);
    const buildOptions = {
        ...getEsBuildCommonOptions(options),
        platform: 'browser',
        // Note: `es2015` is needed for RxJS v6. If not specified, `module` would
        // match and the ES5 distribution would be bundled and ends up breaking at
        // runtime with the RxJS testing library.
        // More details: https://github.com/angular/angular-cli/issues/25405.
        mainFields: ['es2020', 'es2015', 'browser', 'module', 'main'],
        entryNames: outputNames.bundles,
        entryPoints,
        target,
        supported: (0, utils_1.getFeatureSupport)(target, zoneless),
        plugins: [
            (0, loader_import_attribute_plugin_1.createLoaderImportAttributePlugin)(),
            (0, wasm_plugin_1.createWasmPlugin)({ allowAsync: zoneless, cache: sourceFileCache?.loadResultCache }),
            (0, sourcemap_ignorelist_plugin_1.createSourcemapIgnorelistPlugin)(),
            (0, compiler_plugin_1.createCompilerPlugin)(
            // JS/TS options
            pluginOptions, 
            // Component stylesheet options
            styleOptions),
        ],
    };
    if (options.plugins) {
        buildOptions.plugins?.push(...options.plugins);
    }
    if (options.externalPackages) {
        // Package files affected by a customized loader should not be implicitly marked as external
        if (options.loaderExtensions ||
            options.plugins ||
            typeof options.externalPackages === 'object') {
            // Plugin must be added after custom plugins to ensure any added loader options are considered
            buildOptions.plugins?.push((0, external_packages_plugin_1.createExternalPackagesPlugin)(options.externalPackages !== true ? options.externalPackages : undefined));
        }
        else {
            // Safe to use the packages external option directly
            buildOptions.packages = 'external';
        }
    }
    return buildOptions;
}
function createBrowserPolyfillBundleOptions(options, target, sourceFileCache) {
    const namespace = 'angular:polyfills';
    const polyfillBundleOptions = getEsBuildCommonPolyfillsOptions(options, namespace, true, sourceFileCache);
    if (!polyfillBundleOptions) {
        return;
    }
    const { outputNames, polyfills } = options;
    const hasTypeScriptEntries = polyfills?.some((entry) => /\.[cm]?tsx?$/.test(entry));
    const buildOptions = {
        ...polyfillBundleOptions,
        platform: 'browser',
        // Note: `es2015` is needed for RxJS v6. If not specified, `module` would
        // match and the ES5 distribution would be bundled and ends up breaking at
        // runtime with the RxJS testing library.
        // More details: https://github.com/angular/angular-cli/issues/25405.
        mainFields: ['es2020', 'es2015', 'browser', 'module', 'main'],
        entryNames: outputNames.bundles,
        target,
        entryPoints: {
            'polyfills': namespace,
        },
    };
    // Only add the Angular TypeScript compiler if TypeScript files are provided in the polyfills
    if (hasTypeScriptEntries) {
        buildOptions.plugins ??= [];
        const { pluginOptions, styleOptions } = (0, compiler_plugin_options_1.createCompilerPluginOptions)(options, target, sourceFileCache);
        buildOptions.plugins.push((0, compiler_plugin_1.createCompilerPlugin)(
        // JS/TS options
        { ...pluginOptions, noopTypeScriptCompilation: true }, 
        // Component stylesheet options are unused for polyfills but required by the plugin
        styleOptions));
    }
    // Use an options factory to allow fully incremental bundling when no TypeScript files are present.
    // The TypeScript compilation is not currently integrated into the bundler invalidation so
    // cannot be used with fully incremental bundling yet.
    return hasTypeScriptEntries ? buildOptions : () => buildOptions;
}
/**
 * Create an esbuild 'build' options object for the server bundle.
 * @param options The builder's user-provider normalized options.
 * @returns An esbuild BuildOptions object.
 */
function createServerCodeBundleOptions(options, target, sourceFileCache) {
    const { serverEntryPoint, workspaceRoot, ssrOptions, watch, externalPackages, prerenderOptions, polyfills, } = options;
    (0, node_assert_1.default)(serverEntryPoint, 'createServerCodeBundleOptions should not be called without a defined serverEntryPoint.');
    const { pluginOptions, styleOptions } = (0, compiler_plugin_options_1.createCompilerPluginOptions)(options, target, sourceFileCache);
    const mainServerNamespace = 'angular:server-render-utils';
    const entryPoints = {
        'render-utils.server': mainServerNamespace,
        'main.server': serverEntryPoint,
    };
    const ssrEntryPoint = ssrOptions?.entry;
    if (ssrEntryPoint) {
        entryPoints['server'] = ssrEntryPoint;
    }
    const zoneless = (0, utils_1.isZonelessApp)(polyfills);
    const buildOptions = {
        ...getEsBuildCommonOptions(options),
        platform: 'node',
        splitting: true,
        outExtension: { '.js': '.mjs' },
        // Note: `es2015` is needed for RxJS v6. If not specified, `module` would
        // match and the ES5 distribution would be bundled and ends up breaking at
        // runtime with the RxJS testing library.
        // More details: https://github.com/angular/angular-cli/issues/25405.
        mainFields: ['es2020', 'es2015', 'module', 'main'],
        entryNames: '[name]',
        target,
        banner: {
            js: `import './polyfills.server.mjs';`,
        },
        entryPoints,
        supported: (0, utils_1.getFeatureSupport)(target, zoneless),
        plugins: [
            (0, loader_import_attribute_plugin_1.createLoaderImportAttributePlugin)(),
            (0, wasm_plugin_1.createWasmPlugin)({ allowAsync: zoneless, cache: sourceFileCache?.loadResultCache }),
            (0, sourcemap_ignorelist_plugin_1.createSourcemapIgnorelistPlugin)(),
            (0, compiler_plugin_1.createCompilerPlugin)(
            // JS/TS options
            { ...pluginOptions, noopTypeScriptCompilation: true }, 
            // Component stylesheet options
            styleOptions),
        ],
    };
    buildOptions.plugins ??= [];
    if (externalPackages) {
        buildOptions.packages = 'external';
    }
    else {
        buildOptions.plugins.push((0, rxjs_esm_resolution_plugin_1.createRxjsEsmResolutionPlugin)());
    }
    buildOptions.plugins.push((0, virtual_module_plugin_1.createVirtualModulePlugin)({
        namespace: mainServerNamespace,
        cache: sourceFileCache?.loadResultCache,
        loadContent: async () => {
            const contents = [
                `export { ɵConsole } from '@angular/core';`,
                `export { renderApplication, renderModule, ɵSERVER_CONTEXT } from '@angular/platform-server';`,
            ];
            if (watch) {
                contents.push(`export { ɵresetCompiledComponents } from '@angular/core';`);
            }
            if (prerenderOptions?.discoverRoutes) {
                // We do not import it directly so that node.js modules are resolved using the correct context.
                const routesExtractorCode = await (0, promises_1.readFile)((0, node_path_1.join)(__dirname, '../../utils/routes-extractor/extractor.js'), 'utf-8');
                // Remove source map URL comments from the code if a sourcemap is present as this will not match the file.
                contents.push(routesExtractorCode.replace(/^\/\/# sourceMappingURL=[^\r\n]*/gm, ''));
            }
            return {
                contents: contents.join('\n'),
                loader: 'js',
                resolveDir: workspaceRoot,
            };
        },
    }));
    if (options.plugins) {
        buildOptions.plugins.push(...options.plugins);
    }
    return buildOptions;
}
function createServerPolyfillBundleOptions(options, target, sourceFileCache) {
    const serverPolyfills = [];
    const polyfillsFromConfig = new Set(options.polyfills);
    if (!(0, utils_1.isZonelessApp)(options.polyfills)) {
        serverPolyfills.push('zone.js/node');
    }
    if (polyfillsFromConfig.has('@angular/localize') ||
        polyfillsFromConfig.has('@angular/localize/init')) {
        serverPolyfills.push('@angular/localize/init');
    }
    serverPolyfills.push('@angular/platform-server/init');
    const namespace = 'angular:polyfills-server';
    const polyfillBundleOptions = getEsBuildCommonPolyfillsOptions({
        ...options,
        polyfills: serverPolyfills,
    }, namespace, false, sourceFileCache);
    if (!polyfillBundleOptions) {
        return;
    }
    const buildOptions = {
        ...polyfillBundleOptions,
        platform: 'node',
        outExtension: { '.js': '.mjs' },
        // Note: `es2015` is needed for RxJS v6. If not specified, `module` would
        // match and the ES5 distribution would be bundled and ends up breaking at
        // runtime with the RxJS testing library.
        // More details: https://github.com/angular/angular-cli/issues/25405.
        mainFields: ['es2020', 'es2015', 'module', 'main'],
        entryNames: '[name]',
        banner: {
            js: [
                // Note: Needed as esbuild does not provide require shims / proxy from ESModules.
                // See: https://github.com/evanw/esbuild/issues/1921.
                `import { createRequire } from 'node:module';`,
                `globalThis['require'] ??= createRequire(import.meta.url);`,
            ].join('\n'),
        },
        target,
        entryPoints: {
            'polyfills.server': namespace,
        },
    };
    return () => buildOptions;
}
function getEsBuildCommonOptions(options) {
    const { workspaceRoot, outExtension, optimizationOptions, sourcemapOptions, tsconfig, externalDependencies, outputNames, preserveSymlinks, jit, loaderExtensions, jsonLogs, } = options;
    // Ensure unique hashes for i18n translation changes when using post-process inlining.
    // This hash value is added as a footer to each file and ensures that the output file names (with hashes)
    // change when translation files have changed. If this is not done the post processed files may have
    // different content but would retain identical production file names which would lead to browser caching problems.
    let footer;
    if (options.i18nOptions.shouldInline) {
        // Update file hashes to include translation file content
        const i18nHash = Object.values(options.i18nOptions.locales).reduce((data, locale) => data + locale.files.map((file) => file.integrity || '').join('|'), '');
        footer = { js: `/**i18n:${(0, node_crypto_1.createHash)('sha256').update(i18nHash).digest('hex')}*/` };
    }
    return {
        absWorkingDir: workspaceRoot,
        format: 'esm',
        bundle: true,
        packages: 'bundle',
        assetNames: outputNames.media,
        conditions: ['es2020', 'es2015', 'module'],
        resolveExtensions: ['.ts', '.tsx', '.mjs', '.js', '.cjs'],
        metafile: true,
        legalComments: options.extractLicenses ? 'none' : 'eof',
        logLevel: options.verbose && !jsonLogs ? 'debug' : 'silent',
        minifyIdentifiers: optimizationOptions.scripts && environment_options_1.allowMangle,
        minifySyntax: optimizationOptions.scripts,
        minifyWhitespace: optimizationOptions.scripts,
        pure: ['forwardRef'],
        outdir: workspaceRoot,
        outExtension: outExtension ? { '.js': `.${outExtension}` } : undefined,
        sourcemap: sourcemapOptions.scripts && (sourcemapOptions.hidden ? 'external' : true),
        splitting: true,
        chunkNames: options.namedChunks ? '[name]-[hash]' : 'chunk-[hash]',
        tsconfig,
        external: externalDependencies,
        write: false,
        preserveSymlinks,
        define: {
            ...options.define,
            // Only set to false when script optimizations are enabled. It should not be set to true because
            // Angular turns `ngDevMode` into an object for development debugging purposes when not defined
            // which a constant true value would break.
            ...(optimizationOptions.scripts ? { 'ngDevMode': 'false' } : undefined),
            'ngJitMode': jit ? 'true' : 'false',
        },
        loader: loaderExtensions,
        footer,
    };
}
function getEsBuildCommonPolyfillsOptions(options, namespace, tryToResolvePolyfillsAsRelative, sourceFileCache) {
    const { jit, workspaceRoot, i18nOptions } = options;
    const buildOptions = {
        ...getEsBuildCommonOptions(options),
        splitting: false,
        plugins: [(0, sourcemap_ignorelist_plugin_1.createSourcemapIgnorelistPlugin)()],
    };
    let polyfills = options.polyfills ? [...options.polyfills] : [];
    // Angular JIT mode requires the runtime compiler
    if (jit) {
        polyfills.unshift('@angular/compiler');
    }
    // Add Angular's global locale data if i18n options are present.
    // Locale data should go first so that project provided polyfill code can augment if needed.
    let needLocaleDataPlugin = false;
    if (i18nOptions.shouldInline) {
        // Remove localize polyfill as this is not needed for build time i18n.
        polyfills = polyfills.filter((path) => !path.startsWith('@angular/localize'));
        // Add locale data for all active locales
        // TODO: Inject each individually within the inlining process itself
        for (const locale of i18nOptions.inlineLocales) {
            polyfills.unshift(`angular:locale/data:${locale}`);
        }
        needLocaleDataPlugin = true;
    }
    else if (i18nOptions.hasDefinedSourceLocale) {
        // When not inlining and a source local is present, use the source locale data directly
        polyfills.unshift(`angular:locale/data:${i18nOptions.sourceLocale}`);
        needLocaleDataPlugin = true;
    }
    if (needLocaleDataPlugin) {
        buildOptions.plugins?.push((0, i18n_locale_plugin_1.createAngularLocaleDataPlugin)());
    }
    if (polyfills.length === 0) {
        return;
    }
    buildOptions.plugins?.push((0, virtual_module_plugin_1.createVirtualModulePlugin)({
        namespace,
        cache: sourceFileCache?.loadResultCache,
        loadContent: async (_, build) => {
            let hasLocalizePolyfill = false;
            let polyfillPaths = polyfills;
            let warnings;
            if (tryToResolvePolyfillsAsRelative) {
                polyfillPaths = await Promise.all(polyfills.map(async (path) => {
                    hasLocalizePolyfill ||= path.startsWith('@angular/localize');
                    if (path.startsWith('zone.js') || !(0, node_path_1.extname)(path)) {
                        return path;
                    }
                    const potentialPathRelative = './' + path;
                    const result = await build.resolve(potentialPathRelative, {
                        kind: 'import-statement',
                        resolveDir: workspaceRoot,
                    });
                    return result.path ? potentialPathRelative : path;
                }));
            }
            else {
                hasLocalizePolyfill = polyfills.some((p) => p.startsWith('@angular/localize'));
            }
            // Add localize polyfill if needed.
            // TODO: remove in version 19 or later.
            if (!i18nOptions.shouldInline && !hasLocalizePolyfill) {
                const result = await build.resolve('@angular/localize', {
                    kind: 'import-statement',
                    resolveDir: workspaceRoot,
                });
                if (result.path) {
                    polyfillPaths.push('@angular/localize/init');
                    (warnings ??= []).push({
                        text: 'Polyfill for "@angular/localize/init" was added automatically.',
                        notes: [
                            {
                                text: 'In the future, this functionality will be removed. ' +
                                    'Please add this polyfill in the "polyfills" section of your "angular.json" instead.',
                            },
                        ],
                    });
                }
            }
            // Generate module contents with an import statement per defined polyfill
            let contents = polyfillPaths
                .map((file) => `import '${file.replace(/\\/g, '/')}';`)
                .join('\n');
            // The below should be done after loading `$localize` as otherwise the locale will be overridden.
            if (i18nOptions.shouldInline) {
                // When inlining, a placeholder is used to allow the post-processing step to inject the $localize locale identifier.
                contents += '(globalThis.$localize ??= {}).locale = "___NG_LOCALE_INSERT___";\n';
            }
            else if (i18nOptions.hasDefinedSourceLocale) {
                // If not inlining translations and source locale is defined, inject the locale specifier.
                contents += `(globalThis.$localize ??= {}).locale = "${i18nOptions.sourceLocale}";\n`;
            }
            return {
                contents,
                loader: 'js',
                warnings,
                resolveDir: workspaceRoot,
            };
        },
    }));
    return buildOptions;
}
