@thomas.f/prnext 0.1.17-alpha → 0.1.18-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 (137) hide show
  1. package/README.md +26 -869
  2. package/build/actions.mjs +1 -1
  3. package/build/analyze.mjs +180 -24
  4. package/build/app-config.mjs +13 -0
  5. package/build/app-static.mjs +34 -12
  6. package/build/app-styles.mjs +79 -0
  7. package/build/app.mjs +81 -26
  8. package/build/bundle-runtime.mjs +22 -9
  9. package/build/cache-components.mjs +1 -1
  10. package/build/cache-handlers.mjs +1 -1
  11. package/build/compact-css.mjs +53 -0
  12. package/build/compilation-cache.mjs +269 -0
  13. package/build/compilation-store.mjs +142 -0
  14. package/build/compiler.mjs +31 -8
  15. package/build/config.mjs +30 -1
  16. package/build/create.mjs +239 -0
  17. package/build/css-modules.mjs +56 -12
  18. package/build/deployment-assets.mjs +89 -0
  19. package/build/deployment-config.mjs +18 -0
  20. package/build/dev-css-update.mjs +4 -2
  21. package/build/dev-dependencies.mjs +1 -1
  22. package/build/dev-discovery.mjs +3 -3
  23. package/build/dev.mjs +15 -8
  24. package/build/dynamic.mjs +1 -1
  25. package/build/edge.mjs +3 -3
  26. package/build/fonts.mjs +1 -1
  27. package/build/framework-imports.mjs +15 -5
  28. package/build/html-only-config.mjs +53 -0
  29. package/build/html-only-graph.mjs +67 -0
  30. package/build/index.mjs +59 -26
  31. package/build/instrumentation.mjs +30 -0
  32. package/build/metadata-routes.mjs +1 -1
  33. package/build/migration-audit.mjs +1 -1
  34. package/build/native-type-config.mjs +37 -0
  35. package/build/persistent-cache.mjs +18 -3
  36. package/build/precompress.mjs +1 -1
  37. package/build/prerender-cache.mjs +70 -0
  38. package/build/prerender-config.mjs +29 -0
  39. package/build/prerender-pages.mjs +19 -9
  40. package/build/prerender-pool.mjs +165 -0
  41. package/build/prerender-process-worker.mjs +58 -0
  42. package/build/prerender-worker.mjs +5 -5
  43. package/build/production-builder.mjs +58 -9
  44. package/build/react-compiler-report.mjs +225 -0
  45. package/build/react-compiler.mjs +139 -0
  46. package/build/runtime-contract.mjs +2 -2
  47. package/build/static-export.mjs +4 -4
  48. package/build/styles.mjs +6 -4
  49. package/build/transform.mjs +1 -1
  50. package/build/typed-routes.mjs +4 -0
  51. package/build/typescript-dev.mjs +35 -8
  52. package/build/typescript-native-worker.mjs +55 -0
  53. package/build/typescript-native.mjs +18 -0
  54. package/build/typescript-toolchain.mjs +19 -0
  55. package/build/typescript.mjs +6 -4
  56. package/build/webpack-graph.mjs +6 -3
  57. package/build/worker-memory.mjs +9 -0
  58. package/cli.mjs +60 -4
  59. package/compat/activity-cache.cjs +26 -0
  60. package/compat/after.cjs +6 -4
  61. package/compat/app-layout-context.cjs +52 -2
  62. package/compat/base64.cjs +10 -0
  63. package/compat/cache-handlers.cjs +12 -2
  64. package/compat/cache-wire.cjs +28 -0
  65. package/compat/cache.cjs +2 -1
  66. package/compat/cache.d.cts +4 -0
  67. package/compat/cookies.cjs +21 -5
  68. package/compat/data-cache.cjs +12 -8
  69. package/compat/flight-delta.cjs +150 -0
  70. package/compat/headers.cjs +4 -2
  71. package/compat/incremental-cache.cjs +20 -7
  72. package/compat/index.d.ts +68 -3
  73. package/compat/link.cjs +12 -9
  74. package/compat/navigation-authorization.cjs +9 -0
  75. package/compat/navigation-stages.cjs +19 -0
  76. package/compat/offline-store.cjs +11 -0
  77. package/compat/offline.cjs +5 -0
  78. package/compat/offline.d.cts +2 -0
  79. package/compat/redis-cache.cjs +201 -0
  80. package/compat/redis-cache.d.cts +37 -0
  81. package/compat/server-primitives.cjs +31 -11
  82. package/compat/server.d.cts +15 -2
  83. package/compat/static-generation.cjs +7 -0
  84. package/compat/use-cache.cjs +3 -0
  85. package/docs/ai/compatibility.md +171 -2
  86. package/docs/ai/data.md +165 -5
  87. package/docs/ai/deployment.md +158 -0
  88. package/docs/ai/quickstart.md +29 -4
  89. package/docs/ai/routing.md +82 -0
  90. package/docs/ai/troubleshooting.md +37 -2
  91. package/package.json +26 -7
  92. package/runtime/action-client.mjs +10 -4
  93. package/runtime/action-server.mjs +2 -0
  94. package/runtime/app-client.mjs +212 -65
  95. package/runtime/app-content.mjs +4 -3
  96. package/runtime/app-partial-client.mjs +3 -0
  97. package/runtime/app-partial.mjs +1 -1
  98. package/runtime/app-prefetch-constants.mjs +4 -0
  99. package/runtime/app-prefetch.mjs +2 -3
  100. package/runtime/app-render-key.mjs +5 -0
  101. package/runtime/app-render.mjs +88 -28
  102. package/runtime/app-routing.mjs +22 -9
  103. package/runtime/app-static.mjs +18 -8
  104. package/runtime/app-stream-html.mjs +10 -53
  105. package/runtime/app-style-selection.mjs +34 -0
  106. package/runtime/app-styles.mjs +154 -0
  107. package/runtime/cache-components-rsc.mjs +4 -1
  108. package/runtime/cache-request.mjs +2 -1
  109. package/runtime/deployment-client.mjs +19 -0
  110. package/runtime/dev-client.mjs +7 -2
  111. package/runtime/dev-compiler-broker.mjs +42 -0
  112. package/runtime/dev-indicator.mjs +8 -3
  113. package/runtime/dev-lazy-tool.mjs +16 -0
  114. package/runtime/development.mjs +29 -12
  115. package/runtime/document.mjs +1 -1
  116. package/runtime/edge.mjs +6 -5
  117. package/runtime/fetch-cache.mjs +10 -8
  118. package/runtime/flight-delta.mjs +50 -0
  119. package/runtime/flight-tap.mjs +28 -0
  120. package/runtime/html-only.mjs +27 -0
  121. package/runtime/http.mjs +2 -0
  122. package/runtime/incremental-cache.mjs +25 -4
  123. package/runtime/instrumentation.mjs +49 -0
  124. package/runtime/navigation-recovery.mjs +36 -0
  125. package/runtime/navigation.mjs +7 -0
  126. package/runtime/node-html-stream.mjs +47 -0
  127. package/runtime/pages-client.mjs +14 -7
  128. package/runtime/pages-navigation.mjs +48 -63
  129. package/runtime/prefetch-scheduler.mjs +110 -0
  130. package/runtime/profile.mjs +14 -0
  131. package/runtime/profiles.json +2 -2
  132. package/runtime/render.mjs +3 -0
  133. package/runtime/route-pattern.mjs +44 -0
  134. package/runtime/rsc-worker.mjs +34 -12
  135. package/runtime/trace.mjs +78 -0
  136. package/runtime/watch-project.mjs +19 -15
  137. package/runtime/worker.mjs +70 -26
@@ -0,0 +1,225 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { mkdir, open, writeFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+
5
+ const reports = new AsyncLocalStorage(), graphs = new AsyncLocalStorage();
6
+ const FILE_LIMIT = 1024, FUNCTION_LIMIT = 4096, EVENT_LIMIT = 16, TOTAL_EVENT_LIMIT = 8192;
7
+ const GRAPH_INPUT_LIMIT = 4096;
8
+ const REPORT_LIMIT = 8 * 1024 * 1024;
9
+ export const REACT_COMPILER_REPORT_FILE = 'react-compiler-report.json';
10
+ const statuses = new Set(['optimized', 'skipped', 'error', 'diagnostic', 'mixed']);
11
+ const kinds = new Set(['CompileSuccess', 'CompileSkip', 'CompileError', 'CompileDiagnostic', 'PipelineError']);
12
+ const slash = value => value.split(path.sep).join('/');
13
+ const position = value => value && Number.isSafeInteger(value.line) && value.line > 0 && Number.isSafeInteger(value.column) && value.column >= 0
14
+ ? { line: value.line, column: value.column } : null;
15
+ function text(value, root = '') {
16
+ if (typeof value !== 'string') return null;
17
+ return value.replaceAll(root || '\0', '<project>').replace(/https?:\/\/\S+/g, '')
18
+ .replace(/(?:file:\/\/\/)?[A-Za-z]:[\\/][^\s'")]+/g, '<path>')
19
+ .replace(/(?:^|\s)\/(?:[^\s/'"]+\/)+[^\s'")]+/g, ' <path>')
20
+ .replace(/[\u0000-\u001f\u007f]/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 384) || null;
21
+ }
22
+ function relativeFile(root, file) {
23
+ if (typeof root !== 'string' || typeof file !== 'string') return null;
24
+ const relative = path.relative(root, file);
25
+ return !relative || relative.length > 1024 || relative === '..' || relative.startsWith('..' + path.sep) || path.isAbsolute(relative) ? null : slash(relative);
26
+ }
27
+
28
+ /** Each build owns its collector. Failed/overlapping rebuilds cannot replace it. */
29
+ export const withReactCompilerReporting = run => reports.run({ files: new Map(), graphs: new Map(), functionCount: 0, eventCount: 0, truncated: false }, run);
30
+ export function configureReactCompilerReporting({ root, config, buildId, dev, compilerVersion }) {
31
+ const state = reports.getStore();
32
+ if (state) Object.assign(state, { root: path.resolve(root), enabled: !!config?.reactCompiler,
33
+ options: config?.reactCompiler || null, buildId, dev: !!dev, compilerVersion });
34
+ }
35
+ export const withReactCompilerGraph = (target, run) => graphs.run(target, run);
36
+
37
+ /** Capture a narrow, source-free subset of the pinned compiler's public events. */
38
+ export function compilerEvent(event, functions, { moduleOptOut = false, root = '' } = {}) {
39
+ if (!event || !kinds.has(event.kind)) return null;
40
+ const start = position(event.fnLoc?.start), end = position(event.fnLoc?.end);
41
+ const contains = fn => start && fn.start && fn.end && (fn.start.line < start.line || fn.start.line === start.line && fn.start.column <= start.column)
42
+ && (fn.end.line > start.line || fn.end.line === start.line && fn.end.column >= start.column);
43
+ const fn = functions.filter(contains).sort((a, b) => b.start.line - a.start.line || b.start.column - a.start.column)[0];
44
+ const detail = event.detail?.options || event.detail;
45
+ let status = event.kind === 'CompileSuccess' ? 'optimized' : event.kind === 'CompileSkip' ? 'skipped'
46
+ : event.kind === 'CompileDiagnostic' ? 'diagnostic' : 'error';
47
+ let reason = event.kind === 'PipelineError' ? 'React Compiler could not complete this function.' : text(event.reason ?? detail?.reason, root);
48
+ let category = text(detail?.category, root), policy = false;
49
+ // Compiler 1.0.0 emits CompileSuccess even when its module opt-out prevents
50
+ // emitting the compiled function. Success is not proof of emitted memoization.
51
+ if (moduleOptOut && event.kind === 'CompileSuccess') {
52
+ status = 'skipped'; reason = `Module uses "${moduleOptOut === true ? 'use no memo' : moduleOptOut}".`; category = 'Directive'; policy = true;
53
+ } else if (event.kind === 'CompileSkip' && fn?.optOut) {
54
+ reason = `Function uses "${fn.optOut === true ? 'use no memo' : fn.optOut}".`; category = 'Directive'; policy = true;
55
+ }
56
+ const output = { kind: event.kind, status, name: text(fn?.name || event.fnName, root),
57
+ start: fn?.start || start, end: fn?.end || end, reason, category, policy };
58
+ if (status === 'optimized') for (const key of ['memoSlots', 'memoBlocks', 'memoValues']) {
59
+ if (Number.isSafeInteger(event[key]) && event[key] >= 0) output[key] = event[key];
60
+ }
61
+ return output;
62
+ }
63
+
64
+ /** Cache payloads contain code and these immutable event values, never ASTs. */
65
+ export function recordReactCompilerTransform(file, payload, { cacheHit = false } = {}) {
66
+ const state = reports.getStore();
67
+ if (!state?.enabled) return;
68
+ const relative = relativeFile(state.root, file);
69
+ if (!relative) return;
70
+ if (!state.files.has(relative) && state.files.size >= FILE_LIMIT) { state.truncated = true; return; }
71
+ let record = state.files.get(relative);
72
+ if (!record) state.files.set(relative, record = { file: relative, transforms: 0, cacheReuses: 0,
73
+ targets: new Set(), notes: new Set(), functions: new Map() });
74
+ record.transforms++; record.cacheReuses += Number(cacheHit);
75
+ const target = graphs.getStore() || 'unspecified';
76
+ record.targets.add(target);
77
+ for (const note of payload.notes || []) record.notes.add(note);
78
+ for (const event of payload.events || []) {
79
+ const key = JSON.stringify([event.start, event.name]);
80
+ let fn = record.functions.get(key);
81
+ if (!fn) {
82
+ if (state.functionCount >= FUNCTION_LIMIT) { state.truncated = true; continue; }
83
+ record.functions.set(key, fn = { name: event.name, start: event.start, end: event.end,
84
+ statuses: new Set(), targets: new Set(), events: new Map() });
85
+ state.functionCount++;
86
+ }
87
+ fn.statuses.add(event.status); fn.targets.add(target);
88
+ const detail = { kind: event.kind, status: event.status, reason: event.reason, category: event.category,
89
+ ...(event.policy ? { policy: true } : {}), ...Object.fromEntries(['memoSlots', 'memoBlocks', 'memoValues']
90
+ .filter(key => event[key] !== undefined).map(key => [key, event[key]])) };
91
+ const detailKey = JSON.stringify(detail);
92
+ if (fn.events.has(detailKey)) { fn.events.get(detailKey).targets.add(target); continue; }
93
+ if (fn.events.size < EVENT_LIMIT && state.eventCount < TOTAL_EVENT_LIMIT) { fn.events.set(detailKey, { ...detail, targets: new Set([target]) }); state.eventCount++; }
94
+ else state.truncated = true;
95
+ }
96
+ if (payload.truncated) state.truncated = true;
97
+ }
98
+ export function recordReactCompilerGraph(result) {
99
+ const state = reports.getStore(), target = graphs.getStore();
100
+ if (!state?.enabled || !target) return;
101
+ let record = state.graphs.get(target);
102
+ if (!record) state.graphs.set(target, record = { target, inputs: new Set(), metadataAvailable: true });
103
+ if (!result?.metafile?.inputs) { record.metadataAvailable = false; return; }
104
+ for (const file of Object.keys(result.metafile.inputs)) {
105
+ if (!/\.(?:[cm]?[jt]s|[jt]sx)$/i.test(file)) continue;
106
+ const relative = relativeFile(state.root, path.resolve(state.root, file));
107
+ if (relative && !relative.split('/').some(part => part === 'node_modules' || part.startsWith('.prnext'))) {
108
+ if (record.inputs.size < GRAPH_INPUT_LIMIT || record.inputs.has(relative)) record.inputs.add(relative);
109
+ else { state.truncated = true; record.metadataAvailable = false; }
110
+ }
111
+ }
112
+ }
113
+ export function reactCompilerReport() {
114
+ const state = reports.getStore();
115
+ if (!state) return null;
116
+ const files = [...state.files.values()].sort((a, b) => a.file.localeCompare(b.file)).map(file => ({
117
+ file: file.file, transforms: file.transforms, cacheReuses: file.cacheReuses, targets: [...file.targets].sort(), notes: [...file.notes].sort(),
118
+ functions: [...file.functions.values()].map(fn => ({ name: fn.name, start: fn.start, end: fn.end,
119
+ status: fn.statuses.size === 1 ? [...fn.statuses][0] : 'mixed', targets: [...fn.targets].sort(),
120
+ events: [...fn.events.values()].map(({ targets, ...event }) => ({ ...event, targets: [...targets].sort() })) }))
121
+ .sort((a, b) => (a.start?.line || 0) - (b.start?.line || 0) || (a.start?.column || 0) - (b.start?.column || 0)),
122
+ }));
123
+ const summary = summarize(files);
124
+ return { version: 1, compilerVersion: state.compilerVersion || null, enabled: !!state.enabled,
125
+ buildId: state.buildId || null, mode: state.dev ? 'development' : 'production', scope: 'observed-transforms',
126
+ options: state.enabled ? state.options : null, truncated: state.truncated, summary, files,
127
+ graphs: [...state.graphs.values()].map(graph => ({ target: graph.target, metadataAvailable: graph.metadataAvailable,
128
+ projectJavaScriptInputs: graph.inputs.size, observedFiles: files.filter(file => file.targets.includes(graph.target)).length,
129
+ unobservedInputs: [...graph.inputs].filter(file => !state.files.get(file)?.targets.has(graph.target)).length })),
130
+ };
131
+ }
132
+
133
+ export async function writeReactCompilerReport(directory, report = reactCompilerReport()) {
134
+ if (!report) return null;
135
+ let serialized = JSON.stringify(report) + '\n';
136
+ // Developer diagnostics must never make an otherwise valid large app fail.
137
+ while (Buffer.byteLength(serialized) > REPORT_LIMIT && report.files.length) {
138
+ const files = report.files.slice(0, Math.floor(report.files.length / 2));
139
+ report = { ...report, truncated: true, files, summary: summarize(files), graphs: report.graphs.map(graph => ({ ...graph,
140
+ metadataAvailable: false, observedFiles: files.filter(file => file.targets.includes(graph.target)).length,
141
+ unobservedInputs: graph.projectJavaScriptInputs })) };
142
+ serialized = JSON.stringify(report) + '\n';
143
+ }
144
+ if (Buffer.byteLength(serialized) > REPORT_LIMIT) throw new Error('Invalid React Compiler report metadata.');
145
+ await mkdir(directory, { recursive: true });
146
+ await writeFile(path.join(directory, REACT_COMPILER_REPORT_FILE), serialized);
147
+ return report;
148
+ }
149
+ export async function readReactCompilerReport(directory, { buildId } = {}) {
150
+ let serialized, handle;
151
+ try {
152
+ handle = await open(path.join(directory, REACT_COMPILER_REPORT_FILE), 'r');
153
+ const stat = await handle.stat();
154
+ if (!stat.isFile() || stat.size > REPORT_LIMIT) throw new Error('Invalid React Compiler report: size limit exceeded.');
155
+ const buffer = Buffer.alloc(Math.min(stat.size + 1, REPORT_LIMIT + 1));
156
+ let length = 0;
157
+ while (length < buffer.length) {
158
+ const result = await handle.read(buffer, length, buffer.length - length, length);
159
+ if (!result.bytesRead) break;
160
+ length += result.bytesRead;
161
+ }
162
+ // A concurrently growing/replaced report is not a successful build artifact.
163
+ if (length > stat.size || (await handle.stat()).size !== stat.size) throw new Error('Invalid or stale React Compiler report. Rebuild the application.');
164
+ serialized = buffer.subarray(0, length);
165
+ } catch (error) { if (error.code === 'ENOENT') return null; throw error; }
166
+ finally { await handle?.close(); }
167
+ let value;
168
+ try { value = JSON.parse(serialized); } catch { throw new Error('Invalid React Compiler report JSON.'); }
169
+ if (!validReport(value) || buildId !== undefined && value.buildId !== buildId) throw new Error('Invalid or stale React Compiler report. Rebuild the application.');
170
+ return value;
171
+ }
172
+ function validReport(report) {
173
+ const keys = (value, allowed) => value && typeof value === 'object' && !Array.isArray(value) && Object.keys(value).every(key => allowed.includes(key));
174
+ const safePath = file => typeof file === 'string' && file.length <= 1024 && !file.includes('\\') && !/[\u0000-\u001f\u007f]/.test(file)
175
+ && !path.posix.isAbsolute(file) && !/^[A-Za-z]:/.test(file) && file.split('/').every(part => part && part !== '.' && part !== '..');
176
+ const boundedText = value => value === null || typeof value === 'string' && value.length <= 384 && !/[\u0000-\u001f\u007f]/.test(value);
177
+ const count = value => Number.isSafeInteger(value) && value >= 0;
178
+ const targets = value => Array.isArray(value) && value.length <= 4 && value.every(target => ['browser', 'node', 'edge', 'unspecified'].includes(target));
179
+ const validPosition = value => value === null || keys(value, ['line', 'column']) && !!position(value);
180
+ const options = report?.options;
181
+ const validOptions = options === null || options === true || keys(options, ['compilationMode', 'panicThreshold', 'target'])
182
+ && ['infer', 'annotation'].includes(options.compilationMode) && ['none', 'critical_errors', 'all_errors'].includes(options.panicThreshold) && options.target === '19';
183
+ return keys(report, ['version', 'compilerVersion', 'enabled', 'buildId', 'mode', 'scope', 'options', 'truncated', 'summary', 'files', 'graphs'])
184
+ && report.version === 1 && report.scope === 'observed-transforms' && typeof report.enabled === 'boolean' && validOptions
185
+ && typeof report.truncated === 'boolean' && ['development', 'production'].includes(report.mode)
186
+ && boundedText(report.buildId) && boundedText(report.compilerVersion)
187
+ && keys(report.summary, ['files', 'functions', 'optimized', 'skipped', 'errors', 'diagnostics', 'mixed', 'transforms', 'cacheReuses'])
188
+ && ['files', 'functions', 'optimized', 'skipped', 'errors', 'diagnostics', 'mixed', 'transforms', 'cacheReuses'].every(key => count(report.summary[key]))
189
+ && Array.isArray(report.files) && report.files.length <= FILE_LIMIT && report.summary.files === report.files.length
190
+ && report.files.every(file => file && typeof file === 'object')
191
+ && new Set(report.files.map(file => file.file)).size === report.files.length
192
+ && report.files.every(file => keys(file, ['file', 'transforms', 'cacheReuses', 'targets', 'notes', 'functions'])
193
+ && safePath(file.file) && count(file.transforms) && count(file.cacheReuses) && file.cacheReuses <= file.transforms && targets(file.targets)
194
+ && Array.isArray(file.notes) && file.notes.length <= 16 && file.notes.every(boundedText)
195
+ && Array.isArray(file.functions) && file.functions.length <= FUNCTION_LIMIT && file.functions.every(fn => keys(fn, ['name', 'start', 'end', 'status', 'targets', 'events'])
196
+ && boundedText(fn.name) && statuses.has(fn.status) && validPosition(fn.start) && validPosition(fn.end) && targets(fn.targets)
197
+ && Array.isArray(fn.events) && fn.events.length <= EVENT_LIMIT && fn.events.every(event => keys(event, ['kind', 'status', 'reason', 'category', 'policy', 'memoSlots', 'memoBlocks', 'memoValues', 'targets'])
198
+ && kinds.has(event.kind) && statuses.has(event.status) && boundedText(event.reason) && boundedText(event.category) && targets(event.targets)
199
+ && (event.policy === undefined || typeof event.policy === 'boolean')
200
+ && ['memoSlots', 'memoBlocks', 'memoValues'].every(key => event[key] === undefined || count(event[key])))))
201
+ && report.files.reduce((sum, file) => sum + file.functions.length, 0) <= FUNCTION_LIMIT
202
+ && Object.entries(summarize(report.files)).every(([key, value]) => report.summary[key] === value)
203
+ && (!report.enabled ? report.files.length === 0 && options === null : options !== null)
204
+ && Array.isArray(report.graphs) && report.graphs.length <= 4 && report.graphs.every(graph => keys(graph, ['target', 'metadataAvailable', 'projectJavaScriptInputs', 'observedFiles', 'unobservedInputs'])
205
+ && targets([graph.target]) && typeof graph.metadataAvailable === 'boolean' && ['projectJavaScriptInputs', 'observedFiles', 'unobservedInputs'].every(key => count(graph[key]))
206
+ && graph.unobservedInputs <= graph.projectJavaScriptInputs);
207
+ }
208
+ function summarize(files) {
209
+ const summary = { files: files.length, functions: 0, optimized: 0, skipped: 0, errors: 0, diagnostics: 0, mixed: 0, transforms: 0, cacheReuses: 0 };
210
+ for (const file of files) { summary.transforms += file.transforms; summary.cacheReuses += file.cacheReuses;
211
+ for (const fn of file.functions) { summary.functions++; summary[({ error: 'errors', diagnostic: 'diagnostics' })[fn.status] || fn.status]++; } }
212
+ return summary;
213
+ }
214
+ export function printReactCompilerReport(report, { details = false, log = console.log } = {}) {
215
+ if (!report.enabled) { log('React Compiler: disabled. Enable reactCompiler in the application configuration.'); return; }
216
+ const summary = report.summary;
217
+ log(`React Compiler ${report.compilerVersion}: ${summary.optimized} optimized, ${summary.skipped} skipped, ${summary.errors} errors, ${summary.diagnostics} diagnostics, ${summary.mixed} mixed.`);
218
+ log(`Observed transforms only: ${summary.files} files; ${summary.cacheReuses}/${summary.transforms} transforms reused cached compiler results.${report.truncated ? ' Report truncated at its safety limit.' : ''}`);
219
+ if (report.graphs.some(graph => graph.unobservedInputs || !graph.metadataAvailable)) log('Some graph inputs were not observed by React Compiler; totals do not describe every application component.');
220
+ if (details) for (const file of report.files) {
221
+ log(file.file);
222
+ for (const fn of file.functions) log(` ${fn.name || '(anonymous)'}${fn.start ? ':' + fn.start.line : ''}: ${fn.status}${fn.events.some(event => event.reason) ? ' — ' + [...new Set(fn.events.map(event => event.reason).filter(Boolean))].join('; ') : ''}`);
223
+ for (const note of file.notes) log(' ' + note);
224
+ }
225
+ }
@@ -0,0 +1,139 @@
1
+ import { realpath } from 'node:fs/promises';
2
+ import { createRequire } from 'node:module';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { cachedAsyncTransform } from './transform-cache.mjs';
6
+ import { extractSourceMap, inlineSourceMap, sourceMapsEnabled } from './source-maps.mjs';
7
+ import { compilerEvent, recordReactCompilerTransform } from './react-compiler-report.mjs';
8
+
9
+ const require = createRequire(import.meta.url);
10
+ // Only metadata is loaded here. Disabled builds do not load Babel or the compiler.
11
+ export const REACT_COMPILER_VERSION = require('babel-plugin-react-compiler/package.json').version;
12
+ const BABEL_VERSION = require('@babel/core/package.json').version;
13
+ const frameworkRoot = fileURLToPath(new URL('../', import.meta.url));
14
+ const options = new Set(['compilationMode', 'panicThreshold', 'target']);
15
+ let implementation;
16
+ const REPORT_CACHE_VERSION = 1;
17
+ const optOutDirective = directives => directives?.map(item => item.value.value).find(value => value === 'use no memo' || value === 'use no forget') || false;
18
+
19
+ function functionLocations(ast) {
20
+ const functions = [], stack = [{ node: ast.program, parent: null }];
21
+ while (stack.length && functions.length < 4096) {
22
+ const { node, parent } = stack.pop();
23
+ if (!node || typeof node !== 'object' || typeof node.type !== 'string') continue;
24
+ if (['FunctionDeclaration', 'FunctionExpression', 'ArrowFunctionExpression'].includes(node.type) && node.loc) {
25
+ const name = node.id?.name || (parent?.type === 'VariableDeclarator' ? parent.id?.name : null)
26
+ || (parent?.type === 'ObjectProperty' ? parent.key?.name : null);
27
+ functions.push({ name: name || null, start: { line: node.loc.start.line, column: node.loc.start.column },
28
+ end: { line: node.loc.end.line, column: node.loc.end.column },
29
+ optOut: optOutDirective(node.body?.directives) });
30
+ }
31
+ for (const [key, value] of Object.entries(node)) {
32
+ if (['loc', 'extra', 'leadingComments', 'innerComments', 'trailingComments', 'comments', 'tokens'].includes(key)) continue;
33
+ if (Array.isArray(value)) for (const child of value) stack.push({ node: child, parent: node });
34
+ else if (value && typeof value === 'object') stack.push({ node: value, parent: node });
35
+ }
36
+ }
37
+ return functions;
38
+ }
39
+
40
+ /** Deliberately expose only supported, serializable compiler options. */
41
+ export function normalizeReactCompilerConfig(value) {
42
+ if (value === undefined || value === false) return false;
43
+ const input = value === true ? {} : value;
44
+ if (!input || typeof input !== 'object' || Array.isArray(input) || ![Object.prototype, null].includes(Object.getPrototypeOf(input))) {
45
+ throw new TypeError('reactCompiler must be false, true or an options object.');
46
+ }
47
+ for (const key of Reflect.ownKeys(input)) {
48
+ if (!options.has(key)) throw new TypeError(`reactCompiler.${String(key)} is not supported by PRNext.`);
49
+ }
50
+ const compilationMode = input.compilationMode === undefined ? 'infer' : input.compilationMode;
51
+ const panicThreshold = input.panicThreshold === undefined ? 'none' : input.panicThreshold;
52
+ const target = input.target === undefined ? '19' : input.target;
53
+ if (!['infer', 'annotation'].includes(compilationMode)) throw new TypeError('reactCompiler.compilationMode must be infer or annotation.');
54
+ if (!['none', 'critical_errors', 'all_errors'].includes(panicThreshold)) throw new TypeError('reactCompiler.panicThreshold must be none, critical_errors or all_errors.');
55
+ if (target !== '19') throw new TypeError("reactCompiler.target must be '19'; PRNext requires React 19.");
56
+ return Object.freeze({ compilationMode, panicThreshold, target });
57
+ }
58
+
59
+ function inside(root, filename) {
60
+ const relative = path.relative(root, filename);
61
+ return relative !== '' && relative !== '..' && !relative.startsWith('..' + path.sep) && !path.isAbsolute(relative);
62
+ }
63
+
64
+ function excluded(root, filename) {
65
+ if (!inside(root, filename)) return true;
66
+ const relative = path.relative(root, filename).split(path.sep);
67
+ return relative.some(part => /^(?:node_modules|\.git|\.next|\.contentlayer|\.prnext|\.prnext-cache|\.prnext-dev|\.prnext-types|out|dist)$/.test(part)
68
+ || part.startsWith('.prnext-build-') || part.startsWith('.prnext-incremental-'));
69
+ }
70
+
71
+ function loadImplementation() {
72
+ implementation ??= Promise.all([import('@babel/core'), import('babel-plugin-react-compiler')]).then(([babel, plugin]) => ({
73
+ babel, compiler: plugin.default?.default ?? plugin.default,
74
+ }));
75
+ return implementation;
76
+ }
77
+
78
+ /**
79
+ * Run the official compiler on application source before JSX lowering, React
80
+ * Refresh and framework instrumentation. JSX/TypeScript/module format remain
81
+ * intact for the existing graph compiler. No app Babel configuration is loaded.
82
+ */
83
+ export function createReactCompilerTransform(projectRoot, config, { dev = false } = {}) {
84
+ const normalized = normalizeReactCompilerConfig(config);
85
+ if (!normalized) return async source => source;
86
+ const root = path.resolve(projectRoot);
87
+ const canonicalRoot = realpath(root).catch(error => {
88
+ if (error.code === 'ENOENT') return root;
89
+ throw error;
90
+ });
91
+ const canonicalFramework = realpath(frameworkRoot).catch(() => frameworkRoot);
92
+ return async (source, filename, { loaderOutput = false } = {}) => {
93
+ filename = path.resolve(filename);
94
+ if ((!loaderOutput && !/\.(?:[cm]?[jt]s|[jt]sx)$/i.test(filename)) || /\.d\.[cm]?ts$/i.test(filename)) return source;
95
+ const actualRoot = await canonicalRoot;
96
+ if (excluded(root, filename) && excluded(actualRoot, filename)) return source;
97
+ // Bundlers canonicalize linked project roots. Also resolve linked source
98
+ // files so a project symlink cannot pull dependencies/framework/generated
99
+ // files into the application compiler accidentally.
100
+ const actualFile = await realpath(filename).catch(error => {
101
+ if (error.code === 'ENOENT') return inside(root, filename) ? path.resolve(actualRoot, path.relative(root, filename)) : filename;
102
+ throw error;
103
+ });
104
+ if (excluded(actualRoot, actualFile) || inside(await canonicalFramework, actualFile)) return source;
105
+ let cacheHit = true;
106
+ const serialized = await cachedAsyncTransform('react-compiler-report', source,
107
+ [REPORT_CACHE_VERSION, REACT_COMPILER_VERSION, BABEL_VERSION, normalized, root, actualRoot, filename, actualFile, !!dev, !!loaderOutput], async () => {
108
+ cacheHit = false;
109
+ const { babel, compiler } = await loadImplementation();
110
+ const mapped = sourceMapsEnabled();
111
+ const { code, map } = extractSourceMap(source);
112
+ const syntax = { filename, babelrc: false, configFile: false, sourceType: 'unambiguous',
113
+ parserOpts: { plugins: ['jsx', ...(/\.(?:[cm]?ts|tsx)$/i.test(filename) ? ['typescript'] : [])] } };
114
+ const ast = await babel.parseAsync(code, syntax);
115
+ // Server Action files are executable server modules, not render graphs.
116
+ if (ast.program.directives.some(item => item.value.value === 'use server')) return JSON.stringify({ code: source, events: [], notes: ['Server Action module excluded.'] });
117
+ const functions = functionLocations(ast), events = [], notes = [];
118
+ const moduleOptOut = optOutDirective(ast.program.directives);
119
+ if (moduleOptOut) notes.push(`Module uses "${moduleOptOut}"; compiled functions are not emitted.`);
120
+ if (normalized.compilationMode === 'annotation') notes.push('Annotation mode only selects functions with "use memo". Unselected functions have no compiler events.');
121
+ if (loaderOutput) notes.push('Locations refer to JavaScript produced by the configured loader.');
122
+ let truncated = false;
123
+ const logger = { logEvent(_filename, event) {
124
+ const value = compilerEvent(event, functions, { moduleOptOut, root });
125
+ if (value) { if (events.length < 256) events.push(value); else truncated = true; }
126
+ } };
127
+ const result = await babel.transformFromAstAsync(ast, code, { ...syntax, cloneInputAst: false,
128
+ sourceMaps: mapped, sourceFileName: filename, inputSourceMap: mapped ? map ?? false : false,
129
+ // Boundary checks above use real path segments. Avoid the plugin's
130
+ // default substring test rejecting e.g. /my-node_modules-example/.
131
+ plugins: [[compiler, { ...normalized, sources: null, logger }]],
132
+ });
133
+ return JSON.stringify({ code: mapped ? inlineSourceMap(result.code, result.map) : result.code, events, notes, truncated });
134
+ });
135
+ const payload = JSON.parse(serialized);
136
+ recordReactCompilerTransform(inside(root, filename) ? filename : path.resolve(root, path.relative(actualRoot, actualFile)), payload, { cacheHit });
137
+ return payload.code;
138
+ };
139
+ }
@@ -5,12 +5,12 @@ import path from 'node:path';
5
5
  // Protocol compatibility does not imply safe retirement. These modules jointly
6
6
  // implement activity tracking, cancellation and post-response draining.
7
7
  export const lifecycleModules = ['worker.mjs', 'transport.mjs', 'request-work.mjs', 'app-render.mjs', 'app-stream-html.mjs', 'rsc-worker.mjs'];
8
- export async function writeRuntimeContract(stage) {
8
+ export async function writeRuntimeContract(stage, {deployment=false,htmlOnly=false,workerMemory=false,incrementalCache=false} = {}) {
9
9
  const files = (await Promise.all(['runtime', 'compat'].map(async directory =>
10
10
  (await readdir(path.join(stage,directory))).filter(name => /\.(?:mjs|cjs|json)$/.test(name)).map(name => `${directory}/${name}`)))).flat().sort();
11
11
  const modules = Object.fromEntries(await Promise.all(files.map(async name =>
12
12
  [name, createHash('sha256').update(await readFile(path.join(stage, name))).digest('hex')])));
13
- const contract = { schema: 1, protocol: 2, lifecycle: 1,
13
+ const contract = { schema: 1, protocol: 2, lifecycle: 1, ...(deployment ? {deployment:1} : {}), ...(htmlOnly ? {htmlOnly:1} : {}), ...(workerMemory ? {workerMemory:1} : {}), ...(incrementalCache ? {incrementalCache:1} : {}),
14
14
  capabilities: ['activity', 'tracked-finalization', 'graceful-drain', 'lane-cancellation'], modules };
15
15
  await writeFile(path.join(stage, 'runtime-contract.json'), JSON.stringify(contract, null, 2) + '\n');
16
16
  }
@@ -65,16 +65,16 @@ export async function prepareStaticExport({ project, stage, manifest, config })
65
65
  if (route?.kind === 'api') { await put(mount + pathname, body); continue; }
66
66
  let html = body.toString();
67
67
  if (html.includes(config.images.path + '?')) reject(`optimized images in ${pathname} require images.unoptimized or a custom loader`);
68
- html = html.replace(/<head(?:\s[^>]*)?>/i, value => value + '<script>window.__PRNEXT_STATIC_EXPORT__=true;</script>');
68
+ if (!route.htmlOnly) html = html.replace(/<head(?:\s[^>]*)?>/i, value => value + '<script>window.__PRNEXT_STATIC_EXPORT__=true;</script>');
69
69
  const output = pathname === '/' ? '/index.html' : config.trailingSlash && !['/404', '/500'].includes(pathname) ? pathname + '/index.html' : pathname + '.html';
70
70
  await put(mount + output, html);
71
71
  if (entry.dataFile) {
72
72
  const data = await readFile(path.join(stage, entry.dataFile));
73
- if (route?.router === 'app') await put(mount + '/_prnext/flight' + (pathname === '/' ? '' : pathname) + '/index.txt', data);
74
- else for (const prefix of ['/_prnext/data/', '/_next/data/']) await put(mount + prefix + manifest.buildId + (pathname === '/' ? '/index' : pathname) + '.json', data);
73
+ if (route?.router === 'app') await put(mount + '/_prnext/flight' + (manifest.config.deploymentId ? '/' + encodeURIComponent(manifest.config.deploymentId) : '') + (pathname === '/' ? '' : pathname) + '/index.txt', data);
74
+ else for (const prefix of ['/_prnext/data/', '/_next/data/']) await put(mount + prefix + encodeURIComponent(manifest.config.deploymentId ? manifest.buildId + '~' + manifest.config.deploymentId : manifest.buildId) + (pathname === '/' ? '/index' : pathname) + '.json', data);
75
75
  }
76
76
  }
77
- await writeFile(path.join(folder, marker), JSON.stringify({ version: 1, buildId: manifest.buildId, basePath: mount, trailingSlash: config.trailingSlash }));
77
+ await writeFile(path.join(folder, marker), JSON.stringify({ version: 1, buildId: manifest.buildId, deploymentId: manifest.config.deploymentId, basePath: mount, trailingSlash: config.trailingSlash }));
78
78
  return folder;
79
79
  }
80
80
 
package/build/styles.mjs CHANGED
@@ -39,10 +39,12 @@ async function loadConfiguration(root) {
39
39
  return /\.(?:js|mjs|cjs)$/.test(filename) ? importConfiguration(filename) : JSON.parse(await readFile(filename, 'utf8'));
40
40
  }
41
41
 
42
- async function createProcessor(root, engine) {
42
+ export const resolveStandardBrowserTargets = root => browserslist(undefined,{path:path.resolve(root)});
43
+
44
+ async function createProcessor(root, engine, browserTargets) {
43
45
  const config = await loadConfiguration(root);
44
46
  if (config === undefined && engine !== 'postcss') {
45
- const targets=browserslistToTargets(browserslist(undefined,{path:root}));
47
+ const targets=browserslistToTargets(browserTargets ? await browserTargets() : resolveStandardBrowserTargets(root));
46
48
  return {async process(css,options){
47
49
  const result=transformCss({filename:options.from,code:Buffer.from(css),targets,minify:false,sourceMap:!!options.map,
48
50
  ...(options.map?.prev ? {inputSourceMap:JSON.stringify(options.map.prev)} : {}),drafts:{customMedia:true}});
@@ -134,7 +136,7 @@ function packageImporter(require) {
134
136
  }
135
137
 
136
138
  /** Build-only transformations, shared and deduplicated across all three React graphs. */
137
- export function createStyleProcessor({ projectRoot, sassOptions = {}, cssEngine = 'auto' }) {
139
+ export function createStyleProcessor({ projectRoot, sassOptions = {}, cssEngine = 'auto', browserTargets }) {
138
140
  const root = path.resolve(projectRoot);
139
141
  const require = createRequire(path.join(root, 'package.json'));
140
142
  const cache = new Map();
@@ -168,7 +170,7 @@ export function createStyleProcessor({ projectRoot, sassOptions = {}, cssEngine
168
170
  sourceMap = compiled.sourceMap;
169
171
  for (const url of compiled.loadedUrls) if (url.protocol === 'file:') dependencies.add(fileURLToPath(url));
170
172
  }
171
- processor ??= createProcessor(root,cssEngine);
173
+ processor ??= createProcessor(root,cssEngine,browserTargets);
172
174
  const processed = await (await processor).process(css, { from: file, to: file, map: sourceMap ? { prev: sourceMap, inline: false, annotation: false } : false });
173
175
  for (const message of processed.messages) if (message.type === 'dependency' && message.file) dependencies.add(path.resolve(root, message.file));
174
176
  rebaseReferences(processed.root, file);
@@ -51,7 +51,7 @@ export async function rewriteServerChunks(file,chunks,publicPath){
51
51
  }
52
52
 
53
53
  function parserOptions(filename) {
54
- return { sourceType: 'unambiguous', sourceFilename: filename, plugins: ['jsx', ...( /\.tsx?$/.test(filename) ? ['typescript'] : [])] };
54
+ return { sourceType: 'unambiguous', sourceFilename: filename, plugins: ['jsx', ...( /\.[cm]?tsx?$/.test(filename) ? ['typescript'] : [])] };
55
55
  }
56
56
 
57
57
  export function assertSupportedSource(source, filename) {
@@ -3,6 +3,8 @@ import {createRequire} from 'node:module';
3
3
  import {randomUUID} from 'node:crypto';
4
4
  import path from 'node:path';
5
5
  import {fileURLToPath} from 'node:url';
6
+ import {resolveTypeScript} from './typescript-toolchain.mjs';
7
+ import {nativeTypeConfig} from './native-type-config.mjs';
6
8
 
7
9
  const marker='// Generated by PRNext: typed route registry. Do not edit.';
8
10
  async function changed(file,contents,updates){
@@ -52,6 +54,8 @@ async function bindTypes(filename,ts,updates){
52
54
  async function includeTypes(root,config,helper,updates){
53
55
  const filename=path.resolve(root,config.typescript?.tsconfigPath||'tsconfig.json');
54
56
  const source=await readFile(filename,'utf8').catch(error=>{if(error.code==='ENOENT')return;throw error;});if(source===undefined)return;
57
+ const toolchain=await resolveTypeScript(root);
58
+ if(toolchain.native){await changed(filename,await nativeTypeConfig(filename,helper),updates);return;}
55
59
  let ts;try{ts=createRequire(path.join(root,'package.json'))('typescript');}catch{throw Error('typedRoutes requires TypeScript installed in the application');}
56
60
  const parsed=ts.getParsedCommandLineOfConfigFile(filename,{noEmit:true},{...ts.sys,onUnRecoverableConfigFileDiagnostic:diagnostic=>{throw Error(ts.flattenDiagnosticMessageText(diagnostic.messageText,'\n'));}});
57
61
  if(!parsed||parsed.errors.some(error=>error.code!==18003))throw Error('Cannot configure typed routes: invalid tsconfig');
@@ -1,22 +1,49 @@
1
1
  import {fork} from 'node:child_process';
2
2
  import {access} from 'node:fs/promises';
3
- import {createRequire} from 'node:module';
3
+ import {resolveTypeScript} from './typescript-toolchain.mjs';
4
4
  import path from 'node:path';
5
5
  import {privateCacheDirectory} from './persistent-cache.mjs';
6
- export async function startTypeChecker(root,config,notify){
6
+ export async function startTypeChecker(root,config,notify,{fork:spawnChecker=fork,recheckDelayMs=3000}={}){
7
7
  const noop={inactive:true,async close(){}};
8
8
  if(config.typescript?.devChecks===false)return noop;
9
9
  const filename=path.resolve(root,config.typescript?.tsconfigPath||'tsconfig.json');try{await access(filename);}catch{return noop;}
10
- let compiler;try{compiler=createRequire(path.join(root,'package.json')).resolve('typescript');}catch{console.warn('TypeScript diagnostics require typescript installed in the application.');return noop;}
10
+ let toolchain;try{toolchain=await resolveTypeScript(root);}catch(error){console.warn('TypeScript diagnostics could not start: '+error.message);return noop;}
11
11
  const directory=await privateCacheDirectory(root,'typescript-dev');
12
- let child,closed=false,failures=0,timer;
12
+ let child,closed=false,failures=0,timer,restartTimer,planned,pendingInvalidation;
13
13
  function launch(){
14
14
  if(closed)return;
15
- child=fork(new URL('./typescript-dev-worker.mjs',import.meta.url),[compiler,filename,directory?path.join(directory,'project.tsbuildinfo'):''],{cwd:root,execArgv:['--max-old-space-size=512'],stdio:['ignore','ignore','pipe','ipc'],windowsHide:true});
15
+ const worker=toolchain.native?'./typescript-native-worker.mjs':'./typescript-dev-worker.mjs';
16
+ child=spawnChecker(new URL(worker,import.meta.url),[toolchain.compiler,filename,toolchain.native?'watch':directory?path.join(directory,'project.tsbuildinfo'):''],{cwd:root,execArgv:['--max-old-space-size=512'],stdio:['ignore','ignore','pipe','ipc'],windowsHide:true});
17
+ const current=child;
16
18
  let details='',healthyAt=0;child.stderr.on('data',chunk=>{details=(details+chunk).slice(-2048);});
17
- child.on('message',message=>{if(message?.type==='diagnostics'&&Array.isArray(message.errors)){healthyAt ||= Date.now();notify(message.errors.filter(value=>typeof value==='string').slice(0,40));}});
19
+ child.on('message',message=>{
20
+ if(closed||child!==current||planned===current)return;
21
+ if(message?.type==='diagnostics'&&Array.isArray(message.errors)){
22
+ // The native watcher normally handles edits incrementally. Its startup
23
+ // can miss an edit; retain one bounded fallback when no report arrives.
24
+ if(pendingInvalidation?.worker===current&&!pendingInvalidation.forced){clearTimeout(restartTimer);pendingInvalidation=undefined;}
25
+ healthyAt ||= Date.now();notify(message.errors.filter(value=>typeof value==='string').slice(0,40));
26
+ }
27
+ });
18
28
  child.on('error',error=>{notify(['TypeScript checker: '+error.message]);});
19
- child.once('exit',()=>{if(closed)return;if(healthyAt&&Date.now()-healthyAt>=60000)failures=0;if(++failures>=3){notify(['TypeScript checker stopped after repeated failures. '+details]);return;}timer=setTimeout(launch,1000*failures);});
29
+ child.once('exit',()=>{
30
+ if(pendingInvalidation?.worker===current){clearTimeout(restartTimer);pendingInvalidation=undefined;}
31
+ if(closed)return;if(planned===current){planned=undefined;launch();return;}if(healthyAt&&Date.now()-healthyAt>=60000)failures=0;if(++failures>=3){notify(['TypeScript checker stopped after repeated failures. '+details]);return;}timer=setTimeout(launch,1000*failures);
32
+ });
20
33
  }
21
- launch();return {async close(){closed=true;clearTimeout(timer);const old=child;if(!old||old.exitCode!==null||old.signalCode)return;await new Promise(resolve=>{const timer=setTimeout(()=>old.kill('SIGKILL'),3000);old.once('exit',()=>{clearTimeout(timer);resolve();});if(old.connected)old.disconnect();else old.kill();});}};
34
+ launch();return {
35
+ invalidate(file,event){
36
+ if(!toolchain.native||closed||!child?.connected)return;
37
+ const extension=path.extname(file);if(extension&&!/^\.(?:[cm]?[jt]sx?|json)$/i.test(extension))return;
38
+ const forced=event==='rename'||/\.json$/i.test(file);
39
+ if(!forced&&event!=='change')return;
40
+ clearTimeout(restartTimer);
41
+ const pending={worker:child,forced:forced||pendingInvalidation?.forced===true};pendingInvalidation=pending;
42
+ restartTimer=setTimeout(()=>{
43
+ if(closed||pendingInvalidation!==pending||child!==pending.worker||!child?.connected)return;
44
+ pendingInvalidation=undefined;planned=child;child.disconnect();
45
+ },pending.forced?150:recheckDelayMs);
46
+ },
47
+ async close(){closed=true;clearTimeout(timer);clearTimeout(restartTimer);const old=child;if(!old||old.exitCode!==null||old.signalCode)return;await new Promise(resolve=>{const timer=setTimeout(()=>old.kill('SIGKILL'),5000);old.once('exit',()=>{clearTimeout(timer);resolve();});if(old.connected)old.disconnect();else old.kill();});},
48
+ };
22
49
  }
@@ -0,0 +1,55 @@
1
+ import {spawn} from 'node:child_process';
2
+ import {StringDecoder} from 'node:string_decoder';
3
+ import {mkdtemp,rm} from 'node:fs/promises';
4
+ import {tmpdir} from 'node:os';
5
+ import path from 'node:path';
6
+
7
+ const [compiler,filename,mode]=process.argv.slice(2);
8
+ const watching=mode==='watch';
9
+ let closing=false,buffer='',output='',overflow=false;
10
+ const cache=await mkdtemp(path.join(tmpdir(),'prnext-typescript-native-'));
11
+ const child=spawn(process.execPath,[compiler,'--project',filename,'--noEmit','--incremental','true','--tsBuildInfoFile',path.join(cache,'project.tsbuildinfo'),'--pretty','false','--locale','en',...(watching?['--watch','--preserveWatchOutput']:[])],{
12
+ cwd:process.cwd(),windowsHide:true,detached:process.platform!=='win32',stdio:['ignore','pipe','pipe'],
13
+ // Native TS respects explicit project watch options. The polling defaults
14
+ // also cover editors/filesystems that silently lose native watch events.
15
+ env:{...process.env,TSC_WATCHFILE:process.env.TSC_WATCHFILE||'DynamicPriorityPolling',TSC_WATCHDIRECTORY:process.env.TSC_WATCHDIRECTORY||'DynamicPriorityPolling'},
16
+ });
17
+ function send(value){if(process.connected)process.send(value);}
18
+ function consume(chunk){
19
+ const text=chunk.toString('utf8').replace(/\x1b\[[0-9;]*[A-Za-z]/g,'');
20
+ if(!watching){output+=text;if(output.length>4*1024*1024){overflow=true;void close();}return;}
21
+ buffer+=text;
22
+ // Consume complete lines, not arbitrary OS chunks. Start/completion markers
23
+ // delimit each diagnostic generation; a successful edit clears old errors.
24
+ let end;
25
+ while((end=buffer.indexOf('\n'))!==-1){
26
+ const line=buffer.slice(0,end).replace(/\r$/,'');buffer=buffer.slice(end+1);
27
+ if(/Starting (?:incremental )?compilation in watch mode|File change detected/.test(line))output='';
28
+ else if(/Found \d+ errors?\. Watching for file changes\./.test(line)){
29
+ send({type:'diagnostics',errors:/Found 0 errors/.test(line)?[]:output.trim()?[output.trim()]:['TypeScript reported errors. Run prnext typecheck for details.']});output='';
30
+ }else if(line.trim()&&output.length<20000)output+=(line+'\n').slice(0,20000-output.length);
31
+ }
32
+ if(buffer.length>20000)buffer=buffer.slice(-20000);
33
+ }
34
+ for(const stream of [child.stdout,child.stderr]){
35
+ const decoder=new StringDecoder('utf8');stream.on('data',chunk=>consume(decoder.write(chunk)));stream.on('end',()=>consume(decoder.end()));
36
+ }
37
+ child.once('error',error=>{send({type:'failure',message:error.message});process.exitCode=1;});
38
+ child.once('close',async(code,signal)=>{
39
+ // Only the directory returned by mkdtemp above is removed.
40
+ if(path.dirname(cache)===path.resolve(tmpdir())&&path.basename(cache).startsWith('prnext-typescript-native-'))await rm(cache,{recursive:true,force:true,maxRetries:5,retryDelay:100});
41
+ send({type:'complete',code:overflow?1:code??1,output:overflow?'TypeScript output exceeded 4 MiB. Run tsc directly for the complete output.':output,signal});
42
+ if(process.connected)process.disconnect();
43
+ });
44
+ async function close(){
45
+ if(closing)return;closing=true;
46
+ if(child.exitCode!==null||child.signalCode)return;
47
+ if(process.platform==='win32'){
48
+ // Own exactly this compiler subtree; TypeScript's official Node launcher
49
+ // starts its native executable as a child on Windows.
50
+ const killer=spawn('taskkill',['/PID',String(child.pid),'/T','/F'],{windowsHide:true,stdio:'ignore'});
51
+ killer.on('error',()=>child.kill('SIGKILL'));
52
+ }else{try{process.kill(-child.pid,'SIGTERM');}catch{child.kill('SIGTERM');}}
53
+ }
54
+ process.once('disconnect',()=>{void close();});
55
+ process.once('SIGTERM',()=>{void close();});process.once('SIGINT',()=>{void close();});
@@ -0,0 +1,18 @@
1
+ import {fork} from 'node:child_process';
2
+
3
+ export function checkNativeTypeScript(compiler,filename,root){
4
+ return new Promise((resolve,reject)=>{
5
+ const child=fork(new URL('./typescript-native-worker.mjs',import.meta.url),[compiler,filename,'check'],{cwd:root,windowsHide:true,execArgv:[],stdio:['ignore','ignore','pipe','ipc']});
6
+ let result,details='',timedOut=false,force;
7
+ const deadline=setTimeout(()=>{timedOut=true;if(child.connected)child.disconnect();force=setTimeout(()=>child.kill('SIGKILL'),5000);},120000);
8
+ child.stderr.on('data',chunk=>{details=(details+chunk).slice(-8192);});
9
+ child.on('message',message=>{if(message?.type==='complete')result=message;else if(message?.type==='failure')details=String(message.message).slice(0,8192);});
10
+ child.once('error',error=>{clearTimeout(deadline);clearTimeout(force);reject(error);});
11
+ child.once('exit',()=>{
12
+ clearTimeout(deadline);clearTimeout(force);
13
+ if(timedOut)reject(new Error('TypeScript check exceeded 120 seconds. Run tsc directly to inspect the project.'));
14
+ else if(result?.code===0)resolve();
15
+ else reject(new Error(result?.output||details||'TypeScript native checker exited before returning diagnostics.'));
16
+ });
17
+ });
18
+ }