@thomas.f/prnext 0.1.18-alpha → 0.1.20-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/build/actions.mjs +26 -14
- package/build/app.mjs +90 -101
- package/build/build-lock.mjs +2 -71
- package/build/build-retention.mjs +119 -0
- package/build/bundle-runtime.mjs +9 -8
- package/build/cache-command.mjs +31 -0
- package/build/check.mjs +41 -3
- package/build/compilation-cache.mjs +32 -12
- package/build/compilation-storage.mjs +190 -0
- package/build/compiler.mjs +8 -7
- package/build/config.mjs +25 -2
- package/build/create.mjs +1 -1
- package/build/css-modules.mjs +4 -0
- package/build/dev-action-graph.mjs +21 -0
- package/build/dev-css-assets.mjs +173 -0
- package/build/dev-css-update.mjs +14 -12
- package/build/dev-lazy-build.mjs +77 -0
- package/build/dynamic.mjs +13 -6
- package/build/font-cache.mjs +9 -6
- package/build/font-files.mjs +1 -1
- package/build/incremental.mjs +112 -18
- package/build/index.mjs +61 -15
- package/build/lazy-dynamic-imports.mjs +137 -0
- package/build/migrate.mjs +4 -4
- package/build/migration-audit.mjs +19 -31
- package/build/migration-imports.mjs +245 -0
- package/build/migration-scenarios-runner.mjs +73 -0
- package/build/migration-scenarios.mjs +72 -0
- package/build/native-discovery.mjs +65 -5
- package/build/native-packages.mjs +2 -0
- package/build/native-ppr.mjs +32 -0
- package/build/npm-packages.mjs +1 -0
- package/build/og.mjs +121 -1
- package/build/parallel-plan.mjs +55 -0
- package/build/persistent-cache.mjs +29 -24
- package/build/production-builder.mjs +1 -1
- package/build/production-check.mjs +34 -0
- package/build/react-compiler-report.mjs +31 -8
- package/build/react-compiler.mjs +101 -10
- package/build/server-config.mjs +35 -0
- package/build/source-analysis.mjs +54 -4
- package/build/standalone.mjs +21 -3
- package/build/static-adapter.mjs +7 -3
- package/build/transform.mjs +38 -6
- package/build/typescript-dev.mjs +3 -1
- package/build/webpack-graph.mjs +13 -3
- package/cli.mjs +24 -6
- package/compat/app-dynamic.cjs +12 -1
- package/compat/app-layout-context.cjs +7 -3
- package/compat/cache-handlers.cjs +3 -1
- package/compat/flight-delta.cjs +50 -13
- package/compat/index.d.ts +24 -1
- package/compat/link.cjs +2 -2
- package/compat/navigation-authorization.cjs +8 -1
- package/docs/ai/compatibility.md +187 -7
- package/docs/ai/deployment.md +141 -0
- package/docs/ai/quickstart.md +5 -0
- package/docs/ai/routing.md +11 -0
- package/docs/ai/troubleshooting.md +79 -0
- package/package.json +26 -11
- package/release/fontkit.mjs +31 -0
- package/release/verify.mjs +3 -0
- package/runtime/api.mjs +5 -2
- package/runtime/app-client.mjs +254 -53
- package/runtime/app-partial.mjs +2 -10
- package/runtime/app-prefetch.mjs +2 -2
- package/runtime/app-render.mjs +11 -7
- package/runtime/app-stream-channel.mjs +3 -2
- package/runtime/app-styles.mjs +22 -4
- package/runtime/atomic-file.mjs +27 -0
- package/runtime/cache-components-rsc.mjs +3 -1
- package/runtime/compile-cache-retention.mjs +53 -0
- package/runtime/compile-cache.mjs +28 -1
- package/runtime/dev-client.mjs +13 -5
- package/runtime/dev-file-changes.mjs +123 -8
- package/runtime/dev-gateway.mjs +3 -2
- package/runtime/dev-lazy-imports.mjs +121 -0
- package/runtime/dev-lazy-staging.mjs +14 -0
- package/runtime/dev-route-selection.mjs +64 -0
- package/runtime/dev-runtime-pool.mjs +3 -2
- package/runtime/dev-runtime-spare.mjs +1 -1
- package/runtime/dev-session-retention.mjs +154 -0
- package/runtime/dev-style-retention.mjs +15 -0
- package/runtime/development.mjs +86 -26
- package/runtime/env.mjs +9 -0
- package/runtime/flight-delta.mjs +59 -9
- package/runtime/fs.mjs +15 -8
- package/runtime/lazy-import-client.mjs +38 -0
- package/runtime/native-ppr.mjs +35 -0
- package/runtime/og/NOTICE.txt +1 -1
- package/runtime/og/index.node.mjs +76 -31216
- package/runtime/og/upstream.json +115 -0
- package/runtime/persistent/client.mjs +4 -2
- package/runtime/persistent/daemon.mjs +3 -11
- package/runtime/persistent/health.mjs +58 -0
- package/runtime/project-lock.mjs +71 -0
- package/runtime/request-transport.mjs +112 -32
- package/runtime/storage-path.mjs +36 -0
- package/runtime/trace.mjs +44 -4
- package/runtime/worker.mjs +9 -4
- package/vendor/fontkit/NOTICE.txt +5 -0
- package/vendor/fontkit/SOURCE-NOTICES.txt +209 -0
- package/vendor/fontkit/index.mjs +3 -0
- package/vendor/fontkit/licenses/@swc__helpers/LICENSE +201 -0
- package/vendor/fontkit/licenses/base64-js/LICENSE +21 -0
- package/vendor/fontkit/licenses/brotli/readme.md +61 -0
- package/vendor/fontkit/licenses/clone/LICENSE +18 -0
- package/vendor/fontkit/licenses/dfa/README.md +83 -0
- package/vendor/fontkit/licenses/fast-deep-equal/LICENSE +21 -0
- package/vendor/fontkit/licenses/fontkit/README.md +266 -0
- package/vendor/fontkit/licenses/restructure/LICENSE +21 -0
- package/vendor/fontkit/licenses/tiny-inflate/LICENSE +21 -0
- package/vendor/fontkit/licenses/tslib/LICENSE.txt +12 -0
- package/vendor/fontkit/licenses/unicode-properties/LICENSE +8 -0
- package/vendor/fontkit/licenses/unicode-trie/LICENSE +8 -0
- package/vendor/fontkit/upstream.json +215 -0
- package/vendor/nft/LICENSE +7 -0
- package/vendor/nft/README.md +16 -0
- package/vendor/nft/out/analyze.d.ts +8 -0
- package/vendor/nft/out/analyze.js +1216 -0
- package/vendor/nft/out/analyze.js.map +1 -0
- package/vendor/nft/out/cli.d.ts +2 -0
- package/vendor/nft/out/cli.js +94 -0
- package/vendor/nft/out/cli.js.map +1 -0
- package/vendor/nft/out/fs.d.ts +22 -0
- package/vendor/nft/out/fs.js +106 -0
- package/vendor/nft/out/fs.js.map +1 -0
- package/vendor/nft/out/index.d.ts +4 -0
- package/vendor/nft/out/index.js +25 -0
- package/vendor/nft/out/index.js.map +1 -0
- package/vendor/nft/out/node-file-trace.d.ts +41 -0
- package/vendor/nft/out/node-file-trace.js +380 -0
- package/vendor/nft/out/node-file-trace.js.map +1 -0
- package/vendor/nft/out/resolve-dependency.d.ts +6 -0
- package/vendor/nft/out/resolve-dependency.js +427 -0
- package/vendor/nft/out/resolve-dependency.js.map +1 -0
- package/vendor/nft/out/types.d.ts +66 -0
- package/vendor/nft/out/types.js +2 -0
- package/vendor/nft/out/types.js.map +1 -0
- package/vendor/nft/out/utils/ast-helpers.js +49 -0
- package/vendor/nft/out/utils/ast-helpers.js.map +1 -0
- package/vendor/nft/out/utils/binary-locators.js +88 -0
- package/vendor/nft/out/utils/binary-locators.js.map +1 -0
- package/vendor/nft/out/utils/get-package-base.js +30 -0
- package/vendor/nft/out/utils/get-package-base.js.map +1 -0
- package/vendor/nft/out/utils/interop-require.js +24 -0
- package/vendor/nft/out/utils/interop-require.js.map +1 -0
- package/vendor/nft/out/utils/node-version.js +10 -0
- package/vendor/nft/out/utils/node-version.js.map +1 -0
- package/vendor/nft/out/utils/sharedlib-emit.js +33 -0
- package/vendor/nft/out/utils/sharedlib-emit.js.map +1 -0
- package/vendor/nft/out/utils/special-cases.js +347 -0
- package/vendor/nft/out/utils/special-cases.js.map +1 -0
- package/vendor/nft/out/utils/static-eval.js +579 -0
- package/vendor/nft/out/utils/static-eval.js.map +1 -0
- package/vendor/nft/out/utils/types.js +2 -0
- package/vendor/nft/out/utils/types.js.map +1 -0
- package/vendor/nft/out/utils/wrappers.js +659 -0
- package/vendor/nft/out/utils/wrappers.js.map +1 -0
- package/vendor/nft/package.json +4 -0
- package/vendor/nft/upstream.json +51 -0
package/build/npm-packages.mjs
CHANGED
|
@@ -33,6 +33,7 @@ export function npmPackagesPlugin({ pages = false, projectRoot, transpilePackage
|
|
|
33
33
|
return { name: 'prnext-npm-packages', setup(build) {
|
|
34
34
|
const resolving = { prnextNpmResolving: true, prnextNativeResolving: true };
|
|
35
35
|
const resolve = (name, importer, kind) => build.resolve(name, { importer, resolveDir: path.dirname(importer), kind, pluginData: resolving });
|
|
36
|
+
native.setResolver('ssr', resolve);
|
|
36
37
|
function classify(file) {
|
|
37
38
|
if (!decisions.has(file)) decisions.set(file, (async () => {
|
|
38
39
|
const queue = [file], visited = new Set();
|
package/build/og.mjs
CHANGED
|
@@ -1,6 +1,126 @@
|
|
|
1
|
-
import { cp, mkdir } from 'node:fs/promises';
|
|
1
|
+
import { cp, mkdir, readdir, lstat, open } from 'node:fs/promises';
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { builtinModules } from 'node:module';
|
|
5
|
+
import { analyzeSource } from './source-analysis.mjs';
|
|
6
|
+
|
|
7
|
+
const packageRoot = fileURLToPath(new URL('../', import.meta.url));
|
|
8
|
+
const inside = (root, file) => file === root || file.startsWith(root + path.sep);
|
|
9
|
+
const builtins = new Set([...builtinModules, ...builtinModules.map(name => 'node:' + name)]);
|
|
10
|
+
const unsafeBuiltins = /^(?:node:)?(?:vm|worker_threads|child_process)$/;
|
|
11
|
+
const react = /^(?:react|react-dom|react-server-dom-webpack)(?:\/|$)/;
|
|
12
|
+
const maxFiles = 4096, maxBytes = 64 * 1024 * 1024, maxFileBytes = 8 * 1024 * 1024;
|
|
13
|
+
|
|
14
|
+
/** Certification examines the actual emitted server graph, never source-text
|
|
15
|
+
* matches. An unknown loader/dependency is an inclusion, not an omission. The
|
|
16
|
+
* installed framework modules are a fixed boundary; application graph roots,
|
|
17
|
+
* middleware, instrumentation, cache handlers and client SSR are all inspected. */
|
|
18
|
+
export async function imageResponseRequirement(stage, {dev = false, manifest = {}} = {}) {
|
|
19
|
+
const included = reason => ({included: true, reason});
|
|
20
|
+
if (dev) return included('development');
|
|
21
|
+
if (manifest.middleware?.runtime === 'edge' || manifest.routes?.some(route => route.handlerConfig?.runtime === 'edge' || route.cacheConfig?.runtime === 'edge')) return included('edge-runtime');
|
|
22
|
+
stage = path.resolve(stage);
|
|
23
|
+
const pending = [], seen = new Set();
|
|
24
|
+
let bytes = 0;
|
|
25
|
+
try {
|
|
26
|
+
for (const name of ['server', 'app-ssr', 'app-ssr-edge']) {
|
|
27
|
+
const directory = path.join(stage, name);
|
|
28
|
+
let entries;
|
|
29
|
+
try { const info = await lstat(directory); if (!info.isDirectory() || info.isSymbolicLink()) return included('unverified-output'); entries = await readdir(directory, {withFileTypes: true}); }
|
|
30
|
+
catch (error) { if (error.code === 'ENOENT') continue; throw error; }
|
|
31
|
+
if (entries.some(entry => !entry.isFile())) return included('unverified-output');
|
|
32
|
+
for (const entry of entries) if (/\.(?:[cm]?js)$/.test(entry.name)) pending.push(path.join(directory, entry.name));
|
|
33
|
+
}
|
|
34
|
+
for (const entry of [manifest.middleware?.module, manifest.config?.cacheHandler, ...Object.values(manifest.config?.cacheHandlers || {})]) if (entry) pending.push(path.resolve(stage, entry));
|
|
35
|
+
while (pending.length) {
|
|
36
|
+
const file = pending.pop();
|
|
37
|
+
if (seen.has(file)) continue;
|
|
38
|
+
if (!inside(stage, file) || seen.size >= maxFiles) return included('unverified-output');
|
|
39
|
+
seen.add(file);
|
|
40
|
+
// An actual application import of the compatibility layer or loader is
|
|
41
|
+
// sufficient evidence, even if the ImageResponse binding is only dynamic.
|
|
42
|
+
if (file === path.join(stage, 'compat/og.cjs') || file === path.join(stage, 'runtime/og-loader.mjs') || inside(path.join(stage, 'runtime/og'), file)) return included('image-response-import');
|
|
43
|
+
const relative = path.relative(stage, file);
|
|
44
|
+
const frameworkFile = relative.startsWith('compat' + path.sep) || relative.startsWith('runtime' + path.sep);
|
|
45
|
+
if (frameworkFile) {
|
|
46
|
+
// Only exact installed files are trusted. Configured output overrides
|
|
47
|
+
// and missing/unknown framework modules retain the complete renderer.
|
|
48
|
+
const known = path.join(packageRoot, relative), [a, b] = await Promise.all([lstat(file), lstat(known)]);
|
|
49
|
+
if (!a.isFile() || a.isSymbolicLink() || !b.isFile() || b.isSymbolicLink() || a.size !== b.size) return included('unverified-framework');
|
|
50
|
+
const [actual, expected] = await Promise.all([readBounded(file, maxFileBytes), readBounded(known, maxFileBytes)]);
|
|
51
|
+
if (!actual.equals(expected)) return included('unverified-framework');
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const info = await lstat(file);
|
|
55
|
+
if (!info.isFile() || info.isSymbolicLink() || info.size > maxFileBytes || (bytes += info.size) > maxBytes) return included('unverified-output');
|
|
56
|
+
const source = await readBounded(file, maxFileBytes), analysis = analyzeSource(source.toString(), file);
|
|
57
|
+
if (hasUnknownLoading(analysis.program)) return included('dynamic-loader');
|
|
58
|
+
for (const dependency of analysis.imports) {
|
|
59
|
+
const specifier = dependency.path;
|
|
60
|
+
if (typeof specifier !== 'string' || specifier.includes('\0') || unsafeBuiltins.test(specifier)) return included('unverified-dependency');
|
|
61
|
+
if (builtins.has(specifier) || react.test(specifier)) continue;
|
|
62
|
+
if (/^(?:next|prnext|@thomas\.f\/prnext)\/og(?:\.js)?$/.test(specifier)) return included('image-response-import');
|
|
63
|
+
if (!specifier.startsWith('.')) return included('external-dependency');
|
|
64
|
+
const target = path.resolve(path.dirname(file), specifier);
|
|
65
|
+
// JSON/data imports cannot load executable OG code. Other extensions
|
|
66
|
+
// and extensionless resolution are deliberately not guessed.
|
|
67
|
+
if (target.endsWith('.json')) {
|
|
68
|
+
if (!inside(stage, target) || !(await lstat(target)).isFile()) return included('unverified-dependency');
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (!/\.(?:[cm]?js)$/.test(target)) return included('unverified-dependency');
|
|
72
|
+
pending.push(target);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return {included: false, reason: 'certified-unused', files: seen.size, bytes};
|
|
76
|
+
} catch { return included('unverified-output'); }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function readBounded(filename, limit) {
|
|
80
|
+
const file = await open(filename, 'r');
|
|
81
|
+
try {
|
|
82
|
+
const before = await file.stat();
|
|
83
|
+
if (!before.isFile() || before.size > limit) throw Error('ImageResponse graph exceeds its file budget');
|
|
84
|
+
const buffer = Buffer.alloc(before.size + 1);
|
|
85
|
+
let length = 0;
|
|
86
|
+
while (length < buffer.length) { const next = await file.read(buffer, length, buffer.length - length, null); if (!next.bytesRead) break; length += next.bytesRead; }
|
|
87
|
+
const after = await file.stat();
|
|
88
|
+
if (length !== before.size || before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs) throw Error('ImageResponse graph changed during certification');
|
|
89
|
+
return buffer.subarray(0, length);
|
|
90
|
+
} finally { await file.close(); }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function hasUnknownLoading(program) {
|
|
94
|
+
const factories = new Set(program.body.filter(node => node.type === 'ImportDeclaration' && ['module', 'node:module'].includes(node.source?.value)).flatMap(node => node.specifiers.filter(item => item.type === 'ImportSpecifier' && item.imported?.name === 'createRequire').map(item => item.local.name)));
|
|
95
|
+
const stack = [[program, null]];
|
|
96
|
+
while (stack.length) {
|
|
97
|
+
const [node, parent] = stack.pop();
|
|
98
|
+
if (node.type === 'Identifier' && (['eval', 'Function', 'Reflect'].includes(node.name) || /webpack_require/.test(node.name))) return true;
|
|
99
|
+
if (node.type === 'MemberExpression' && (node.property?.name ?? node.property?.value) === 'constructor') return true;
|
|
100
|
+
if (node.type === 'ImportExpression' && typeof node.source?.value !== 'string') return true;
|
|
101
|
+
if ((node.type === 'NewExpression' || node.type === 'CallExpression') && (node.callee?.name === 'eval' || node.callee?.name === 'Function' || ['eval', 'Function', 'constructor', 'runInContext', 'runInNewContext', 'compileFunction', 'dlopen', 'getBuiltinModule'].includes(node.callee?.property?.name ?? node.callee?.property?.value))) return true;
|
|
102
|
+
if (node.type === 'CallExpression' && factories.has(node.callee?.name) && (parent?.type !== 'VariableDeclarator' || parent.id?.name !== 'require')) return true;
|
|
103
|
+
if (node.type === 'ImportDeclaration' && ['module', 'node:module'].includes(node.source?.value) && node.specifiers.some(item => item.type !== 'ImportSpecifier' || item.imported?.name !== 'createRequire')) return true;
|
|
104
|
+
if (node.type === 'Identifier' && /^_*require\d*$/.test(node.name)) {
|
|
105
|
+
const binding = parent?.type === 'VariableDeclarator' && parent.id === node && parent.init?.type === 'CallExpression' && factories.has(parent.init.callee?.name);
|
|
106
|
+
const call = node.name === 'require' && parent?.type === 'CallExpression' && parent.callee === node;
|
|
107
|
+
if (!binding && (!call || parent.arguments.length !== 1 || typeof parent.arguments[0]?.value !== 'string')) return true;
|
|
108
|
+
}
|
|
109
|
+
if (node.type === 'CallExpression' && node.callee?.type === 'MemberExpression' && ['require', '_load', 'createRequire'].includes(node.callee.property?.name)) return true;
|
|
110
|
+
if (node.type === 'Identifier' && factories.has(node.name)) {
|
|
111
|
+
const specifier = parent?.type === 'ImportSpecifier';
|
|
112
|
+
const call = parent?.type === 'CallExpression' && parent.callee === node && parent.arguments.length === 1 && parent.arguments[0]?.type === 'MemberExpression' && parent.arguments[0].object?.type === 'MetaProperty' && parent.arguments[0].property?.name === 'url';
|
|
113
|
+
if (!specifier && !call) return true;
|
|
114
|
+
}
|
|
115
|
+
if (node.type === 'VariableDeclarator' && node.init?.type === 'CallExpression' && factories.has(node.init.callee?.name) && node.id?.name !== 'require') return true;
|
|
116
|
+
for (const value of Object.values(node)) {
|
|
117
|
+
if (Array.isArray(value)) { for (const item of value) if (item?.type) stack.push([item, node]); }
|
|
118
|
+
else if (value?.type) stack.push([value, node]);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
4
124
|
export async function prepareImageResponse(stage) {
|
|
5
125
|
const target = path.join(stage, 'runtime/og');
|
|
6
126
|
await mkdir(target, { recursive: true });
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import {availableParallelism, freemem} from 'node:os';
|
|
2
|
+
import {mapConcurrent} from './concurrent.mjs';
|
|
3
|
+
import {mkdir, writeFile} from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import {createHash} from 'node:crypto';
|
|
6
|
+
|
|
7
|
+
// Budget admission, not a claim to cap esbuild/PostCSS process RSS. A single
|
|
8
|
+
// heavy custom transform can exceed its estimate, so unknown hooks stay serial.
|
|
9
|
+
export function buildPlanConcurrency(options, {cpus = availableParallelism(), freeBytes = freemem()} = {}) {
|
|
10
|
+
if (!options) return 1;
|
|
11
|
+
return Math.max(1, Math.min(options.maxWorkers, cpus, Math.floor(options.maxMemoryMb / 256), Math.floor(freeBytes / (512 * 1024 * 1024))));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function immutableStylePlan(routes, getPlan) {
|
|
15
|
+
const jobs = new Map();
|
|
16
|
+
const plannedRoutes = routes.map(route => Object.freeze({id: route.id, parallel: !!route.parallel,
|
|
17
|
+
scopes: Object.freeze(getPlan(route).map(scope => {
|
|
18
|
+
const styles = Object.freeze([...scope.styles]);
|
|
19
|
+
const signature = JSON.stringify(styles);
|
|
20
|
+
if (!jobs.has(signature)) jobs.set(signature, Object.freeze({signature, styles}));
|
|
21
|
+
return Object.freeze({signature, path: scope.path, kind: scope.kind});
|
|
22
|
+
})),
|
|
23
|
+
}));
|
|
24
|
+
return Object.freeze({routes: Object.freeze(plannedRoutes), jobs: Object.freeze([...jobs.values()])});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function executeStylePlan(plan, run, options) {
|
|
28
|
+
const concurrency = buildPlanConcurrency(options);
|
|
29
|
+
const outputs = await mapConcurrent(plan.jobs, run, concurrency);
|
|
30
|
+
return {outputs: new Map(plan.jobs.map((job, index) => [job.signature, outputs[index]])), concurrency};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Independent CSS graphs can emit the same url() asset. Compile in parallel,
|
|
34
|
+
// but publish each exact output once. Keep only hashes and settled promises,
|
|
35
|
+
// not all completed bundles, so memory stays bounded by admitted jobs.
|
|
36
|
+
export function createStyleOutputWriter(directory) {
|
|
37
|
+
const root = path.resolve(directory), outputs = new Map();
|
|
38
|
+
return async files => {
|
|
39
|
+
for (const file of files) {
|
|
40
|
+
const target = path.resolve(file.path), relative = path.relative(root, target);
|
|
41
|
+
if (!relative || relative === '..' || relative.startsWith('..' + path.sep) || path.isAbsolute(relative)) throw new Error('Style output escaped its build directory');
|
|
42
|
+
const key = process.platform === 'win32' ? target.toLowerCase() : target;
|
|
43
|
+
const hash = createHash('sha256').update(file.contents).digest('hex');
|
|
44
|
+
const previous = outputs.get(key);
|
|
45
|
+
if (previous) {
|
|
46
|
+
if (previous.hash !== hash) throw new Error(`Conflicting style output: ${relative}`);
|
|
47
|
+
await previous.written;
|
|
48
|
+
} else {
|
|
49
|
+
const written = (async () => {await mkdir(path.dirname(target), {recursive: true}); await writeFile(target, file.contents);})();
|
|
50
|
+
outputs.set(key, {hash, written});
|
|
51
|
+
await written;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -6,10 +6,15 @@ import {open,lstat,mkdir,realpath,readFile,readdir,rename,unlink} from 'node:fs/
|
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import {fileURLToPath} from 'node:url';
|
|
8
8
|
import {createRequire} from 'node:module';
|
|
9
|
+
import {deflateRaw,inflateRaw} from 'node:zlib';
|
|
10
|
+
import {promisify} from 'node:util';
|
|
11
|
+
export {pruneCompilerCache} from '../runtime/compile-cache-retention.mjs';
|
|
9
12
|
|
|
10
13
|
export const cacheHash=value=>createHash('sha256').update(value).digest('hex');
|
|
11
14
|
const packageRoot=fileURLToPath(new URL('../',import.meta.url)),require=createRequire(import.meta.url);
|
|
12
15
|
const MAX_DISK=64*1024*1024;
|
|
16
|
+
const compress=promisify(deflateRaw),decompress=promisify(inflateRaw);
|
|
17
|
+
const MAGIC=Buffer.from('PRNC2\0'),HEADER=MAGIC.length+4+32+32;
|
|
13
18
|
let frameworkIdentity;
|
|
14
19
|
async function frameworkHash(){
|
|
15
20
|
frameworkIdentity??=(async()=>{
|
|
@@ -59,45 +64,45 @@ export async function privateCacheDirectory(root,kind){
|
|
|
59
64
|
return directory;
|
|
60
65
|
}catch{return null;}
|
|
61
66
|
}
|
|
62
|
-
/** Best-effort V8 cache retention; never follow links or traverse app sources. */
|
|
63
|
-
export async function pruneCompilerCache(directory,{maxBytes=64*1024*1024,maxFiles=2048}={}){
|
|
64
|
-
if(!directory)return;
|
|
65
|
-
try{
|
|
66
|
-
const files=[];let scanned=0;
|
|
67
|
-
async function collect(folder,depth){
|
|
68
|
-
if(depth>3||scanned>=8192)return;
|
|
69
|
-
for(const entry of await readdir(folder,{withFileTypes:true})){
|
|
70
|
-
if(++scanned>8192)break;
|
|
71
|
-
const name=path.join(folder,entry.name),info=await lstat(name).catch(()=>null);
|
|
72
|
-
if(!info||info.isSymbolicLink())continue;
|
|
73
|
-
if(info.isDirectory())await collect(name,depth+1);
|
|
74
|
-
else if(info.isFile())files.push({name,size:info.size,time:info.mtimeMs});
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
const info=await lstat(directory);if(!info.isDirectory()||info.isSymbolicLink())return;
|
|
78
|
-
await collect(directory,0);files.sort((a,b)=>b.time-a.time);let bytes=0;
|
|
79
|
-
for(let i=0;i<files.length;i++){bytes+=files[i].size;if(i>=maxFiles||bytes>maxBytes)await unlink(files[i].name).catch(()=>{});}
|
|
80
|
-
}catch{/* Optional caches must never prevent compilation. */}
|
|
81
|
-
}
|
|
82
67
|
export async function readCacheRecord(directory,key,limit=16*1024*1024){
|
|
83
|
-
if(!directory||!/^[a-f0-9]{64}$/.test(key))return null;
|
|
68
|
+
if(!directory||!/^[a-f0-9]{64}$/.test(key)||!Number.isSafeInteger(limit)||limit<1||limit>64*1024*1024)return null;
|
|
84
69
|
let file;
|
|
85
70
|
try{
|
|
86
71
|
const name=path.join(directory,key+'.json'),info=await lstat(name);
|
|
87
72
|
if(!info.isFile()||info.isSymbolicLink()||info.size>limit)return null;
|
|
88
73
|
file=await open(name,fsConstants.O_RDONLY|(fsConstants.O_NOFOLLOW||0));
|
|
89
74
|
const stat=await file.stat();if(!stat.isFile()||stat.size>limit)return null;
|
|
90
|
-
const
|
|
75
|
+
const body=await file.readFile();
|
|
76
|
+
if(body.subarray(0,MAGIC.length).equals(MAGIC)){
|
|
77
|
+
if(body.length<HEADER)return null;
|
|
78
|
+
const size=body.readUInt32BE(MAGIC.length);
|
|
79
|
+
if(size>limit||body.subarray(MAGIC.length+4,MAGIC.length+36).toString('hex')!==key)return null;
|
|
80
|
+
const payload=await decompress(body.subarray(HEADER),{maxOutputLength:Math.max(1,size)});
|
|
81
|
+
if(payload.length!==size||cacheHash(payload)!==body.subarray(MAGIC.length+36,HEADER).toString('hex'))return null;
|
|
82
|
+
return JSON.parse(payload.toString('utf8'));
|
|
83
|
+
}
|
|
84
|
+
const record=JSON.parse(body.toString('utf8'));
|
|
91
85
|
if(record.version!==1||record.key!==key||typeof record.payload!=='string'||cacheHash(record.payload)!==record.checksum)return null;
|
|
92
86
|
return JSON.parse(record.payload);
|
|
93
87
|
}catch{return null;}finally{await file?.close().catch(()=>{});}
|
|
94
88
|
}
|
|
95
89
|
export async function writeCacheRecord(directory,key,value,limit=16*1024*1024,{maxFiles=64}={}){
|
|
96
|
-
if(!directory||!/^[a-f0-9]{64}$/.test(key))return false;
|
|
90
|
+
if(!directory||!/^[a-f0-9]{64}$/.test(key)||!Number.isSafeInteger(limit)||limit<1||limit>64*1024*1024)return false;
|
|
97
91
|
if(!Number.isInteger(maxFiles)||maxFiles<1||maxFiles>4096)return false;
|
|
98
92
|
const filename=path.join(directory,key+'.json'),temporary=filename+'.'+randomUUID()+'.tmp';
|
|
99
93
|
try{
|
|
100
|
-
const payload=JSON.stringify(value)
|
|
94
|
+
const payload=JSON.stringify(value);if(payload===undefined||Buffer.byteLength(payload)>limit)return false;
|
|
95
|
+
let body=JSON.stringify({version:1,key,checksum:cacheHash(payload),payload});
|
|
96
|
+
// Large, repetitive JSON benefits from one encoding instead of embedding a
|
|
97
|
+
// second escaped JSON string. Keep small records compatible with v1.
|
|
98
|
+
if(Buffer.byteLength(payload)>=16384){
|
|
99
|
+
const bytes=Buffer.from(payload),packed=await compress(bytes,{level:1});
|
|
100
|
+
if(packed.length+HEADER<bytes.length*0.9){
|
|
101
|
+
const header=Buffer.alloc(HEADER);MAGIC.copy(header);header.writeUInt32BE(bytes.length,MAGIC.length);
|
|
102
|
+
Buffer.from(key,'hex').copy(header,MAGIC.length+4);Buffer.from(cacheHash(bytes),'hex').copy(header,MAGIC.length+36);
|
|
103
|
+
body=Buffer.concat([header,packed]);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
101
106
|
if(Buffer.byteLength(body)>limit)return false;
|
|
102
107
|
const file=await open(temporary,'wx',0o600);try{await file.writeFile(body);}finally{await file.close();}
|
|
103
108
|
await rename(temporary,filename);
|
|
@@ -85,8 +85,8 @@ export function isolatedPrerender(root, stage, manifest, options = {}, dependenc
|
|
|
85
85
|
/** Retain production graphs during local rebuilds; no HTTP listener/daemon. */
|
|
86
86
|
export async function watchProduction(root,options={}){
|
|
87
87
|
let worker,watcher,stopping=false,timer,busy=false,queued=false;
|
|
88
|
-
const changed=new Set(),filter=createDevChangeFilter(root);
|
|
89
88
|
let output='.prnext';
|
|
89
|
+
const changed=new Set(),filter=createDevChangeFilter(root,{directoryEventsAreRecursive:process.platform==='darwin',ignore:relative=>relative===output||relative.startsWith(output+'/')});
|
|
90
90
|
const stopWorker=async()=>{
|
|
91
91
|
const old=worker;worker=undefined;if(!old||old.exitCode!==null||old.signalCode)return;
|
|
92
92
|
await new Promise(resolve=>{const timer=setTimeout(()=>old.kill(),5000);old.once('exit',()=>{clearTimeout(timer);resolve();});old.disconnect();});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import {isIP} from 'node:net';
|
|
2
|
+
|
|
3
|
+
/** Configuration checks only: external proxies, databases and sessions need live tests. */
|
|
4
|
+
export function productionDiagnostics(config,project,{env=process.env}={}){
|
|
5
|
+
const diagnostics=[];
|
|
6
|
+
const locations={'production-static':'output','production-origin':'server.publicOrigin','production-origin-https':'server.publicOrigin','production-proxy':'server.trustedProxies','production-proxy-range':'server.trustedProxies','production-auth-cookies':'navigationCache.authCookies','production-action-origins':'experimental.serverActions.allowedOrigins','production-source-maps':'productionBrowserSourceMaps'};
|
|
7
|
+
const add=(id,severity,message,action,location)=>diagnostics.push({id,code:id,path:location||locations[id]||'production',severity,message,...(action?{action}:{})});
|
|
8
|
+
if(config.output==='export'){
|
|
9
|
+
add('production-static','info','Static export does not run the PRNext HTTP server.','Check HTTPS, headers and health checks at the static host.');
|
|
10
|
+
}else{
|
|
11
|
+
const origin=config.server?.publicOrigin;
|
|
12
|
+
if(!origin)add('production-origin','warning','The public server origin is not configured.','Set server.publicOrigin to the public HTTPS origin used by redirects, authentication callbacks and request URLs.');
|
|
13
|
+
else{
|
|
14
|
+
const url=new URL(origin),hostname=url.hostname,loopback=hostname==='localhost'||hostname==='[::1]'||(isIP(hostname)===4&&hostname.startsWith('127.'));
|
|
15
|
+
if(url.protocol!=='https:'&&!loopback)add('production-origin-https','warning','The public server origin uses HTTP.','Use an HTTPS public origin and test secure cookies and authentication callbacks through the real proxy.');
|
|
16
|
+
else add('production-origin','info','The public server origin is configured.');
|
|
17
|
+
}
|
|
18
|
+
if(config.server?.trustedProxies?.length)add('production-proxy','info','Forwarded host and protocol are accepted only from the configured proxy addresses.','Bind the application to the private interface and configure the proxy to replace forwarded headers.');
|
|
19
|
+
if(config.server?.trustedProxies?.some(value=>/\/0$/.test(value)))add('production-proxy-range','error','A trusted proxy range includes every remote address.','Replace /0 ranges with the exact proxy IP addresses or private CIDRs.');
|
|
20
|
+
}
|
|
21
|
+
if(project?.routes.some(route=>route.router==='app')&&config.output!=='export'){
|
|
22
|
+
if(config.navigationCache?.maxEntries>0&&!config.navigationCache?.authCookies?.length)add('production-auth-cookies','warning','Retained App pages have no explicit authentication cookie names.','For authenticated applications, set navigationCache.authCookies to the exact session cookie names, including secure variants. Test account A → sign out → account B without retaining private drafts.');
|
|
23
|
+
else if(config.navigationCache?.authCookies?.length)add('production-auth-cookies','info','Authentication cookie changes enable strict retained-page state resets.','Test account A → sign out → account B in the browser; this check does not verify application session behavior.');
|
|
24
|
+
if(config.experimental?.serverActions?.allowedOrigins?.some(value=>value.includes('*')))add('production-action-origins','warning','Server Actions allow wildcard origin patterns.','Use the narrowest trusted host patterns and test rejected cross-origin mutations.');
|
|
25
|
+
}
|
|
26
|
+
const experiments=['nextScriptWorkers','lazyDynamicImports','streamingRequestBodies','nativePprShell','parallelBuilds'].filter(key=>config.experimental?.[key]);
|
|
27
|
+
if(config.reactCompiler?.backend==='oxc')experiments.push('reactCompiler.backend=oxc');
|
|
28
|
+
if(config.partialPrefetching)experiments.push('partialPrefetching');
|
|
29
|
+
if(env.PRNEXT_SSR_STREAM==='node')experiments.push('PRNEXT_SSR_STREAM=node');
|
|
30
|
+
for(const option of experiments)add('production-experimental','warning',`Experimental option enabled: ${option}.`,'Qualify this exact option on the deployment workload and keep its documented fallback available.',option.includes('=')?option.split('=')[0]:option==='partialPrefetching'?option:'experimental.'+option);
|
|
31
|
+
if(config.productionBrowserSourceMaps)add('production-source-maps','warning','Production browser source maps are enabled.','Confirm that exposing original browser sources is intentional.');
|
|
32
|
+
add('production-validation','info','Configuration preflight does not prove production readiness.','Build the final artifact, test public HTTPS, account changes, health checks, rollback and application dependencies before switching traffic.');
|
|
33
|
+
return diagnostics;
|
|
34
|
+
}
|
|
@@ -30,9 +30,10 @@ export const withReactCompilerReporting = run => reports.run({ files: new Map(),
|
|
|
30
30
|
export function configureReactCompilerReporting({ root, config, buildId, dev, compilerVersion }) {
|
|
31
31
|
const state = reports.getStore();
|
|
32
32
|
if (state) Object.assign(state, { root: path.resolve(root), enabled: !!config?.reactCompiler,
|
|
33
|
-
options: config?.reactCompiler || null, buildId, dev: !!dev, compilerVersion });
|
|
33
|
+
options: config?.reactCompiler || null, buildId, dev: !!dev, compilerVersion, backend: config?.reactCompiler?.backend || 'babel' });
|
|
34
34
|
}
|
|
35
35
|
export const withReactCompilerGraph = (target, run) => graphs.run(target, run);
|
|
36
|
+
export const reactCompilerGraphTarget = () => graphs.getStore();
|
|
36
37
|
|
|
37
38
|
/** Capture a narrow, source-free subset of the pinned compiler's public events. */
|
|
38
39
|
export function compilerEvent(event, functions, { moduleOptOut = false, root = '' } = {}) {
|
|
@@ -70,7 +71,7 @@ export function recordReactCompilerTransform(file, payload, { cacheHit = false }
|
|
|
70
71
|
if (!state.files.has(relative) && state.files.size >= FILE_LIMIT) { state.truncated = true; return; }
|
|
71
72
|
let record = state.files.get(relative);
|
|
72
73
|
if (!record) state.files.set(relative, record = { file: relative, transforms: 0, cacheReuses: 0,
|
|
73
|
-
targets: new Set(), notes: new Set(), functions: new Map() });
|
|
74
|
+
targets: new Set(), notes: new Set(), functions: new Map(), diagnostics: new Map() });
|
|
74
75
|
record.transforms++; record.cacheReuses += Number(cacheHit);
|
|
75
76
|
const target = graphs.getStore() || 'unspecified';
|
|
76
77
|
record.targets.add(target);
|
|
@@ -93,6 +94,16 @@ export function recordReactCompilerTransform(file, payload, { cacheHit = false }
|
|
|
93
94
|
if (fn.events.size < EVENT_LIMIT && state.eventCount < TOTAL_EVENT_LIMIT) { fn.events.set(detailKey, { ...detail, targets: new Set([target]) }); state.eventCount++; }
|
|
94
95
|
else state.truncated = true;
|
|
95
96
|
}
|
|
97
|
+
// Oxc reports native diagnostics, not function-success events. Keep these at
|
|
98
|
+
// file scope rather than inventing function identities or optimized counts.
|
|
99
|
+
for (const event of payload.diagnostics || []) {
|
|
100
|
+
const detail = { kind: event.kind, status: event.status, reason: event.reason, category: event.category };
|
|
101
|
+
const key = JSON.stringify(detail);
|
|
102
|
+
if (record.diagnostics.has(key)) { record.diagnostics.get(key).targets.add(target); continue; }
|
|
103
|
+
if (record.diagnostics.size < EVENT_LIMIT && state.eventCount < TOTAL_EVENT_LIMIT) {
|
|
104
|
+
record.diagnostics.set(key, { ...detail, targets: new Set([target]) }); state.eventCount++;
|
|
105
|
+
} else state.truncated = true;
|
|
106
|
+
}
|
|
96
107
|
if (payload.truncated) state.truncated = true;
|
|
97
108
|
}
|
|
98
109
|
export function recordReactCompilerGraph(result) {
|
|
@@ -115,13 +126,14 @@ export function reactCompilerReport() {
|
|
|
115
126
|
if (!state) return null;
|
|
116
127
|
const files = [...state.files.values()].sort((a, b) => a.file.localeCompare(b.file)).map(file => ({
|
|
117
128
|
file: file.file, transforms: file.transforms, cacheReuses: file.cacheReuses, targets: [...file.targets].sort(), notes: [...file.notes].sort(),
|
|
129
|
+
diagnostics: [...file.diagnostics.values()].map(({ targets, ...event }) => ({ ...event, targets: [...targets].sort() })),
|
|
118
130
|
functions: [...file.functions.values()].map(fn => ({ name: fn.name, start: fn.start, end: fn.end,
|
|
119
131
|
status: fn.statuses.size === 1 ? [...fn.statuses][0] : 'mixed', targets: [...fn.targets].sort(),
|
|
120
132
|
events: [...fn.events.values()].map(({ targets, ...event }) => ({ ...event, targets: [...targets].sort() })) }))
|
|
121
133
|
.sort((a, b) => (a.start?.line || 0) - (b.start?.line || 0) || (a.start?.column || 0) - (b.start?.column || 0)),
|
|
122
134
|
}));
|
|
123
135
|
const summary = summarize(files);
|
|
124
|
-
return { version: 1, compilerVersion: state.compilerVersion || null, enabled: !!state.enabled,
|
|
136
|
+
return { version: 1, backend: state.backend || 'babel', compilerVersion: state.compilerVersion || null, enabled: !!state.enabled,
|
|
125
137
|
buildId: state.buildId || null, mode: state.dev ? 'development' : 'production', scope: 'observed-transforms',
|
|
126
138
|
options: state.enabled ? state.options : null, truncated: state.truncated, summary, files,
|
|
127
139
|
graphs: [...state.graphs.values()].map(graph => ({ target: graph.target, metadataAvailable: graph.metadataAvailable,
|
|
@@ -178,9 +190,15 @@ function validReport(report) {
|
|
|
178
190
|
const targets = value => Array.isArray(value) && value.length <= 4 && value.every(target => ['browser', 'node', 'edge', 'unspecified'].includes(target));
|
|
179
191
|
const validPosition = value => value === null || keys(value, ['line', 'column']) && !!position(value);
|
|
180
192
|
const options = report?.options;
|
|
181
|
-
const validOptions = options === null || options === true || keys(options, ['compilationMode', 'panicThreshold', 'target'])
|
|
193
|
+
const validOptions = options === null || options === true || keys(options, ['backend', 'compilationMode', 'panicThreshold', 'target'])
|
|
194
|
+
&& (options.backend === undefined || ['babel', 'oxc'].includes(options.backend))
|
|
182
195
|
&& ['infer', 'annotation'].includes(options.compilationMode) && ['none', 'critical_errors', 'all_errors'].includes(options.panicThreshold) && options.target === '19';
|
|
183
|
-
|
|
196
|
+
const validDiagnostic = event => keys(event, ['kind', 'status', 'reason', 'category', 'targets'])
|
|
197
|
+
&& ['CompileError', 'CompileDiagnostic'].includes(event.kind) && ['error', 'diagnostic'].includes(event.status)
|
|
198
|
+
&& boundedText(event.reason) && boundedText(event.category) && targets(event.targets);
|
|
199
|
+
return keys(report, ['version', 'backend', 'compilerVersion', 'enabled', 'buildId', 'mode', 'scope', 'options', 'truncated', 'summary', 'files', 'graphs'])
|
|
200
|
+
&& (report.backend === undefined || ['babel', 'oxc'].includes(report.backend))
|
|
201
|
+
&& (report.backend || 'babel') === (options?.backend || 'babel')
|
|
184
202
|
&& report.version === 1 && report.scope === 'observed-transforms' && typeof report.enabled === 'boolean' && validOptions
|
|
185
203
|
&& typeof report.truncated === 'boolean' && ['development', 'production'].includes(report.mode)
|
|
186
204
|
&& boundedText(report.buildId) && boundedText(report.compilerVersion)
|
|
@@ -189,8 +207,9 @@ function validReport(report) {
|
|
|
189
207
|
&& Array.isArray(report.files) && report.files.length <= FILE_LIMIT && report.summary.files === report.files.length
|
|
190
208
|
&& report.files.every(file => file && typeof file === 'object')
|
|
191
209
|
&& 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'])
|
|
210
|
+
&& report.files.every(file => keys(file, ['file', 'transforms', 'cacheReuses', 'targets', 'notes', 'functions', 'diagnostics'])
|
|
193
211
|
&& safePath(file.file) && count(file.transforms) && count(file.cacheReuses) && file.cacheReuses <= file.transforms && targets(file.targets)
|
|
212
|
+
&& (file.diagnostics === undefined || Array.isArray(file.diagnostics) && file.diagnostics.length <= EVENT_LIMIT && file.diagnostics.every(validDiagnostic))
|
|
194
213
|
&& Array.isArray(file.notes) && file.notes.length <= 16 && file.notes.every(boundedText)
|
|
195
214
|
&& Array.isArray(file.functions) && file.functions.length <= FUNCTION_LIMIT && file.functions.every(fn => keys(fn, ['name', 'start', 'end', 'status', 'targets', 'events'])
|
|
196
215
|
&& boundedText(fn.name) && statuses.has(fn.status) && validPosition(fn.start) && validPosition(fn.end) && targets(fn.targets)
|
|
@@ -208,18 +227,22 @@ function validReport(report) {
|
|
|
208
227
|
function summarize(files) {
|
|
209
228
|
const summary = { files: files.length, functions: 0, optimized: 0, skipped: 0, errors: 0, diagnostics: 0, mixed: 0, transforms: 0, cacheReuses: 0 };
|
|
210
229
|
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]++; }
|
|
230
|
+
for (const fn of file.functions) { summary.functions++; summary[({ error: 'errors', diagnostic: 'diagnostics' })[fn.status] || fn.status]++; }
|
|
231
|
+
for (const diagnostic of file.diagnostics || []) summary[diagnostic.status === 'error' ? 'errors' : 'diagnostics']++;
|
|
232
|
+
}
|
|
212
233
|
return summary;
|
|
213
234
|
}
|
|
214
235
|
export function printReactCompilerReport(report, { details = false, log = console.log } = {}) {
|
|
215
236
|
if (!report.enabled) { log('React Compiler: disabled. Enable reactCompiler in the application configuration.'); return; }
|
|
216
237
|
const summary = report.summary;
|
|
217
|
-
log(`React Compiler ${report.compilerVersion}:
|
|
238
|
+
if (report.backend === 'oxc') log(`React Compiler oxc ${report.compilerVersion}: function optimization totals unavailable; ${summary.errors} errors, ${summary.diagnostics} diagnostics.`);
|
|
239
|
+
else log(`React Compiler ${report.compilerVersion}: ${summary.optimized} optimized, ${summary.skipped} skipped, ${summary.errors} errors, ${summary.diagnostics} diagnostics, ${summary.mixed} mixed.`);
|
|
218
240
|
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
241
|
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
242
|
if (details) for (const file of report.files) {
|
|
221
243
|
log(file.file);
|
|
222
244
|
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('; ') : ''}`);
|
|
245
|
+
for (const diagnostic of file.diagnostics || []) log(` ${diagnostic.status}: ${diagnostic.reason || diagnostic.category || 'Oxc diagnostic'}`);
|
|
223
246
|
for (const note of file.notes) log(' ' + note);
|
|
224
247
|
}
|
|
225
248
|
}
|
package/build/react-compiler.mjs
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import { realpath } from 'node:fs/promises';
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
5
5
|
import { cachedAsyncTransform } from './transform-cache.mjs';
|
|
6
6
|
import { extractSourceMap, inlineSourceMap, sourceMapsEnabled } from './source-maps.mjs';
|
|
7
|
-
import { compilerEvent, recordReactCompilerTransform } from './react-compiler-report.mjs';
|
|
7
|
+
import { compilerEvent, recordReactCompilerTransform, reactCompilerGraphTarget } from './react-compiler-report.mjs';
|
|
8
8
|
|
|
9
9
|
const require = createRequire(import.meta.url);
|
|
10
|
-
//
|
|
10
|
+
// Oxc is an optional peer. Disabled/Babel builds must not resolve it at all.
|
|
11
11
|
export const REACT_COMPILER_VERSION = require('babel-plugin-react-compiler/package.json').version;
|
|
12
|
+
export const OXC_REACT_COMPILER_VERSION = require('../package.json').peerDependencies['oxc-transform-react'];
|
|
12
13
|
const BABEL_VERSION = require('@babel/core/package.json').version;
|
|
13
14
|
const frameworkRoot = fileURLToPath(new URL('../', import.meta.url));
|
|
14
|
-
const options = new Set(['compilationMode', 'panicThreshold', 'target']);
|
|
15
|
+
const options = new Set(['backend', 'compilationMode', 'panicThreshold', 'target']);
|
|
15
16
|
let implementation;
|
|
16
|
-
const
|
|
17
|
+
const oxcImplementations = new Map();
|
|
18
|
+
const REPORT_CACHE_VERSION = 2;
|
|
17
19
|
const optOutDirective = directives => directives?.map(item => item.value.value).find(value => value === 'use no memo' || value === 'use no forget') || false;
|
|
18
20
|
|
|
19
21
|
function functionLocations(ast) {
|
|
@@ -48,14 +50,37 @@ export function normalizeReactCompilerConfig(value) {
|
|
|
48
50
|
if (!options.has(key)) throw new TypeError(`reactCompiler.${String(key)} is not supported by PRNext.`);
|
|
49
51
|
}
|
|
50
52
|
const compilationMode = input.compilationMode === undefined ? 'infer' : input.compilationMode;
|
|
53
|
+
const backend = input.backend === undefined ? 'babel' : input.backend;
|
|
51
54
|
const panicThreshold = input.panicThreshold === undefined ? 'none' : input.panicThreshold;
|
|
52
55
|
const target = input.target === undefined ? '19' : input.target;
|
|
53
56
|
if (!['infer', 'annotation'].includes(compilationMode)) throw new TypeError('reactCompiler.compilationMode must be infer or annotation.');
|
|
54
57
|
if (!['none', 'critical_errors', 'all_errors'].includes(panicThreshold)) throw new TypeError('reactCompiler.panicThreshold must be none, critical_errors or all_errors.');
|
|
55
58
|
if (target !== '19') throw new TypeError("reactCompiler.target must be '19'; PRNext requires React 19.");
|
|
56
|
-
|
|
59
|
+
if (!['babel', 'oxc'].includes(backend)) throw new TypeError('reactCompiler.backend must be babel or oxc.');
|
|
60
|
+
return Object.freeze({ ...(backend === 'oxc' ? { backend } : {}), compilationMode, panicThreshold, target });
|
|
57
61
|
}
|
|
58
62
|
|
|
63
|
+
function oxcDependency(projectRoot) {
|
|
64
|
+
const candidates = projectRoot ? [createRequire(path.join(path.resolve(projectRoot), 'package.json')), require] : [require];
|
|
65
|
+
let metadata, owner, missing;
|
|
66
|
+
for (const candidate of candidates) {
|
|
67
|
+
try { metadata = candidate('oxc-transform-react/package.json'); owner = candidate; break; }
|
|
68
|
+
catch (cause) {
|
|
69
|
+
if (cause.code !== 'MODULE_NOT_FOUND' && cause.code !== 'ERR_MODULE_NOT_FOUND') throw cause;
|
|
70
|
+
missing = cause;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (!owner) throw Object.assign(new Error(`reactCompiler.backend="oxc" requires oxc-transform-react@${OXC_REACT_COMPILER_VERSION}. Install it with npm install --save-dev oxc-transform-react@${OXC_REACT_COMPILER_VERSION}, or select backend:"babel".`, {cause: missing}), {code: 'PRNEXT_OXC_MISSING'});
|
|
74
|
+
if (metadata.name !== 'oxc-transform-react' || metadata.version !== OXC_REACT_COMPILER_VERSION) {
|
|
75
|
+
throw Object.assign(new Error(`reactCompiler.backend="oxc" requires oxc-transform-react@${OXC_REACT_COMPILER_VERSION}; found ${metadata.version || 'an unknown version'}. Install the supported version with npm install --save-dev oxc-transform-react@${OXC_REACT_COMPILER_VERSION}.`), {code: 'PRNEXT_OXC_VERSION'});
|
|
76
|
+
}
|
|
77
|
+
// Resolve and import from the same dependency owner. A global CLI must use
|
|
78
|
+
// its application's peer, while nested framework installations still work.
|
|
79
|
+
return {version: metadata.version, entry: owner.resolve('oxc-transform-react')};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const reactCompilerVersion = (config, projectRoot) => config?.backend === 'oxc' ? oxcDependency(projectRoot).version : REACT_COMPILER_VERSION;
|
|
83
|
+
|
|
59
84
|
function inside(root, filename) {
|
|
60
85
|
const relative = path.relative(root, filename);
|
|
61
86
|
return relative !== '' && relative !== '..' && !relative.startsWith('..' + path.sep) && !path.isAbsolute(relative);
|
|
@@ -75,14 +100,74 @@ function loadImplementation() {
|
|
|
75
100
|
return implementation;
|
|
76
101
|
}
|
|
77
102
|
|
|
103
|
+
function loadOxcImplementation(dependency) {
|
|
104
|
+
if (!oxcImplementations.has(dependency.entry)) oxcImplementations.set(dependency.entry, Promise.all([import(pathToFileURL(dependency.entry).href), import('oxc-parser')]).then(([compiler, parser]) => {
|
|
105
|
+
if (compiler.__napiBindingTarget !== 'native') throw new Error('reactCompiler.backend="oxc" requires the native platform binding. Install optional dependencies or select backend:"babel".');
|
|
106
|
+
return { transform: compiler.transform, parse: parser.parseSync };
|
|
107
|
+
}).catch(cause => {
|
|
108
|
+
throw new Error(`Cannot load the native oxc-transform-react@${OXC_REACT_COMPILER_VERSION} compiler. Reinstall it with optional dependencies enabled on this platform, or select backend:"babel". ${cause.message}`, {cause});
|
|
109
|
+
}));
|
|
110
|
+
return oxcImplementations.get(dependency.entry);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Compose only source-map coordinates; application strings never enter this path. */
|
|
114
|
+
async function composeOxcMap(output, input) {
|
|
115
|
+
if (!input || !output) return output;
|
|
116
|
+
const { TraceMap, decodedMappings, originalPositionFor, sourceContentFor, encodedMap } = await import('@jridgewell/trace-mapping');
|
|
117
|
+
const previous = new TraceMap(input), current = new TraceMap(output);
|
|
118
|
+
const sources = [], contents = [], names = [];
|
|
119
|
+
const mappings = decodedMappings(current).map(line => line.map(segment => {
|
|
120
|
+
if (segment.length < 4) return [segment[0]];
|
|
121
|
+
const position = originalPositionFor(previous, { line: segment[2] + 1, column: segment[3] });
|
|
122
|
+
if (position.source === null || position.line === null || position.column === null) return [segment[0]];
|
|
123
|
+
let source = sources.indexOf(position.source);
|
|
124
|
+
if (source < 0) { source = sources.length; sources.push(position.source); contents.push(sourceContentFor(previous, position.source)); }
|
|
125
|
+
const mapped = [segment[0], source, position.line - 1, position.column];
|
|
126
|
+
const name = position.name || (segment.length > 4 ? current.names[segment[4]] : null);
|
|
127
|
+
if (name) { let index = names.indexOf(name); if (index < 0) { index = names.length; names.push(name); } mapped.push(index); }
|
|
128
|
+
return mapped;
|
|
129
|
+
}));
|
|
130
|
+
return encodedMap(new TraceMap({ version: 3, sources, sourcesContent: contents, names, mappings }));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function transformOxc(source, filename, normalized, { mapped, outputMode, loaderOutput, root, dependency }) {
|
|
134
|
+
const { transform, parse } = await loadOxcImplementation(dependency);
|
|
135
|
+
const { code, map } = extractSourceMap(source);
|
|
136
|
+
const lang = /\.tsx$/i.test(filename) ? 'tsx' : /\.[cm]?ts$/i.test(filename) ? 'ts' : 'jsx';
|
|
137
|
+
// The native transform does not expose directives. Inspect its original input
|
|
138
|
+
// with the existing Rust parser, never Babel, before entering executable modules.
|
|
139
|
+
const parsed = parse(filename, code, { lang, sourceType: 'unambiguous' });
|
|
140
|
+
const directives = [];
|
|
141
|
+
if (!parsed.errors.length) for (const statement of parsed.program.body) {
|
|
142
|
+
if (!statement.directive) break;
|
|
143
|
+
directives.push(statement.directive);
|
|
144
|
+
}
|
|
145
|
+
if (directives.includes('use server')) return { code: source, events: [], notes: ['Server Action module excluded.'] };
|
|
146
|
+
const result = await transform(filename, code, { lang, sourceType: 'unambiguous', jsx: 'preserve', sourcemap: mapped,
|
|
147
|
+
reactCompiler: { compilationMode: normalized.compilationMode, panicThreshold: normalized.panicThreshold,
|
|
148
|
+
target: normalized.target, outputMode, reportDiagnostics: true, sources: [''] } });
|
|
149
|
+
const diagnostics = result.errors.slice(0, 16).map(error => compilerEvent({
|
|
150
|
+
kind: result.fatal ? 'CompileError' : 'CompileDiagnostic', reason: error.message, detail: { category: 'Oxc' },
|
|
151
|
+
}, [], { root })).filter(Boolean);
|
|
152
|
+
const notes = ['Experimental Oxc backend; upstream does not expose per-function optimization events. Function optimization totals are unavailable.',
|
|
153
|
+
`Oxc output mode: ${outputMode}. TypeScript syntax is removed; JSX is preserved for the graph compiler.`];
|
|
154
|
+
if (normalized.compilationMode === 'annotation') notes.push('Annotation mode only selects functions with "use memo".');
|
|
155
|
+
if (loaderOutput) notes.push('Locations refer to JavaScript produced by the configured loader.');
|
|
156
|
+
return { code: result.fatal ? source : mapped ? inlineSourceMap(result.code, await composeOxcMap(result.map, map)) : result.code,
|
|
157
|
+
events: [], diagnostics, notes, fatal: result.fatal, truncated: result.errors.length > 16 };
|
|
158
|
+
}
|
|
159
|
+
|
|
78
160
|
/**
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
161
|
+
* Compile application source before JSX lowering and React Refresh. The Oxc
|
|
162
|
+
* development path instruments original JSX first, before native hoisting.
|
|
163
|
+
* JSX remains available to the graph compiler; Oxc also removes TypeScript.
|
|
164
|
+
* No application Babel configuration is loaded.
|
|
82
165
|
*/
|
|
83
166
|
export function createReactCompilerTransform(projectRoot, config, { dev = false } = {}) {
|
|
84
167
|
const normalized = normalizeReactCompilerConfig(config);
|
|
85
168
|
if (!normalized) return async source => source;
|
|
169
|
+
const dependency = normalized.backend === 'oxc' ? oxcDependency(projectRoot) : null;
|
|
170
|
+
const compilerVersion = dependency?.version || REACT_COMPILER_VERSION;
|
|
86
171
|
const root = path.resolve(projectRoot);
|
|
87
172
|
const canonicalRoot = realpath(root).catch(error => {
|
|
88
173
|
if (error.code === 'ENOENT') return root;
|
|
@@ -102,10 +187,15 @@ export function createReactCompilerTransform(projectRoot, config, { dev = false
|
|
|
102
187
|
throw error;
|
|
103
188
|
});
|
|
104
189
|
if (excluded(actualRoot, actualFile) || inside(await canonicalFramework, actualFile)) return source;
|
|
190
|
+
const backend = normalized.backend || 'babel';
|
|
191
|
+
const graph = reactCompilerGraphTarget();
|
|
192
|
+
const outputMode = graph === 'node' || graph === 'edge' ? 'ssr' : 'client';
|
|
105
193
|
let cacheHit = true;
|
|
106
194
|
const serialized = await cachedAsyncTransform('react-compiler-report', source,
|
|
107
|
-
[REPORT_CACHE_VERSION,
|
|
195
|
+
[REPORT_CACHE_VERSION, backend, compilerVersion, dependency?.entry, backend === 'babel' ? BABEL_VERSION : null,
|
|
196
|
+
normalized, root, actualRoot, filename, actualFile, !!dev, !!loaderOutput, backend === 'oxc' ? outputMode : null], async () => {
|
|
108
197
|
cacheHit = false;
|
|
198
|
+
if (backend === 'oxc') return JSON.stringify(await transformOxc(source, filename, normalized, { mapped: sourceMapsEnabled(), outputMode, loaderOutput, root, dependency }));
|
|
109
199
|
const { babel, compiler } = await loadImplementation();
|
|
110
200
|
const mapped = sourceMapsEnabled();
|
|
111
201
|
const { code, map } = extractSourceMap(source);
|
|
@@ -134,6 +224,7 @@ export function createReactCompilerTransform(projectRoot, config, { dev = false
|
|
|
134
224
|
});
|
|
135
225
|
const payload = JSON.parse(serialized);
|
|
136
226
|
recordReactCompilerTransform(inside(root, filename) ? filename : path.resolve(root, path.relative(actualRoot, actualFile)), payload, { cacheHit });
|
|
227
|
+
if (payload.fatal) throw new Error(`React Compiler (oxc ${OXC_REACT_COMPILER_VERSION}) failed for ${filename}: ${payload.diagnostics.map(value => value.reason).join('; ') || 'Native compilation failed.'}`);
|
|
137
228
|
return payload.code;
|
|
138
229
|
};
|
|
139
230
|
}
|