@jundle/cloudflare 0.0.0-stage → 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +22 -3
- package/src/bindings.js +295 -0
- package/src/build.d.ts +145 -0
- package/src/build.js +327 -0
- package/src/config.js +82 -0
- package/src/harness.js +707 -0
- package/src/index.d.ts +389 -0
- package/src/index.js +446 -0
- package/README.md +0 -3
package/src/harness.js
ADDED
|
@@ -0,0 +1,707 @@
|
|
|
1
|
+
// Jundle workerd module-runner harness (SPEC §16.11).
|
|
2
|
+
//
|
|
3
|
+
// Static worker script, constructed ONCE per dev session by the
|
|
4
|
+
// CF plugin's runner (Miniflare: this script + the unsafe-eval
|
|
5
|
+
// binding + the wrangler-derived bindings). It contains NO app
|
|
6
|
+
// code: app modules are fetched as text from the dev server
|
|
7
|
+
// over HTTP and evaluated through the unsafe-eval binding as
|
|
8
|
+
// CJS-converted code with a require hook — the workerd
|
|
9
|
+
// counterpart of the Node runner's CJS evaluation
|
|
10
|
+
// (packages/jundle/src/dev/runner.js), sharing its semantics:
|
|
11
|
+
// same registry keys, same importer eviction (via the dev
|
|
12
|
+
// server's version route), same loud failures for targets the
|
|
13
|
+
// closure walk missed.
|
|
14
|
+
//
|
|
15
|
+
// This file is a TEMPLATE: the plugin reads it as text and
|
|
16
|
+
// replaces the `// __JUNDLE_WRAPPERS__` line with generated
|
|
17
|
+
// export wrappers (one per DO / Workflow / entrypoint class
|
|
18
|
+
// export, discovered by static analysis on the dev server).
|
|
19
|
+
// It must stay free of Node-only imports: everything here runs
|
|
20
|
+
// in the worker (static node: imports + cloudflare:workers
|
|
21
|
+
// only). It is dev-only machinery: the unsafe-eval binding is
|
|
22
|
+
// a Miniflare option on this worker, never a wrangler binding,
|
|
23
|
+
// never production, never in a prod bundle.
|
|
24
|
+
import * as streamNs from "node:stream";
|
|
25
|
+
import * as bufferNs from "node:buffer";
|
|
26
|
+
import * as cryptoNs from "node:crypto";
|
|
27
|
+
import * as eventsNs from "node:events";
|
|
28
|
+
import * as pathNs from "node:path";
|
|
29
|
+
import * as urlNs from "node:url";
|
|
30
|
+
import * as utilNs from "node:util";
|
|
31
|
+
import * as querystringNs from "node:querystring";
|
|
32
|
+
import * as stringDecoderNs from "node:string_decoder";
|
|
33
|
+
import * as asyncHooksNs from "node:async_hooks";
|
|
34
|
+
import * as timersNs from "node:timers";
|
|
35
|
+
import * as diagnosticsChannelNs from "node:diagnostics_channel";
|
|
36
|
+
import * as cwNs from "cloudflare:workers";
|
|
37
|
+
|
|
38
|
+
// Internal binding names. They are stripped from user `env`
|
|
39
|
+
// before app code ever sees it (SPEC §16.11: user code receives
|
|
40
|
+
// exactly its own bindings, identical in shape to production).
|
|
41
|
+
const EVAL_NAME = "__jundle_runner_eval";
|
|
42
|
+
const BASE_NAME = "__jundle_runner_base";
|
|
43
|
+
const ENTRY_NAME = "__jundle_runner_entry";
|
|
44
|
+
const INTERNAL = new Set([EVAL_NAME, BASE_NAME, ENTRY_NAME]);
|
|
45
|
+
|
|
46
|
+
// Node builtins the require hook serves. Only builtins this
|
|
47
|
+
// workerd's nodejs_compat actually provides are listed here
|
|
48
|
+
// (verified by construction: node:os, for example, is NOT
|
|
49
|
+
// provided and must stay out, or the worker fails to start).
|
|
50
|
+
// Anything else fails loudly at require time, exactly like
|
|
51
|
+
// the Node runner's un-pre-loaded error.
|
|
52
|
+
const BUILTINS = {
|
|
53
|
+
"node:stream": streamNs,
|
|
54
|
+
"node:buffer": bufferNs,
|
|
55
|
+
"node:crypto": cryptoNs,
|
|
56
|
+
"node:events": eventsNs,
|
|
57
|
+
"node:path": pathNs,
|
|
58
|
+
"node:url": urlNs,
|
|
59
|
+
"node:util": utilNs,
|
|
60
|
+
"node:querystring": querystringNs,
|
|
61
|
+
"node:string_decoder": stringDecoderNs,
|
|
62
|
+
"node:async_hooks": asyncHooksNs,
|
|
63
|
+
"node:timers": timersNs,
|
|
64
|
+
"node:diagnostics_channel": diagnosticsChannelNs,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// No React Refresh globals are defined here, by design
|
|
68
|
+
// (SPEC §16.6): server runtimes never evaluate a
|
|
69
|
+
// Refresh-carrying module — client modules fetched for
|
|
70
|
+
// SSR arrive as refresh-free SSR variants, and server
|
|
71
|
+
// modules never get Refresh calls. A $RefreshReg$ call
|
|
72
|
+
// evaluated here would throw a ReferenceError, which
|
|
73
|
+
// indicates a variant-routing bug (a browser variant
|
|
74
|
+
// fetched for SSR), not a missing runtime, and must
|
|
75
|
+
// surface loudly rather than silently no-op.
|
|
76
|
+
|
|
77
|
+
// process.env.NODE_ENV (dev parity with the dev server, which
|
|
78
|
+
// fills NODE_ENV=development when unset). Workerd provides a
|
|
79
|
+
// `process` global with an (empty) env; fill only when unset.
|
|
80
|
+
try {
|
|
81
|
+
if (typeof process !== "undefined" && process && process.env && !process.env.NODE_ENV)
|
|
82
|
+
process.env.NODE_ENV = "development";
|
|
83
|
+
} catch {}
|
|
84
|
+
|
|
85
|
+
// ---- Runner state (per worker instance; lost on crash,
|
|
86
|
+
// rebuilt lazily on next request — see SPEC §16.11) ----
|
|
87
|
+
let evalBinding = null;
|
|
88
|
+
let baseUrl = "";
|
|
89
|
+
let entryUrl = "";
|
|
90
|
+
let lastSeenVersion = 0;
|
|
91
|
+
let manifestsCache = null;
|
|
92
|
+
let lastGoodEntry = null;
|
|
93
|
+
// Dev-module text cache: key `${env}\0${file}` -> {code, edges, filename, dirname}.
|
|
94
|
+
const textCache = new Map();
|
|
95
|
+
// Evaluated dev modules: key -> {exports, failed, error}.
|
|
96
|
+
const evaluated = new Map();
|
|
97
|
+
// Dep bundles: key (fileName, or `${entry}\0${rel}`) -> {code, modules, entryRel}.
|
|
98
|
+
const depText = new Map();
|
|
99
|
+
// Evaluated dep bundles: key -> {exports, failed, error}.
|
|
100
|
+
const depEvaluated = new Map();
|
|
101
|
+
// Parsed JSON values: key `${env}\0${file}` -> value.
|
|
102
|
+
const jsonCache = new Map();
|
|
103
|
+
// Prefetched asset values: key `${env}\0${file}\0${type}` ->
|
|
104
|
+
// the namespace-shaped object the worker returns for that
|
|
105
|
+
// file's asset-value URL ({ __esModule: true, default:
|
|
106
|
+
// <value> }), mirroring NodeRunner's assetValueCache.
|
|
107
|
+
// The key distinguishes type: one file as raw and as blob
|
|
108
|
+
// are different values. One entry per file+env+type, so
|
|
109
|
+
// every SSR importer in one env shares one value object.
|
|
110
|
+
const assetValueCache = new Map();
|
|
111
|
+
|
|
112
|
+
function devKey(url) {
|
|
113
|
+
const u = new URL(url, "http://dev");
|
|
114
|
+
return `${u.searchParams.get("env") || ""}\0${u.searchParams.get("file") || ""}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function devAssetKey(url) {
|
|
118
|
+
const u = new URL(url, "http://dev");
|
|
119
|
+
return `${u.searchParams.get("env") || ""}\0${u.searchParams.get("file") || ""}\0${u.searchParams.get("type") || ""}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function depKeyFromUrl(url) {
|
|
123
|
+
const u = new URL(url, "http://dev");
|
|
124
|
+
const rest = u.pathname.slice("/@jundle/deps/".length);
|
|
125
|
+
const slash = rest.indexOf("/");
|
|
126
|
+
if (slash === -1) return rest;
|
|
127
|
+
return `${rest.slice(0, slash)}\0${rest.slice(slash + 1)}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function stripEnv(env) {
|
|
131
|
+
const out = {};
|
|
132
|
+
for (const k of Object.keys(env)) {
|
|
133
|
+
if (!INTERNAL.has(k)) out[k] = env[k];
|
|
134
|
+
}
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function fetchJson(url) {
|
|
139
|
+
const res = await fetch(baseUrl + url);
|
|
140
|
+
if (!res.ok) {
|
|
141
|
+
const body = await res.text().catch(() => "");
|
|
142
|
+
throw new Error(`ModuleRunner: fetch ${url} failed (${res.status}): ${body.slice(0, 500)}`);
|
|
143
|
+
}
|
|
144
|
+
return res.json();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Pull-based invalidation (SPEC §16.11): one loopback fetch per
|
|
148
|
+
// dispatch. Evicts invalidated dev-module keys (evaluated +
|
|
149
|
+
// text) and JSON values for edited JSON files, then advances
|
|
150
|
+
// the seen version. The dev server prunes acked batches.
|
|
151
|
+
// After eviction, pushes fresh SSR-variant instances for the
|
|
152
|
+
// browser-env subset (see pushSsrUpdates) before the entry
|
|
153
|
+
// re-imports, so the entry's decode already sees them.
|
|
154
|
+
async function syncVersion() {
|
|
155
|
+
const data = await fetchJson(`/@jundle/runner-version?since=${lastSeenVersion}`);
|
|
156
|
+
lastSeenVersion = data.version;
|
|
157
|
+
for (const key of data.invalidated || []) {
|
|
158
|
+
evaluated.delete(key);
|
|
159
|
+
textCache.delete(key);
|
|
160
|
+
}
|
|
161
|
+
for (const file of data.jsonFiles || []) {
|
|
162
|
+
for (const k of jsonCache.keys()) {
|
|
163
|
+
if (k.split("\0")[1] === file) jsonCache.delete(k);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
for (const file of data.assetFiles || []) {
|
|
167
|
+
for (const k of assetValueCache.keys()) {
|
|
168
|
+
if (k.split("\0")[1] === file) assetValueCache.delete(k);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
await pushSsrUpdates(data.clientInvalidated || []);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Server-side updateModule push, runner path (SPEC §16.11):
|
|
175
|
+
// after eviction, re-evaluate each evicted browser-env key
|
|
176
|
+
// (server variant — ensureText fetches consumer=server)
|
|
177
|
+
// and push the fresh exports into the dev shim's cache
|
|
178
|
+
// under the stable dev URL, mirroring the browser HMR
|
|
179
|
+
// client's update handler. React's Flight client skips
|
|
180
|
+
// chunk loads on reload (its own module-level chunkCache),
|
|
181
|
+
// so without the push a reloaded page keeps rendering the
|
|
182
|
+
// first SSR instance. Failure keeps last-good silently:
|
|
183
|
+
// the old shim entry stays (the push is skipped, never a
|
|
184
|
+
// delete) and the failure is logged only, never surfaced
|
|
185
|
+
// through the runner-error header. A browser module
|
|
186
|
+
// failing worker evaluation is expected (DOM code has no
|
|
187
|
+
// document in a worker), and surfacing it would trip the
|
|
188
|
+
// hot client's first-update overlay rule and force a full
|
|
189
|
+
// reload on the next update instead of HMR.
|
|
190
|
+
async function pushSsrUpdates(clientKeys) {
|
|
191
|
+
for (const key of clientKeys) {
|
|
192
|
+
const [env, file] = key.split("\0");
|
|
193
|
+
const url = `/@jundle/module?env=${encodeURIComponent(env)}&file=${encodeURIComponent(file)}`;
|
|
194
|
+
try {
|
|
195
|
+
const rec = await ensureModuleByUrl(url);
|
|
196
|
+
globalThis.__jundleFlight?.updateModule?.(url, rec.exports);
|
|
197
|
+
} catch (e) {
|
|
198
|
+
console.error(`[jundle runner] SSR push ${url}: ${e && e.message ? e.message : String(e)}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Closure walk (async, mirrors NodeRunner.ensureClosure): fetch
|
|
204
|
+
// texts for the module and everything it reaches, so the sync
|
|
205
|
+
// evaluation below never has to load anything. Dev edges,
|
|
206
|
+
// client-ref edges, and server-action edges are all fetched
|
|
207
|
+
// (client modules are transformed server-side by the fetch,
|
|
208
|
+
// which is what populates the dev manifests, but never
|
|
209
|
+
// evaluated — server code references them, it does not import
|
|
210
|
+
// them). Dep edges fetch dep units; JSON edges fetch parsed
|
|
211
|
+
// values; asset-value edges fetch values. CSS / host /
|
|
212
|
+
// external edges fetch nothing.
|
|
213
|
+
async function ensureText(url, visiting = new Set()) {
|
|
214
|
+
const key = devKey(url);
|
|
215
|
+
if (textCache.has(key) || visiting.has(key)) return;
|
|
216
|
+
visiting.add(key);
|
|
217
|
+
const u = new URL(url, "http://dev");
|
|
218
|
+
// Server consumer (SPEC §16.6): everything the harness
|
|
219
|
+
// fetches runs in the worker, so every module fetch
|
|
220
|
+
// asks for the SSR variant (refresh-free for client
|
|
221
|
+
// modules). Keys ignore the extra param (devKey reads
|
|
222
|
+
// env+file only), so registry/textCache identities and
|
|
223
|
+
// version-check eviction are unchanged.
|
|
224
|
+
u.searchParams.set("consumer", "server");
|
|
225
|
+
// Dev URL params are always sorted (SPEC §16.12), so the
|
|
226
|
+
// added consumer param must not land last out of order.
|
|
227
|
+
u.searchParams.sort();
|
|
228
|
+
const data = await fetchJson(`/@jundle/runner-module?${u.searchParams.toString()}`);
|
|
229
|
+
textCache.set(key, data);
|
|
230
|
+
for (const edge of data.edges || []) {
|
|
231
|
+
if (edge.kind === "dev" || edge.kind === "client-ref" || edge.kind === "server-action") {
|
|
232
|
+
await ensureText(edge.url, visiting);
|
|
233
|
+
} else if (edge.kind === "dep") {
|
|
234
|
+
await ensureDepText(edge.url, new Set());
|
|
235
|
+
} else if (edge.kind === "json") {
|
|
236
|
+
const jk = devKey(edge.url);
|
|
237
|
+
if (!jsonCache.has(jk)) {
|
|
238
|
+
const ju = new URL(edge.url, "http://dev");
|
|
239
|
+
jsonCache.set(jk, await fetchJson(`/@jundle/runner-json?${ju.searchParams.toString()}`));
|
|
240
|
+
}
|
|
241
|
+
} else if (edge.kind === "asset-value") {
|
|
242
|
+
// Asset-value edges (SPEC §16.12): the harness
|
|
243
|
+
// prefetches the value like a JSON value, then
|
|
244
|
+
// reconstructs the namespace the worker returns
|
|
245
|
+
// for the asset-value URL. Blob values arrive as
|
|
246
|
+
// base64 + MIME and are rebuilt here (Blob exists
|
|
247
|
+
// in workerd); raw and json values arrive as-is.
|
|
248
|
+
const ak = devAssetKey(edge.url);
|
|
249
|
+
if (!assetValueCache.has(ak)) {
|
|
250
|
+
const au = new URL(edge.url, "http://dev");
|
|
251
|
+
const data = await fetchJson(`/@jundle/runner-asset-value?${au.searchParams.toString()}`);
|
|
252
|
+
let value;
|
|
253
|
+
if (data.kind === "blob") {
|
|
254
|
+
value = new Blob([Uint8Array.from(atob(data.base64), (c) => c.charCodeAt(0))], {
|
|
255
|
+
type: data.mime,
|
|
256
|
+
});
|
|
257
|
+
} else {
|
|
258
|
+
value = data.value;
|
|
259
|
+
}
|
|
260
|
+
assetValueCache.set(ak, { __esModule: true, default: value });
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Dep-unit walk. Dep CJS text carries require literals, so
|
|
267
|
+
// edges are derived from them (same literal-call shape the
|
|
268
|
+
// CJS transform emits). A require that resolves to nothing
|
|
269
|
+
// is skipped here — it fails loudly at eval time if actually
|
|
270
|
+
// required, which is the correct (non-wedging) behavior for
|
|
271
|
+
// both genuine misses and string-literal false positives.
|
|
272
|
+
function extractRequires(code) {
|
|
273
|
+
const out = [];
|
|
274
|
+
const re = /require\("([^"]+)"\)/g;
|
|
275
|
+
let m;
|
|
276
|
+
while ((m = re.exec(code)) !== null) out.push(m[1]);
|
|
277
|
+
return out;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function ensureDepText(depUrl, visiting = new Set()) {
|
|
281
|
+
const key = depKeyFromUrl(depUrl);
|
|
282
|
+
if (depText.has(key) || visiting.has(key)) return;
|
|
283
|
+
visiting.add(key);
|
|
284
|
+
const u = new URL(depUrl, "http://dev");
|
|
285
|
+
const data = await fetchJson(
|
|
286
|
+
`/@jundle/runner-dep${u.pathname.slice("/@jundle/deps".length)}${u.search}`,
|
|
287
|
+
);
|
|
288
|
+
depText.set(key, data);
|
|
289
|
+
for (const spec of extractRequires(data.code)) {
|
|
290
|
+
if (spec.startsWith("/@jundle/deps/")) {
|
|
291
|
+
await ensureDepText(spec, visiting);
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (spec.startsWith("./") || spec.startsWith("../")) {
|
|
295
|
+
const slash = key.indexOf("\0");
|
|
296
|
+
if (slash !== -1) {
|
|
297
|
+
const bundleEntry = key.slice(0, slash);
|
|
298
|
+
const rel = key.slice(slash + 1);
|
|
299
|
+
const normalized = pathNs.posix.normalize(
|
|
300
|
+
pathNs.posix.join(pathNs.posix.dirname(rel), spec),
|
|
301
|
+
);
|
|
302
|
+
if (data.modules && data.modules.includes(normalized)) {
|
|
303
|
+
await ensureDepText(`/@jundle/deps/${bundleEntry}/${normalized}`, visiting);
|
|
304
|
+
} else {
|
|
305
|
+
const base = normalized.split("/").pop();
|
|
306
|
+
await ensureDepText(`/@jundle/deps/${base}`, visiting).catch(() => {});
|
|
307
|
+
}
|
|
308
|
+
} else {
|
|
309
|
+
await ensureDepText(`/@jundle/deps/${spec.slice(2).split("?")[0]}`, visiting).catch(
|
|
310
|
+
() => {},
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Sync evaluation (mirrors NodeRunner.evaluate, including the
|
|
318
|
+
// record-then-swap failure rule: circulars see partial
|
|
319
|
+
// exports during evaluation; on failure the record becomes
|
|
320
|
+
// FAILED (not half-populated), so the next request retries
|
|
321
|
+
// instead of serving a partial module).
|
|
322
|
+
function evaluateDev(key) {
|
|
323
|
+
const existing = evaluated.get(key);
|
|
324
|
+
if (existing && !existing.failed) return existing;
|
|
325
|
+
const text = textCache.get(key);
|
|
326
|
+
if (!text)
|
|
327
|
+
throw new Error(
|
|
328
|
+
`ModuleRunner: module '${key}' was not pre-fetched (the closure walk missed it); use a static, literal specifier so the walk pre-fetches it; dynamic specifiers are not pre-fetched.`,
|
|
329
|
+
);
|
|
330
|
+
const record = { exports: {}, failed: false };
|
|
331
|
+
evaluated.set(key, record);
|
|
332
|
+
const module = { exports: record.exports };
|
|
333
|
+
try {
|
|
334
|
+
if (!evalBinding)
|
|
335
|
+
throw new Error(
|
|
336
|
+
"ModuleRunner: unsafe-eval binding missing (Miniflare did not provide __jundle_runner_eval).",
|
|
337
|
+
);
|
|
338
|
+
const fn = evalBinding.eval(
|
|
339
|
+
"(function(require, module, exports, __filename, __dirname) {\n" +
|
|
340
|
+
text.code +
|
|
341
|
+
"\n})\n//# sourceURL=" +
|
|
342
|
+
(key.split("\0")[1] || key),
|
|
343
|
+
);
|
|
344
|
+
fn((spec) => requireHook(spec, key), module, module.exports, text.filename, text.dirname);
|
|
345
|
+
} catch (e) {
|
|
346
|
+
evaluated.set(key, { exports: {}, failed: true, error: e });
|
|
347
|
+
throw e;
|
|
348
|
+
}
|
|
349
|
+
record.exports = module.exports;
|
|
350
|
+
return record;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function evaluateDep(depUrl) {
|
|
354
|
+
const key = depKeyFromUrl(depUrl);
|
|
355
|
+
const existing = depEvaluated.get(key);
|
|
356
|
+
if (existing && !existing.failed) return existing;
|
|
357
|
+
const text = depText.get(key);
|
|
358
|
+
if (!text)
|
|
359
|
+
throw new Error(
|
|
360
|
+
`ModuleRunner: dep bundle '${key}' was not pre-fetched (the closure walk missed it).`,
|
|
361
|
+
);
|
|
362
|
+
// Bundle-dir entry redirect (NodeRunner parity: an entry
|
|
363
|
+
// with an entryRel evaluates through the bundle path).
|
|
364
|
+
if (!key.includes("\0") && text.entryRel)
|
|
365
|
+
return evaluateDep(`/@jundle/deps/${key}/${text.entryRel}`);
|
|
366
|
+
const record = { exports: {}, failed: false };
|
|
367
|
+
depEvaluated.set(key, record);
|
|
368
|
+
const module = { exports: record.exports };
|
|
369
|
+
const slash = key.indexOf("\0");
|
|
370
|
+
const rel = slash === -1 ? key : key.slice(slash + 1);
|
|
371
|
+
try {
|
|
372
|
+
if (!evalBinding)
|
|
373
|
+
throw new Error(
|
|
374
|
+
"ModuleRunner: unsafe-eval binding missing (Miniflare did not provide __jundle_runner_eval).",
|
|
375
|
+
);
|
|
376
|
+
const fn = evalBinding.eval(
|
|
377
|
+
"(function(require, module, exports, __filename, __dirname) {\n" +
|
|
378
|
+
text.code +
|
|
379
|
+
"\n})\n//# sourceURL=" +
|
|
380
|
+
rel,
|
|
381
|
+
);
|
|
382
|
+
fn((spec) => depRequireHook(spec, key, text), module, module.exports, rel, "");
|
|
383
|
+
} catch (e) {
|
|
384
|
+
depEvaluated.set(key, { exports: {}, failed: true, error: e });
|
|
385
|
+
throw e;
|
|
386
|
+
}
|
|
387
|
+
record.exports = module.exports;
|
|
388
|
+
return record;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function depRequireHook(spec, depKey, depTextRec) {
|
|
392
|
+
if (spec.startsWith("/@jundle/deps/")) return evaluateDep(spec).exports;
|
|
393
|
+
// Workerd provides node: builtins (via the harness's
|
|
394
|
+
// static imports) and cloudflare:workers to any code it
|
|
395
|
+
// runs, including dep bundles — so the dep hook serves
|
|
396
|
+
// them, exactly as the workerd bundle's static imports
|
|
397
|
+
// receive them. (NodeRunner's dep hook fails these; that
|
|
398
|
+
// is a Node-dev gap, not a runner gap.)
|
|
399
|
+
if (spec.startsWith("node:")) {
|
|
400
|
+
if (BUILTINS[spec]) return BUILTINS[spec];
|
|
401
|
+
throw new Error(
|
|
402
|
+
`ModuleRunner: dep bundle '${depKey}' requires unsupported Node builtin '${spec}'.`,
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
if (spec === "cloudflare:workers") return cwNs;
|
|
406
|
+
if (spec.startsWith("./") || spec.startsWith("../")) {
|
|
407
|
+
const slash = depKey.indexOf("\0");
|
|
408
|
+
if (slash !== -1) {
|
|
409
|
+
const bundleEntry = depKey.slice(0, slash);
|
|
410
|
+
const rel = depKey.slice(slash + 1);
|
|
411
|
+
const normalized = pathNs.posix.normalize(pathNs.posix.join(pathNs.posix.dirname(rel), spec));
|
|
412
|
+
if (depTextRec.modules && depTextRec.modules.includes(normalized))
|
|
413
|
+
return evaluateDep(`/@jundle/deps/${bundleEntry}/${normalized}`).exports;
|
|
414
|
+
const base = normalized.split("/").pop();
|
|
415
|
+
if (depText.has(base)) return evaluateDep(`/@jundle/deps/${base}`).exports;
|
|
416
|
+
throw new Error(
|
|
417
|
+
`ModuleRunner: dep bundle '${depKey}' requires unexpected specifier '${spec}' (dep bundles only import sibling dep files).`,
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
return evaluateDep(`/@jundle/deps/${spec.slice(2).split("?")[0]}`).exports;
|
|
421
|
+
}
|
|
422
|
+
throw new Error(
|
|
423
|
+
`ModuleRunner: dep bundle '${depKey}' requires unexpected specifier '${spec}' (dep bundles only import sibling dep files).`,
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Sync require hook for evaluated app modules (mirrors
|
|
428
|
+
// NodeRunner.requireHook, minus the native-external path:
|
|
429
|
+
// the worker has no native import, so bare non-node specs
|
|
430
|
+
// fail loudly instead of loading natively).
|
|
431
|
+
function requireHook(spec, importerKey) {
|
|
432
|
+
if (spec.startsWith("/@jundle/module?")) {
|
|
433
|
+
return evaluateDev(devKey(spec)).exports;
|
|
434
|
+
}
|
|
435
|
+
if (spec.startsWith("/@jundle/deps/")) {
|
|
436
|
+
return evaluateDep(spec).exports;
|
|
437
|
+
}
|
|
438
|
+
if (spec.startsWith("/@jundle/json?")) {
|
|
439
|
+
const key = devKey(spec);
|
|
440
|
+
if (!jsonCache.has(key))
|
|
441
|
+
throw new Error(`ModuleRunner: JSON module '${spec}' was not pre-fetched.`);
|
|
442
|
+
return { __esModule: true, default: jsonCache.get(key) };
|
|
443
|
+
}
|
|
444
|
+
if (spec.startsWith("/@jundle/asset-value?")) {
|
|
445
|
+
const key = devAssetKey(spec);
|
|
446
|
+
if (!assetValueCache.has(key))
|
|
447
|
+
throw new Error(`ModuleRunner: asset value '${spec}' was not pre-fetched.`);
|
|
448
|
+
return assetValueCache.get(key);
|
|
449
|
+
}
|
|
450
|
+
if (spec.startsWith("/@jundle/css?")) return {};
|
|
451
|
+
if (spec === "jundle:dev") return hostContext;
|
|
452
|
+
if (spec === "cloudflare:workers") return cwNs;
|
|
453
|
+
if (spec.startsWith("node:")) {
|
|
454
|
+
if (BUILTINS[spec]) return BUILTINS[spec];
|
|
455
|
+
throw new Error(
|
|
456
|
+
`ModuleRunner: Node builtin '${spec}' is not available in workerd dev (unsupported by this workerd's nodejs_compat).`,
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
// JSON requires via the importer's server-computed edges
|
|
460
|
+
// (literal require() calls resolving to JSON): the value
|
|
461
|
+
// directly, not the namespace (NodeRunner require-json
|
|
462
|
+
// parity).
|
|
463
|
+
const importerText = importerKey ? textCache.get(importerKey) : null;
|
|
464
|
+
if (importerText) {
|
|
465
|
+
for (const edge of importerText.edges || []) {
|
|
466
|
+
if (edge.kind === "json" && edge.spec === spec) {
|
|
467
|
+
const key = devKey(edge.url);
|
|
468
|
+
if (!jsonCache.has(key))
|
|
469
|
+
throw new Error(`ModuleRunner: JSON module '${spec}' was not pre-fetched.`);
|
|
470
|
+
return jsonCache.get(key);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
// Relative JSON requires (NodeRunner parity for the common
|
|
475
|
+
// case): the server pre-fetches the value via the
|
|
476
|
+
// importer's requireJsonFiles edges, and the hook resolves
|
|
477
|
+
// the relative spec against the importer file. Both the
|
|
478
|
+
// exact name and the +.json form are tried (NodeRunner
|
|
479
|
+
// resolves "./data" to data.json the same way). Bare
|
|
480
|
+
// JSON requires are NOT resolved here (no resolver in
|
|
481
|
+
// the worker) — they fall through to the loud error
|
|
482
|
+
// below; use an import for those.
|
|
483
|
+
if (
|
|
484
|
+
importerKey &&
|
|
485
|
+
typeof spec === "string" &&
|
|
486
|
+
(spec.startsWith("./") || spec.startsWith("../")) &&
|
|
487
|
+
!spec.startsWith("/@jundle/")
|
|
488
|
+
) {
|
|
489
|
+
const parts = importerKey.split("\0");
|
|
490
|
+
const importerFile = parts[1] || "";
|
|
491
|
+
const importerEnv = parts[0] || "";
|
|
492
|
+
const normalized = pathNs.posix.normalize(
|
|
493
|
+
pathNs.posix.join(pathNs.posix.dirname(importerFile), spec),
|
|
494
|
+
);
|
|
495
|
+
for (const candidate of [normalized, `${normalized}.json`]) {
|
|
496
|
+
const key = `${importerEnv}\0${candidate}`;
|
|
497
|
+
if (jsonCache.has(key)) return jsonCache.get(key);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
throw new Error(
|
|
501
|
+
`ModuleRunner: '${spec}' was not pre-loaded (the closure walk missed it); use a static, literal specifier for this import so the walk pre-loads it; dynamic specifiers are not pre-loaded.`,
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Host context (the harness side of jundle:dev, mirroring
|
|
506
|
+
// NodeRunner's hostContext: manifests are fetched per
|
|
507
|
+
// dispatch (after import, so they are fresh); importModule
|
|
508
|
+
// resolves through the dev server so absolute manifest file
|
|
509
|
+
// paths map to canonical dev URLs; loadChunk parses the
|
|
510
|
+
// dev-URL chunk id directly.
|
|
511
|
+
const hostContext = {
|
|
512
|
+
getManifests: () => {
|
|
513
|
+
return (
|
|
514
|
+
manifestsCache || { clientManifest: {}, serverManifest: {}, clientEntry: [], clientCss: [] }
|
|
515
|
+
);
|
|
516
|
+
},
|
|
517
|
+
importModule: async (file, env) => {
|
|
518
|
+
const resolved = await fetchJson(
|
|
519
|
+
`/@jundle/runner-resolve?env=${encodeURIComponent(env)}&file=${encodeURIComponent(file)}`,
|
|
520
|
+
);
|
|
521
|
+
return (await ensureModuleByUrl(resolved.url)).exports;
|
|
522
|
+
},
|
|
523
|
+
loadChunk: async (chunkId) => {
|
|
524
|
+
const u = new URL(chunkId, "http://dev");
|
|
525
|
+
if (u.pathname !== "/@jundle/module")
|
|
526
|
+
throw new Error(`ModuleRunner: refusing to load chunk (not a dev module URL): ${chunkId}`);
|
|
527
|
+
const url = `/@jundle/module?env=${encodeURIComponent(u.searchParams.get("env") || "react-client")}&file=${encodeURIComponent(u.searchParams.get("file") || "")}`;
|
|
528
|
+
return (await ensureModuleByUrl(url)).exports;
|
|
529
|
+
},
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
async function ensureModuleByUrl(url) {
|
|
533
|
+
await ensureText(url, new Set());
|
|
534
|
+
return evaluateDev(devKey(url));
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Entry import for wrapper classes (DO / Workflow /
|
|
538
|
+
// entrypoint methods run during dispatch, but an alarm can
|
|
539
|
+
// fire outside any dispatch): always version-check first,
|
|
540
|
+
// so a lazily-created real instance never uses a stale
|
|
541
|
+
// class after an edit.
|
|
542
|
+
async function __jundleImportEntry() {
|
|
543
|
+
await ensureEvalShims();
|
|
544
|
+
await syncVersion();
|
|
545
|
+
const rec = await ensureModuleByUrl(entryUrl);
|
|
546
|
+
return rec.exports;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Eval-realm global shims (SPEC §16.11): this workerd
|
|
550
|
+
// provides no WeakRef / FinalizationRegistry, but React's
|
|
551
|
+
// Flight client constructs a WeakRef per response (and
|
|
552
|
+
// other deps may use FinalizationRegistry). Install
|
|
553
|
+
// dev-only shims in the EVAL realm (via the binding —
|
|
554
|
+
// the eval realm may or may not share the worker's
|
|
555
|
+
// global, so assigning from the harness is not enough)
|
|
556
|
+
// before any module evaluates, and only when missing
|
|
557
|
+
// (a future workerd with real WeakRef keeps its own).
|
|
558
|
+
// WeakRef holds strongly: safe here, because React pairs
|
|
559
|
+
// it with a strong `response` field it nulls when done,
|
|
560
|
+
// and the shim reference itself is a collectable cycle.
|
|
561
|
+
// The FinalizationRegistry shim never fires callbacks:
|
|
562
|
+
// finalization is non-deterministic by spec, so dev code
|
|
563
|
+
// must not depend on it firing.
|
|
564
|
+
let evalShimsInstalled = false;
|
|
565
|
+
async function ensureEvalShims() {
|
|
566
|
+
if (evalShimsInstalled || !evalBinding) return;
|
|
567
|
+
evalShimsInstalled = true;
|
|
568
|
+
const missing = evalBinding.eval(
|
|
569
|
+
"typeof WeakRef === 'undefined' || typeof FinalizationRegistry === 'undefined'",
|
|
570
|
+
);
|
|
571
|
+
if (!missing) return;
|
|
572
|
+
evalBinding.eval(
|
|
573
|
+
"if (typeof WeakRef === 'undefined') { globalThis.WeakRef = class WeakRef { constructor(t) { this._t = t; } deref() { return this._t; } }; }" +
|
|
574
|
+
"if (typeof FinalizationRegistry === 'undefined') { globalThis.FinalizationRegistry = class FinalizationRegistry { constructor() {} register() {} unregister() { return false; } }; }",
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Per-dispatch preamble, shared by fetch / scheduled /
|
|
579
|
+
// queue: bind the internal bindings, pull invalidation,
|
|
580
|
+
// (re-)import the entry, refresh manifests on success.
|
|
581
|
+
// Entry failures keep the last-good entry for dispatch
|
|
582
|
+
// (fetch only — scheduled/queue throw, they have no page
|
|
583
|
+
// to serve stale); manifests stay at their previous
|
|
584
|
+
// version on entry failure, so a stale entry never renders
|
|
585
|
+
// against fresh manifests.
|
|
586
|
+
async function prepareDispatch(env) {
|
|
587
|
+
evalBinding = env[EVAL_NAME];
|
|
588
|
+
baseUrl = env[BASE_NAME] || "";
|
|
589
|
+
entryUrl = env[ENTRY_NAME] || "";
|
|
590
|
+
if (!evalBinding)
|
|
591
|
+
throw new Error(
|
|
592
|
+
"ModuleRunner: unsafe-eval binding missing (Miniflare did not provide __jundle_runner_eval).",
|
|
593
|
+
);
|
|
594
|
+
// Dev-only global eval polyfill (SPEC §16.11): workerd bans
|
|
595
|
+
// codegen from strings at request time, so app code calling
|
|
596
|
+
// eval (notably React's dev Flight client, which warns and
|
|
597
|
+
// loses its stack-reconstruction features without it) would
|
|
598
|
+
// throw. The harness replaces the global eval with a
|
|
599
|
+
// binding-backed one before any app module evaluates: bare
|
|
600
|
+
// `eval` (including React's indirect `(0, eval)`) resolves
|
|
601
|
+
// to this, and the binding evaluates the string as a script
|
|
602
|
+
// in the worker's global scope — the same semantics as
|
|
603
|
+
// Node's indirect eval. Non-string arguments return as-is,
|
|
604
|
+
// per spec. Dev-only by construction: the binding never
|
|
605
|
+
// exists in production, so prod workerd still throws — and
|
|
606
|
+
// user code calling eval therefore works in runner dev but
|
|
607
|
+
// throws in production, a deliberate dev/prod gap stated
|
|
608
|
+
// in SPEC, not hidden.
|
|
609
|
+
// eslint-disable-next-line no-eval
|
|
610
|
+
globalThis.eval = (code) => (typeof code === "string" ? evalBinding.eval(code) : code);
|
|
611
|
+
if (!baseUrl)
|
|
612
|
+
throw new Error(`ModuleRunner: dev-server base URL missing (binding ${BASE_NAME} not set).`);
|
|
613
|
+
if (!entryUrl)
|
|
614
|
+
throw new Error(`ModuleRunner: entry URL missing (binding ${ENTRY_NAME} not set).`);
|
|
615
|
+
await ensureEvalShims();
|
|
616
|
+
await syncVersion();
|
|
617
|
+
let entryError = null;
|
|
618
|
+
let entry = null;
|
|
619
|
+
try {
|
|
620
|
+
const rec = await ensureModuleByUrl(entryUrl);
|
|
621
|
+
entry = rec.exports;
|
|
622
|
+
lastGoodEntry = entry;
|
|
623
|
+
} catch (e) {
|
|
624
|
+
entryError = e;
|
|
625
|
+
entry = lastGoodEntry;
|
|
626
|
+
}
|
|
627
|
+
if (entry && !entryError) {
|
|
628
|
+
manifestsCache = await fetchJson("/@jundle/runner-manifests");
|
|
629
|
+
}
|
|
630
|
+
return { entry, entryError };
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function errorHeaderValue(e) {
|
|
634
|
+
return String((e && e.message) || e)
|
|
635
|
+
.replace(/[\r\n]+/g, " ")
|
|
636
|
+
.slice(0, 500);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function entryAppOf(entry) {
|
|
640
|
+
return entry ? entry.default || entry.app : null;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
export default {
|
|
644
|
+
async fetch(request, env, ctx) {
|
|
645
|
+
try {
|
|
646
|
+
const { entry, entryError } = await prepareDispatch(env);
|
|
647
|
+
if (!entry)
|
|
648
|
+
// No last-good entry: surface the original import
|
|
649
|
+
// failure, not a generic message, so the overlay
|
|
650
|
+
// and the 500 body name the real cause.
|
|
651
|
+
throw (
|
|
652
|
+
entryError ||
|
|
653
|
+
new Error(
|
|
654
|
+
"ModuleRunner: no entry available (first import failed and no last-good entry exists).",
|
|
655
|
+
)
|
|
656
|
+
);
|
|
657
|
+
const app = entryAppOf(entry);
|
|
658
|
+
if (!app || typeof app.fetch !== "function")
|
|
659
|
+
throw new Error("ModuleRunner: entry's default export must be an app with a fetch method.");
|
|
660
|
+
const res = await app.fetch(request, stripEnv(env), ctx);
|
|
661
|
+
// Only entry errors set the runner-error header (the
|
|
662
|
+
// dev server broadcasts it to the overlay and strips
|
|
663
|
+
// it before returning). Push failures are silent
|
|
664
|
+
// keep-last-good (see pushSsrUpdates) and set no
|
|
665
|
+
// header: the entry itself is fine and the render
|
|
666
|
+
// keeps the last-good instance.
|
|
667
|
+
const headerError = entryError;
|
|
668
|
+
if (headerError) {
|
|
669
|
+
const headers = new Headers(res.headers);
|
|
670
|
+
headers.set("x-jundle-runner-error", errorHeaderValue(headerError));
|
|
671
|
+
return new Response(res.body, { status: res.status, headers });
|
|
672
|
+
}
|
|
673
|
+
return res;
|
|
674
|
+
} catch (e) {
|
|
675
|
+
return new Response(
|
|
676
|
+
"ModuleRunner: " +
|
|
677
|
+
(e && e.message ? e.message : String(e)) +
|
|
678
|
+
(e && e.stack ? "\n" + e.stack : ""),
|
|
679
|
+
{
|
|
680
|
+
status: 500,
|
|
681
|
+
headers: { "x-jundle-runner-error": errorHeaderValue(e), "content-type": "text/plain" },
|
|
682
|
+
},
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
},
|
|
686
|
+
async scheduled(controller, env, ctx) {
|
|
687
|
+
const { entry, entryError } = await prepareDispatch(env);
|
|
688
|
+
if (entryError) throw entryError;
|
|
689
|
+
if (!entry) throw new Error("ModuleRunner: no entry available.");
|
|
690
|
+
const app = entryAppOf(entry);
|
|
691
|
+
const handler = app && app.scheduled;
|
|
692
|
+
if (typeof handler !== "function")
|
|
693
|
+
throw new Error("ModuleRunner: entry has no scheduled handler.");
|
|
694
|
+
return handler(controller, stripEnv(env), ctx);
|
|
695
|
+
},
|
|
696
|
+
async queue(batch, env, ctx) {
|
|
697
|
+
const { entry, entryError } = await prepareDispatch(env);
|
|
698
|
+
if (entryError) throw entryError;
|
|
699
|
+
if (!entry) throw new Error("ModuleRunner: no entry available.");
|
|
700
|
+
const app = entryAppOf(entry);
|
|
701
|
+
const handler = app && app.queue;
|
|
702
|
+
if (typeof handler !== "function") throw new Error("ModuleRunner: entry has no queue handler.");
|
|
703
|
+
return handler(batch, stripEnv(env), ctx);
|
|
704
|
+
},
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
// __JUNDLE_WRAPPERS__
|