1import {
2 EXPORT_TYPES_MODULE_ID,
3 exportTypesModuleSource,
4 RESOLVED_EXPORT_TYPES_MODULE_ID,
5 WORKER_EXPORT_TYPES_EVENT,
6} from "../export-types.ts";
7import { createPlugin } from "../factory.ts";
8import { resolvePluginApi } from "../utils.ts";
9import type { UnenvApi } from "./nodejs-compat.ts";
10
11// The export-types module is the one virtual module that is imported by
12// specifier (both from the Worker entry and, in dev, by the module runner), so
13// the filter has to match it before it has been resolved to its `\0` id.
14// oxlint-disable-next-line no-control-regex
15const VIRTUAL_MODULE_REGEXP = /^\0?distilled:.*$/;
16
17export const WORKER_ENTRY_PREFIX = "\0distilled:worker-entry:" as const;
18const USER_ENTRY_PREFIX = "\0distilled:user-entry:" as const;
19const INJECT_PREFIX = "\0distilled:inject:" as const;
20
21/**
22 * The id of the generated Worker entry that wraps the user entry `id`.
23 *
24 * Entry ids are embedded into the specifier and round-tripped through
25 * Rolldown/Vite id handling, which expects POSIX-style separators. On Windows
26 * the input is an absolute path with `\` separators (e.g. `D:\app\entry.ts`),
27 * which would corrupt the virtual id and break resolution. Normalizing to
28 * forward slashes is a no-op on POSIX, where ids already use `/`.
29 */
30export const workerEntryId = (id: string) =>
31 `${WORKER_ENTRY_PREFIX}${id.replace(/\\/g, "/")}` as const;
32
33export const virtualModulesPlugin = createPlugin(
34 "virtual-modules",
35 (options) => {
36 let unenvApi: UnenvApi | undefined;
37 const inject = () => {
38 if (!unenvApi) return [];
39 return [
40 ...unenvApi.polyfill.map((module) => `import "${module}";`),
41 ...Object.keys(unenvApi.inject).map(
42 (injectedName) => `import "${INJECT_PREFIX}${injectedName}";`,
43 ),
44 ];
45 };
46 return {
47 vite: {
48 enforce: "pre",
49 },
50 shared: {
51 buildStart({ plugins }) {
52 unenvApi = resolvePluginApi<UnenvApi>(
53 plugins,
54 "distilled-cloudflare:nodejs-unenv",
55 );
56 },
57 resolveId: {
58 filter: { id: VIRTUAL_MODULE_REGEXP },
59 handler(id) {
60 if (
61 id === EXPORT_TYPES_MODULE_ID ||
62 id === RESOLVED_EXPORT_TYPES_MODULE_ID
63 ) {
64 return { id: RESOLVED_EXPORT_TYPES_MODULE_ID };
65 }
66 if (
67 id.startsWith(WORKER_ENTRY_PREFIX) ||
68 id.startsWith(INJECT_PREFIX)
69 ) {
70 return { id };
71 }
72 if (id.startsWith(USER_ENTRY_PREFIX)) {
73 return this.resolve(
74 id.slice(USER_ENTRY_PREFIX.length),
75 undefined,
76 {
77 isEntry: true,
78 kind: "import-statement",
79 },
80 );
81 }
82 },
83 },
84 load: {
85 filter: { id: VIRTUAL_MODULE_REGEXP },
86 handler(id) {
87 if (id.startsWith(WORKER_ENTRY_PREFIX)) {
88 const userEntryId = id.replace(
89 WORKER_ENTRY_PREFIX,
90 USER_ENTRY_PREFIX,
91 );
92
93 return [
94 ...inject(),
95 ...(options.exports
96 ? [
97 `export { ${options.exports.join(", ")} } from "${userEntryId}";`,
98 ]
99 : [
100 `import * as userEntry from "${userEntryId}";`,
101 `export * from "${userEntryId}";`,
102 `export default userEntry.default ?? {};`,
103 ]),
104 "if (import.meta.hot) {",
105 ` const { getExportTypes } = await import("${EXPORT_TYPES_MODULE_ID}");`,
106 " import.meta.hot.accept((module) => {",
107 " const exportTypes = getExportTypes(module);",
108 ` import.meta.hot.send("${WORKER_EXPORT_TYPES_EVENT}", exportTypes);`,
109 " });",
110 "}",
111 ].join("\n");
112 }
113 if (id === RESOLVED_EXPORT_TYPES_MODULE_ID) {
114 return exportTypesModuleSource;
115 }
116 if (id.startsWith(INJECT_PREFIX)) {
117 const injectedName = id.slice(INJECT_PREFIX.length);
118 const moduleSpecifier = unenvApi?.inject[injectedName];
119 if (!moduleSpecifier) {
120 throw new Error(
121 `Expected module specifier for "${injectedName}" to be defined`,
122 );
123 }
124 return [
125 `import ${injectedName} from "${moduleSpecifier}";`,
126 `globalThis.${injectedName} = ${injectedName};`,
127 ].join("\n");
128 }
129 },
130 },
131 },
132 };
133 },
134);