@thomas.f/prnext 0.1.16-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.
- package/AI.md +63 -0
- package/README.md +35 -503
- package/build/ADAPTERS.md +27 -2
- package/build/actions.mjs +6 -1
- package/build/analyze.mjs +340 -0
- package/build/app-config.mjs +20 -8
- package/build/app-static.mjs +41 -14
- package/build/app-styles.mjs +79 -0
- package/build/app.mjs +99 -31
- package/build/build-lock.mjs +6 -6
- package/build/bundle-runtime.mjs +39 -11
- package/build/cache-components.mjs +1 -1
- package/build/cache-handlers.mjs +1 -1
- package/build/check.mjs +56 -11
- package/build/compact-css.mjs +53 -0
- package/build/compilation-cache.mjs +269 -0
- package/build/compilation-store.mjs +142 -0
- package/build/compiler.mjs +51 -4
- package/build/concurrent.mjs +12 -0
- package/build/config.mjs +50 -13
- package/build/create.mjs +239 -0
- package/build/css-modules.mjs +65 -10
- package/build/deployment-assets.mjs +89 -0
- package/build/deployment-config.mjs +18 -0
- package/build/dev-compiler-health.mjs +11 -0
- package/build/dev-css-update.mjs +85 -0
- package/build/dev-dependencies.mjs +44 -0
- package/build/dev-discovery.mjs +28 -0
- package/build/dev-worker.mjs +14 -5
- package/build/dev.mjs +64 -6
- package/build/dynamic.mjs +1 -1
- package/build/edge.mjs +5 -4
- package/build/fonts.mjs +1 -1
- package/build/framework-imports.mjs +50 -0
- package/build/html-only-config.mjs +53 -0
- package/build/html-only-graph.mjs +67 -0
- package/build/incremental.mjs +8 -4
- package/build/index.mjs +140 -112
- package/build/inspect.mjs +3 -0
- package/build/inspection-summary.mjs +37 -4
- package/build/instrumentation.mjs +30 -0
- package/build/loader-cache.mjs +53 -26
- package/build/metadata-routes.mjs +3 -4
- package/build/migrate.mjs +60 -57
- package/build/migration-audit.mjs +1 -1
- package/build/native-type-config.mjs +37 -0
- package/build/npm-packages.mjs +3 -18
- package/build/package-manager.mjs +308 -0
- package/build/persistent-cache.mjs +112 -0
- package/build/precompress.mjs +62 -30
- package/build/prerender-cache.mjs +70 -0
- package/build/prerender-config.mjs +29 -0
- package/build/prerender-pages.mjs +100 -0
- package/build/prerender-pool.mjs +165 -0
- package/build/prerender-process-worker.mjs +58 -0
- package/build/prerender-worker.mjs +10 -0
- package/build/production-builder.mjs +129 -0
- package/build/react-compiler-report.mjs +225 -0
- package/build/react-compiler.mjs +139 -0
- package/build/runtime-contract.mjs +2 -2
- package/build/scan.mjs +4 -4
- package/build/source-analysis.mjs +42 -0
- package/build/static-adapter.mjs +13 -5
- package/build/static-export.mjs +4 -4
- package/build/static-input.mjs +48 -0
- package/build/styles.mjs +17 -3
- package/build/transform-cache.mjs +37 -4
- package/build/transform.mjs +1 -1
- package/build/type-command.mjs +41 -0
- package/build/typed-routes.mjs +95 -0
- package/build/typescript-dev-worker.mjs +18 -0
- package/build/typescript-dev.mjs +49 -0
- package/build/typescript-native-worker.mjs +55 -0
- package/build/typescript-native.mjs +18 -0
- package/build/typescript-toolchain.mjs +19 -0
- package/build/typescript-watch.mjs +22 -0
- package/build/typescript.mjs +11 -8
- package/build/webpack-graph.mjs +170 -30
- package/build/worker-memory.mjs +9 -0
- package/build/worker-preload.mjs +9 -0
- package/cli.mjs +162 -122
- package/compat/activity-cache.cjs +26 -0
- package/compat/after.cjs +6 -4
- package/compat/app-layout-context.cjs +52 -2
- package/compat/base64.cjs +10 -0
- package/compat/cache-handlers.cjs +12 -2
- package/compat/cache-wire.cjs +28 -0
- package/compat/cache.cjs +2 -1
- package/compat/cache.d.cts +4 -0
- package/compat/cookies.cjs +21 -5
- package/compat/data-cache.cjs +12 -8
- package/compat/flight-delta.cjs +150 -0
- package/compat/headers.cjs +4 -2
- package/compat/incremental-cache.cjs +20 -7
- package/compat/index.d.ts +89 -10
- package/compat/link.cjs +12 -9
- package/compat/link.d.cts +4 -3
- package/compat/navigation-authorization.cjs +9 -0
- package/compat/navigation-stages.cjs +19 -0
- package/compat/navigation.d.cts +4 -3
- package/compat/offline-store.cjs +11 -0
- package/compat/offline.cjs +5 -0
- package/compat/offline.d.cts +2 -0
- package/compat/redis-cache.cjs +201 -0
- package/compat/redis-cache.d.cts +37 -0
- package/compat/router.d.cts +4 -3
- package/compat/server-primitives.cjs +31 -11
- package/compat/server.d.cts +15 -2
- package/compat/static-generation.cjs +7 -0
- package/compat/typed-route.d.cts +17 -0
- package/compat/use-cache.cjs +3 -0
- package/docs/ai/compatibility.md +275 -0
- package/docs/ai/data.md +288 -0
- package/docs/ai/deployment.md +269 -0
- package/docs/ai/manifest.json +11 -0
- package/docs/ai/quickstart.md +171 -0
- package/docs/ai/routing.md +214 -0
- package/docs/ai/troubleshooting.md +155 -0
- package/index.mjs +2 -0
- package/native/diagnostics.mjs +34 -0
- package/native/resolve.mjs +7 -2
- package/package.json +44 -7
- package/release/ai-docs.mjs +48 -0
- package/release/verify.mjs +2 -0
- package/runtime/action-client.mjs +10 -4
- package/runtime/action-server.mjs +2 -0
- package/runtime/ai-docs.mjs +202 -0
- package/runtime/app-client.mjs +226 -68
- package/runtime/app-content.mjs +4 -3
- package/runtime/app-partial-client.mjs +3 -0
- package/runtime/app-partial.mjs +1 -1
- package/runtime/app-prefetch-constants.mjs +4 -0
- package/runtime/app-prefetch.mjs +2 -3
- package/runtime/app-render-key.mjs +5 -0
- package/runtime/app-render.mjs +100 -28
- package/runtime/app-routing.mjs +22 -9
- package/runtime/app-static.mjs +18 -8
- package/runtime/app-stream-html.mjs +10 -53
- package/runtime/app-style-selection.mjs +34 -0
- package/runtime/app-styles.mjs +154 -0
- package/runtime/cache-components-rsc.mjs +4 -1
- package/runtime/cache-request.mjs +2 -1
- package/runtime/cloud/archive.mjs +5 -6
- package/runtime/compile-cache.mjs +8 -0
- package/runtime/deployment-client.mjs +19 -0
- package/runtime/dev-ai-brands.mjs +26 -0
- package/runtime/dev-ai-style.mjs +99 -0
- package/runtime/dev-build-reasons.mjs +25 -0
- package/runtime/dev-cache-control.mjs +53 -0
- package/runtime/dev-client.mjs +245 -46
- package/runtime/dev-compiler-broker.mjs +42 -0
- package/runtime/dev-console-copy.mjs +19 -0
- package/runtime/dev-console.mjs +106 -0
- package/runtime/dev-dependency-graph.mjs +40 -0
- package/runtime/dev-element-picker-style.mjs +27 -0
- package/runtime/dev-element-picker.mjs +213 -0
- package/runtime/dev-error-overlay-style.mjs +41 -0
- package/runtime/dev-error-overlay.mjs +167 -0
- package/runtime/dev-error-report.mjs +121 -0
- package/runtime/dev-file-changes.mjs +1 -1
- package/runtime/dev-gateway.mjs +44 -0
- package/runtime/dev-host-relay.mjs +66 -0
- package/runtime/dev-indicator.mjs +191 -0
- package/runtime/dev-issue-motion.mjs +214 -0
- package/runtime/dev-lazy-tool.mjs +34 -0
- package/runtime/dev-logo.mjs +2 -0
- package/runtime/dev-notification.mjs +81 -0
- package/runtime/dev-popout.mjs +34 -0
- package/runtime/dev-position.mjs +107 -0
- package/runtime/dev-preview-touch.mjs +122 -0
- package/runtime/dev-runtime-pool.mjs +45 -0
- package/runtime/dev-runtime-spare.mjs +57 -0
- package/runtime/dev-tools-client.mjs +155 -0
- package/runtime/dev-tools-mcp.mjs +126 -0
- package/runtime/dev-tools-metrics.mjs +31 -0
- package/runtime/dev-tools-model.mjs +42 -0
- package/runtime/dev-tools-panel.mjs +341 -0
- package/runtime/dev-tools-result.mjs +37 -0
- package/runtime/dev-tools-server.mjs +256 -0
- package/runtime/dev-tools-source.mjs +104 -0
- package/runtime/dev-tooltip.mjs +94 -0
- package/runtime/dev-ui-icons.mjs +8 -0
- package/runtime/dev-workbench-style.mjs +106 -0
- package/runtime/dev-workbench.mjs +428 -0
- package/runtime/dev-worker-logs.mjs +31 -0
- package/runtime/development.mjs +324 -0
- package/runtime/document.mjs +1 -1
- package/runtime/edge.mjs +6 -5
- package/runtime/env.mjs +2 -1
- package/runtime/fetch-cache.mjs +10 -8
- package/runtime/flight-delta.mjs +50 -0
- package/runtime/flight-tap.mjs +28 -0
- package/runtime/html-only.mjs +27 -0
- package/runtime/http.mjs +2 -0
- package/runtime/incremental-cache.mjs +25 -4
- package/runtime/instrumentation.mjs +49 -0
- package/runtime/navigation-recovery.mjs +36 -0
- package/runtime/navigation.mjs +7 -0
- package/runtime/node-html-stream.mjs +47 -0
- package/runtime/pages-client.mjs +24 -8
- package/runtime/pages-navigation.mjs +54 -64
- package/runtime/persistent/client.mjs +6 -5
- package/runtime/persistent/daemon.mjs +66 -56
- package/runtime/persistent/native-proxy.mjs +63 -0
- package/runtime/prefetch-scheduler.mjs +110 -0
- package/runtime/preload.mjs +12 -0
- package/runtime/profile.mjs +14 -0
- package/runtime/profiles.json +2 -2
- package/runtime/render.mjs +3 -0
- package/runtime/route-pattern.mjs +44 -0
- package/runtime/rsc-worker.mjs +41 -12
- package/runtime/trace.mjs +78 -0
- package/runtime/typed-route.mjs +19 -0
- package/runtime/watch-project.mjs +29 -18
- package/runtime/worker.mjs +102 -24
|
@@ -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
|
}
|
package/build/scan.mjs
CHANGED
|
@@ -51,7 +51,7 @@ function validatePattern(segments, relativeFile) {
|
|
|
51
51
|
return pattern;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
export async function scanProject(projectRoot, { basePath = '', pageExtensions, i18n } = {}) {
|
|
54
|
+
export async function scanProject(projectRoot, { basePath = '', pageExtensions, i18n, devAppFiles } = {}) {
|
|
55
55
|
const sourceExtension = pageExtensionPattern(pageExtensions);
|
|
56
56
|
const root = path.resolve(projectRoot);
|
|
57
57
|
const middleware = await scanMiddleware(root, { basePath, pageExtensions, i18n });
|
|
@@ -96,7 +96,7 @@ export async function scanProject(projectRoot, { basePath = '', pageExtensions,
|
|
|
96
96
|
});
|
|
97
97
|
}
|
|
98
98
|
const appRoot = appRoots[0];
|
|
99
|
-
const { appNotFound, appGlobalError } = appRoot ? await scanAppRoutes({ root, appRoot, routes, patterns, sourceExtension, pageExtensions }) : {};
|
|
99
|
+
const { appNotFound, appGlobalError } = appRoot ? await scanAppRoutes({ root, appRoot, routes, patterns, sourceExtension, pageExtensions, devAppFiles }) : {};
|
|
100
100
|
const metadataManifest = appRoot ? await scanMetadataRoutes({ root, appRoot, files: await walk(appRoot), routes, patterns, validatePattern, basePath, pageExtensions, appNotFound }) : undefined;
|
|
101
101
|
if (metadataManifest) for (const route of routes) if (route.router === 'app' && route.kind === 'page') route.staticMetadata = { manifest: metadataManifest };
|
|
102
102
|
if (!routes.length && !appNotFound) throw new Error('No page or API route found in the Pages Router or App Router directories.');
|
|
@@ -108,7 +108,7 @@ const appConventions = new Map([
|
|
|
108
108
|
['loading', 'loading'], ['error', 'error'], ['not-found', 'notFound'], ['default', 'default'],
|
|
109
109
|
]);
|
|
110
110
|
|
|
111
|
-
async function scanAppRoutes({ root, appRoot, routes, patterns, sourceExtension, pageExtensions = defaultPageExtensions }) {
|
|
111
|
+
async function scanAppRoutes({ root, appRoot, routes, patterns, sourceExtension, pageExtensions = defaultPageExtensions, devAppFiles }) {
|
|
112
112
|
const directories = new Map();
|
|
113
113
|
const configs = new Map();
|
|
114
114
|
let appGlobalError;
|
|
@@ -135,7 +135,7 @@ async function scanAppRoutes({ root, appRoot, routes, patterns, sourceExtension,
|
|
|
135
135
|
const descriptor = directories.get(directory) || { directory, files: {} };
|
|
136
136
|
const name = appConventions.get(filename);
|
|
137
137
|
if (descriptor.files[name]) { console.warn(`Duplicate ${filename} file ${relative}; using ${path.relative(appRoot, descriptor.files[name])} according to pageExtensions.`); continue; }
|
|
138
|
-
if (['page', 'layout', 'route'].includes(filename)) configs.set(file, await validateAppConfig(file));
|
|
138
|
+
if (['page', 'layout', 'route'].includes(filename)) configs.set(file, !devAppFiles || devAppFiles.has(file) ? await validateAppConfig(file) : {});
|
|
139
139
|
descriptor.files[name] = file;
|
|
140
140
|
directories.set(directory, descriptor);
|
|
141
141
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import {parseSync} from 'oxc-parser';
|
|
2
|
+
import {createHash} from 'node:crypto';
|
|
3
|
+
|
|
4
|
+
// Read-only analyses share one Rust parse. Transformations still use Babel's
|
|
5
|
+
// binding-aware AST: ESTree nodes must never be passed to Babel transforms.
|
|
6
|
+
const cache=new Map();
|
|
7
|
+
let bytes=0,hits=0,misses=0;
|
|
8
|
+
export const sourceAnalysisStats=()=>({entries:cache.size,bytes,hits,misses});
|
|
9
|
+
export function analyzeSource(source,file='source.js') {
|
|
10
|
+
const key=file+'\0'+createHash('sha256').update(source).digest('hex');
|
|
11
|
+
if(cache.has(key)){const hit=cache.get(key);cache.delete(key);cache.set(key,hit);hits++;return hit.value;}
|
|
12
|
+
misses++;
|
|
13
|
+
const parsed=parseSync(file,source,{sourceType:'unambiguous',allowReturnOutsideFunction:true});
|
|
14
|
+
if(parsed.errors.length)throw new SyntaxError(`${file}: ${parsed.errors.map(error=>error.message).join('; ')}`);
|
|
15
|
+
const program=parsed.program,directives=[];
|
|
16
|
+
for(const node of program.body){if(!node.directive)break;directives.push(node.directive);}
|
|
17
|
+
const imports=[];
|
|
18
|
+
let computedRequire=false;
|
|
19
|
+
const pending=[program];
|
|
20
|
+
while(pending.length){
|
|
21
|
+
const node=pending.pop();
|
|
22
|
+
if(['ImportDeclaration','ExportNamedDeclaration','ExportAllDeclaration'].includes(node.type)&&node.source&&node.importKind!=='type'&&node.exportKind!=='type')imports.push({path:node.source.value,kind:'import-statement'});
|
|
23
|
+
if(node.type==='ImportExpression'&&typeof node.source?.value==='string')imports.push({path:node.source.value,kind:'dynamic-import'});
|
|
24
|
+
if(node.type==='CallExpression'&&node.callee?.type==='Identifier'&&node.callee.name==='require'){
|
|
25
|
+
const argument=node.arguments[0];
|
|
26
|
+
if(argument?.type==='Literal'&&typeof argument.value==='string')imports.push({path:argument.value,kind:'require-call'});else computedRequire=true;
|
|
27
|
+
}
|
|
28
|
+
for(const value of Object.values(node)){
|
|
29
|
+
if(Array.isArray(value)){for(let i=value.length-1;i>=0;i--)if(value[i]?.type)pending.push(value[i]);}
|
|
30
|
+
else if(value&&typeof value==='object'&&value.type)pending.push(value);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const value={program,directives,imports,computedRequire,frameworkBoundary:directives.some(item=>item==='use client'||item==='use server')};
|
|
34
|
+
// ASTs are substantially larger than source. Bound both count and estimated
|
|
35
|
+
// retained size; large individual npm modules are analyzed without retention.
|
|
36
|
+
const size=Buffer.byteLength(source)*16+Buffer.byteLength(file);
|
|
37
|
+
if(size<=4*1024*1024){
|
|
38
|
+
while(cache.size&&(cache.size>=64||bytes+size>16*1024*1024)){const [old,hit]=cache.entries().next().value;cache.delete(old);bytes-=hit.size;}
|
|
39
|
+
cache.set(key,{value,size});bytes+=size;
|
|
40
|
+
}
|
|
41
|
+
return value;
|
|
42
|
+
}
|
package/build/static-adapter.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import { OUTPUT_POINTER, prepareBuildDirectory, readBuildDirectory, validateDist
|
|
|
7
7
|
import { writeBuildOutput } from './output-contract.mjs';
|
|
8
8
|
import { precompressBuild } from './precompress.mjs';
|
|
9
9
|
import { withBuildLock } from './build-lock.mjs';
|
|
10
|
+
import { plainStaticPath } from './static-input.mjs';
|
|
10
11
|
|
|
11
12
|
const MAX_FILES = 20000, MAX_BYTES = 512 * 1024 * 1024;
|
|
12
13
|
const inside = (root, file) => file.startsWith(root + path.sep);
|
|
@@ -23,17 +24,18 @@ async function importStatic(directory, { from, spa, distDir, dev }) {
|
|
|
23
24
|
const root = await realpath(directory);
|
|
24
25
|
// Reuse the output-directory restrictions to keep imports within this project
|
|
25
26
|
// and away from source/dependency/repository trees.
|
|
26
|
-
const
|
|
27
|
+
const rootInput = from === '.';
|
|
28
|
+
const relative = rootInput ? '.' : validateDistDir(from);
|
|
27
29
|
const output = distDir === undefined ? await readBuildDirectory(root) : validateDistDir(distDir);
|
|
28
30
|
const source = path.join(root, relative), destination = await prepareBuildDirectory(root, output);
|
|
29
|
-
if (source === destination || inside(source, destination) || inside(destination, source)) throw new Error('Static input and PRNext output directories must not overlap.');
|
|
31
|
+
if (!rootInput && (source === destination || inside(source, destination) || inside(destination, source))) throw new Error('Static input and PRNext output directories must not overlap.');
|
|
30
32
|
let ancestor = root;
|
|
31
|
-
for (const part of relative.split('/')) {
|
|
33
|
+
for (const part of rootInput ? [] : relative.split('/')) {
|
|
32
34
|
ancestor = path.join(ancestor, part);
|
|
33
35
|
const entry = await lstat(ancestor);
|
|
34
36
|
if (entry.isSymbolicLink() || !entry.isDirectory()) throw new Error('Static input must use real directories, not symbolic links.');
|
|
35
37
|
}
|
|
36
|
-
if (!inside(root, await realpath(source))) throw new Error('Static input must stay inside the project.');
|
|
38
|
+
if (!rootInput && !inside(root, await realpath(source))) throw new Error('Static input must stay inside the project.');
|
|
37
39
|
const index = await lstat(path.join(source, 'index.html')).catch(() => null);
|
|
38
40
|
if (!index?.isFile() || index.isSymbolicLink()) throw new Error('Static input needs index.html. Run your framework build first and select its public output with --from.');
|
|
39
41
|
|
|
@@ -50,12 +52,18 @@ async function importStatic(directory, { from, spa, distDir, dev }) {
|
|
|
50
52
|
async function copy(folder = '', depth = 0) {
|
|
51
53
|
if (depth > 64) throw new Error('Static input exceeds 64 directory levels.');
|
|
52
54
|
for (const name of (await readdir(path.join(source, folder))).sort()) {
|
|
55
|
+
const file = folder ? `${folder}/${name}` : name;
|
|
56
|
+
if (rootInput && (!plainStaticPath(file, true) || file === output || file.startsWith(output + '/'))) continue;
|
|
57
|
+
if (rootInput) {
|
|
58
|
+
const entry = await lstat(path.join(source, file));
|
|
59
|
+
if (entry.isSymbolicLink()) throw new Error(`Static input cannot contain symbolic links: ${file}`);
|
|
60
|
+
if (!entry.isDirectory() && !plainStaticPath(file)) continue;
|
|
61
|
+
}
|
|
53
62
|
if (name.includes('\\') || /[\x00-\x1f\x7f:]/.test(name) ||
|
|
54
63
|
name.startsWith('.') && !(folder === '' && name === '.well-known') ||
|
|
55
64
|
['node_modules', '_prnext', '_next'].includes(name) || folder === '' && name === 'server') {
|
|
56
65
|
throw new Error(`Static input contains a private, reserved or server path: ${folder ? folder + '/' : ''}${name}. Select only browser-ready files.`);
|
|
57
66
|
}
|
|
58
|
-
const file = folder ? `${folder}/${name}` : name;
|
|
59
67
|
const entry = await lstat(path.join(source, file));
|
|
60
68
|
if (entry.isSymbolicLink()) throw new Error(`Static input cannot contain symbolic links: ${file}`);
|
|
61
69
|
if (entry.isDirectory()) { await copy(file, depth + 1); continue; }
|
package/build/static-export.mjs
CHANGED
|
@@ -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
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { lstat, readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const outputs = ['dist', 'build', 'out', '.output/public'];
|
|
5
|
+
async function file(root, name) {
|
|
6
|
+
try { const entry = await lstat(path.join(root, name)); return entry.isFile() && !entry.isSymbolicLink(); }
|
|
7
|
+
catch (error) { if (error.code === 'ENOENT' || error.code === 'ENOTDIR') return false; throw error; }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Root imports deliberately publish browser assets only, never a project tree. */
|
|
11
|
+
export function plainStaticPath(file, directory = false) {
|
|
12
|
+
const parts = file.split('/'), name = parts.at(-1);
|
|
13
|
+
if (parts.some(part => part.startsWith('.') && part !== '.well-known' ||
|
|
14
|
+
/^(?:node_modules|server|api|functions|config|src|app|pages|components|lib|packages|tests?|coverage|dist|build|out)$/i.test(part))) return false;
|
|
15
|
+
if (directory) return true;
|
|
16
|
+
if (/(?:^|[.-])(?:config|server|test|spec)(?:[.-]|$)/i.test(name)) return false;
|
|
17
|
+
if (parts[0] === '.well-known') return /\.(?:txt|json)$/.test(name);
|
|
18
|
+
return /\.(?:html?|css|js|mjs|wasm|webmanifest|svg|png|jpe?g|gif|webp|avif|ico|woff2?|ttf|otf|eot|mp4|webm|mp3|ogg|wav|pdf)$/i.test(name) || /^(?:robots|ads|security)\.txt$/i.test(name);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Inspect metadata only; never execute a framework config or build command. */
|
|
22
|
+
export async function detectBuildAdapter(root, { prepared = false } = {}) {
|
|
23
|
+
let manifest = {};
|
|
24
|
+
try {
|
|
25
|
+
const contents = await readFile(path.join(root, 'package.json'));
|
|
26
|
+
if (contents.length > 256 * 1024) throw new Error('package.json exceeds 256 KiB');
|
|
27
|
+
manifest = JSON.parse(contents);
|
|
28
|
+
if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) throw new Error('Invalid package.json');
|
|
29
|
+
} catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
30
|
+
const deps = { ...manifest.devDependencies, ...manifest.dependencies };
|
|
31
|
+
if ('next' in deps) return { adapter: 'react' };
|
|
32
|
+
// A Vite index.html is source, and must not be mistaken for a completed site.
|
|
33
|
+
const builder = ['vite', 'astro', '@sveltejs/kit', 'nuxt', 'react-scripts', '@angular/core', '@tanstack/react-start', '@tanstack/solid-start'].some(name => name in deps);
|
|
34
|
+
if (!builder) for (const name of ['app', 'pages', 'src/app', 'src/pages']) {
|
|
35
|
+
try { if ((await lstat(path.join(root, name))).isDirectory()) return { adapter: 'react' }; }
|
|
36
|
+
catch (error) { if (error.code !== 'ENOENT' && error.code !== 'ENOTDIR') throw error; }
|
|
37
|
+
}
|
|
38
|
+
const script = manifest.scripts?.build;
|
|
39
|
+
if (!builder && (!script || /^(?:prnext|prn) build(?:\s|$)/.test(script)) && await file(root, 'index.html')) return { adapter: 'static', from: '.' };
|
|
40
|
+
if (prepared) {
|
|
41
|
+
const found = [];
|
|
42
|
+
for (const from of outputs) if (await file(root, from + '/index.html')) found.push(from);
|
|
43
|
+
if (found.length > 1) throw new Error('Multiple static outputs found. Select one with --adapter static --from <directory>.');
|
|
44
|
+
if (found.length === 1) return { adapter: 'static', from: found[0] };
|
|
45
|
+
throw new Error('No completed browser build found. Run your framework build, then use --adapter static --from <directory>. Plain HTML sites need index.html at the root.');
|
|
46
|
+
}
|
|
47
|
+
return { adapter: 'react' };
|
|
48
|
+
}
|