@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.
- package/README.md +2 -0
- package/build/actions.mjs +26 -14
- package/build/app.mjs +46 -25
- package/build/compiler.mjs +5 -4
- package/build/config.mjs +4 -2
- package/build/dev-css-assets.mjs +173 -0
- package/build/dev-css-update.mjs +14 -12
- package/build/dev-lazy-build.mjs +77 -0
- package/build/dynamic.mjs +13 -6
- package/build/incremental.mjs +112 -18
- package/build/index.mjs +32 -3
- package/build/lazy-dynamic-imports.mjs +137 -0
- package/build/migrate.mjs +4 -4
- package/build/migration-audit.mjs +19 -31
- package/build/migration-imports.mjs +245 -0
- package/build/migration-scenarios-runner.mjs +73 -0
- package/build/migration-scenarios.mjs +72 -0
- package/build/native-discovery.mjs +65 -5
- package/build/native-packages.mjs +2 -0
- package/build/npm-packages.mjs +1 -0
- package/build/production-builder.mjs +1 -1
- package/build/standalone.mjs +21 -3
- package/build/webpack-graph.mjs +13 -3
- package/cli.mjs +10 -1
- package/compat/app-dynamic.cjs +12 -1
- package/compat/cache-handlers.cjs +3 -1
- package/compat/flight-delta.cjs +3 -1
- package/compat/index.d.ts +1 -1
- package/compat/link.cjs +2 -2
- package/docs/ai/compatibility.md +77 -0
- package/docs/ai/routing.md +11 -0
- package/docs/ai/troubleshooting.md +79 -0
- package/package.json +21 -9
- package/runtime/app-client.mjs +119 -42
- package/runtime/atomic-file.mjs +27 -0
- package/runtime/cache-components-rsc.mjs +3 -1
- package/runtime/dev-client.mjs +7 -2
- package/runtime/dev-file-changes.mjs +123 -8
- package/runtime/dev-lazy-imports.mjs +121 -0
- package/runtime/dev-lazy-staging.mjs +14 -0
- package/runtime/dev-style-retention.mjs +15 -0
- package/runtime/development.mjs +60 -19
- package/runtime/flight-delta.mjs +28 -4
- package/runtime/fs.mjs +15 -8
- package/runtime/lazy-import-client.mjs +38 -0
- package/vendor/nft/LICENSE +7 -0
- package/vendor/nft/README.md +16 -0
- package/vendor/nft/out/analyze.d.ts +8 -0
- package/vendor/nft/out/analyze.js +1216 -0
- package/vendor/nft/out/analyze.js.map +1 -0
- package/vendor/nft/out/cli.d.ts +2 -0
- package/vendor/nft/out/cli.js +94 -0
- package/vendor/nft/out/cli.js.map +1 -0
- package/vendor/nft/out/fs.d.ts +22 -0
- package/vendor/nft/out/fs.js +106 -0
- package/vendor/nft/out/fs.js.map +1 -0
- package/vendor/nft/out/index.d.ts +4 -0
- package/vendor/nft/out/index.js +25 -0
- package/vendor/nft/out/index.js.map +1 -0
- package/vendor/nft/out/node-file-trace.d.ts +41 -0
- package/vendor/nft/out/node-file-trace.js +380 -0
- package/vendor/nft/out/node-file-trace.js.map +1 -0
- package/vendor/nft/out/resolve-dependency.d.ts +6 -0
- package/vendor/nft/out/resolve-dependency.js +427 -0
- package/vendor/nft/out/resolve-dependency.js.map +1 -0
- package/vendor/nft/out/types.d.ts +66 -0
- package/vendor/nft/out/types.js +2 -0
- package/vendor/nft/out/types.js.map +1 -0
- package/vendor/nft/out/utils/ast-helpers.js +49 -0
- package/vendor/nft/out/utils/ast-helpers.js.map +1 -0
- package/vendor/nft/out/utils/binary-locators.js +88 -0
- package/vendor/nft/out/utils/binary-locators.js.map +1 -0
- package/vendor/nft/out/utils/get-package-base.js +30 -0
- package/vendor/nft/out/utils/get-package-base.js.map +1 -0
- package/vendor/nft/out/utils/interop-require.js +24 -0
- package/vendor/nft/out/utils/interop-require.js.map +1 -0
- package/vendor/nft/out/utils/node-version.js +10 -0
- package/vendor/nft/out/utils/node-version.js.map +1 -0
- package/vendor/nft/out/utils/sharedlib-emit.js +33 -0
- package/vendor/nft/out/utils/sharedlib-emit.js.map +1 -0
- package/vendor/nft/out/utils/special-cases.js +347 -0
- package/vendor/nft/out/utils/special-cases.js.map +1 -0
- package/vendor/nft/out/utils/static-eval.js +579 -0
- package/vendor/nft/out/utils/static-eval.js.map +1 -0
- package/vendor/nft/out/utils/types.js +2 -0
- package/vendor/nft/out/utils/types.js.map +1 -0
- package/vendor/nft/out/utils/wrappers.js +659 -0
- package/vendor/nft/out/utils/wrappers.js.map +1 -0
- package/vendor/nft/package.json +4 -0
- package/vendor/nft/upstream.json +51 -0
package/runtime/app-client.mjs
CHANGED
|
@@ -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
|
-
|
|
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 => {
|
|
65
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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],
|
|
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
|
|
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
|
|
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
|
|
425
|
-
if (
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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
|
-
|
|
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=>
|
|
550
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(); } }), {
|
package/runtime/dev-client.mjs
CHANGED
|
@@ -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)
|
|
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)
|
|
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:()=>
|
|
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=>{
|
|
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
|
}
|