/**
 * Copied from https://github.com/cloudflare/workers-sdk/blob/main/packages/wrangler/src/deployment-bundle/esbuild-plugins/hybrid-nodejs-compat.ts#L17
 */
import assert from "node:assert";
import { createRequire } from "node:module";
import nodePath from "pathe";
import { dedent } from "../../util/dedent.js";
import { NODEJS_MODULES_RE } from "./nodejs-builtin-modules.js";
const _require = typeof require === "undefined" ? createRequire(import.meta.url) : require;
const REQUIRED_NODE_BUILT_IN_NAMESPACE = "node-built-in-modules";
const REQUIRED_UNENV_ALIAS_NAMESPACE = "required-unenv-alias";
/**
 * ESBuild plugin to apply the unenv preset.
 *
 * @returns ESBuild plugin
 */
export function esbuildPluginHybridNodeCompat({ compatibilityDate, compatibilityFlags, }) {
    return {
        name: "hybrid-nodejs_compat",
        async setup(build) {
            // `unenv` and `@cloudflare/unenv-preset` only publish esm
            const { defineEnv } = await import("unenv");
            const { getCloudflarePreset } = await import("@cloudflare/unenv-preset");
            const { alias, inject, external, polyfill } = defineEnv({
                presets: [
                    getCloudflarePreset({
                        compatibilityDate,
                        compatibilityFlags,
                    }),
                    {
                        alias: {
                            // Force esbuild to use the node implementation of debug instead of unenv's no-op stub.
                            // The alias is processed by handleUnenvAliasedPackages which uses require.resolve().
                            debug: "debug",
                        },
                    },
                ],
                npmShims: true,
            }).env;
            errorOnServiceWorkerFormat(build);
            handleRequireCallsToNodeJSBuiltins(build);
            handleUnenvAliasedPackages(build, alias, external);
            handleNodeJSGlobals(build, inject, polyfill);
        },
    };
}
/**
 * If we are bundling a "Service Worker" formatted Worker, imports of external modules,
 * which won't be inlined/bundled by esbuild, are invalid.
 *
 * This `onResolve()` handler will error if it identifies node.js external imports.
 */
function errorOnServiceWorkerFormat(build) {
    const paths = new Set();
    build.onStart(() => paths.clear());
    build.onResolve({ filter: NODEJS_MODULES_RE }, (args) => {
        paths.add(args.path);
        return null;
    });
    build.onEnd(() => {
        if (build.initialOptions.format === "iife" && paths.size > 0) {
            const pathList = new Intl.ListFormat("en-US").format(Array.from(paths.keys())
                .map((p) => `"${p}"`)
                .sort());
            return {
                errors: [
                    {
                        text: dedent `
                Unexpected external import of ${pathList}.
                Your worker has no default export, which means it is assumed to be a Service Worker format Worker.
                Did you mean to create a ES Module format Worker?
                If so, try adding \`export default { ... }\` in your entry-point.
                See https://developers.cloudflare.com/workers/reference/migrate-to-module-workers/.
            `,
                    },
                ],
            };
        }
    });
}
/**
 * We must convert `require()` calls for Node.js modules to a virtual ES Module that can be imported avoiding the require calls.
 * We do this by creating a special virtual ES module that re-exports the library in an onLoad handler.
 * The onLoad handler is triggered by matching the "namespace" added to the resolve.
 */
function handleRequireCallsToNodeJSBuiltins(build) {
    build.onResolve({ filter: NODEJS_MODULES_RE }, (args) => {
        if (args.kind === "require-call") {
            return {
                path: args.path,
                namespace: REQUIRED_NODE_BUILT_IN_NAMESPACE,
            };
        }
    });
    build.onLoad({ filter: /.*/, namespace: REQUIRED_NODE_BUILT_IN_NAMESPACE }, ({ path }) => {
        return {
            contents: dedent `
          import libDefault from '${path}';
          module.exports = libDefault;`,
            loader: "js",
        };
    });
}
/**
 * Handles aliased NPM packages.
 *
 * @param build ESBuild PluginBuild.
 * @param alias Aliases resolved to absolute paths.
 * @param external external modules.
 */
function handleUnenvAliasedPackages(build, alias, external) {
    // esbuild expects alias paths to be absolute
    const aliasAbsolute = {};
    for (const [module, unresolvedAlias] of Object.entries(alias)) {
        try {
            aliasAbsolute[module] = _require.resolve(unresolvedAlias);
        }
        catch {
            // this is an alias for package that is not installed in the current app => ignore
        }
    }
    const UNENV_ALIAS_RE = new RegExp(`^(${Object.keys(aliasAbsolute).join("|")})$`);
    build.onResolve({ filter: UNENV_ALIAS_RE }, (args) => {
        const unresolvedAlias = alias[args.path];
        // Convert `require()` calls for NPM packages to a virtual ES Module that can be imported avoiding the require calls.
        // Note: Does not apply to Node.js packages that are handled in `handleRequireCallsToNodeJSBuiltins`
        if (args.kind === "require-call" &&
            (unresolvedAlias.startsWith("unenv/npm/") ||
                unresolvedAlias.startsWith("unenv/mock/"))) {
            return {
                path: args.path,
                namespace: REQUIRED_UNENV_ALIAS_NAMESPACE,
            };
        }
        // Resolve the alias to its absolute path and potentially mark it as external
        return {
            path: aliasAbsolute[args.path],
            external: external.includes(unresolvedAlias),
        };
    });
    build.onLoad({ filter: /.*/, namespace: REQUIRED_UNENV_ALIAS_NAMESPACE }, ({ path }) => {
        return {
            contents: dedent `
            import * as esm from '${path}';
            module.exports = Object.entries(esm)
                .filter(([k,]) => k !== 'default')
                .reduce((cjs, [k, value]) =>
                    Object.defineProperty(cjs, k, { value, enumerable: true }),
                    "default" in esm ? esm.default : {}
                );`,
            loader: "js",
        };
    });
}
/**
 * Inject node globals defined in unenv's preset `inject` and `polyfill` properties.
 *
 * - an `inject` injects virtual module defining the name on `globalThis`
 * - a `polyfill` is injected directly
 */
function handleNodeJSGlobals(build, inject, polyfill) {
    const UNENV_VIRTUAL_MODULE_RE = /_virtual_unenv_global_polyfill-(.+)$/;
    const prefix = nodePath.resolve(import.meta.dirname, "_virtual_unenv_global_polyfill-");
    /**
     * Map of module identifiers to
     * - `injectedName`: the name injected on `globalThis`
     * - `exportName`: the export name from the module
     * - `importName`: the imported name
     */
    const injectsByModule = new Map();
    // Module specifier (i.e. `/unenv/runtime/node/...`) keyed by path (i.e. `/prefix/_virtual_unenv_global_polyfill-...`)
    const virtualModulePathToSpecifier = new Map();
    for (const [injectedName, moduleSpecifier] of Object.entries(inject)) {
        const [module, exportName, importName] = Array.isArray(moduleSpecifier)
            ? [moduleSpecifier[0], moduleSpecifier[1], moduleSpecifier[1]]
            : [moduleSpecifier, "default", "defaultExport"];
        if (!injectsByModule.has(module)) {
            injectsByModule.set(module, []);
            virtualModulePathToSpecifier.set(prefix + module.replaceAll("/", "-"), module);
        }
        // eslint-disable-next-line  @typescript-eslint/no-non-null-assertion
        injectsByModule.get(module).push({ injectedName, exportName, importName });
    }
    build.initialOptions.inject = [
        ...(build.initialOptions.inject ?? []),
        // Inject the virtual modules
        ...virtualModulePathToSpecifier.keys(),
        // Inject the polyfills - needs an absolute path
        ...polyfill.map((m) => _require.resolve(m)),
    ];
    build.onResolve({ filter: UNENV_VIRTUAL_MODULE_RE }, ({ path }) => ({
        path,
    }));
    build.onLoad({ filter: UNENV_VIRTUAL_MODULE_RE }, ({ path }) => {
        const module = virtualModulePathToSpecifier.get(path);
        assert(module, `Expected ${path} to be mapped to a module specifier`);
        const injects = injectsByModule.get(module);
        assert(injects, `Expected ${module} to inject values`);
        const imports = injects.map(({ exportName, importName }) => importName === exportName ? exportName : `${exportName} as ${importName}`);
        return {
            contents: dedent `
        import { ${imports.join(", ")} } from "${module}";
        ${injects.map(({ injectedName, importName }) => `globalThis.${injectedName} = ${importName};`).join("\n")}`,
        };
    });
}
//# sourceMappingURL=plugin-hybrid-node-compat.js.map