@thomas.f/prnext 0.1.19-alpha → 0.1.20-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/build/app.mjs +46 -78
  2. package/build/build-lock.mjs +2 -71
  3. package/build/build-retention.mjs +119 -0
  4. package/build/bundle-runtime.mjs +9 -8
  5. package/build/cache-command.mjs +31 -0
  6. package/build/check.mjs +41 -3
  7. package/build/compilation-cache.mjs +32 -12
  8. package/build/compilation-storage.mjs +190 -0
  9. package/build/compiler.mjs +3 -3
  10. package/build/config.mjs +23 -2
  11. package/build/create.mjs +1 -1
  12. package/build/css-modules.mjs +4 -0
  13. package/build/dev-action-graph.mjs +21 -0
  14. package/build/font-cache.mjs +9 -6
  15. package/build/font-files.mjs +1 -1
  16. package/build/index.mjs +30 -13
  17. package/build/native-ppr.mjs +32 -0
  18. package/build/og.mjs +121 -1
  19. package/build/parallel-plan.mjs +55 -0
  20. package/build/persistent-cache.mjs +29 -24
  21. package/build/production-check.mjs +34 -0
  22. package/build/react-compiler-report.mjs +31 -8
  23. package/build/react-compiler.mjs +101 -10
  24. package/build/server-config.mjs +35 -0
  25. package/build/source-analysis.mjs +54 -4
  26. package/build/static-adapter.mjs +7 -3
  27. package/build/transform.mjs +38 -6
  28. package/build/typescript-dev.mjs +3 -1
  29. package/cli.mjs +14 -5
  30. package/compat/app-layout-context.cjs +7 -3
  31. package/compat/flight-delta.cjs +47 -12
  32. package/compat/index.d.ts +24 -1
  33. package/compat/navigation-authorization.cjs +8 -1
  34. package/docs/ai/compatibility.md +110 -7
  35. package/docs/ai/deployment.md +141 -0
  36. package/docs/ai/quickstart.md +5 -0
  37. package/package.json +12 -9
  38. package/release/fontkit.mjs +31 -0
  39. package/release/verify.mjs +3 -0
  40. package/runtime/api.mjs +5 -2
  41. package/runtime/app-client.mjs +150 -26
  42. package/runtime/app-partial.mjs +2 -10
  43. package/runtime/app-prefetch.mjs +2 -2
  44. package/runtime/app-render.mjs +11 -7
  45. package/runtime/app-stream-channel.mjs +3 -2
  46. package/runtime/app-styles.mjs +22 -4
  47. package/runtime/compile-cache-retention.mjs +53 -0
  48. package/runtime/compile-cache.mjs +28 -1
  49. package/runtime/dev-client.mjs +6 -3
  50. package/runtime/dev-gateway.mjs +3 -2
  51. package/runtime/dev-route-selection.mjs +64 -0
  52. package/runtime/dev-runtime-pool.mjs +3 -2
  53. package/runtime/dev-runtime-spare.mjs +1 -1
  54. package/runtime/dev-session-retention.mjs +154 -0
  55. package/runtime/development.mjs +27 -8
  56. package/runtime/env.mjs +9 -0
  57. package/runtime/flight-delta.mjs +31 -5
  58. package/runtime/native-ppr.mjs +35 -0
  59. package/runtime/og/NOTICE.txt +1 -1
  60. package/runtime/og/index.node.mjs +76 -31216
  61. package/runtime/og/upstream.json +115 -0
  62. package/runtime/persistent/client.mjs +4 -2
  63. package/runtime/persistent/daemon.mjs +3 -11
  64. package/runtime/persistent/health.mjs +58 -0
  65. package/runtime/project-lock.mjs +71 -0
  66. package/runtime/request-transport.mjs +112 -32
  67. package/runtime/storage-path.mjs +36 -0
  68. package/runtime/trace.mjs +44 -4
  69. package/runtime/worker.mjs +9 -4
  70. package/vendor/fontkit/NOTICE.txt +5 -0
  71. package/vendor/fontkit/SOURCE-NOTICES.txt +209 -0
  72. package/vendor/fontkit/index.mjs +3 -0
  73. package/vendor/fontkit/licenses/@swc__helpers/LICENSE +201 -0
  74. package/vendor/fontkit/licenses/base64-js/LICENSE +21 -0
  75. package/vendor/fontkit/licenses/brotli/readme.md +61 -0
  76. package/vendor/fontkit/licenses/clone/LICENSE +18 -0
  77. package/vendor/fontkit/licenses/dfa/README.md +83 -0
  78. package/vendor/fontkit/licenses/fast-deep-equal/LICENSE +21 -0
  79. package/vendor/fontkit/licenses/fontkit/README.md +266 -0
  80. package/vendor/fontkit/licenses/restructure/LICENSE +21 -0
  81. package/vendor/fontkit/licenses/tiny-inflate/LICENSE +21 -0
  82. package/vendor/fontkit/licenses/tslib/LICENSE.txt +12 -0
  83. package/vendor/fontkit/licenses/unicode-properties/LICENSE +8 -0
  84. package/vendor/fontkit/licenses/unicode-trie/LICENSE +8 -0
  85. package/vendor/fontkit/upstream.json +215 -0
package/build/index.mjs CHANGED
@@ -6,12 +6,15 @@ import { writeRuntimeContract } from './runtime-contract.mjs';
6
6
  import { writeBuildOutput } from './output-contract.mjs';
7
7
  import { bundleRuntime } from './bundle-runtime.mjs';
8
8
  import {withPersistentTransforms} from './transform-cache.mjs';
9
+ import {withCompilationStorage,configureCompilationStorage} from './compilation-storage.mjs';
10
+ import {reclaimBuildStages,trackBuildStage} from './build-retention.mjs';
9
11
  import {applicationEntry, frameworkImportName, frameworkRuntimeImportPattern} from './framework-imports.mjs';
10
12
  import { expandLocales, localizedStaticPaths } from './i18n.mjs';
11
13
  import { build as bundle, withCompilation, configureCompiler, compilerSource,buildAnalysisRecords,buildAnalysisRoutes,devDependencies,devStylePlans,devStyleDependencies,devModules,readCompilerFile,compilationGraphCacheReport } from './compiler.mjs';
12
14
  import {retainDeploymentAssets} from './deployment-assets.mjs';
13
15
  import {selectHtmlOnlyRoutes} from './html-only-config.mjs';
14
16
  import {writeBuildAnalysis} from './analyze.mjs';
17
+ import {prepareNativePprShells} from './native-ppr.mjs';
15
18
  import {routeFiles} from './dev-discovery.mjs';
16
19
  import {tryBuildLazyGraph,rememberLazyBuildContext,forgetLazyBuildContext} from './dev-lazy-build.mjs';
17
20
  import {writeTypedRoutes} from './typed-routes.mjs';
@@ -37,7 +40,7 @@ import { compileCustomRoutes } from './custom-routes.mjs';
37
40
  import { compileMiddleware } from './middleware.mjs';
38
41
  import { prepareScriptWorkers } from './script-workers.mjs';
39
42
  import { prepareMetadataRoutes } from './metadata-routes.mjs';
40
- import { prepareImageResponse } from './og.mjs';
43
+ import { prepareImageResponse, imageResponseRequirement } from './og.mjs';
41
44
  import { prepareCacheHandler, prepareCacheHandlers } from './cache-handlers.mjs';
42
45
  import { npmPackagesPlugin } from './npm-packages.mjs';
43
46
  import { createRefreshTransform, createSourceLocationTransform, devSingletonsPlugin, devClientFile } from './dev.mjs';
@@ -74,7 +77,7 @@ async function copyRuntimeFiles(stage,{development=true}={}) {
74
77
  if (failed) throw failed.reason;
75
78
  }
76
79
 
77
- function compatibilityPlugin({ browser, pageFiles, projectRoot, documentFile, serverOnlyFiles, fonts, refresh, locations, reactCompiler }) {
80
+ function compatibilityPlugin({ browser, pageFiles, projectRoot, documentFile, serverOnlyFiles, fonts, refresh, locations, reactCompiler, nativeCompiler = false }) {
78
81
  return {
79
82
  name: 'prnext-pages-compatibility',
80
83
  setup(esbuild) {
@@ -108,8 +111,13 @@ function compatibilityPlugin({ browser, pageFiles, projectRoot, documentFile, se
108
111
  // loaders. Keep that entire graph original: an uppercase JSX utility
109
112
  // may be called normally there, outside a React render. Browser render
110
113
  // graphs receive memoization before instrumentation and JSX lowering.
111
- const compiled=browser && reactCompiler && args.path!==documentFile && !serverOnlyFiles.has(args.path) ? await reactCompiler(original,args.path,{loaderOutput:args.prnextSource!==undefined}) : original;
112
- const source = await fonts.transform(locations?await locations(compiled,args.path):compiled, args.path);
114
+ const compile = browser && reactCompiler && args.path!==documentFile && !serverOnlyFiles.has(args.path);
115
+ // Oxc can hoist static JSX without a source-map position. Attach the
116
+ // same original inspector attributes as SSR before that transformation.
117
+ const locateFirst = !!(compile && nativeCompiler && locations);
118
+ const input = locateFirst ? await locations(original,args.path) : original;
119
+ const compiled = compile ? await reactCompiler(input,args.path,{loaderOutput:args.prnextSource!==undefined}) : input;
120
+ const source = await fonts.transform(locations && !locateFirst ? await locations(compiled,args.path) : compiled, args.path);
113
121
  assertSupportedSource(source, args.path);
114
122
  const dynamicSource = transformDynamicImports(source, args.path, { projectRoot, mode: browser ? 'browser' : 'server' });
115
123
  let contents = browser && pageFiles.has(args.path) ? stripServerCode(dynamicSource, args.path) : dynamicSource;
@@ -179,8 +187,9 @@ export function build(projectRoot, options = {}) {
179
187
  const envMode = options.envMode || (process.env.NODE_ENV === 'test' ? 'test' : options.dev ? 'development' : 'production');
180
188
  process.env.NODE_ENV = options.dev ? 'development' : 'production';
181
189
  try { return await withBuildLock(projectRoot, async root => {
190
+ await reclaimBuildStages(root);
182
191
  const started=performance.now();
183
- const result=await withPersistentTransforms(root,{dev:!!options.dev},()=>withCompilation(() => buildProject(root, { ...options, envMode })));
192
+ const result=await withCompilationStorage(root,()=>withPersistentTransforms(root,{dev:!!options.dev},()=>withCompilation(() => buildProject(root, { ...options, envMode }))));
184
193
  return {...result,durationMs:Math.round(performance.now()-started)};
185
194
  }); }
186
195
  finally { restore(); }
@@ -198,9 +207,10 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
198
207
  if (deferred) return deferred;
199
208
  }
200
209
  const config = await loadProjectConfig(projectRoot, { dev, envMode, devInputs });
210
+ configureCompilationStorage(config.compilationCache);
201
211
  const tsconfig = config.typescript ? await projectTsconfig(projectRoot, config) : undefined;
202
212
  const environment = defineEnvironment(config);
203
- const publicPaths = { trailingSlash: config.trailingSlash, skipTrailingSlashRedirect: config.skipTrailingSlashRedirect, basePath: config.basePath, assetPrefix: config.assetPrefix, assetBase: publicAssetBase(config) };
213
+ const publicPaths = { server: config.server, trailingSlash: config.trailingSlash, skipTrailingSlashRedirect: config.skipTrailingSlashRedirect, basePath: config.basePath, assetPrefix: config.assetPrefix, assetBase: publicAssetBase(config) };
204
214
  const customRoutes = await compileCustomRoutes(config);
205
215
  const buildId = await generateBuildId(config, { dev });
206
216
  const scanOptions={basePath:config.basePath,pageExtensions:config.pageExtensions,i18n:config.i18n};
@@ -222,12 +232,14 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
222
232
  const stage = path.join(project.root, `.prnext-build-${randomUUID()}`);
223
233
  const destination = await prepareBuildDirectory(project.root, config.distDir);
224
234
  const backup = path.join(project.root, `.prnext-backup-${randomUUID()}`);
235
+ const recovery=await trackBuildStage(project.root,{stage,backup}).catch(()=>null);
236
+ try {
225
237
  await mkdir(path.join(stage, '.entries'), { recursive: true });
226
238
  await mkdir(path.join(stage, 'static'), { recursive: true });
227
239
  await addBuiltinPagesErrors(project, stage);
228
240
  const manifest = { version: 1, dev, buildId, cacheId: randomUUID(), previewModeId: randomUUID().replaceAll('-', ''), previewModeEncryptionKey: randomBytes(32).toString('hex'),
229
241
  ...(dev && Array.isArray(devRoutes) ? {devOnDemand:true} : {}), ...(dev && devSession ? {devSequence,devSession} : {}), ...(dev ? {devForceRefresh,devAffectedModules,devIndicators:config.devIndicators,...(devTools?{devTools}:{})} : {}),
230
- config: { i18n: config.i18n, skipMiddlewareUrlNormalize: config.skipMiddlewareUrlNormalize, compress: config.compress, poweredByHeader: config.poweredByHeader, reactStrictMode: config.reactStrictMode, cacheComponents: config.cacheComponents, partialPrefetching: config.partialPrefetching, navigationCache: config.navigationCache, cacheLife: config.cacheLife, htmlOnly:config.htmlOnly, images: config.images, ...publicPaths }, customRoutes, routes: [], prerendered: [],
242
+ config: { i18n: config.i18n, skipMiddlewareUrlNormalize: config.skipMiddlewareUrlNormalize, compress: config.compress, poweredByHeader: config.poweredByHeader, reactStrictMode: config.reactStrictMode, cacheComponents: config.cacheComponents, partialPrefetching: config.partialPrefetching, navigationCache: config.navigationCache, cacheLife: config.cacheLife, htmlOnly:config.htmlOnly, images: config.images, streamingRequestBodies: config.experimental.streamingRequestBodies, nativePprShell: config.experimental.nativePprShell, ...publicPaths }, customRoutes, routes: [], prerendered: [],
231
243
  ...(project.pagesErrors ? { pagesErrors: project.pagesErrors } : {}) };
232
244
  // Its build-unique name is known before bundling and avoids a hash cycle
233
245
  // between page entry URLs and the manifest that lists those entries.
@@ -250,7 +262,6 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
250
262
  // Runtime copies must work even when dotenv is nested beneath the PRNext
251
263
  // package instead of hoisted beside the application's dependencies.
252
264
  await bundle({ entryPoints: [path.join(packageRoot, 'runtime/env.mjs')], outfile: path.join(stage, 'runtime/env.mjs'), bundle: true, platform: 'node', format: 'esm', target: 'node22', logLevel: 'silent', banner: { js: "import {createRequire as __prnextCreateRequire} from 'node:module';const require=__prnextCreateRequire(import.meta.url);" } });
253
- await prepareImageResponse(stage);
254
265
  manifest.config.serverActions = config.experimental.serverActions;
255
266
  if(config.workerMemory && !dev)manifest.config.workerMemory=config.workerMemory;
256
267
  if (config.workerRetirement) manifest.config.workerRetirement = config.workerRetirement;
@@ -295,7 +306,7 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
295
306
  }
296
307
  }
297
308
  if (Object.keys(clientEntries).length) {
298
- const browser = await bundle({ ...common, entryPoints: clientEntries, outdir: path.join(stage, 'assets'), entryNames: '[name]-[hash]', chunkNames: 'chunk-[hash]', platform: 'browser', format: 'esm', target: ['es2022'], splitting: true, minify: !dev, sourcemap: dev || config.productionBrowserSourceMaps, define: { 'process.env': '{}', ...common.define }, plugins: [moduleResolutionPlugin(config.turbopack, project.root), ...(dev ? [devSingletonsPlugin()] : []), images.plugin(), fonts.plugin(), compatibilityPlugin({ browser: true, pageFiles, projectRoot: project.root, documentFile: project.document, serverOnlyFiles, fonts, refresh, locations, reactCompiler }), cssModules.plugin(true)] });
309
+ const browser = await bundle({ ...common, entryPoints: clientEntries, outdir: path.join(stage, 'assets'), entryNames: '[name]-[hash]', chunkNames: 'chunk-[hash]', platform: 'browser', format: 'esm', target: ['es2022'], splitting: true, minify: !dev, sourcemap: dev || config.productionBrowserSourceMaps, define: { 'process.env': '{}', ...common.define }, plugins: [moduleResolutionPlugin(config.turbopack, project.root), ...(dev ? [devSingletonsPlugin()] : []), images.plugin(), fonts.plugin(), compatibilityPlugin({ browser: true, pageFiles, projectRoot: project.root, documentFile: project.document, serverOnlyFiles, fonts, refresh, locations, reactCompiler, nativeCompiler: config.reactCompiler?.backend === 'oxc' }), cssModules.plugin(true)] });
299
310
  fonts.record(browser.metafile);
300
311
  const byEntry = new Map(Object.entries(clientEntries).map(([id, file]) => [path.resolve(file), id]));
301
312
  for (const [output, metadata] of Object.entries(browser.metafile.outputs)) {
@@ -312,7 +323,7 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
312
323
  await compileMiddleware({ project, stage, manifest, dev, moduleResolution: config.turbopack, defineEnvironment: environment });
313
324
  if (manifest.middleware) manifest.middleware.bodySizeLimit = config.experimental.proxyClientMaxBodySize;
314
325
  await prepareMetadataRoutes(project, stage);
315
- await compileApp({ project, stage, manifest, reactCompiler: config.reactCompiler, moduleResolution: config.turbopack, cssModules, fonts, images, dev, devActionSecret:dev?devActionSecret:undefined, defineEnvironment: environment, productionBrowserSourceMaps: config.productionBrowserSourceMaps, serverExternalPackages: config.serverExternalPackages, lazyDynamicImports:dev&&config.experimental.lazyDynamicImports,devLazyRequest,devLazyActivated });
326
+ await compileApp({ project, stage, manifest, reactCompiler: config.reactCompiler, moduleResolution: config.turbopack, cssModules, fonts, images, dev, devActionSecret:dev?devActionSecret:undefined, defineEnvironment: environment, productionBrowserSourceMaps: config.productionBrowserSourceMaps, serverExternalPackages: config.serverExternalPackages, lazyDynamicImports:dev&&config.experimental.lazyDynamicImports,devLazyRequest,devLazyActivated, parallelBuilds: !dev && config.experimental.parallelBuilds && await cssModules.parallelSafe() ? config.experimental.parallelBuilds : false });
316
327
  if(manifest.devLazy){
317
328
  const css=Object.values(manifest.devLazy.outputs).flatMap(module=>module.css);
318
329
  for(const route of manifest.routes)if(route.router==='app'&&route.kind==='page'){
@@ -338,14 +349,18 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
338
349
  await checkTypeScript(project.root, config, { dev, tsconfig });
339
350
  fonts.attach(manifest);
340
351
  expandLocales(manifest);
352
+ const imageResponse = await imageResponseRequirement(stage, {dev, manifest});
353
+ if (imageResponse.included) await prepareImageResponse(stage);
354
+ else await rm(path.join(stage, 'compat/og.cjs'), {force: true});
341
355
  // All runtime preparation must finish before its shared modules are built;
342
356
  // no staged renderer has been imported before this production-only point.
343
- if (!dev) await bundleRuntime(stage,{projectRoot:project.root,sourceDirectory:path.join(packageRoot,'runtime')});
357
+ if (!dev) await bundleRuntime(stage,{projectRoot:project.root,sourceDirectory:path.join(packageRoot,'runtime'),imageResponse:imageResponse.included});
344
358
  // Node caches CJS React by filename, not NODE_ENV. Once development metadata
345
359
  // imports load it, subsequent production renders need a clean process too.
346
360
  if(dev) developmentModulesLoaded = true;
347
361
  if(!dev) Object.assign(manifest,await (incremental || developmentModulesLoaded ? isolatedPrerender : prerenderProduction)(project.root,stage,manifest,{prerender:config.prerender}));
348
362
  else {await prerender(stage, manifest, dev);await prerenderApp(stage, manifest, dev);}
363
+ if (!dev) await prepareNativePprShells(stage, manifest);
349
364
  const pages = manifest.routes.filter(route => route.kind === 'page' && route.router !== 'app' && !route.internal && !route.devDeferred);
350
365
  const publicPage = route => ({ ...(route.locale ? {locale:route.locale,originalPattern:route.originalPattern} : {}), id: route.id, pattern: route.pattern, client: route.client, css: route.css, ...(route.fonts ? { fonts: route.fonts } : {}), ssg: !!route.ssg, ssp: !!route.ssp, gip: !!route.gip, appGip: !!route.appGip });
351
366
  if (pages.length || manifest.pagesErrors) {
@@ -420,12 +435,14 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
420
435
  throw error;
421
436
  } finally { await rm(pointerTemporary, {force:true}); }
422
437
  await exported?.commit();
423
- if (hadDestination) await rm(backup, { recursive: true, force: true });
438
+ await recovery?.committed().catch(()=>{});
439
+ if(!recovery&&hadDestination)await rm(backup,{recursive:true,force:true});
424
440
  if(dev&&!devLazyRequest)rememberLazyBuildContext(project.root,{manifest,config,environment,tsconfig,envMode});
425
441
  return { ...manifest, outputDirectory: destination, durationMs: Math.round(performance.now() - started),reactCompilerReport:compilerReport,compilationCache };
426
442
  } catch (error) {
427
- await rm(stage, { recursive: true, force: true });
443
+ if(!recovery)await rm(stage, { recursive: true, force: true });
428
444
  if (error.errors) error.message = error.errors.map(item => `${item.location ? `${item.location.file}:${item.location.line}: ` : ''}${item.text}`).join('\n');
429
445
  throw error;
430
446
  }
447
+ }finally{await recovery?.cleanup();}
431
448
  }
@@ -0,0 +1,32 @@
1
+ import {readFile, writeFile, stat} from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import {partialShell, sha256, NATIVE_PPR_ARTIFACT_BYTES, NATIVE_PPR_PREFIX_BYTES} from '../runtime/native-ppr.mjs';
4
+
5
+ // Only concrete build-time shells are certified. Runtime regeneration, generic
6
+ // params, middleware and external caches stay on the authoritative Node path.
7
+ export async function prepareNativePprShells(stage, manifest) {
8
+ delete manifest.config.nativePprArtifacts;
9
+ if (!manifest.config.nativePprShell || !manifest.config.cacheComponents || manifest.dev || manifest.middleware || manifest.config.basePath || manifest.config.cacheHandler || Object.keys(manifest.config.cacheHandlers || {}).length) return;
10
+ const descriptors = [];
11
+ let total = 0;
12
+ for (const route of [...manifest.routes].sort((a, b) => a.id.localeCompare(b.id))) {
13
+ if (route.kind !== 'page' || route.router !== 'app' || route.internal || route.parallel || route.cacheConfig?.runtime === 'edge') continue;
14
+ for (const [pathname, artifactFile] of Object.entries(route.ppr || {}).sort(([a], [b]) => a.localeCompare(b))) {
15
+ if (descriptors.length >= 32) break;
16
+ const file = path.resolve(stage, artifactFile);
17
+ if (!artifactFile.startsWith('static/') || !file.startsWith(path.resolve(stage) + path.sep) || (await stat(file)).size > NATIVE_PPR_ARTIFACT_BYTES) continue;
18
+ const bytes = await readFile(file);
19
+ const artifact = JSON.parse(bytes);
20
+ if (artifact.version !== 1 || !artifact.postponed || artifact.complete || artifact.generic || artifact.externalCache || artifact.revalidate === 0 || typeof artifact.flight !== 'string' || Object.keys(artifact.headers || {}).length) continue;
21
+ const prefix = Buffer.from(partialShell(artifact, route).replace(/<\/body><\/html>$/i, ''));
22
+ if (prefix.length > NATIVE_PPR_PREFIX_BYTES || total + prefix.length > 4 * 1024 * 1024) continue;
23
+ if (!Number.isSafeInteger(artifact.generatedAt) || artifact.generatedAt < 0 || !(artifact.revalidate == null || artifact.revalidate === false || Number.isSafeInteger(artifact.revalidate) && artifact.revalidate > 0)) continue;
24
+ const prefixHash = sha256(prefix);
25
+ const prefixFile = `static/native-ppr-${prefixHash}.html`;
26
+ await writeFile(path.join(stage, prefixFile), prefix);
27
+ descriptors.push({routeId: route.id, path: pathname, artifact: artifactFile, artifactHash: sha256(bytes), prefix: prefixFile, prefixHash, identity: sha256(artifact.flight), generatedAt: artifact.generatedAt, revalidate: artifact.revalidate ?? false});
28
+ total += prefix.length;
29
+ }
30
+ }
31
+ manifest.config.nativePprArtifacts = descriptors;
32
+ }
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 record=JSON.parse(await file.readFile('utf8'));
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),body=JSON.stringify({version:1,key,checksum:cacheHash(payload),payload});
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);
@@ -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
- return keys(report, ['version', 'compilerVersion', 'enabled', 'buildId', 'mode', 'scope', 'options', 'truncated', 'summary', 'files', 'graphs'])
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}: ${summary.optimized} optimized, ${summary.skipped} skipped, ${summary.errors} errors, ${summary.diagnostics} diagnostics, ${summary.mixed} mixed.`);
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
  }