@thomas.f/prnext 0.1.18-alpha → 0.1.20-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 +90 -101
- package/build/build-lock.mjs +2 -71
- package/build/build-retention.mjs +119 -0
- package/build/bundle-runtime.mjs +9 -8
- package/build/cache-command.mjs +31 -0
- package/build/check.mjs +41 -3
- package/build/compilation-cache.mjs +32 -12
- package/build/compilation-storage.mjs +190 -0
- package/build/compiler.mjs +8 -7
- package/build/config.mjs +25 -2
- package/build/create.mjs +1 -1
- package/build/css-modules.mjs +4 -0
- package/build/dev-action-graph.mjs +21 -0
- 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/font-cache.mjs +9 -6
- package/build/font-files.mjs +1 -1
- package/build/incremental.mjs +112 -18
- package/build/index.mjs +61 -15
- 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/native-ppr.mjs +32 -0
- package/build/npm-packages.mjs +1 -0
- package/build/og.mjs +121 -1
- package/build/parallel-plan.mjs +55 -0
- package/build/persistent-cache.mjs +29 -24
- package/build/production-builder.mjs +1 -1
- package/build/production-check.mjs +34 -0
- package/build/react-compiler-report.mjs +31 -8
- package/build/react-compiler.mjs +101 -10
- package/build/server-config.mjs +35 -0
- package/build/source-analysis.mjs +54 -4
- package/build/standalone.mjs +21 -3
- package/build/static-adapter.mjs +7 -3
- package/build/transform.mjs +38 -6
- package/build/typescript-dev.mjs +3 -1
- package/build/webpack-graph.mjs +13 -3
- package/cli.mjs +24 -6
- package/compat/app-dynamic.cjs +12 -1
- package/compat/app-layout-context.cjs +7 -3
- package/compat/cache-handlers.cjs +3 -1
- package/compat/flight-delta.cjs +50 -13
- package/compat/index.d.ts +24 -1
- package/compat/link.cjs +2 -2
- package/compat/navigation-authorization.cjs +8 -1
- package/docs/ai/compatibility.md +187 -7
- package/docs/ai/deployment.md +141 -0
- package/docs/ai/quickstart.md +5 -0
- package/docs/ai/routing.md +11 -0
- package/docs/ai/troubleshooting.md +79 -0
- package/package.json +26 -11
- package/release/fontkit.mjs +31 -0
- package/release/verify.mjs +3 -0
- package/runtime/api.mjs +5 -2
- package/runtime/app-client.mjs +254 -53
- package/runtime/app-partial.mjs +2 -10
- package/runtime/app-prefetch.mjs +2 -2
- package/runtime/app-render.mjs +11 -7
- package/runtime/app-stream-channel.mjs +3 -2
- package/runtime/app-styles.mjs +22 -4
- package/runtime/atomic-file.mjs +27 -0
- package/runtime/cache-components-rsc.mjs +3 -1
- package/runtime/compile-cache-retention.mjs +53 -0
- package/runtime/compile-cache.mjs +28 -1
- package/runtime/dev-client.mjs +13 -5
- package/runtime/dev-file-changes.mjs +123 -8
- package/runtime/dev-gateway.mjs +3 -2
- package/runtime/dev-lazy-imports.mjs +121 -0
- package/runtime/dev-lazy-staging.mjs +14 -0
- package/runtime/dev-route-selection.mjs +64 -0
- package/runtime/dev-runtime-pool.mjs +3 -2
- package/runtime/dev-runtime-spare.mjs +1 -1
- package/runtime/dev-session-retention.mjs +154 -0
- package/runtime/dev-style-retention.mjs +15 -0
- package/runtime/development.mjs +86 -26
- package/runtime/env.mjs +9 -0
- package/runtime/flight-delta.mjs +59 -9
- package/runtime/fs.mjs +15 -8
- package/runtime/lazy-import-client.mjs +38 -0
- package/runtime/native-ppr.mjs +35 -0
- package/runtime/og/NOTICE.txt +1 -1
- package/runtime/og/index.node.mjs +76 -31216
- package/runtime/og/upstream.json +115 -0
- package/runtime/persistent/client.mjs +4 -2
- package/runtime/persistent/daemon.mjs +3 -11
- package/runtime/persistent/health.mjs +58 -0
- package/runtime/project-lock.mjs +71 -0
- package/runtime/request-transport.mjs +112 -32
- package/runtime/storage-path.mjs +36 -0
- package/runtime/trace.mjs +44 -4
- package/runtime/worker.mjs +9 -4
- package/vendor/fontkit/NOTICE.txt +5 -0
- package/vendor/fontkit/SOURCE-NOTICES.txt +209 -0
- package/vendor/fontkit/index.mjs +3 -0
- package/vendor/fontkit/licenses/@swc__helpers/LICENSE +201 -0
- package/vendor/fontkit/licenses/base64-js/LICENSE +21 -0
- package/vendor/fontkit/licenses/brotli/readme.md +61 -0
- package/vendor/fontkit/licenses/clone/LICENSE +18 -0
- package/vendor/fontkit/licenses/dfa/README.md +83 -0
- package/vendor/fontkit/licenses/fast-deep-equal/LICENSE +21 -0
- package/vendor/fontkit/licenses/fontkit/README.md +266 -0
- package/vendor/fontkit/licenses/restructure/LICENSE +21 -0
- package/vendor/fontkit/licenses/tiny-inflate/LICENSE +21 -0
- package/vendor/fontkit/licenses/tslib/LICENSE.txt +12 -0
- package/vendor/fontkit/licenses/unicode-properties/LICENSE +8 -0
- package/vendor/fontkit/licenses/unicode-trie/LICENSE +8 -0
- package/vendor/fontkit/upstream.json +215 -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-render.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import {reportRequestError,acknowledgeRequestError} from './instrumentation.mjs';
|
|
2
2
|
import {currentTraceparent} from './trace.mjs';
|
|
3
3
|
import React from 'react';
|
|
4
|
-
import { randomUUID, createHmac
|
|
4
|
+
import { randomUUID, createHmac } from 'node:crypto';
|
|
5
|
+
import { developmentNavigationSecret } from '../compat/navigation-authorization.cjs';
|
|
5
6
|
import diagnostics from '../compat/diagnostics.cjs';
|
|
6
7
|
import { rscResourceLimits } from './profile.mjs';
|
|
7
8
|
import { fontPreloads } from './font-preload.mjs';
|
|
@@ -28,6 +29,7 @@ import { installFetchCache } from './fetch-cache.mjs';
|
|
|
28
29
|
import { partialArtifact, partialShell } from './app-partial.mjs';
|
|
29
30
|
import { partialKeyMap } from './app-partial-model.mjs';
|
|
30
31
|
import { partialIdentity } from './partial-artifact-cache.mjs';
|
|
32
|
+
import {nativePprArtifact} from './native-ppr.mjs';
|
|
31
33
|
import { isDraftRequest } from '../compat/draft.cjs';
|
|
32
34
|
import { createRscLifecycle } from './rsc-lifecycle.mjs';
|
|
33
35
|
import { trackRequestWork } from './request-work.mjs';
|
|
@@ -43,7 +45,6 @@ const metadata = new WeakMap();
|
|
|
43
45
|
const artifacts = new WeakMap();
|
|
44
46
|
const edgeManifests = new WeakMap();
|
|
45
47
|
const emptyManifest = Object.freeze({});
|
|
46
|
-
const developmentAuthorizationSecret = randomBytes(32).toString('hex');
|
|
47
48
|
let nextId = 0;
|
|
48
49
|
const pending = new Map();
|
|
49
50
|
const noop = () => {};
|
|
@@ -506,7 +507,7 @@ export async function renderAppPage(options) {
|
|
|
506
507
|
return differentialFlight(response, options);
|
|
507
508
|
}
|
|
508
509
|
|
|
509
|
-
async function renderAppPageInner({ modulePath, route = {}, manifest = {}, distDir = path.dirname(path.dirname(modulePath)), url = 'http://localhost/', originalUrl, params = {}, method = 'GET', headers = {}, routingRequestHeaders, routingResolver, body = '', production = process.env.NODE_ENV === 'production', stream = false, signal, deferFinalizer }) {
|
|
510
|
+
async function renderAppPageInner({ modulePath, route = {}, manifest = {}, distDir = path.dirname(path.dirname(modulePath)), url = 'http://localhost/', originalUrl, params = {}, method = 'GET', headers = {}, routingRequestHeaders, routingResolver, body = '', production = process.env.NODE_ENV === 'production', stream = false, signal, deferFinalizer, renderMode }) {
|
|
510
511
|
if (route.htmlOnly) return { status:409, headers:{'content-type':'text/plain; charset=utf-8','cache-control':'private, no-store','x-prnext-document-navigation':'1'}, body:Buffer.from('This HTML-only page requires its prerendered document.') };
|
|
511
512
|
let clientModules = manifest.app?.clientModules || emptyManifest;
|
|
512
513
|
if (route.cacheConfig?.runtime === 'edge') {
|
|
@@ -522,7 +523,7 @@ async function renderAppPageInner({ modulePath, route = {}, manifest = {}, distD
|
|
|
522
523
|
}
|
|
523
524
|
const responseHeaders = { 'cache-control': 'private, no-cache, no-store, max-age=0', vary: 'RSC' };
|
|
524
525
|
const request = { modulePath, routePattern: route.pattern, cacheConfig: route.cacheConfig, css: route.css || [], cssScopes: route.cssScopes || [], fonts: route.fonts || [], nonce: scriptNonce(headers), basePath: manifest.config?.basePath || '', trailingSlash: manifest?.config?.trailingSlash || false, skipTrailingSlashRedirect: manifest?.config?.skipTrailingSlashRedirect || false, url, originalUrl, params, method, headers, clientModules, production,
|
|
525
|
-
distDir, traceparent:currentTraceparent(), instrumentation:manifest.instrumentation, strictMode: manifest.config?.reactStrictMode !== false, cacheComponents: manifest.config?.cacheComponents, navigationCache: manifest.config?.navigationCache, authorizationSecret: production ? manifest.app?.actionKey :
|
|
526
|
+
distDir, traceparent:currentTraceparent(), instrumentation:manifest.instrumentation, strictMode: manifest.config?.reactStrictMode !== false, cacheComponents: manifest.config?.cacheComponents, navigationCache: manifest.config?.navigationCache, authorizationSecret: production ? manifest.app?.actionKey : developmentNavigationSecret(manifest.app?.actionKey), partialPrefetching: manifest.config?.partialPrefetching, cacheLife: manifest.config?.cacheLife, cacheHandlers: manifest.config?.cacheHandlers, cacheHandler: manifest.config?.cacheHandler, cacheMaxMemorySize: manifest.config?.cacheMaxMemorySize, serverActions: manifest.config?.serverActions, previewModeId: manifest.previewModeId, actions: manifest.app?.actions || emptyManifest, actionKey: manifest.app?.actionKey };
|
|
526
527
|
if (route.parallel) {
|
|
527
528
|
request.routingResolver = routingResolver;
|
|
528
529
|
request.skipProxyUrlNormalize = manifest.config?.skipProxyUrlNormalize;
|
|
@@ -554,6 +555,8 @@ async function renderAppPageInner({ modulePath, route = {}, manifest = {}, distD
|
|
|
554
555
|
}
|
|
555
556
|
}
|
|
556
557
|
const header = name => Object.entries(headers).find(([key]) => key.toLowerCase() === name)?.[1];
|
|
558
|
+
const native = await nativePprArtifact({renderMode, manifest, route, url, distDir});
|
|
559
|
+
if (native) request.nativePpr = native.descriptor;
|
|
557
560
|
if (request.partialPrefetching && manifest.app?.actionKey) {
|
|
558
561
|
// Preview identity includes authorization, but exposes no cookie material or
|
|
559
562
|
// guessable credential hash. A changed login cannot reveal an old preview.
|
|
@@ -591,7 +594,7 @@ async function renderAppPageInner({ modulePath, route = {}, manifest = {}, distD
|
|
|
591
594
|
const generic = route.pprGeneric?.find(item => Object.entries(item.params).every(([name, value]) => JSON.stringify(params[name]) === JSON.stringify(value)));
|
|
592
595
|
const filename = (!originalUrl ? route.ppr?.[pathname] : undefined) || generic?.file;
|
|
593
596
|
if (filename || route.pprFallback) {
|
|
594
|
-
const artifact = await partialArtifact({ request, route, manifest, filename, identity: filename === generic?.file ? filename : `${pathname}:${originalUrl ? new URL(originalUrl).pathname : ''}`, signal,
|
|
597
|
+
const artifact = native?.artifact || await partialArtifact({ request, route, manifest, filename, identity: filename === generic?.file ? filename : `${pathname}:${originalUrl ? new URL(originalUrl).pathname : ''}`, signal,
|
|
595
598
|
regenerate: async build => (await import('./app-static.mjs')).prerenderAppRoute({ modulePath, route, manifest, distDir, production, renderMode:'isr',
|
|
596
599
|
path: build?.generic?.path || pathname, originalUrl: build?.generic ? undefined : originalUrl,
|
|
597
600
|
params: build?.generic?.params || params, partialParams: build?.generic?.unknown }),
|
|
@@ -672,6 +675,7 @@ async function renderPartialAppPage({ request, route, artifact, responseHeaders,
|
|
|
672
675
|
const keys = [...partialKeyMap(artifact.keyScopes, request.params, removeBasePath(new URL(request.url).pathname, request.basePath))];
|
|
673
676
|
const partialRequest = { ...request, partialFlight: artifact.flight, ...(keys.length ? { partialKeys: keys } : {}) };
|
|
674
677
|
const headers = { ...artifact.headers, ...responseHeaders, 'x-prnext-prerender': artifact.complete ? 'complete' : 'partial', 'content-type': rsc ? 'text/x-component; charset=utf-8' : 'text/html; charset=utf-8' };
|
|
678
|
+
if (request.nativePpr) headers['x-prnext-native-ppr'] = request.nativePpr.artifactHash;
|
|
675
679
|
const identity = partialIdentity(artifact);
|
|
676
680
|
headers['x-prnext-ppr-id'] = identity;
|
|
677
681
|
if (prefetch) {
|
|
@@ -703,7 +707,7 @@ async function renderPartialAppPage({ request, route, artifact, responseHeaders,
|
|
|
703
707
|
let rendered;
|
|
704
708
|
const cancel = async reason => { abort.abort(reason || new Error('Partial response cancelled')); await rendered?.cancel?.(reason); };
|
|
705
709
|
const body = (async function* () {
|
|
706
|
-
let sent = 0;
|
|
710
|
+
let sent = request.nativePpr ? Buffer.byteLength(shell.replace(/<\/body><\/html>$/i, '')) : 0;
|
|
707
711
|
const bounded = chunk => {
|
|
708
712
|
sent += chunk.byteLength;
|
|
709
713
|
if (sent > MAX_RESPONSE_BYTES) throw new Error('Response exceeds the 16 MiB PRNext limit');
|
|
@@ -712,7 +716,7 @@ async function renderPartialAppPage({ request, route, artifact, responseHeaders,
|
|
|
712
716
|
try {
|
|
713
717
|
// The committed bytes come straight from prerender. Even a blocked
|
|
714
718
|
// request component cannot hold back this shared document shell.
|
|
715
|
-
yield bounded(Buffer.from(shell.replace(/<\/body><\/html>$/i, '')));
|
|
719
|
+
if (!request.nativePpr) yield bounded(Buffer.from(shell.replace(/<\/body><\/html>$/i, '')));
|
|
716
720
|
const result = await continueFlight(combinedSignal, true);
|
|
717
721
|
rendered = await renderProgressiveAppHtml({ result, request, route, responseHeaders, decodeFlight, signal: combinedSignal,
|
|
718
722
|
partial: { shell, postponed: artifact.postponed, shellSent: true, resumeKeys: keys.map(([from, to]) => [to, from]) } });
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// One initial transferable chunk, then one chunk per reader pull. The initial
|
|
2
2
|
// credit removes a round trip while keeping the queue bounded to 64 KiB.
|
|
3
|
+
import { ownFlightStream } from '../compat/flight-delta.cjs';
|
|
3
4
|
export const APP_STREAM_CHUNK_BYTES = 64 * 1024;
|
|
4
5
|
|
|
5
6
|
export function workerStreamChannel(port, id) {
|
|
@@ -62,7 +63,7 @@ export function parentStreamChannel(port, id, cancelled) {
|
|
|
62
63
|
let controller;
|
|
63
64
|
let settlePull;
|
|
64
65
|
let closed = false;
|
|
65
|
-
const body = new ReadableStream({
|
|
66
|
+
const body = ownFlightStream(new ReadableStream({
|
|
66
67
|
start(value) { controller = value; },
|
|
67
68
|
pull() {
|
|
68
69
|
return new Promise(resolve => {
|
|
@@ -76,7 +77,7 @@ export function parentStreamChannel(port, id, cancelled) {
|
|
|
76
77
|
port.postMessage({ id, type: 'cancel' });
|
|
77
78
|
cancelled(reason);
|
|
78
79
|
},
|
|
79
|
-
}, { highWaterMark: 0 });
|
|
80
|
+
}, { highWaterMark: 0 }));
|
|
80
81
|
return {
|
|
81
82
|
body,
|
|
82
83
|
chunk(buffer, offset = 0, length = buffer?.byteLength) {
|
package/runtime/app-styles.mjs
CHANGED
|
@@ -32,7 +32,23 @@ export function appStyleManager(document, { timeout = 15_000 } = {}) {
|
|
|
32
32
|
if (!records.length) sheets.delete(href);
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
-
|
|
35
|
+
// Initial HTML can contain many sheets. Index it once per synchronous
|
|
36
|
+
// prepare/commit operation, lazily when an untracked URL needs adoption.
|
|
37
|
+
// Never retain this snapshot across operations: external links may change.
|
|
38
|
+
function initialSheets() {
|
|
39
|
+
let index;
|
|
40
|
+
return (href, tracked) => {
|
|
41
|
+
if (!index) {
|
|
42
|
+
index = new Map();
|
|
43
|
+
for (const link of document.querySelectorAll('link[rel="stylesheet"]')) {
|
|
44
|
+
const url = link.href, links = index.get(url) || [];
|
|
45
|
+
links.push(link); index.set(url, links);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return index.get(href)?.find(link => !tracked.has(link));
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function acquire(href, before, replace = false, initial = initialSheets()) {
|
|
36
52
|
const records = sheets.get(href) || [];
|
|
37
53
|
const current = records.at(-1);
|
|
38
54
|
if (current && !replace) return current;
|
|
@@ -40,7 +56,7 @@ export function appStyleManager(document, { timeout = 15_000 } = {}) {
|
|
|
40
56
|
// in place until commit. Discard older abandoned candidates immediately.
|
|
41
57
|
if (replace) for (const record of [...records]) if (!record.active && !record.recovery && !reserved(record)) remove(href, record);
|
|
42
58
|
const tracked = new Set(records.map(record => record.link));
|
|
43
|
-
const existing = replace ? undefined :
|
|
59
|
+
const existing = replace ? undefined : initial(href, tracked);
|
|
44
60
|
const link = existing || document.createElement('link');
|
|
45
61
|
const sheet = { link, active: !!existing, ready: !!existing?.sheet, recovery: false };
|
|
46
62
|
const retained = sheets.get(href) || [];
|
|
@@ -77,6 +93,7 @@ export function appStyleManager(document, { timeout = 15_000 } = {}) {
|
|
|
77
93
|
async prepare(values = [], signal) {
|
|
78
94
|
if (signal?.aborted) throw aborted();
|
|
79
95
|
const wanted = urls(values);
|
|
96
|
+
const initial = initialSheets();
|
|
80
97
|
const prepared = [];
|
|
81
98
|
let before;
|
|
82
99
|
// Establish cascade order before downloading. Re-inserting an already
|
|
@@ -85,7 +102,7 @@ export function appStyleManager(document, { timeout = 15_000 } = {}) {
|
|
|
85
102
|
// when an existing sheet would otherwise need to move across that suffix.
|
|
86
103
|
for (let index = wanted.length - 1; index >= 0; index--) {
|
|
87
104
|
const href = wanted[index];
|
|
88
|
-
let sheet = acquire(href, before);
|
|
105
|
+
let sheet = acquire(href, before, false, initial);
|
|
89
106
|
if (before && !(sheet.link.compareDocumentPosition(before) & 4)) sheet = acquire(href, before, true);
|
|
90
107
|
prepared.push(sheet.promise);
|
|
91
108
|
before = sheet.link;
|
|
@@ -113,12 +130,13 @@ export function appStyleManager(document, { timeout = 15_000 } = {}) {
|
|
|
113
130
|
commit(values = []) {
|
|
114
131
|
cleanup++;
|
|
115
132
|
const wanted = new Set(urls(values));
|
|
133
|
+
const initial = initialSheets();
|
|
116
134
|
const handedUrls = [...handed.keys()];
|
|
117
135
|
const pinned = wanted.size === handed.size && [...wanted].every((href, index) => handedUrls[index] === href) ? handed : null;
|
|
118
136
|
for (const href of wanted) {
|
|
119
137
|
const records = sheets.get(href) || [];
|
|
120
138
|
const prior = !pinned && handed.size ? records.find(record => record.active) || records.find(record => record.recovery) : undefined;
|
|
121
|
-
const sheet = pinned?.get(href) || prior || records.findLast(record => record.ready) || acquire(href);
|
|
139
|
+
const sheet = pinned?.get(href) || prior || records.findLast(record => record.ready) || acquire(href, undefined, false, initial);
|
|
122
140
|
// Initial HTML links are render-blocking. A missing sheet is restored
|
|
123
141
|
// on explicit recovery; normal navigation always prepares first.
|
|
124
142
|
sheet.active = true; sheet.recovery = false; sheet.link.media = '';
|
|
@@ -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(); } }), {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import {lstat,opendir,unlink,realpath} from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {captureStorageDirectory,verifyStorageDirectories,sameStoragePath} from './storage-path.mjs';
|
|
4
|
+
import {withBuildLock} from './project-lock.mjs';
|
|
5
|
+
|
|
6
|
+
/** Bounded maintenance of a known, disposable compiler namespace. Open V8
|
|
7
|
+
* cache readers remain valid; an evicted future lookup is a cache miss. */
|
|
8
|
+
export async function pruneCompilerCache(directory,{maxBytes=64*1024*1024,maxFiles=2048}={}){
|
|
9
|
+
if(!directory)return;
|
|
10
|
+
try{
|
|
11
|
+
const snapshot=await captureStorageDirectory(directory);if(!snapshot)return;
|
|
12
|
+
return await withBuildLock(directory,async canonical=>{
|
|
13
|
+
if(!sameStoragePath(canonical,directory)||!await verifyStorageDirectories([snapshot]))return;
|
|
14
|
+
return prune(canonical,{maxBytes,maxFiles});
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
catch{/* A busy or unverifiable maintenance owner wins; never wait on it. */}
|
|
18
|
+
}
|
|
19
|
+
async function prune(directory,{maxBytes,maxFiles}){
|
|
20
|
+
try{
|
|
21
|
+
const files=[];let scanned=0,complete=true;
|
|
22
|
+
const input=await lstat(directory);if(!input.isDirectory()||input.isSymbolicLink())return;
|
|
23
|
+
directory=await realpath(directory);
|
|
24
|
+
const initial=await captureStorageDirectory(directory);if(!initial)return;
|
|
25
|
+
async function collect(folder,depth,parents){
|
|
26
|
+
if(depth>3){complete=false;return;}
|
|
27
|
+
const snapshot=await captureStorageDirectory(folder),guards=[...parents,snapshot];
|
|
28
|
+
if(!snapshot||!await verifyStorageDirectories(guards)){complete=false;return;}
|
|
29
|
+
const entries=await opendir(folder);
|
|
30
|
+
for await(const entry of entries){
|
|
31
|
+
if(++scanned>8192){complete=false;break;}
|
|
32
|
+
if(entry.name.startsWith('.'))continue;
|
|
33
|
+
const name=path.join(folder,entry.name),info=await lstat(name).catch(()=>null);
|
|
34
|
+
if(!info||info.isSymbolicLink())continue;
|
|
35
|
+
if(info.isDirectory())await collect(name,depth+1,guards);
|
|
36
|
+
else if(info.isFile())files.push({name,size:info.size,time:info.mtimeMs,ino:info.ino,dev:info.dev,guards});
|
|
37
|
+
if(!complete)break;
|
|
38
|
+
}
|
|
39
|
+
if(!await verifyStorageDirectories(guards))complete=false;
|
|
40
|
+
}
|
|
41
|
+
const info=await lstat(directory);if(!info.isDirectory()||info.isSymbolicLink())return;
|
|
42
|
+
await collect(directory,0,[initial]);if(!complete)return;
|
|
43
|
+
files.sort((a,b)=>b.time-a.time);let bytes=0,retained=0;
|
|
44
|
+
for(let i=0;i<files.length;i++){
|
|
45
|
+
const file=files[i];
|
|
46
|
+
if(retained>=maxFiles||bytes+file.size>maxBytes){
|
|
47
|
+
if(!await verifyStorageDirectories(file.guards))return;
|
|
48
|
+
const current=await lstat(file.name).catch(()=>null);
|
|
49
|
+
if(current?.isFile()&&!current.isSymbolicLink()&¤t.ino===file.ino&¤t.dev===file.dev&¤t.mtimeMs===file.time&¤t.size===file.size&&sameStoragePath(await realpath(file.name),file.name)&&await verifyStorageDirectories(file.guards))await unlink(file.name).catch(()=>{});
|
|
50
|
+
}else{bytes+=file.size;retained++;}
|
|
51
|
+
}
|
|
52
|
+
}catch{/* Compiler cache retention cannot fail a request or build. */}
|
|
53
|
+
}
|
|
@@ -1,8 +1,35 @@
|
|
|
1
1
|
import {flushCompileCache} from 'node:module';
|
|
2
|
+
import {lstatSync,realpathSync,writeFileSync,unlinkSync} from 'node:fs';
|
|
3
|
+
import {randomUUID} from 'node:crypto';
|
|
4
|
+
import {hostname} from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import {pruneCompilerCache} from './compile-cache-retention.mjs';
|
|
2
7
|
let pending;
|
|
8
|
+
let managedDirectory;
|
|
9
|
+
let lastMaintenance=0,maintenance;
|
|
10
|
+
// Native workers opt in explicitly. Never maintain a caller-supplied global
|
|
11
|
+
// NODE_COMPILE_CACHE, and reject links in both framework-owned path components.
|
|
12
|
+
try{
|
|
13
|
+
const root=process.env.PRNEXT_MANAGED_COMPILE_CACHE_ROOT;
|
|
14
|
+
if(root&&process.env.NODE_COMPILE_CACHE&&process.env.NODE_DISABLE_COMPILE_CACHE!=='1'){
|
|
15
|
+
const canonical=realpathSync(root),base=path.join(canonical,'.prnext-cache'),directory=path.join(base,'node-compile-v1');
|
|
16
|
+
if(realpathSync(process.env.NODE_COMPILE_CACHE)===directory&&[base,directory,process.env.NODE_COMPILE_CACHE].every(folder=>{const info=lstatSync(folder);return info.isDirectory()&&!info.isSymbolicLink();})){
|
|
17
|
+
const lease=path.join(directory,`.lease-${process.pid}-${randomUUID()}.json`);
|
|
18
|
+
writeFileSync(lease,JSON.stringify({pid:process.pid,host:hostname()}),{flag:'wx',mode:0o600});
|
|
19
|
+
managedDirectory=directory;
|
|
20
|
+
process.once('exit',()=>{try{unlinkSync(lease);}catch{}});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}catch{/* A lease failure leaves this process's cache unmanaged. */}
|
|
3
24
|
// Flush after imports settle so later workers can reuse compiled code even
|
|
4
25
|
// while this worker stays warm. Disk failure is an optimization miss only.
|
|
5
26
|
export function scheduleCompileCacheFlush(){
|
|
6
27
|
if(pending||!process.env.NODE_COMPILE_CACHE||process.env.NODE_DISABLE_COMPILE_CACHE==='1')return;
|
|
7
|
-
pending=setTimeout(()=>{
|
|
28
|
+
pending=setTimeout(()=>{
|
|
29
|
+
pending=undefined;try{flushCompileCache();}catch{}
|
|
30
|
+
if(managedDirectory&&!maintenance&&Date.now()-lastMaintenance>=60000){
|
|
31
|
+
lastMaintenance=Date.now();
|
|
32
|
+
maintenance=pruneCompilerCache(managedDirectory,{maxBytes:128*1024*1024,maxFiles:4096}).finally(()=>{maintenance=undefined;});
|
|
33
|
+
}
|
|
34
|
+
},1000);pending.unref();
|
|
8
35
|
}
|
package/runtime/dev-client.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { createConsoleCapture, isDevicePreview } from './dev-console.mjs';
|
|
|
4
4
|
import { createDevToolsClient } from './dev-tools-client.mjs';
|
|
5
5
|
import { createDevErrorOverlay } from './dev-error-overlay.mjs';
|
|
6
6
|
import { collectDevIssues, formatDevIssuesForAI } from './dev-error-report.mjs';
|
|
7
|
+
import { canSkipDevelopmentUpdate } from './dev-route-selection.mjs';
|
|
7
8
|
|
|
8
9
|
const state = globalThis.__PRNEXT_DEV__ ||= {
|
|
9
10
|
applications: new Map(), modules: new Map(), buildId: null, manifest: null,
|
|
@@ -154,7 +155,7 @@ function typeDiagnostics(errors) {
|
|
|
154
155
|
}
|
|
155
156
|
async function applyUpdate(message, {navigation=false}={}) {
|
|
156
157
|
if(message.session&&state.session&&message.session!==state.session){reload('Development server restarted. Page reloaded.');return;}
|
|
157
|
-
if(message.sequence&&message.sequence<=(state.sequence||0))return;
|
|
158
|
+
if(message.sequence&&message.sequence<=(state.sequence||0)&&!(navigation&&state.skippedUpdate?.buildId===message.buildId))return;
|
|
158
159
|
if (state.applying) {
|
|
159
160
|
if(!state.queued||(message.sequence||0)>=(state.queued.sequence||0))state.queued={...message,navigation};
|
|
160
161
|
const pending=state.applied;await pending;
|
|
@@ -174,7 +175,8 @@ async function applyUpdate(message, {navigation=false}={}) {
|
|
|
174
175
|
const refreshStarted=performance.now();
|
|
175
176
|
const localPath=location.pathname.slice((state.basePath||'').length)||'/';
|
|
176
177
|
const affected=message.affectedRoutes;
|
|
177
|
-
if(state.confirmed
|
|
178
|
+
if(canSkipDevelopmentUpdate({confirmed:state.confirmed,navigation,ensuring:state.ensuring,affectedRoutes:affected,routes:message.routes,pathname:localPath})){
|
|
179
|
+
state.skippedUpdate={buildId:message.buildId};
|
|
178
180
|
state.buildId=message.buildId;state.sequence=message.sequence;state.lastRefresh={buildId:message.buildId,durationMs:0,changed,skipped:true};
|
|
179
181
|
document.documentElement.dataset.prnextDevBuild=message.buildId;
|
|
180
182
|
if(hasCriticalIssue()&&state.issueKind==='build'){clearError();if(state.typeErrors)typeDiagnostics(state.typeErrors);}
|
|
@@ -195,12 +197,17 @@ async function applyUpdate(message, {navigation=false}={}) {
|
|
|
195
197
|
const route = kind === 'pages' ? manifest.pages.find(route => route.pattern === application.router.snapshot().pathname) : null;
|
|
196
198
|
const entry = kind === 'pages' ? route?.client : manifest.app?.client;
|
|
197
199
|
if (!entry) { reload('The current route changed. Page reloaded.'); return; }
|
|
198
|
-
if(!message.cssOnly)
|
|
200
|
+
if(!message.cssOnly){
|
|
201
|
+
await import(/* webpackIgnore: true */ entry);
|
|
202
|
+
// Activated deferred modules must register their updated component
|
|
203
|
+
// families before Refresh runs, even when their loader is already cached.
|
|
204
|
+
await Promise.all((manifest.lazy||[]).map(module=>import(/* webpackIgnore: true */ module.url)));
|
|
205
|
+
}
|
|
199
206
|
const pending = state.pending;
|
|
200
207
|
if (pending) await application.devPrepare?.(pending.options);
|
|
201
208
|
if (state.unsafe) { reload('An edited module exports values outside React components. Page reloaded.'); return; }
|
|
202
209
|
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);
|
|
210
|
+
else await updateStyles([...(kind === 'pages' ? route.css : application.devStyles?.(manifest.app.routes) || manifest.app.css),...(manifest.lazy||[]).flatMap(module=>module.css)]);
|
|
204
211
|
if (kind === 'app') application.devStylesApplied?.(manifest.app.routes, { commit: !!message.cssOnly || !pending });
|
|
205
212
|
// An unrelated type-only edit may publish a new generation without changing
|
|
206
213
|
// any browser module. Keep its existing runtime/hydration issues and current
|
|
@@ -216,6 +223,7 @@ async function applyUpdate(message, {navigation=false}={}) {
|
|
|
216
223
|
state.buildId = message.buildId;
|
|
217
224
|
if(message.sequence)state.sequence=message.sequence;
|
|
218
225
|
state.manifest = manifest;
|
|
226
|
+
state.skippedUpdate=undefined;
|
|
219
227
|
document.documentElement.dataset.prnextDevBuild = message.buildId;
|
|
220
228
|
state.lastRefresh={buildId:message.buildId,durationMs:Math.round(performance.now()-refreshStarted),changed};
|
|
221
229
|
await painted();state.confirmed=true;
|
|
@@ -272,7 +280,7 @@ function connect(options) {
|
|
|
272
280
|
const response=await fetch(`${options.basePath||''}/_prnext/ensure?client=${state.clientId}&path=${encodeURIComponent(url.pathname+url.search)}`,{cache:'no-store',credentials:'same-origin'});
|
|
273
281
|
if(!response.ok)throw Error(await response.text());
|
|
274
282
|
const update=await response.json();
|
|
275
|
-
if(update.buildId!==state.buildId)await applyUpdate(update,{navigation:true});
|
|
283
|
+
if(update.buildId!==state.buildId||state.skippedUpdate?.buildId===update.buildId)await applyUpdate(update,{navigation:true});
|
|
276
284
|
}finally{state.ensuring--;}
|
|
277
285
|
};
|
|
278
286
|
const source = new EventSource(`${options.basePath || ''}/_prnext/dev?client=${state.clientId}&path=${encodeURIComponent(location.pathname+location.search)}`);
|
|
@@ -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
|
}
|
package/runtime/dev-gateway.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import {spawn} from 'node:child_process';
|
|
2
2
|
|
|
3
3
|
export class DevGateway {
|
|
4
|
-
constructor(binary,root,{hostname,port,onEnsure,onOpen,onClose,onFatal}) {
|
|
4
|
+
constructor(binary,root,{hostname,port,onEnsure,onOpen,onClose,onFatal,onBeforeChild=()=>{},onChild=()=>{}}) {
|
|
5
5
|
this.next=0;this.pending=new Map();this.stopping=false;
|
|
6
|
-
|
|
6
|
+
const ticket=onBeforeChild();
|
|
7
|
+
this.child=spawn(binary,['dev-gateway',root,'--hostname',hostname,'--port',String(port)],{stdio:['pipe','pipe','inherit'],windowsHide:true});onChild(this.child,ticket);
|
|
7
8
|
this.closed=new Promise(resolve=>this.child.once('close',resolve));
|
|
8
9
|
this.ready=new Promise((resolve,reject)=>{
|
|
9
10
|
const timer=setTimeout(()=>{reject(Error('Development listener did not start'));this.child.kill();},15000);
|