@thomas.f/prnext 0.1.18-alpha → 0.1.19-alpha

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 (90) hide show
  1. package/README.md +2 -0
  2. package/build/actions.mjs +26 -14
  3. package/build/app.mjs +46 -25
  4. package/build/compiler.mjs +5 -4
  5. package/build/config.mjs +4 -2
  6. package/build/dev-css-assets.mjs +173 -0
  7. package/build/dev-css-update.mjs +14 -12
  8. package/build/dev-lazy-build.mjs +77 -0
  9. package/build/dynamic.mjs +13 -6
  10. package/build/incremental.mjs +112 -18
  11. package/build/index.mjs +32 -3
  12. package/build/lazy-dynamic-imports.mjs +137 -0
  13. package/build/migrate.mjs +4 -4
  14. package/build/migration-audit.mjs +19 -31
  15. package/build/migration-imports.mjs +245 -0
  16. package/build/migration-scenarios-runner.mjs +73 -0
  17. package/build/migration-scenarios.mjs +72 -0
  18. package/build/native-discovery.mjs +65 -5
  19. package/build/native-packages.mjs +2 -0
  20. package/build/npm-packages.mjs +1 -0
  21. package/build/production-builder.mjs +1 -1
  22. package/build/standalone.mjs +21 -3
  23. package/build/webpack-graph.mjs +13 -3
  24. package/cli.mjs +10 -1
  25. package/compat/app-dynamic.cjs +12 -1
  26. package/compat/cache-handlers.cjs +3 -1
  27. package/compat/flight-delta.cjs +3 -1
  28. package/compat/index.d.ts +1 -1
  29. package/compat/link.cjs +2 -2
  30. package/docs/ai/compatibility.md +77 -0
  31. package/docs/ai/routing.md +11 -0
  32. package/docs/ai/troubleshooting.md +79 -0
  33. package/package.json +21 -9
  34. package/runtime/app-client.mjs +119 -42
  35. package/runtime/atomic-file.mjs +27 -0
  36. package/runtime/cache-components-rsc.mjs +3 -1
  37. package/runtime/dev-client.mjs +7 -2
  38. package/runtime/dev-file-changes.mjs +123 -8
  39. package/runtime/dev-lazy-imports.mjs +121 -0
  40. package/runtime/dev-lazy-staging.mjs +14 -0
  41. package/runtime/dev-style-retention.mjs +15 -0
  42. package/runtime/development.mjs +60 -19
  43. package/runtime/flight-delta.mjs +28 -4
  44. package/runtime/fs.mjs +15 -8
  45. package/runtime/lazy-import-client.mjs +38 -0
  46. package/vendor/nft/LICENSE +7 -0
  47. package/vendor/nft/README.md +16 -0
  48. package/vendor/nft/out/analyze.d.ts +8 -0
  49. package/vendor/nft/out/analyze.js +1216 -0
  50. package/vendor/nft/out/analyze.js.map +1 -0
  51. package/vendor/nft/out/cli.d.ts +2 -0
  52. package/vendor/nft/out/cli.js +94 -0
  53. package/vendor/nft/out/cli.js.map +1 -0
  54. package/vendor/nft/out/fs.d.ts +22 -0
  55. package/vendor/nft/out/fs.js +106 -0
  56. package/vendor/nft/out/fs.js.map +1 -0
  57. package/vendor/nft/out/index.d.ts +4 -0
  58. package/vendor/nft/out/index.js +25 -0
  59. package/vendor/nft/out/index.js.map +1 -0
  60. package/vendor/nft/out/node-file-trace.d.ts +41 -0
  61. package/vendor/nft/out/node-file-trace.js +380 -0
  62. package/vendor/nft/out/node-file-trace.js.map +1 -0
  63. package/vendor/nft/out/resolve-dependency.d.ts +6 -0
  64. package/vendor/nft/out/resolve-dependency.js +427 -0
  65. package/vendor/nft/out/resolve-dependency.js.map +1 -0
  66. package/vendor/nft/out/types.d.ts +66 -0
  67. package/vendor/nft/out/types.js +2 -0
  68. package/vendor/nft/out/types.js.map +1 -0
  69. package/vendor/nft/out/utils/ast-helpers.js +49 -0
  70. package/vendor/nft/out/utils/ast-helpers.js.map +1 -0
  71. package/vendor/nft/out/utils/binary-locators.js +88 -0
  72. package/vendor/nft/out/utils/binary-locators.js.map +1 -0
  73. package/vendor/nft/out/utils/get-package-base.js +30 -0
  74. package/vendor/nft/out/utils/get-package-base.js.map +1 -0
  75. package/vendor/nft/out/utils/interop-require.js +24 -0
  76. package/vendor/nft/out/utils/interop-require.js.map +1 -0
  77. package/vendor/nft/out/utils/node-version.js +10 -0
  78. package/vendor/nft/out/utils/node-version.js.map +1 -0
  79. package/vendor/nft/out/utils/sharedlib-emit.js +33 -0
  80. package/vendor/nft/out/utils/sharedlib-emit.js.map +1 -0
  81. package/vendor/nft/out/utils/special-cases.js +347 -0
  82. package/vendor/nft/out/utils/special-cases.js.map +1 -0
  83. package/vendor/nft/out/utils/static-eval.js +579 -0
  84. package/vendor/nft/out/utils/static-eval.js.map +1 -0
  85. package/vendor/nft/out/utils/types.js +2 -0
  86. package/vendor/nft/out/utils/types.js.map +1 -0
  87. package/vendor/nft/out/utils/wrappers.js +659 -0
  88. package/vendor/nft/out/utils/wrappers.js.map +1 -0
  89. package/vendor/nft/package.json +4 -0
  90. package/vendor/nft/upstream.json +51 -0
@@ -39,7 +39,9 @@ function loadClientModule(source) {
39
39
  // supplies ESM import functions so React and application contexts stay shared.
40
40
  export function installFlightModuleLoader(clientModules) {
41
41
  const loaded = new Map();
42
+ const loadedVersions = new Map();
42
43
  const pending = new Map();
44
+ let version = 0;
43
45
  const previousRequire = globalThis.__webpack_require__;
44
46
  const previousLoad = globalThis.__webpack_chunk_load__;
45
47
  globalThis.__webpack_require__ = id => {
@@ -52,32 +54,42 @@ export function installFlightModuleLoader(clientModules) {
52
54
  return typeof source === 'string' ? source : source?.browserModule || String(id);
53
55
  };
54
56
  globalThis.__webpack_get_script_filename__ = id => globalThis.__webpack_require__.u(id);
55
- globalThis.__webpack_chunk_load__ = id => {
56
- if (loaded.has(id)) return Promise.resolve();
57
+ function loadChunk(id) {
57
58
  if (pending.has(id)) return pending.get(id);
59
+ if (loaded.has(id) && loadedVersions.get(id) === version) return Promise.resolve();
58
60
  if (!Object.hasOwn(clientModules, id)) {
59
61
  if (previousLoad) return previousLoad(id);
60
62
  return Promise.reject(new Error(`Unknown PRNext client module ${id}`));
61
63
  }
62
64
  const source = clientModules[id];
65
+ const requestedVersion = version;
66
+ const current = () => {
67
+ if (!Object.hasOwn(clientModules, id)) throw new Error(`Unknown PRNext client module ${id}`);
68
+ return loadChunk(id);
69
+ };
63
70
  const promise = Promise.resolve().then(() => loadClientModule(source))
64
- .then(module => { loaded.set(id, module); pending.delete(id); }, error => {
65
- pending.delete(id);
71
+ .then(module => {
72
+ if (requestedVersion !== version) return current();
73
+ loaded.set(id, module); loadedVersions.set(id, version);
74
+ }, error => {
75
+ if (requestedVersion !== version) return current();
76
+ loaded.delete(id); loadedVersions.delete(id);
66
77
  throw error;
78
+ }).finally(() => {
79
+ if (pending.get(id) === promise) pending.delete(id);
67
80
  });
68
81
  pending.set(id, promise);
69
82
  return promise;
70
- };
83
+ }
84
+ globalThis.__webpack_chunk_load__ = loadChunk;
71
85
  return {
72
86
  async updateModules(nextModules) {
73
- const ids = [...loaded.keys()].filter(id => Object.hasOwn(nextModules, id));
87
+ const ids = [...new Set([...loaded.keys(), ...pending.keys()])].filter(id => Object.hasOwn(nextModules, id));
88
+ version++;
74
89
  clientModules = nextModules;
75
- await Promise.all(ids.map(async id => {
76
- const source = clientModules[id];
77
- const module = await loadClientModule(source);
78
- loaded.set(id, module);
79
- }));
80
- for (const id of loaded.keys()) if (!Object.hasOwn(nextModules, id)) loaded.delete(id);
90
+ pending.clear();
91
+ for (const id of loaded.keys()) if (!Object.hasOwn(nextModules, id)) { loaded.delete(id); loadedVersions.delete(id); }
92
+ await Promise.all(ids.map(loadChunk));
81
93
  },
82
94
  };
83
95
  }
@@ -238,12 +250,18 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
238
250
  if (initialRewrite) initialModel.rewrite = initialRewrite;
239
251
  let currentUrl = new URL(window.location.href);
240
252
  let currentRouting = initialModel.routing;
253
+ // A pending destination may already own the history entry while React still
254
+ // shows the previous view. Rolling back its render must not rewrite that
255
+ // entry with the previous view's routing state on the next scroll event.
256
+ let historyRouting = currentRouting;
241
257
  let currentModel = initialModel;
258
+ let committedView = { model: initialModel, routing: currentRouting, url: currentUrl.href };
259
+ let pendingView;
242
260
  const historyFocus = new Map();
243
261
  let currentKey = window.history.state?.__prnext?.key || 0;
244
262
  let nextKey = currentKey + 1;
245
263
  let navigation = 0;
246
- let activeRequest, finishNavigation, pageHidden = false, notifyingPrefetch = false;
264
+ let activeRequest, finishNavigation, finishCommit, pageHidden = false, notifyingPrefetch = false;
247
265
  const prefetches = new Map(), scheduler = createPrefetchScheduler();
248
266
  const partialSegments = (typeof __PRNEXT_CACHE_COMPONENTS__ === 'undefined' ? cacheComponents : __PRNEXT_CACHE_COMPONENTS__) ? new Map() : null;
249
267
  let segmentBytes = 0;
@@ -254,12 +272,38 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
254
272
  function saveScroll() {
255
273
  // popstate has already changed location; never overwrite its destination.
256
274
  if (window.location.href === currentUrl.href) {
257
- window.history.replaceState(historyState(currentKey, [window.scrollX, window.scrollY], currentRouting), '', currentUrl.href);
275
+ window.history.replaceState(historyState(currentKey, [window.scrollX, window.scrollY], historyRouting), '', currentUrl.href);
258
276
  if (typeof WeakRef !== 'undefined' && document.activeElement && document.activeElement !== document.body) historyFocus.set(currentKey, new WeakRef(document.activeElement));
259
277
  while (historyFocus.size > 16) historyFocus.delete(historyFocus.keys().next().value);
260
278
  }
261
279
  }
262
280
 
281
+ function renderView(model, url, { token = navigation, canonical = true, committed } = {}) {
282
+ const view = { model, token };
283
+ pendingView = view;
284
+ startTransition(() => root.render(appView({ model, controller, url, globalErrorComponent, globalErrorCss,
285
+ committed: () => {
286
+ if (token !== navigation || pendingView !== view) return;
287
+ pendingView = undefined;
288
+ // A PPR preview is authorized UI, but not the completed route model
289
+ // from which a later navigation should inherit its layout cache.
290
+ if (canonical) committedView = { model, routing: model.routing, url };
291
+ committed?.();
292
+ },
293
+ })));
294
+ }
295
+
296
+ function abandonPendingView() {
297
+ if (!pendingView) return;
298
+ pendingView = undefined;
299
+ currentModel = committedView.model;
300
+ currentRouting = committedView.routing;
301
+ // React can already be reading a lazy row after createFromFetch resolves
302
+ // the root model. Replace that suspended render before abort rejects its
303
+ // remaining rows; it must not poison the retained layout or page cache.
304
+ root.render(appView({ model: committedView.model, controller, url: committedView.url, globalErrorComponent, globalErrorCss }));
305
+ }
306
+
263
307
  const flightTransport = createFlightTransport();
264
308
  async function requestFlight(url, signal, routing = currentRouting, onPartial, {skipEnsure=false,fullFlight=false}={}) {
265
309
  if(process.env.NODE_ENV==='development'&&!routing?.refresh&&!skipEnsure)await globalThis.__PRNEXT_DEV__?.ensureRoute?.(url.href);
@@ -365,7 +409,10 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
365
409
  function callServer(id, args) {
366
410
  if (!root) return Promise.reject(new Error('Server Actions cannot run during initial rendering'));
367
411
  const token = ++navigation;
412
+ finishCommit?.();
368
413
  const target = new URL(window.location.href);
414
+ saveScroll();
415
+ abandonPendingView();
369
416
  activeRequest?.abort();
370
417
  appStyleManager(document).cancel();
371
418
  finishNavigation?.(); finishNavigation = undefined;
@@ -384,6 +431,19 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
384
431
  try { return await performAction(id, args, token, target); } finally { release(); }
385
432
  }
386
433
 
434
+ async function waitForDevelopmentUpdate() {
435
+ // A generation updates the Flight loader before React Refresh registers
436
+ // the replacement component families. An already-running action may finish
437
+ // in that interval; rendering its model immediately remounts those client
438
+ // components and loses layout state. Wait only on development bookkeeping,
439
+ // never replay the mutation or cancel a newer user navigation.
440
+ let development = globalThis.__PRNEXT_DEV__;
441
+ while (development?.applying && development.applied) {
442
+ await development.applied;
443
+ development = globalThis.__PRNEXT_DEV__;
444
+ }
445
+ }
446
+
387
447
  async function performAction(id, args, token, target) {
388
448
  invalidatePrefetches();
389
449
  const temporaryReferences = createTemporaryReferenceSet();
@@ -404,15 +464,24 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
404
464
  throw new Error(`Server Action request failed (${response.status})`);
405
465
  }
406
466
  const model = await createFromFetch(Promise.resolve(response), { ...flightOptions, temporaryReferences });
467
+ const outcome = () => {
468
+ if (model?.actionError) {
469
+ const error = new Error(model.actionError.message || 'The Server Action failed');
470
+ if (model.actionError.digest !== undefined) error.digest = model.actionError.digest;
471
+ throw error;
472
+ }
473
+ return model?.actionResult;
474
+ };
475
+ if (process.env.NODE_ENV === 'development') await waitForDevelopmentUpdate();
407
476
  controller._retentionEpoch++;
408
477
  if (model?.routingInvalid || (model?.rootLayout && currentModel.rootLayout && model.rootLayout !== currentModel.rootLayout)) {
409
478
  if (token === navigation) window.location.replace(target.href);
410
- return model.actionResult;
479
+ return outcome();
411
480
  }
412
481
  if (model?.actionRedirect) {
413
482
  const destination = navigationUrl(model.actionRedirect.url);
414
483
  if (token === navigation) await navigate(destination, { replace: model.actionRedirect.type === 'replace' });
415
- return model.actionResult;
484
+ return outcome();
416
485
  }
417
486
  if (model && Object.hasOwn(model, 'tree') && token === navigation) {
418
487
  assertModel(model);
@@ -421,24 +490,22 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
421
490
  if (rewrite) model.rewrite = rewrite;
422
491
  prepareLayoutCache(model, currentModel);
423
492
  try { await appStyleManager(document).prepare(model.globalError ? [] : model.css || []); }
424
- catch (error) { if (token !== navigation) return model.actionResult; throw error; }
425
- if (token !== navigation) return model.actionResult;
493
+ catch (error) { if (token !== navigation) return outcome(); throw error; }
494
+ if (process.env.NODE_ENV === 'development') await waitForDevelopmentUpdate();
495
+ if (token !== navigation) return outcome();
426
496
  currentRouting = model.routing;
427
497
  currentModel = model;
498
+ historyRouting = currentRouting;
428
499
  appStyleManager(document).handoff(model.globalError ? [] : model.css || []);
429
- startTransition(() => root.render(appView({ model, controller, url: target.href, globalErrorComponent, globalErrorCss })));
430
- }
431
- if (model?.actionError) {
432
- const error = new Error(model.actionError.message || 'The Server Action failed');
433
- if (model.actionError.digest !== undefined) error.digest = model.actionError.digest;
434
- throw error;
500
+ renderView(model, target.href, { token });
435
501
  }
502
+ if (model?.actionError) return outcome();
436
503
  // Flight's JSON reviver removes object fields encoded as $undefined. A
437
504
  // successful void action therefore has an updated tree but no own result.
438
505
  if (!model || typeof model !== 'object' || (!Object.hasOwn(model, 'actionResult') && !Object.hasOwn(model, 'tree'))) {
439
506
  throw new Error('The Server Action response did not contain a result');
440
507
  }
441
- return model.actionResult;
508
+ return outcome();
442
509
  }
443
510
 
444
511
  async function navigate(href, { replace = false, scroll = true, pop = false, savedScroll, savedRouting, refresh = false } = {}) {
@@ -450,13 +517,16 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
450
517
  return;
451
518
  }
452
519
  const token = ++navigation;
520
+ finishCommit?.();
521
+ saveScroll();
522
+ abandonPendingView();
453
523
  activeRequest?.abort();
454
524
  appStyleManager(document).cancel();
455
525
  finishNavigation?.(); finishNavigation = undefined;
456
526
  activeRequest = undefined;
457
- saveScroll();
458
527
 
459
528
  function updateHistory(destination) {
529
+ historyRouting = currentRouting;
460
530
  if (!pop && !refresh) {
461
531
  if (!replace) currentKey = nextKey++;
462
532
  const position = scroll ? [0, 0] : [window.scrollX, window.scrollY];
@@ -470,7 +540,9 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
470
540
  currentUrl = destination;
471
541
  }
472
542
 
473
- if (!refresh && target.pathname === currentUrl.pathname && target.search === currentUrl.search && target.hash !== currentUrl.hash) {
543
+ const committedUrl = new URL(committedView.url);
544
+ if (!refresh && target.pathname === currentUrl.pathname && target.search === currentUrl.search && target.hash !== currentUrl.hash &&
545
+ target.pathname === committedUrl.pathname && target.search === committedUrl.search) {
474
546
  updateHistory(target);
475
547
  if (scroll) scrollToTarget(target, savedScroll);
476
548
  return;
@@ -493,15 +565,11 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
493
565
  try { result = await prefetched.promise; }
494
566
  catch (error) { if (token !== navigation || (prefetched.abort.signal.aborted && !prefetched.failed)) throw error; result = null; }
495
567
  if (result && !result.partial) {
496
- if (result.model.authorizationKey) {
497
- // A full private prefetch warmed modules/data, not authorization.
498
- // HttpOnly cookies may have changed since it was produced.
499
- const abort = new AbortController(); activeRequest = abort;
500
- result = await navigateFlight(target, abort.signal, pop && savedRouting ? { ...savedRouting, restore: true } : currentRouting);
501
- } else {
502
- result = { ...result, url: new URL(result.url.href) };
503
- if (!result.url.hash) result.url.hash = target.hash;
504
- }
568
+ // Full prefetch warms modules/data, not a future authorization decision.
569
+ // Static Flight has no authorizationKey, but its proxy checks can still
570
+ // change when an HttpOnly cookie is revoked before this click.
571
+ const abort = new AbortController(); activeRequest = abort;
572
+ result = await navigateFlight(target, abort.signal, pop && savedRouting ? { ...savedRouting, restore: true } : currentRouting);
505
573
  } else {
506
574
  const preview = result;
507
575
  const abort = new AbortController();
@@ -517,7 +585,7 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
517
585
  if (token !== navigation || abort.signal.aborted) return;
518
586
  updateHistory(destination); historyUpdated = true;
519
587
  appStyleManager(document).handoff(preview.model.css || []);
520
- startTransition(() => root.render(appView({ model: preview.model, controller, url: destination.href, globalErrorComponent, globalErrorCss })));
588
+ renderView(preview.model, destination.href, { token, canonical: false });
521
589
  });
522
590
  }
523
591
  } else {
@@ -536,6 +604,7 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
536
604
  if (token !== navigation) return;
537
605
  currentRouting = result.model.routing;
538
606
  currentModel = result.model;
607
+ historyRouting = currentRouting;
539
608
  if (currentRouting) invalidatePrefetches();
540
609
  if (!historyUpdated) updateHistory(result.url);
541
610
  const committed = () => {
@@ -546,8 +615,12 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
546
615
  }
547
616
  };
548
617
  appStyleManager(document).handoff(result.model.globalError ? [] : result.model.css || []);
549
- if(process.env.NODE_ENV==='development'&&refresh)await new Promise(resolve=>startTransition(()=>root.render(appView({model:result.model,controller,committed:()=>{committed();resolve();},url:result.url.href,globalErrorComponent,globalErrorCss}))));
550
- else startTransition(() => root.render(appView({ model: result.model, controller, committed, url: result.url.href, globalErrorComponent, globalErrorCss })));
618
+ if(process.env.NODE_ENV==='development'&&refresh)await new Promise(resolve=>{
619
+ const finish=()=>{if(finishCommit===finish)finishCommit=undefined;resolve();};
620
+ finishCommit=finish;
621
+ renderView(result.model, result.url.href, { token, committed: () => { committed(); finish(); } });
622
+ });
623
+ else renderView(result.model, result.url.href, { token, committed });
551
624
  } catch (error) {
552
625
  if (token !== navigation || error?.name === 'AbortError') return;
553
626
  if(error?.code==='PRNEXT_NAVIGATION_NETWORK'){
@@ -565,9 +638,13 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
565
638
  }
566
639
 
567
640
  function purgeRetainedPages() {
641
+ saveScroll();
642
+ abandonPendingView();
568
643
  controller._retentionEpoch++;
569
644
  historyFocus.clear();
570
- startTransition(() => root?.render(appView({ model: currentModel, controller, url: currentUrl.href, globalErrorComponent, globalErrorCss })));
645
+ // This only updates retention for the already committed model. It must
646
+ // not become a new pending Flight view or a development commit barrier.
647
+ startTransition(() => root.render(appView({ model: currentModel, controller, url: committedView.url, globalErrorComponent, globalErrorCss })));
571
648
  }
572
649
 
573
650
  const controller = {
@@ -650,7 +727,7 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
650
727
  window.addEventListener('popstate', event => {
651
728
  void navigate(new URL(window.location.href), { pop: true, savedScroll: event.state?.__prnext?.scroll, savedRouting: event.state?.__prnext?.routing });
652
729
  });
653
- window.addEventListener('pagehide', () => { saveScroll(); pageHidden = true; navigation++; activeRequest?.abort(); appStyleManager(document).cancel(); finishNavigation?.(); finishNavigation = undefined; invalidatePrefetches(); });
730
+ window.addEventListener('pagehide', () => { saveScroll(); pageHidden = true; navigation++; finishCommit?.(); abandonPendingView(); activeRequest?.abort(); appStyleManager(document).cancel(); finishNavigation?.(); finishNavigation = undefined; invalidatePrefetches(); });
654
731
  window.addEventListener('pageshow', event => { pageHidden = false; if (event.persisted) controller.refresh(); });
655
732
  window.addEventListener('prnext:invalidate-navigation', () => { controller._authorizationEpoch++; controller.refresh(); });
656
733
  let scrollFrame;
@@ -672,7 +749,7 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
672
749
  devStyles: routes => developmentStyles(currentModel, routes).css || [],
673
750
  devPrepareStyles: routes => appStyleManager(document).prepare(developmentStyles(currentModel, routes).css || []),
674
751
  devStylesApplied: (routes, { commit = true } = {}) => {
675
- for (const model of [currentModel]) {
752
+ for (const model of new Set([currentModel, committedView.model, pendingView?.model].filter(Boolean))) {
676
753
  const next = developmentStyles(model, routes);
677
754
  model.css = next.css; model.cssScopes = next.cssScopes;
678
755
  }
@@ -0,0 +1,27 @@
1
+ import {open,rm} from 'node:fs/promises';
2
+ import {randomUUID} from 'node:crypto';
3
+ import path from 'node:path';
4
+ import {rename} from './fs.mjs';
5
+
6
+ // The old destination stays readable until the complete replacement exists.
7
+ // Keeping an injectable I/O boundary lets tests exercise real partial-write and
8
+ // rename failures without depending on an antivirus or a physical disk fault.
9
+ export function createAtomicFileWriter({openFile=open,moveFile=rename,removeFile=rm}={}){
10
+ return async function writeAtomicFile(filename,data){
11
+ const temporary=filename+'.'+randomUUID()+'.tmp';let file,created=false,failure,phase='create';
12
+ try{
13
+ file=await openFile(temporary,'wx',0o600);created=true;phase='write';await file.writeFile(data);phase='close';await file.close();file=undefined;
14
+ phase='publish';await moveFile(temporary,filename);
15
+ }catch(error){
16
+ error.atomicFile={operation:'publish-development-state',phase,code:error.code||'UNKNOWN',destination:path.basename(filename),...(error.atomicRename||{})};
17
+ failure=error;
18
+ throw error;
19
+ }finally{
20
+ await file?.close().catch(()=>{});
21
+ // This unpredictable temporary is owned by this invocation. Never remove
22
+ // the destination to work around a Windows lock.
23
+ if(created)try{await removeFile(temporary,{force:true});}catch(error){if(failure)failure.atomicFile.cleanupCode=error.code||'UNKNOWN';else throw error;}
24
+ }
25
+ };
26
+ }
27
+ export const writeAtomicFile=createAtomicFileWriter();
@@ -57,7 +57,9 @@ globalThis[codecSymbol] = {
57
57
  chunks.push(Buffer.from(value));
58
58
  }
59
59
  if (failure) throw failure;
60
- return Buffer.concat(chunks, size);
60
+ // Each chunk above is already an owned copy. A single chunk needs no
61
+ // second allocation; never expose the producer's mutable stream storage.
62
+ return chunks.length===1?chunks[0]:Buffer.concat(chunks, size);
61
63
  },
62
64
  async decode(bytes, encoded, context) {
63
65
  return createFromReadableStream(new ReadableStream({ start(controller) { controller.enqueue(bytes); controller.close(); } }), {
@@ -195,12 +195,17 @@ async function applyUpdate(message, {navigation=false}={}) {
195
195
  const route = kind === 'pages' ? manifest.pages.find(route => route.pattern === application.router.snapshot().pathname) : null;
196
196
  const entry = kind === 'pages' ? route?.client : manifest.app?.client;
197
197
  if (!entry) { reload('The current route changed. Page reloaded.'); return; }
198
- if(!message.cssOnly)await import(/* webpackIgnore: true */ entry);
198
+ if(!message.cssOnly){
199
+ await import(/* webpackIgnore: true */ entry);
200
+ // Activated deferred modules must register their updated component
201
+ // families before Refresh runs, even when their loader is already cached.
202
+ await Promise.all((manifest.lazy||[]).map(module=>import(/* webpackIgnore: true */ module.url)));
203
+ }
199
204
  const pending = state.pending;
200
205
  if (pending) await application.devPrepare?.(pending.options);
201
206
  if (state.unsafe) { reload('An edited module exports values outside React components. Page reloaded.'); return; }
202
207
  if (kind === 'app' && application.devPrepareStyles) await application.devPrepareStyles(manifest.app.routes);
203
- else await updateStyles(kind === 'pages' ? route.css : application.devStyles?.(manifest.app.routes) || manifest.app.css);
208
+ else await updateStyles([...(kind === 'pages' ? route.css : application.devStyles?.(manifest.app.routes) || manifest.app.css),...(manifest.lazy||[]).flatMap(module=>module.css)]);
204
209
  if (kind === 'app') application.devStylesApplied?.(manifest.app.routes, { commit: !!message.cssOnly || !pending });
205
210
  // An unrelated type-only edit may publish a new generation without changing
206
211
  // any browser module. Keep its existing runtime/hydration issues and current
@@ -1,7 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import {createHash} from 'node:crypto';
3
3
  import {createReadStream} from 'node:fs';
4
- import {stat, readdir} from 'node:fs/promises';
4
+ import {stat, lstat, readdir, opendir} from 'node:fs/promises';
5
5
  import {shouldWatchProjectFile} from './env.mjs';
6
6
 
7
7
  function directoryFingerprint(entries) {
@@ -12,16 +12,27 @@ function directoryFingerprint(entries) {
12
12
 
13
13
  // Remember the bytes actually consumed by webpack, not the file's contents at
14
14
  // the end of a build: an edit made after a read must still trigger a rebuild.
15
- export function createDevInputSnapshot(roots, {maxEntries=512,maxBytes=1024*1024}={}) {
15
+ export function createDevInputSnapshot(roots, {maxEntries=process.platform==='darwin'?8192:512,maxBytes=1024*1024}={}) {
16
16
  const inputs=new Map();
17
+ const limitedPrefixes=new Set();let globalLimited=false;
17
18
  let bytes=0;
19
+ function limited(relative) {
20
+ // Source files may overflow in many sibling folders. One source-root
21
+ // prefix remains bounded without invalidating a captured disjoint generator.
22
+ relative=relative==='.contentlayer/generated'||relative.startsWith('.contentlayer/generated/')?'.contentlayer/generated':relative.split('/')[0];
23
+ if(globalLimited)return;
24
+ if([...limitedPrefixes].some(prefix=>relative===prefix||relative.startsWith(prefix+'/')))return;
25
+ for(const prefix of limitedPrefixes)if(prefix.startsWith(relative+'/'))limitedPrefixes.delete(prefix);
26
+ limitedPrefixes.add(relative);
27
+ if(limitedPrefixes.size>32||[...limitedPrefixes].reduce((sum,value)=>sum+value.length*2,0)>4096){globalLimited=true;limitedPrefixes.clear();}
28
+ }
18
29
  function remember(relative,value) {
19
30
  if (inputs.has(relative)) {
20
31
  if (inputs.get(relative)!==value) inputs.set(relative,null);
21
32
  return;
22
33
  }
23
34
  const size=relative.length*2+value.length*2+64;
24
- if (inputs.size>=maxEntries || bytes+size>maxBytes) return;
35
+ if (inputs.size>=maxEntries || bytes+size>maxBytes) {limited(value.startsWith('directory:')?relative:path.posix.dirname(relative)==='.'?relative:path.posix.dirname(relative));return;}
25
36
  inputs.set(relative,value);bytes+=size;
26
37
  }
27
38
  return {
@@ -38,14 +49,15 @@ export function createDevInputSnapshot(roots, {maxEntries=512,maxBytes=1024*1024
38
49
  async captureDirectory(relative) {
39
50
  // Called after an awaited content generator and before graph resolution.
40
51
  // Include its types/aggregate indexes, which may not be runtime imports.
41
- if (!shouldWatchProjectFile(relative) || inputs.size>=maxEntries || bytes>=maxBytes) return;
52
+ if (!shouldWatchProjectFile(relative))return;
53
+ if(inputs.size>=maxEntries || bytes>=maxBytes){limited(relative);return;}
42
54
  const file=path.join(roots[0],relative);
43
55
  let entries;
44
56
  try {entries=await readdir(file,{withFileTypes:true});}
45
57
  catch(error){if(error.code==='ENOENT')return;throw error;}
46
58
  remember(relative,directoryFingerprint(entries));
47
59
  for(const entry of entries) {
48
- if(inputs.size>=maxEntries || bytes>=maxBytes)break;
60
+ if(inputs.size>=maxEntries || bytes>=maxBytes){limited(relative);break;}
49
61
  const child=relative+'/'+entry.name;
50
62
  if(!shouldWatchProjectFile(child))continue;
51
63
  if(entry.isDirectory())await this.captureDirectory(child);
@@ -58,14 +70,90 @@ export function createDevInputSnapshot(roots, {maxEntries=512,maxBytes=1024*1024
58
70
  }
59
71
  }
60
72
  },
61
- entries:()=>[...inputs],
73
+ entries:()=>{
74
+ const values=[...inputs],coverage={version:1,limitedPrefixes:[...limitedPrefixes],globalLimited};
75
+ if(values.length)values[0]=[...values[0],coverage];
76
+ else values.push([null,null,coverage]);
77
+ return values;
78
+ },
62
79
  };
63
80
  }
64
81
 
65
82
  // Generators may rewrite identical JSON/modules on every compilation. Retain
66
83
  // only bounded path/digest pairs, never file contents or full directory lists.
67
- export function createDevChangeFilter(root, {maxEntries=512,maxBytes=1024*1024}={}) {
84
+ export function createDevChangeFilter(root, {directoryEventsAreRecursive=false,maxEntries=directoryEventsAreRecursive?8192:512,maxBytes=1024*1024,ignore=()=>false,maxTreeEntries=8192,maxTreeBytes=1024*1024,onUncertainDirectory=()=>{}}={}) {
68
85
  const fingerprints=new Map();
86
+ // Before the first build there are no consumed inputs to lose. A later
87
+ // acceptInputs without coverage metadata deliberately replaces this proof.
88
+ const trees=new Map(),diagnostics=new Map();let treeBytes=0,coverage={version:1,limitedPrefixes:[],globalLimited:false},consumedInputs=new Map();
89
+ const contains=(parent,file)=>file===parent||file.startsWith(parent+'/');
90
+ const allowed=file=>shouldWatchProjectFile(file)&&!ignore(file);
91
+ const limitedFor=file=>!coverage||coverage.globalLimited||coverage.limitedPrefixes.some(prefix=>contains(file,prefix)||contains(prefix,file));
92
+ function discardTree(file){const previous=trees.get(file);if(previous){trees.delete(file);treeBytes-=previous.bytes;}}
93
+ function uncertain(file,reason){
94
+ discardTree(file);
95
+ if(diagnostics.get(file)!==reason){
96
+ diagnostics.delete(file);diagnostics.set(file,reason);while(diagnostics.size>32)diagnostics.delete(diagnostics.keys().next().value);
97
+ onUncertainDirectory({directory:file.slice(0,256),reason});
98
+ }
99
+ return Object.assign(Error(`Cannot verify a coalesced directory change in ${file.slice(0,256)} (${reason}). The previous build is kept. Run a full build or narrow generated inputs before retrying.`),{code:'PRNEXT_RECURSIVE_CHANGE_UNVERIFIED'});
100
+ }
101
+ async function treeSnapshot(relative){
102
+ const observed=new Map();let entriesRead=0,retainedBytes=relative.length*2+128;
103
+ const incomplete=reason=>{throw Object.assign(Error('Incomplete recursive source snapshot'),{snapshotReason:reason});};
104
+ async function visit(name){
105
+ const file=path.join(root,name),before=await lstat(file);let value,semantic;
106
+ if(before.isSymbolicLink())incomplete('symbolic-link');
107
+ if(before.isDirectory()){
108
+ const entries=[],directory=await opendir(file);
109
+ for await(const entry of directory){if(++entriesRead>maxTreeEntries)incomplete('entry-capacity');entries.push(entry);}
110
+ value=directoryFingerprint(entries);
111
+ const watched=entries.filter(entry=>allowed(name+'/'+entry.name));semantic=directoryFingerprint(watched);
112
+ rememberObserved(name,value,semantic);
113
+ for(const entry of watched)await visit(name+'/'+entry.name);
114
+ }else if(before.isFile()){
115
+ const hash=createHash('sha256');for await(const chunk of createReadStream(file,{highWaterMark:64*1024}))hash.update(chunk);
116
+ value='file:'+hash.digest('hex');rememberObserved(name,value,value);
117
+ }else incomplete('special-file');
118
+ const after=await lstat(file);
119
+ if(before.dev!==after.dev||before.ino!==after.ino||before.size!==after.size||before.mtimeMs!==after.mtimeMs||before.ctimeMs!==after.ctimeMs)incomplete('changed-during-read');
120
+ }
121
+ function rememberObserved(file,value,semantic){
122
+ retainedBytes+=2*(file.length+value.length+semantic.length)+96;
123
+ if(observed.size>=maxTreeEntries||retainedBytes>maxTreeBytes)incomplete('memory-capacity');
124
+ observed.set(file,{value,semantic});
125
+ }
126
+ try{
127
+ await visit(relative);const hash=createHash('sha256');
128
+ for(const [file,value]of [...observed].sort(([a],[b])=>a.localeCompare(b)))hash.update(JSON.stringify([file,value.semantic]));
129
+ return {complete:true,observed,digest:hash.digest('hex'),bytes:retainedBytes};
130
+ }catch(error){return {complete:false,reason:error.snapshotReason||'read-failure'};}
131
+ }
132
+ function retainTree(file,snapshot){
133
+ discardTree(file);
134
+ while(trees.size&&(trees.size>=32||treeBytes+snapshot.bytes>maxTreeBytes))discardTree(trees.keys().next().value);
135
+ trees.set(file,snapshot);treeBytes+=snapshot.bytes;
136
+ }
137
+ function observeTree(file,snapshot){
138
+ for(const [name,previous]of [...fingerprints])if(contains(file,name)&&allowed(name)&&!snapshot.observed.has(name)&&previous.value!=='missing')remember(name,'missing');
139
+ for(const [name,value]of consumedInputs)if(contains(file,name)&&allowed(name)&&snapshot.observed.get(name)?.value!==value)consumedInputs.delete(name);
140
+ for(const [directory,previous]of trees){
141
+ if(!contains(file,directory)&&!contains(directory,file))continue;
142
+ const same=[...previous.observed].every(([name,value])=>!contains(file,name)||snapshot.observed.get(name)?.semantic===value.semantic)
143
+ &&[...snapshot.observed].every(([name,value])=>!contains(directory,name)||previous.observed.get(name)?.semantic===value.semantic);
144
+ if(!same)discardTree(directory);
145
+ }
146
+ }
147
+ function observePath(file,value){
148
+ for(const [directory,snapshot]of trees){
149
+ if(contains(directory,file)){
150
+ const observed=snapshot.observed.get(file);
151
+ if(value===undefined||(value==='missing'?observed!==undefined:observed?.value!==value))discardTree(directory);
152
+ }else if(contains(file,directory))discardTree(directory);
153
+ }
154
+ for(const [name,previous]of consumedInputs)if(contains(file,name)&&(name!==file||previous!==value))consumedInputs.delete(name);
155
+ if(!value?.startsWith('directory:'))for(const [name]of [...fingerprints])if(name!==file&&contains(file,name))remember(name,value===undefined?undefined:'missing');
156
+ }
69
157
  let bytes=0;
70
158
  function remember(file,value) {
71
159
  const previous=fingerprints.get(file);
@@ -99,12 +187,39 @@ export function createDevChangeFilter(root, {maxEntries=512,maxBytes=1024*1024}=
99
187
  const filter=async files => {
100
188
  const changed=[];
101
189
  for (const file of files) {
190
+ if(directoryEventsAreRecursive&&allowed(file)){
191
+ let directory=false;try{directory=(await stat(path.join(root,file))).isDirectory();}catch{}
192
+ if(directory){
193
+ const snapshot=await treeSnapshot(file);
194
+ if(!snapshot.complete)throw uncertain(file,snapshot.reason);
195
+ if(limitedFor(file))throw uncertain(file,'input-coverage');
196
+ observeTree(file,snapshot);
197
+ const previous=trees.get(file);
198
+ const consumed=[...snapshot.observed].every(([name,value])=>consumedInputs.get(name)===value.value)
199
+ &&[...consumedInputs].every(([name,value])=>!contains(file,name)||!allowed(name)||!value.startsWith('file:')||snapshot.observed.has(name));
200
+ if((!previous||previous.digest!==snapshot.digest)&&!consumed)changed.push(file);
201
+ retainTree(file,snapshot);
202
+ diagnostics.delete(file);
203
+ continue;
204
+ }
205
+ }
102
206
  const value=await fingerprint(file), previous=fingerprints.get(file);
207
+ if(directoryEventsAreRecursive)observePath(file,value);
103
208
  if (value===undefined || !previous || previous.value!==value) changed.push(file);
104
209
  remember(file,value);
105
210
  }
106
211
  return changed;
107
212
  };
108
- filter.acceptInputs=inputs=>{const changed=[];for(const [file,value] of inputs){if(fingerprints.get(file)?.value!==value)changed.push(file);remember(file,value);}return changed;};
213
+ filter.acceptInputs=inputs=>{
214
+ coverage=inputs.find(entry=>entry[2]?.version===1)?.[2];
215
+ // Only the latest bounded compiler receipt can acknowledge an uncached
216
+ // subtree. Event deduplication may keep older values in its independent LRU.
217
+ consumedInputs=new Map();for(const [file,value]of inputs)if(typeof file==='string'&&typeof value==='string')consumedInputs.set(file,value);
218
+ for(const [directory,snapshot]of trees){
219
+ if(limitedFor(directory)||inputs.some(([file,value])=>typeof file==='string'&&contains(directory,file)&&allowed(file)&&(value===null||snapshot.observed.get(file)?.value!==value)))discardTree(directory);
220
+ }
221
+ const changed=[];for(const [file,value] of inputs){if(typeof file!=='string')continue;if(fingerprints.get(file)?.value!==value)changed.push(file);remember(file,value);}return changed;
222
+ };
223
+ filter.recursiveDiagnostics=()=>[...diagnostics].map(([directory,reason])=>({directory,reason}));
109
224
  return filter;
110
225
  }