@jundle/cloudflare 0.0.1-14 → 0.0.1-16

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/harness.js +90 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jundle/cloudflare",
3
- "version": "0.0.1-14",
3
+ "version": "0.0.1-16",
4
4
  "description": "Cloudflare Workers runtime for the jundle dev server (workerd via Miniflare).",
5
5
  "license": "ISC",
6
6
  "type": "module",
package/src/harness.js CHANGED
@@ -98,6 +98,28 @@ const evaluated = new Map();
98
98
  const depText = new Map();
99
99
  // Evaluated dep bundles: key -> {exports, failed, error}.
100
100
  const depEvaluated = new Map();
101
+ // Serializes dep-text walks (FIFO promise queue):
102
+ // concurrent chunk loads walk the same shared bundles,
103
+ // and ensureDepText's "already fetched" early-return
104
+ // does not wait for an in-flight subtree walk — a
105
+ // second walk can see a bundle's text present, skip its
106
+ // subtree, and evaluate it while the first walk is
107
+ // still fetching its transitive deps ("was not
108
+ // pre-fetched", non-deterministic in which bundle). One
109
+ // walk at a time closes the race; the recursion inside
110
+ // a walk never re-enters the queue, so it cannot
111
+ // deadlock. A failed walk rejects to its caller but
112
+ // resolves the queue slot, so it never poisons later
113
+ // walks.
114
+ let depWalkQueue = Promise.resolve();
115
+ function runDepWalk(fn) {
116
+ const run = depWalkQueue.then(fn);
117
+ depWalkQueue = run.then(
118
+ () => {},
119
+ () => {},
120
+ );
121
+ return run;
122
+ }
101
123
  // Parsed JSON values: key `${env}\0${file}` -> value.
102
124
  const jsonCache = new Map();
103
125
  // Prefetched asset values: key `${env}\0${file}\0${type}` ->
@@ -220,16 +242,50 @@ async function syncVersion() {
220
242
  // document in a worker), and surfacing it would trip the
221
243
  // hot client's first-update overlay rule and force a full
222
244
  // reload on the next update instead of HMR.
245
+ //
246
+ // The re-import can install a second (browser) shim over
247
+ // the dev shim's globals — client.browser.js imports
248
+ // ./shim.js first, so any module importing the client
249
+ // runtime installs it as a side effect — and the
250
+ // client's chunkCache skips re-loading chunks, so a
251
+ // poisoned global makes the next render require through
252
+ // an empty shim ("has not been loaded yet"). Snapshot the
253
+ // four shim globals before the re-import and restore
254
+ // them before the push lands (finally, so a failed
255
+ // import cannot leave them poisoned). A snapshot of
256
+ // undefined restores by deleting the global, not by
257
+ // assigning undefined (the shim was not installed yet).
258
+ const SHIM_GLOBALS = [
259
+ "__webpack_require__",
260
+ "__webpack_chunk_load__",
261
+ "__webpack_get_script_filename__",
262
+ "__jundleFlight",
263
+ ];
264
+ function snapshotShimGlobals() {
265
+ return SHIM_GLOBALS.map((name) => [name, globalThis[name]]);
266
+ }
267
+ function restoreShimGlobals(saved) {
268
+ for (const [name, value] of saved) {
269
+ if (value === undefined) delete globalThis[name];
270
+ else globalThis[name] = value;
271
+ }
272
+ }
223
273
  async function pushSsrUpdates(clientKeys) {
224
274
  for (const key of clientKeys) {
225
275
  const [env, file] = key.split("\0");
226
276
  const url = `/@jundle/module?env=${encodeURIComponent(env)}&file=${encodeURIComponent(file)}`;
277
+ const saved = snapshotShimGlobals();
278
+ let rec;
279
+ let ok = false;
227
280
  try {
228
- const rec = await ensureModuleByUrl(url);
229
- globalThis.__jundleFlight?.updateModule?.(url, rec.exports);
281
+ rec = await ensureModuleByUrl(url);
282
+ ok = true;
230
283
  } catch (e) {
231
284
  console.error(`[jundle runner] SSR push ${url}: ${e && e.message ? e.message : String(e)}`);
285
+ } finally {
286
+ restoreShimGlobals(saved);
232
287
  }
288
+ if (ok) globalThis.__jundleFlight?.updateModule?.(url, rec.exports);
233
289
  }
234
290
  }
235
291
 
@@ -264,7 +320,12 @@ async function ensureText(url, visiting = new Set()) {
264
320
  if (edge.kind === "dev" || edge.kind === "client-ref" || edge.kind === "server-action") {
265
321
  await ensureText(edge.url, visiting);
266
322
  } else if (edge.kind === "dep") {
267
- await ensureDepText(edge.url, new Set());
323
+ // Serialized through the dep walk queue: two
324
+ // concurrent dispatches walk the same shared
325
+ // bundles, and an unserialized walk can evaluate a
326
+ // bundle whose subtree another walk is still
327
+ // fetching (same race as loadChunk, below).
328
+ await runDepWalk(() => ensureDepText(edge.url, new Set()));
268
329
  } else if (edge.kind === "json") {
269
330
  const jk = devKey(edge.url);
270
331
  if (!jsonCache.has(jk)) {
@@ -529,6 +590,16 @@ function requireHook(spec, importerKey) {
529
590
  const key = `${importerEnv}\0${candidate}`;
530
591
  if (jsonCache.has(key)) return jsonCache.get(key);
531
592
  }
593
+ // Literal dynamic imports (import("./route")) compile
594
+ // to a require of the raw relative spec (the harness
595
+ // has no async __jundleImport), and the closure walk
596
+ // pre-fetches them via their dev edges — so resolve
597
+ // the spec the same way and evaluate the pre-fetched
598
+ // module. A spec with no pre-fetched text falls
599
+ // through to the loud error below (a truly-dynamic
600
+ // specifier has no static target to pre-fetch).
601
+ const devModuleKey = `${importerEnv}\0${normalized}`;
602
+ if (textCache.has(devModuleKey)) return evaluateDev(devModuleKey).exports;
532
603
  }
533
604
  throw new Error(
534
605
  `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.`,
@@ -554,6 +625,22 @@ const hostContext = {
554
625
  return (await ensureModuleByUrl(resolved.url)).exports;
555
626
  },
556
627
  loadChunk: async (chunkId) => {
628
+ // Dep chunk ids (the Flight client preloads dep
629
+ // client components by their bundle URL): fetch the
630
+ // dep text on demand — the closure walk does not
631
+ // pre-fetch dep client-ref bundles (their client-ref
632
+ // edges carry a prebundleSpec and are omitted) — then
633
+ // evaluate it, mirroring NodeRunner's hostContext.
634
+ // Serialized through the dep walk queue: concurrent
635
+ // chunk loads share transitive bundles, and an
636
+ // unserialized walk can evaluate a bundle whose
637
+ // subtree another walk is still fetching.
638
+ if (chunkId.startsWith("/@jundle/deps/")) {
639
+ return runDepWalk(async () => {
640
+ await ensureDepText(chunkId);
641
+ return evaluateDep(chunkId).exports;
642
+ });
643
+ }
557
644
  const u = new URL(chunkId, "http://dev");
558
645
  if (u.pathname !== "/@jundle/module")
559
646
  throw new Error(`ModuleRunner: refusing to load chunk (not a dev module URL): ${chunkId}`);