"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.createWasmPlugin = createWasmPlugin;
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 error_1 = require("../../utils/error");
const load_result_cache_1 = require("./load-result-cache");
const WASM_INIT_NAMESPACE = 'angular:wasm:init';
const WASM_CONTENTS_NAMESPACE = 'angular:wasm:contents';
const WASM_RESOLVE_SYMBOL = Symbol('WASM_RESOLVE_SYMBOL');
// See: https://github.com/tc39/proposal-regexp-unicode-property-escapes/blob/fe6d07fad74cd0192d154966baa1e95e7cda78a1/README.md#other-examples
const ecmaIdentifierNameRegExp = /^(?:[$_\p{ID_Start}])(?:[$_\u200C\u200D\p{ID_Continue}])*$/u;
/**
 * Creates an esbuild plugin to use WASM files with import statements and expressions.
 * The default behavior follows the WebAssembly/ES mode integration proposal found at
 * https://github.com/WebAssembly/esm-integration/tree/main/proposals/esm-integration.
 * This behavior requires top-level await support which is only available in zoneless
 * Angular applications.
 * @returns An esbuild plugin.
 */
function createWasmPlugin(options) {
    const { allowAsync = false, cache } = options;
    return {
        name: 'angular-wasm',
        setup(build) {
            build.onResolve({ filter: /\.wasm$/ }, async (args) => {
                // Skip if already resolving the WASM file to avoid infinite resolution
                if (args.pluginData?.[WASM_RESOLVE_SYMBOL]) {
                    return;
                }
                // Skip if not an import statement or expression
                if (args.kind !== 'import-statement' && args.kind !== 'dynamic-import') {
                    return;
                }
                // When in the initialization namespace, the content has already been resolved
                // and only needs to be loaded for use with the initialization code.
                if (args.namespace === WASM_INIT_NAMESPACE) {
                    return {
                        namespace: WASM_CONTENTS_NAMESPACE,
                        path: (0, node_path_1.join)(args.resolveDir, args.path),
                        pluginData: args.pluginData,
                    };
                }
                // Skip if a custom loader is defined
                if (build.initialOptions.loader?.['.wasm'] || args.with['loader']) {
                    return;
                }
                // Attempt full resolution of the WASM file
                const resolveOptions = {
                    ...args,
                    pluginData: { [WASM_RESOLVE_SYMBOL]: true },
                };
                // The "path" property will cause an error if used in the resolve call
                delete resolveOptions.path;
                const result = await build.resolve(args.path, resolveOptions);
                // Skip if there are errors, is external, or another plugin resolves to a custom namespace
                if (result.errors.length > 0 || result.external || result.namespace !== 'file') {
                    // Reuse already resolved result
                    return result;
                }
                return {
                    ...result,
                    namespace: WASM_INIT_NAMESPACE,
                };
            });
            build.onLoad({ filter: /\.wasm$/, namespace: WASM_INIT_NAMESPACE }, (0, load_result_cache_1.createCachedLoad)(cache, async (args) => {
                // Ensure async mode is supported
                if (!allowAsync) {
                    return {
                        errors: [
                            {
                                text: 'WASM/ES module integration imports are not supported with Zone.js applications',
                                notes: [
                                    {
                                        text: 'Information about zoneless Angular applications can be found here: https://angular.dev/guide/experimental/zoneless',
                                    },
                                ],
                            },
                        ],
                    };
                }
                const wasmContents = await (0, promises_1.readFile)(args.path);
                // Inline WASM code less than 10kB
                const inlineWasm = wasmContents.byteLength < 10_000;
                // Add import of WASM contents
                let initContents = `import ${inlineWasm ? 'wasmData' : 'wasmPath'} from ${JSON.stringify((0, node_path_1.basename)(args.path))}`;
                initContents += inlineWasm ? ' with { loader: "binary" };' : ';\n\n';
                // Read from the file system when on Node.js (SSR) and not inline
                if (!inlineWasm && build.initialOptions.platform === 'node') {
                    initContents += 'import { readFile } from "node:fs/promises";\n';
                    initContents +=
                        'const wasmData = await readFile(new URL(wasmPath, import.meta.url));\n';
                }
                // Create initialization function
                initContents += generateInitHelper(!inlineWasm && build.initialOptions.platform !== 'node', wasmContents);
                // Analyze WASM for imports and exports
                let importModuleNames, exportNames;
                try {
                    const wasm = await WebAssembly.compile(wasmContents);
                    importModuleNames = new Set(WebAssembly.Module.imports(wasm).map((value) => value.module));
                    exportNames = WebAssembly.Module.exports(wasm).map((value) => value.name);
                }
                catch (error) {
                    (0, error_1.assertIsError)(error);
                    return {
                        errors: [{ text: 'Unable to analyze WASM file', notes: [{ text: error.message }] }],
                    };
                }
                // Ensure export names are valid JavaScript identifiers
                const invalidExportNames = exportNames.filter((name) => !ecmaIdentifierNameRegExp.test(name));
                if (invalidExportNames.length > 0) {
                    return {
                        errors: invalidExportNames.map((name) => ({
                            text: 'WASM export names must be valid JavaScript identifiers',
                            notes: [
                                {
                                    text: `The export "${name}" is not valid. The WASM file should be updated to remove this error.`,
                                },
                            ],
                        })),
                    };
                }
                // Add import statements and setup import object
                initContents += 'const importObject = Object.create(null);\n';
                let importIndex = 0;
                for (const moduleName of importModuleNames) {
                    // Add a namespace import for each module name
                    initContents += `import * as wasm_import_${++importIndex} from ${JSON.stringify(moduleName)};\n`;
                    // Add the namespace object to the import object
                    initContents += `importObject[${JSON.stringify(moduleName)}] = wasm_import_${importIndex};\n`;
                }
                // Instantiate the module
                initContents += 'const instance = await init(importObject);\n';
                // Add exports
                const exportNameList = exportNames.join(', ');
                initContents += `const { ${exportNameList} } = instance.exports;\n`;
                initContents += `export { ${exportNameList} }\n`;
                return {
                    contents: initContents,
                    loader: 'js',
                    resolveDir: (0, node_path_1.dirname)(args.path),
                    pluginData: { wasmContents },
                    watchFiles: [args.path],
                };
            }));
            build.onLoad({ filter: /\.wasm$/, namespace: WASM_CONTENTS_NAMESPACE }, async (args) => {
                const contents = args.pluginData.wasmContents ?? (await (0, promises_1.readFile)(args.path));
                let loader = 'file';
                if (args.with.loader) {
                    (0, node_assert_1.default)(args.with.loader === 'binary' || args.with.loader === 'file', 'WASM loader type should only be binary or file.');
                    loader = args.with.loader;
                }
                return {
                    contents,
                    loader,
                    watchFiles: [args.path],
                };
            });
        },
    };
}
/**
 * Generates the string content of the WASM initialization helper function.
 * This function supports both file fetching and inline byte data depending on
 * the preferred option for the WASM file. When fetching, an integrity hash is
 * also generated and used with the fetch action.
 *
 * @param streaming Uses fetch and WebAssembly.instantiateStreaming.
 * @param wasmContents The binary contents to generate an integrity hash.
 * @returns A string containing the initialization function.
 */
function generateInitHelper(streaming, wasmContents) {
    let resultContents;
    if (streaming) {
        const fetchOptions = {
            integrity: 'sha256-' + (0, node_crypto_1.createHash)('sha256').update(wasmContents).digest('base64'),
        };
        const fetchContents = `fetch(new URL(wasmPath, import.meta.url), ${JSON.stringify(fetchOptions)})`;
        resultContents = `await WebAssembly.instantiateStreaming(${fetchContents}, imports)`;
    }
    else {
        resultContents = 'await WebAssembly.instantiate(wasmData, imports)';
    }
    const contents = `
let mod;
async function init(imports) {
  if (mod) {
    return await WebAssembly.instantiate(mod, imports);
  }

  const result = ${resultContents};
  mod = result.module;

  return result.instance;
}
`;
    return contents;
}
