1/**
2 * `import source from "./hasher.ts?worker"` — the target is bundled into
3 * one self-contained ES module (the same Cloudflare-flavored build the
4 * parent gets: workerd externals, node builtins, Alchemy defines) and
5 * imported as a **string**, ready to hand to a `WorkerLoader`
6 * (`modules: { "worker.js": source }`). Nested `?worker` imports inside
7 * the target work the same way.
8 *
9 * Bundling happens at build time, so the string is fixed per deploy; the
10 * dynamic worker it becomes is then instantiated at runtime as many times
11 * as the caller wants.
12 */
13import type * as rolldown from "rolldown";
14import { splitFileAndPostfix } from "../../../Bundle/RawPlugin.ts";
15
16export const WORKER_MODULE_RE: RegExp = /(\?|&)worker(?:&|$)/;
17
18export const workerModulePlugin = (options: {
19 /**
20 * The nested build's input options (plugins, externals, cwd) for a given
21 * entry; `input` is set by the plugin. Built lazily per import.
22 */
23 readonly nested: (entry: string) => Promise<rolldown.InputOptions>;
24 readonly loadRolldown: () => Promise<typeof import("rolldown")>;
25}): rolldown.Plugin => ({
26 name: "alchemy:worker-module",
27 resolveId: {
28 filter: { id: WORKER_MODULE_RE },
29 async handler(source, importer) {
30 const [base, query] = splitFileAndPostfix(source);
31 const resolved = await this.resolve(base, importer, { skipSelf: true });
32 if (!resolved || resolved.external) return null;
33 return { id: resolved.id + query, moduleSideEffects: false };
34 },
35 },
36 load: {
37 filter: { id: WORKER_MODULE_RE },
38 async handler(id) {
39 const entry = id.replace(/[?#].*$/, "");
40 this.addWatchFile(entry);
41 const rolldownLib = await options.loadRolldown();
42 const input = await options.nested(entry);
43 const bundle = await rolldownLib.rolldown({ ...input, input: entry });
44 try {
45 const { output } = await bundle.generate({
46 format: "esm",
47 minify: true,
48 keepNames: true,
49 inlineDynamicImports: true,
50 sourcemap: false,
51 });
52 const chunk = output.find(
53 (item) => item.type === "chunk" && item.isEntry,
54 );
55 if (chunk === undefined || chunk.type !== "chunk") {
56 throw new Error(`worker module ${entry}: no entry chunk produced`);
57 }
58 for (const item of output) {
59 if (item.type === "chunk" && item !== chunk) {
60 throw new Error(
61 `worker module ${entry}: produced a second chunk (${item.fileName}); a dynamic worker is one module`,
62 );
63 }
64 }
65 return {
66 code: `export default ${JSON.stringify(chunk.code)};`,
67 map: { mappings: "" },
68 moduleType: "js",
69 };
70 } finally {
71 await bundle.close();
72 }
73 },
74 },
75});