@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.
Files changed (162) hide show
  1. package/README.md +2 -0
  2. package/build/actions.mjs +26 -14
  3. package/build/app.mjs +90 -101
  4. package/build/build-lock.mjs +2 -71
  5. package/build/build-retention.mjs +119 -0
  6. package/build/bundle-runtime.mjs +9 -8
  7. package/build/cache-command.mjs +31 -0
  8. package/build/check.mjs +41 -3
  9. package/build/compilation-cache.mjs +32 -12
  10. package/build/compilation-storage.mjs +190 -0
  11. package/build/compiler.mjs +8 -7
  12. package/build/config.mjs +25 -2
  13. package/build/create.mjs +1 -1
  14. package/build/css-modules.mjs +4 -0
  15. package/build/dev-action-graph.mjs +21 -0
  16. package/build/dev-css-assets.mjs +173 -0
  17. package/build/dev-css-update.mjs +14 -12
  18. package/build/dev-lazy-build.mjs +77 -0
  19. package/build/dynamic.mjs +13 -6
  20. package/build/font-cache.mjs +9 -6
  21. package/build/font-files.mjs +1 -1
  22. package/build/incremental.mjs +112 -18
  23. package/build/index.mjs +61 -15
  24. package/build/lazy-dynamic-imports.mjs +137 -0
  25. package/build/migrate.mjs +4 -4
  26. package/build/migration-audit.mjs +19 -31
  27. package/build/migration-imports.mjs +245 -0
  28. package/build/migration-scenarios-runner.mjs +73 -0
  29. package/build/migration-scenarios.mjs +72 -0
  30. package/build/native-discovery.mjs +65 -5
  31. package/build/native-packages.mjs +2 -0
  32. package/build/native-ppr.mjs +32 -0
  33. package/build/npm-packages.mjs +1 -0
  34. package/build/og.mjs +121 -1
  35. package/build/parallel-plan.mjs +55 -0
  36. package/build/persistent-cache.mjs +29 -24
  37. package/build/production-builder.mjs +1 -1
  38. package/build/production-check.mjs +34 -0
  39. package/build/react-compiler-report.mjs +31 -8
  40. package/build/react-compiler.mjs +101 -10
  41. package/build/server-config.mjs +35 -0
  42. package/build/source-analysis.mjs +54 -4
  43. package/build/standalone.mjs +21 -3
  44. package/build/static-adapter.mjs +7 -3
  45. package/build/transform.mjs +38 -6
  46. package/build/typescript-dev.mjs +3 -1
  47. package/build/webpack-graph.mjs +13 -3
  48. package/cli.mjs +24 -6
  49. package/compat/app-dynamic.cjs +12 -1
  50. package/compat/app-layout-context.cjs +7 -3
  51. package/compat/cache-handlers.cjs +3 -1
  52. package/compat/flight-delta.cjs +50 -13
  53. package/compat/index.d.ts +24 -1
  54. package/compat/link.cjs +2 -2
  55. package/compat/navigation-authorization.cjs +8 -1
  56. package/docs/ai/compatibility.md +187 -7
  57. package/docs/ai/deployment.md +141 -0
  58. package/docs/ai/quickstart.md +5 -0
  59. package/docs/ai/routing.md +11 -0
  60. package/docs/ai/troubleshooting.md +79 -0
  61. package/package.json +26 -11
  62. package/release/fontkit.mjs +31 -0
  63. package/release/verify.mjs +3 -0
  64. package/runtime/api.mjs +5 -2
  65. package/runtime/app-client.mjs +254 -53
  66. package/runtime/app-partial.mjs +2 -10
  67. package/runtime/app-prefetch.mjs +2 -2
  68. package/runtime/app-render.mjs +11 -7
  69. package/runtime/app-stream-channel.mjs +3 -2
  70. package/runtime/app-styles.mjs +22 -4
  71. package/runtime/atomic-file.mjs +27 -0
  72. package/runtime/cache-components-rsc.mjs +3 -1
  73. package/runtime/compile-cache-retention.mjs +53 -0
  74. package/runtime/compile-cache.mjs +28 -1
  75. package/runtime/dev-client.mjs +13 -5
  76. package/runtime/dev-file-changes.mjs +123 -8
  77. package/runtime/dev-gateway.mjs +3 -2
  78. package/runtime/dev-lazy-imports.mjs +121 -0
  79. package/runtime/dev-lazy-staging.mjs +14 -0
  80. package/runtime/dev-route-selection.mjs +64 -0
  81. package/runtime/dev-runtime-pool.mjs +3 -2
  82. package/runtime/dev-runtime-spare.mjs +1 -1
  83. package/runtime/dev-session-retention.mjs +154 -0
  84. package/runtime/dev-style-retention.mjs +15 -0
  85. package/runtime/development.mjs +86 -26
  86. package/runtime/env.mjs +9 -0
  87. package/runtime/flight-delta.mjs +59 -9
  88. package/runtime/fs.mjs +15 -8
  89. package/runtime/lazy-import-client.mjs +38 -0
  90. package/runtime/native-ppr.mjs +35 -0
  91. package/runtime/og/NOTICE.txt +1 -1
  92. package/runtime/og/index.node.mjs +76 -31216
  93. package/runtime/og/upstream.json +115 -0
  94. package/runtime/persistent/client.mjs +4 -2
  95. package/runtime/persistent/daemon.mjs +3 -11
  96. package/runtime/persistent/health.mjs +58 -0
  97. package/runtime/project-lock.mjs +71 -0
  98. package/runtime/request-transport.mjs +112 -32
  99. package/runtime/storage-path.mjs +36 -0
  100. package/runtime/trace.mjs +44 -4
  101. package/runtime/worker.mjs +9 -4
  102. package/vendor/fontkit/NOTICE.txt +5 -0
  103. package/vendor/fontkit/SOURCE-NOTICES.txt +209 -0
  104. package/vendor/fontkit/index.mjs +3 -0
  105. package/vendor/fontkit/licenses/@swc__helpers/LICENSE +201 -0
  106. package/vendor/fontkit/licenses/base64-js/LICENSE +21 -0
  107. package/vendor/fontkit/licenses/brotli/readme.md +61 -0
  108. package/vendor/fontkit/licenses/clone/LICENSE +18 -0
  109. package/vendor/fontkit/licenses/dfa/README.md +83 -0
  110. package/vendor/fontkit/licenses/fast-deep-equal/LICENSE +21 -0
  111. package/vendor/fontkit/licenses/fontkit/README.md +266 -0
  112. package/vendor/fontkit/licenses/restructure/LICENSE +21 -0
  113. package/vendor/fontkit/licenses/tiny-inflate/LICENSE +21 -0
  114. package/vendor/fontkit/licenses/tslib/LICENSE.txt +12 -0
  115. package/vendor/fontkit/licenses/unicode-properties/LICENSE +8 -0
  116. package/vendor/fontkit/licenses/unicode-trie/LICENSE +8 -0
  117. package/vendor/fontkit/upstream.json +215 -0
  118. package/vendor/nft/LICENSE +7 -0
  119. package/vendor/nft/README.md +16 -0
  120. package/vendor/nft/out/analyze.d.ts +8 -0
  121. package/vendor/nft/out/analyze.js +1216 -0
  122. package/vendor/nft/out/analyze.js.map +1 -0
  123. package/vendor/nft/out/cli.d.ts +2 -0
  124. package/vendor/nft/out/cli.js +94 -0
  125. package/vendor/nft/out/cli.js.map +1 -0
  126. package/vendor/nft/out/fs.d.ts +22 -0
  127. package/vendor/nft/out/fs.js +106 -0
  128. package/vendor/nft/out/fs.js.map +1 -0
  129. package/vendor/nft/out/index.d.ts +4 -0
  130. package/vendor/nft/out/index.js +25 -0
  131. package/vendor/nft/out/index.js.map +1 -0
  132. package/vendor/nft/out/node-file-trace.d.ts +41 -0
  133. package/vendor/nft/out/node-file-trace.js +380 -0
  134. package/vendor/nft/out/node-file-trace.js.map +1 -0
  135. package/vendor/nft/out/resolve-dependency.d.ts +6 -0
  136. package/vendor/nft/out/resolve-dependency.js +427 -0
  137. package/vendor/nft/out/resolve-dependency.js.map +1 -0
  138. package/vendor/nft/out/types.d.ts +66 -0
  139. package/vendor/nft/out/types.js +2 -0
  140. package/vendor/nft/out/types.js.map +1 -0
  141. package/vendor/nft/out/utils/ast-helpers.js +49 -0
  142. package/vendor/nft/out/utils/ast-helpers.js.map +1 -0
  143. package/vendor/nft/out/utils/binary-locators.js +88 -0
  144. package/vendor/nft/out/utils/binary-locators.js.map +1 -0
  145. package/vendor/nft/out/utils/get-package-base.js +30 -0
  146. package/vendor/nft/out/utils/get-package-base.js.map +1 -0
  147. package/vendor/nft/out/utils/interop-require.js +24 -0
  148. package/vendor/nft/out/utils/interop-require.js.map +1 -0
  149. package/vendor/nft/out/utils/node-version.js +10 -0
  150. package/vendor/nft/out/utils/node-version.js.map +1 -0
  151. package/vendor/nft/out/utils/sharedlib-emit.js +33 -0
  152. package/vendor/nft/out/utils/sharedlib-emit.js.map +1 -0
  153. package/vendor/nft/out/utils/special-cases.js +347 -0
  154. package/vendor/nft/out/utils/special-cases.js.map +1 -0
  155. package/vendor/nft/out/utils/static-eval.js +579 -0
  156. package/vendor/nft/out/utils/static-eval.js.map +1 -0
  157. package/vendor/nft/out/utils/types.js +2 -0
  158. package/vendor/nft/out/utils/types.js.map +1 -0
  159. package/vendor/nft/out/utils/wrappers.js +659 -0
  160. package/vendor/nft/out/utils/wrappers.js.map +1 -0
  161. package/vendor/nft/package.json +4 -0
  162. package/vendor/nft/upstream.json +51 -0
@@ -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, randomBytes } from 'node:crypto';
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 : developmentAuthorizationSecret, 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
+ 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) {
@@ -32,7 +32,23 @@ export function appStyleManager(document, { timeout = 15_000 } = {}) {
32
32
  if (!records.length) sheets.delete(href);
33
33
  }
34
34
  }
35
- function acquire(href, before, replace = false) {
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 : [...document.querySelectorAll('link[rel="stylesheet"]')].find(link => link.href === href && !tracked.has(link));
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
- 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(); } }), {
@@ -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()&&current.ino===file.ino&&current.dev===file.dev&&current.mtimeMs===file.time&&current.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(()=>{pending=undefined;try{flushCompileCache();}catch{}},1000);pending.unref();
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
  }
@@ -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&&affected&&!affected.some(pattern=>pattern.includes('[')||pattern===localPath||pattern+'/'===localPath)){
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)await import(/* webpackIgnore: true */ entry);
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) || 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
  }
@@ -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
- this.child=spawn(binary,['dev-gateway',root,'--hostname',hostname,'--port',String(port)],{stdio:['pipe','pipe','inherit'],windowsHide:true});
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);