import esbuild from "esbuild";
import { globIterate } from "glob";
import { err, ok } from "neverthrow";
import fs from "node:fs/promises";
import path from "pathe";
import { logger } from "../util/logger.js";
import { cloudflareInternalPlugin } from "./bundle/cloudflare-internal.js";
import { esbuildPluginAlias } from "./bundle/plugin-alias.js";
import { esbuildPluginAsyncLocalStorage } from "./bundle/plugin-als-external.js";
import { createHotReloadPlugin } from "./bundle/plugin-hot-reload.js";
import { esbuildPluginHybridNodeCompat } from "./bundle/plugin-hybrid-node-compat.js";
import { esbuildPluginNodeCompatImports } from "./bundle/plugin-node-compat.js";
import { createWasmPlugin } from "./bundle/plugin-wasm.js";
import { validateNodeCompat } from "./bundle/validate-node-compat.js";
export function normalizeWorkerBundle(props) {
    return validateNodeCompat({
        compatibilityDate: props.compatibilityDate,
        compatibilityFlags: props.compatibilityFlags,
        noBundle: props.noBundle ?? false,
    }).andThen((nodeCompat) => {
        if (props.script) {
            return ok(new WorkerBundleSource.Inline({
                content: props.script,
                format: props.format ?? "esm",
                nodeCompat,
            }));
        }
        if (!props.entrypoint) {
            return err("Either `script` or `entrypoint` must be provided for workers");
        }
        return ok(props.noBundle
            ? new WorkerBundleSource.FS({
                entrypoint: props.entrypoint,
                format: props.format ?? "esm",
                nodeCompat,
                cwd: props.cwd,
                globs: props.rules?.flatMap((rule) => rule.globs),
                sourcemaps: props.sourceMap !== false,
            })
            : new WorkerBundleSource.ESBuild({
                id: props.id,
                entrypoint: props.entrypoint,
                format: props.format ?? "esm",
                nodeCompat,
                cwd: props.cwd,
                outdir: props.outdir,
                sourcemap: props.sourceMap !== false ? true : undefined,
                compatibilityDate: props.compatibilityDate,
                compatibilityFlags: props.compatibilityFlags,
                ...props.bundle,
            }));
    });
}
export var WorkerBundle;
(function (WorkerBundle) {
    WorkerBundle.parseModules = (paths, format) => {
        return paths.map((filePath) => {
            const normalizedPath = path.normalize(filePath);
            const ext = normalizedPath.split(".").pop();
            switch (ext) {
                case "js":
                    return { type: format, path: normalizedPath };
                case "mjs":
                    return { type: "esm", path: normalizedPath };
                case "cjs":
                    return { type: "cjs", path: normalizedPath };
                case "wasm":
                    return { type: "wasm", path: normalizedPath };
                case "map":
                    return { type: "sourcemap", path: normalizedPath };
                default:
                    return { type: "text", path: normalizedPath };
            }
        });
    };
    WorkerBundle.toFormData = async (bundle) => {
        const types = {
            esm: "application/javascript+module",
            cjs: "application/javascript",
            text: "text/plain",
            data: "application/octet-stream",
            wasm: "application/wasm",
            sourcemap: "application/source-map",
        };
        const form = new FormData();
        await Promise.all(bundle.modules.map(async (module) => {
            const content = module.content ??
                (await fs.readFile(path.join(bundle.root, module.path)));
            form.append(module.path, 
            // TODO(sam): tsc -b geting a weird error about SharedArrayBuffer
            new Blob([content], {
                type: types[module.type],
            }), module.path);
        }));
        return form;
    };
})(WorkerBundle || (WorkerBundle = {}));
export var WorkerBundleSource;
(function (WorkerBundleSource) {
    class Inline {
        props;
        constructor(props) {
            this.props = props;
        }
        async create() {
            return {
                entrypoint: "worker.js",
                modules: [
                    {
                        path: "worker.js",
                        type: this.props.format,
                        content: this.props.content,
                    },
                ],
            };
        }
        async *watch() {
            yield await this.create();
        }
    }
    WorkerBundleSource.Inline = Inline;
    class FS {
        root;
        entrypoint;
        globs;
        format;
        constructor(props) {
            const entrypoint = path.resolve(props.cwd, props.entrypoint);
            this.root = path.dirname(entrypoint);
            this.entrypoint = path.relative(this.root, entrypoint);
            this.globs = props.globs ?? [
                "**/*.js",
                "**/*.mjs",
                "**/*.wasm",
                ...(props.sourcemaps ? ["**/*.js.map"] : []),
            ];
            this.format = props.format;
        }
        async create() {
            return {
                entrypoint: this.entrypoint,
                root: this.root,
                modules: await this.readFiles(),
            };
        }
        async *watch(signal) {
            const watcher = fs.watch(this.root, { recursive: true, signal });
            for await (const event of watcher) {
                if (event.eventType === "change") {
                    yield await this.create();
                }
            }
        }
        async readFiles() {
            const fileNames = new Set();
            await Promise.all(this.globs.map(async (glob) => {
                for await (const file of globIterate(glob, { cwd: this.root })) {
                    fileNames.add(file);
                }
            }));
            if (fileNames.size === 0) {
                throw new Error(`No files found matching ${this.globs.join(", ")} in ${this.root}`);
            }
            return WorkerBundle.parseModules(Array.from(fileNames), this.format);
        }
    }
    WorkerBundleSource.FS = FS;
    class ESBuild {
        props;
        constructor(props) {
            this.props = props;
            // Normalize paths to ensure consistency between esbuild inputs and outputs:
            // - the `cwd` is absolute
            // - the `entrypoint` and `outdir` are relative to the `cwd`
            // This is so we can identify the entrypoint from the metafile.
            this.props.cwd = path.resolve(this.props.cwd);
            this.props.entrypoint = path.isAbsolute(props.entrypoint)
                ? path.relative(this.props.cwd, props.entrypoint)
                : path.normalize(props.entrypoint);
            this.props.outdir = path.isAbsolute(props.outdir)
                ? path.relative(this.props.cwd, props.outdir)
                : path.normalize(props.outdir);
        }
        async create() {
            const wasmPlugin = createWasmPlugin();
            const options = this.buildOptions([wasmPlugin.plugin]);
            const result = await esbuild.build(options);
            const { entrypoint, root, modules } = await this.formatBuildOutput(result.metafile);
            return {
                entrypoint,
                root,
                modules: [...modules, ...wasmPlugin.modules.values()],
            };
        }
        async *watch(signal) {
            const wasm = createWasmPlugin();
            let count = 0;
            const hotReload = createHotReloadPlugin({
                onBuildStart: () => {
                    if (count > 0) {
                        logger.task(this.props.id, {
                            message: "Rebuilding",
                            status: "pending",
                            resource: this.props.id,
                            prefix: "dev",
                            prefixColor: "cyanBright",
                        });
                    }
                },
            });
            const options = this.buildOptions([wasm.plugin, hotReload.plugin]);
            const context = await esbuild.context(options);
            signal.addEventListener("abort", () => context.dispose());
            await context.watch();
            for await (const result of hotReload.iterator) {
                count++;
                if (result.errors.length > 0 || result.metafile == null) {
                    logger.error(`Error bundling worker \`${this.props.id}\`, see detailed logs above.`);
                    continue;
                }
                const { entrypoint, root, modules } = await this.formatBuildOutput(result.metafile);
                yield {
                    entrypoint,
                    root,
                    modules: [...modules, ...wasm.modules.values()],
                };
            }
        }
        async delete() {
            await fs.rm(path.join(this.props.cwd, this.props.outdir), {
                recursive: true,
                force: true,
            });
        }
        buildOptions(additionalPlugins) {
            const { id: _, entrypoint, nodeCompat, cwd, format, compatibilityDate: _compatibilityDate, compatibilityFlags: _compatibilityFlags, ...props } = this.props;
            return {
                entryPoints: [entrypoint],
                absWorkingDir: cwd,
                target: "es2022",
                loader: {
                    ".js": "jsx",
                    ".mjs": "jsx",
                    ".cjs": "jsx",
                    ".sql": "text",
                },
                format,
                sourceRoot: this.props.outdir,
                jsxFactory: "React.createElement",
                jsxFragment: "React.Fragment",
                keepNames: true,
                ...props,
                metafile: true,
                write: true,
                bundle: true,
                define: {
                    "navigator.userAgent": '"Cloudflare-Workers"',
                    "process.env.NODE_ENV": '"undefined"',
                    ...props.define,
                },
                conditions: props.conditions ?? ["workerd", "worker", "browser"],
                plugins: [
                    esbuildPluginAlias(props.alias ?? {}, this.props.cwd),
                    cloudflareInternalPlugin,
                    ...this.getNodeJSCompatPlugins({
                        mode: nodeCompat,
                        compatibilityDate: this.props.compatibilityDate,
                        compatibilityFlags: this.props.compatibilityFlags,
                    }),
                    ...(props.plugins ?? []),
                    ...additionalPlugins,
                ],
                external: [...(props.external ?? [])],
            };
        }
        /**
         * Returns the list of ESBuild plugins to use for a given compat mode.
         */
        getNodeJSCompatPlugins({ mode, compatibilityDate, compatibilityFlags, }) {
            switch (mode) {
                case "als":
                    return [
                        esbuildPluginAsyncLocalStorage,
                        esbuildPluginNodeCompatImports(mode),
                    ];
                case "v2":
                    return [
                        esbuildPluginHybridNodeCompat({
                            compatibilityDate,
                            compatibilityFlags,
                        }),
                    ];
                case null:
                    return [esbuildPluginNodeCompatImports(mode)];
            }
        }
        async formatBuildOutput(metafile) {
            const outdir = path.resolve(this.props.cwd, this.props.outdir);
            await fs.writeFile(path.join(outdir, "metafile.json"), JSON.stringify(metafile, null, 2));
            const paths = [];
            let entrypoint;
            let sourcemap;
            for (const [key, value] of Object.entries(metafile.outputs)) {
                const name = path.relative(outdir, path.resolve(this.props.cwd, key));
                paths.push(name);
                if (value.entryPoint === this.props.entrypoint) {
                    entrypoint = name;
                }
                if (name.endsWith(".map")) {
                    sourcemap = name;
                }
            }
            if (!entrypoint) {
                throw new Error(`Failed to find entrypoint in metafile - expected ${this.props.entrypoint} but found ${Object.values(metafile.outputs)
                    .map((v) => v.entryPoint)
                    .filter((v) => v !== undefined)
                    .join(", ")}`);
            }
            if (sourcemap != null) {
                const map = JSON.parse(await fs.readFile(path.join(outdir, sourcemap), "utf-8"));
                map.sources = map.sources.map((source) => {
                    const originalPath = path.join(outdir, source);
                    const entrypointDir = path.dirname(path.resolve(this.props.cwd, this.props.entrypoint));
                    return path.relative(entrypointDir, originalPath);
                });
                await fs.writeFile(path.join(outdir, sourcemap), JSON.stringify(map), "utf-8");
            }
            return {
                entrypoint,
                root: outdir,
                modules: WorkerBundle.parseModules(paths, this.props.format),
            };
        }
    }
    WorkerBundleSource.ESBuild = ESBuild;
})(WorkerBundleSource || (WorkerBundleSource = {}));
//# sourceMappingURL=worker-bundle.js.map