@thomas.f/prnext 0.1.19-alpha → 0.1.21-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/build/app.mjs +46 -78
- package/build/build-lock.mjs +2 -71
- package/build/build-retention.mjs +119 -0
- package/build/build-timings.mjs +38 -0
- package/build/bundle-runtime.mjs +17 -16
- 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 +5 -4
- package/build/config.mjs +23 -2
- package/build/create.mjs +1 -1
- package/build/css-modules.mjs +4 -0
- package/build/dev-action-graph.mjs +21 -0
- package/build/flight-summary.mjs +64 -0
- package/build/font-cache.mjs +9 -6
- package/build/font-files.mjs +1 -1
- package/build/index.mjs +57 -36
- package/build/inspect.mjs +3 -0
- package/build/inspection-summary.mjs +7 -2
- package/build/native-discovery.mjs +18 -6
- package/build/native-ppr.mjs +32 -0
- package/build/npm-packages.mjs +2 -2
- package/build/og.mjs +121 -1
- package/build/parallel-plan.mjs +55 -0
- package/build/persistent-cache.mjs +29 -24
- 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 +108 -5
- package/build/standalone-copy.mjs +48 -0
- package/build/standalone.mjs +2 -22
- package/build/static-adapter.mjs +7 -3
- package/build/transform-cache.mjs +17 -3
- package/build/transform.mjs +38 -6
- package/build/typescript-dev.mjs +3 -1
- package/cli.mjs +27 -5
- package/compat/app-layout-context.cjs +7 -3
- package/compat/diagnostics.cjs +5 -4
- package/compat/flight-delta.cjs +47 -12
- package/compat/index.d.ts +24 -1
- package/compat/navigation-authorization.cjs +8 -1
- package/compat/runtime-sample.cjs +56 -0
- package/compat/use-cache.cjs +5 -2
- package/docs/ai/compatibility.md +113 -10
- package/docs/ai/deployment.md +144 -0
- package/docs/ai/quickstart.md +5 -0
- package/docs/ai/troubleshooting.md +22 -0
- package/package.json +25 -22
- package/release/fontkit.mjs +31 -0
- package/release/verify.mjs +3 -0
- package/runtime/api.mjs +5 -2
- package/runtime/app-client.mjs +246 -54
- package/runtime/app-partial.mjs +2 -10
- package/runtime/app-prefetch.mjs +2 -2
- package/runtime/app-render.mjs +42 -13
- package/runtime/app-stream-channel.mjs +20 -6
- package/runtime/app-stream-html.mjs +30 -11
- package/runtime/app-styles.mjs +22 -4
- package/runtime/cache-components-rsc.mjs +26 -6
- package/runtime/cache-result.mjs +58 -0
- package/runtime/compile-cache-retention.mjs +53 -0
- package/runtime/compile-cache.mjs +28 -1
- package/runtime/dev-client.mjs +32 -8
- package/runtime/dev-compiler-broker.mjs +20 -23
- package/runtime/dev-gateway.mjs +23 -6
- 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/development-flight.mjs +59 -0
- package/runtime/development-queue.mjs +33 -0
- package/runtime/development.mjs +37 -16
- package/runtime/env.mjs +9 -0
- package/runtime/flight-delta.mjs +31 -5
- package/runtime/flight-diagnostics.mjs +48 -0
- package/runtime/flight-tap.mjs +10 -5
- package/runtime/html-envelope.mjs +34 -0
- package/runtime/loopback-rpc.mjs +92 -0
- package/runtime/native-ppr.mjs +88 -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 +5 -3
- package/runtime/persistent/common.mjs +7 -18
- package/runtime/persistent/daemon.mjs +4 -12
- package/runtime/persistent/health.mjs +58 -0
- package/runtime/persistent/native-proxy.mjs +1 -1
- package/runtime/persistent/watchdog.mjs +12 -27
- package/runtime/prewarm.mjs +35 -0
- package/runtime/project-lock.mjs +71 -0
- package/runtime/request-transport.mjs +112 -32
- package/runtime/rsc-worker.mjs +58 -8
- package/runtime/storage-path.mjs +36 -0
- package/runtime/trace.mjs +44 -4
- package/runtime/worker.mjs +37 -17
- 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/build/font-files.mjs
CHANGED
|
@@ -183,7 +183,7 @@ async function google(name, options, context) {
|
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
async function fallbackMetrics(faces, fallbackName) {
|
|
186
|
-
const { create } = await import('fontkit');
|
|
186
|
+
const { create } = await import('../vendor/fontkit/index.mjs');
|
|
187
187
|
// Prefer an upright face near the normal text weight for a shared fallback.
|
|
188
188
|
const candidates = [...faces].sort((a, b) => Number(a.style === 'italic') - Number(b.style === 'italic') || Math.abs((parseFloat(a.weight) || 400) - 400) - Math.abs((parseFloat(b.weight) || 400) - 400));
|
|
189
189
|
for (const face of candidates) {
|
package/build/index.mjs
CHANGED
|
@@ -5,13 +5,18 @@ import { rename } from '../runtime/fs.mjs';
|
|
|
5
5
|
import { writeRuntimeContract } from './runtime-contract.mjs';
|
|
6
6
|
import { writeBuildOutput } from './output-contract.mjs';
|
|
7
7
|
import { bundleRuntime } from './bundle-runtime.mjs';
|
|
8
|
+
import {withBuildTimings, measureBuildPhase} from './build-timings.mjs';
|
|
9
|
+
import {mapConcurrent} from './concurrent.mjs';
|
|
8
10
|
import {withPersistentTransforms} from './transform-cache.mjs';
|
|
11
|
+
import {withCompilationStorage,configureCompilationStorage} from './compilation-storage.mjs';
|
|
12
|
+
import {reclaimBuildStages,trackBuildStage} from './build-retention.mjs';
|
|
9
13
|
import {applicationEntry, frameworkImportName, frameworkRuntimeImportPattern} from './framework-imports.mjs';
|
|
10
14
|
import { expandLocales, localizedStaticPaths } from './i18n.mjs';
|
|
11
15
|
import { build as bundle, withCompilation, configureCompiler, compilerSource,buildAnalysisRecords,buildAnalysisRoutes,devDependencies,devStylePlans,devStyleDependencies,devModules,readCompilerFile,compilationGraphCacheReport } from './compiler.mjs';
|
|
12
16
|
import {retainDeploymentAssets} from './deployment-assets.mjs';
|
|
13
17
|
import {selectHtmlOnlyRoutes} from './html-only-config.mjs';
|
|
14
18
|
import {writeBuildAnalysis} from './analyze.mjs';
|
|
19
|
+
import {prepareNativePprShells} from './native-ppr.mjs';
|
|
15
20
|
import {routeFiles} from './dev-discovery.mjs';
|
|
16
21
|
import {tryBuildLazyGraph,rememberLazyBuildContext,forgetLazyBuildContext} from './dev-lazy-build.mjs';
|
|
17
22
|
import {writeTypedRoutes} from './typed-routes.mjs';
|
|
@@ -23,7 +28,7 @@ import { randomUUID, randomBytes, createHash } from 'node:crypto';
|
|
|
23
28
|
import { prepareBuildDirectory, OUTPUT_POINTER } from '../runtime/build-directory.mjs';
|
|
24
29
|
import { moduleResolutionPlugin } from './module-resolution.mjs';
|
|
25
30
|
import { scanProject } from './scan.mjs';
|
|
26
|
-
import { stripServerCode, assertSupportedSource,
|
|
31
|
+
import { stripServerCode, assertSupportedSource, rewriteServerChunkOutputFiles } from './transform.mjs';
|
|
27
32
|
import { transformDynamicImports } from './dynamic.mjs';
|
|
28
33
|
import { createCssModules } from './css-modules.mjs';
|
|
29
34
|
import { createFonts } from './fonts.mjs';
|
|
@@ -37,7 +42,7 @@ import { compileCustomRoutes } from './custom-routes.mjs';
|
|
|
37
42
|
import { compileMiddleware } from './middleware.mjs';
|
|
38
43
|
import { prepareScriptWorkers } from './script-workers.mjs';
|
|
39
44
|
import { prepareMetadataRoutes } from './metadata-routes.mjs';
|
|
40
|
-
import { prepareImageResponse } from './og.mjs';
|
|
45
|
+
import { prepareImageResponse, imageResponseRequirement } from './og.mjs';
|
|
41
46
|
import { prepareCacheHandler, prepareCacheHandlers } from './cache-handlers.mjs';
|
|
42
47
|
import { npmPackagesPlugin } from './npm-packages.mjs';
|
|
43
48
|
import { createRefreshTransform, createSourceLocationTransform, devSingletonsPlugin, devClientFile } from './dev.mjs';
|
|
@@ -74,7 +79,7 @@ async function copyRuntimeFiles(stage,{development=true}={}) {
|
|
|
74
79
|
if (failed) throw failed.reason;
|
|
75
80
|
}
|
|
76
81
|
|
|
77
|
-
function compatibilityPlugin({ browser, pageFiles, projectRoot, documentFile, serverOnlyFiles, fonts, refresh, locations, reactCompiler }) {
|
|
82
|
+
function compatibilityPlugin({ browser, pageFiles, projectRoot, documentFile, serverOnlyFiles, fonts, refresh, locations, reactCompiler, nativeCompiler = false }) {
|
|
78
83
|
return {
|
|
79
84
|
name: 'prnext-pages-compatibility',
|
|
80
85
|
setup(esbuild) {
|
|
@@ -108,8 +113,13 @@ function compatibilityPlugin({ browser, pageFiles, projectRoot, documentFile, se
|
|
|
108
113
|
// loaders. Keep that entire graph original: an uppercase JSX utility
|
|
109
114
|
// may be called normally there, outside a React render. Browser render
|
|
110
115
|
// graphs receive memoization before instrumentation and JSX lowering.
|
|
111
|
-
const
|
|
112
|
-
|
|
116
|
+
const compile = browser && reactCompiler && args.path!==documentFile && !serverOnlyFiles.has(args.path);
|
|
117
|
+
// Oxc can hoist static JSX without a source-map position. Attach the
|
|
118
|
+
// same original inspector attributes as SSR before that transformation.
|
|
119
|
+
const locateFirst = !!(compile && nativeCompiler && locations);
|
|
120
|
+
const input = locateFirst ? await locations(original,args.path) : original;
|
|
121
|
+
const compiled = compile ? await reactCompiler(input,args.path,{loaderOutput:args.prnextSource!==undefined}) : input;
|
|
122
|
+
const source = await fonts.transform(locations && !locateFirst ? await locations(compiled,args.path) : compiled, args.path);
|
|
113
123
|
assertSupportedSource(source, args.path);
|
|
114
124
|
const dynamicSource = transformDynamicImports(source, args.path, { projectRoot, mode: browser ? 'browser' : 'server' });
|
|
115
125
|
let contents = browser && pageFiles.has(args.path) ? stripServerCode(dynamicSource, args.path) : dynamicSource;
|
|
@@ -179,9 +189,10 @@ export function build(projectRoot, options = {}) {
|
|
|
179
189
|
const envMode = options.envMode || (process.env.NODE_ENV === 'test' ? 'test' : options.dev ? 'development' : 'production');
|
|
180
190
|
process.env.NODE_ENV = options.dev ? 'development' : 'production';
|
|
181
191
|
try { return await withBuildLock(projectRoot, async root => {
|
|
192
|
+
await reclaimBuildStages(root);
|
|
182
193
|
const started=performance.now();
|
|
183
|
-
const
|
|
184
|
-
return {...result,durationMs:Math.round(performance.now()-started)};
|
|
194
|
+
const measured=await withBuildTimings(()=>withCompilationStorage(root,()=>withPersistentTransforms(root,{dev:!!options.dev},()=>withCompilation(() => buildProject(root, { ...options, envMode })))));
|
|
195
|
+
return {...measured.result,durationMs:Math.round(performance.now()-started),buildTimings:measured.timings};
|
|
185
196
|
}); }
|
|
186
197
|
finally { restore(); }
|
|
187
198
|
});
|
|
@@ -197,21 +208,22 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
|
|
|
197
208
|
devInputs,devSession,devActionSecret,incremental});
|
|
198
209
|
if (deferred) return deferred;
|
|
199
210
|
}
|
|
200
|
-
const config = await loadProjectConfig(projectRoot, { dev, envMode, devInputs });
|
|
211
|
+
const config = await measureBuildPhase('configuration',()=>loadProjectConfig(projectRoot, { dev, envMode, devInputs }));
|
|
212
|
+
configureCompilationStorage(config.compilationCache);
|
|
201
213
|
const tsconfig = config.typescript ? await projectTsconfig(projectRoot, config) : undefined;
|
|
202
214
|
const environment = defineEnvironment(config);
|
|
203
|
-
const publicPaths = { trailingSlash: config.trailingSlash, skipTrailingSlashRedirect: config.skipTrailingSlashRedirect, basePath: config.basePath, assetPrefix: config.assetPrefix, assetBase: publicAssetBase(config) };
|
|
215
|
+
const publicPaths = { server: config.server, trailingSlash: config.trailingSlash, skipTrailingSlashRedirect: config.skipTrailingSlashRedirect, basePath: config.basePath, assetPrefix: config.assetPrefix, assetBase: publicAssetBase(config) };
|
|
204
216
|
const customRoutes = await compileCustomRoutes(config);
|
|
205
217
|
const buildId = await generateBuildId(config, { dev });
|
|
206
218
|
const scanOptions={basePath:config.basePath,pageExtensions:config.pageExtensions,i18n:config.i18n};
|
|
207
219
|
let project,deferredRoutes=[];
|
|
208
220
|
if(dev && Array.isArray(devRoutes)) {
|
|
209
|
-
const discovery=await scanProject(projectRoot,{...scanOptions,devAppFiles:new Set()});
|
|
210
|
-
project=await scanProject(projectRoot,{...scanOptions,devAppFiles:routeFiles(discovery,devRoutes)});
|
|
221
|
+
const discovery=await measureBuildPhase('discovery',()=>scanProject(projectRoot,{...scanOptions,devAppFiles:new Set()}));
|
|
222
|
+
project=await measureBuildPhase('discovery',()=>scanProject(projectRoot,{...scanOptions,devAppFiles:routeFiles(discovery,devRoutes)}));
|
|
211
223
|
await writeTypedRoutes(projectRoot,discovery,config);
|
|
212
224
|
deferredRoutes=discovery.routes.filter(route=>!devRoutes.includes(route.pattern)&&!route.internal&&!route.errorStatus);
|
|
213
225
|
project.routes=project.routes.filter(route=>devRoutes.includes(route.pattern)||route.internal||route.errorStatus);
|
|
214
|
-
} else project=await scanProject(projectRoot,scanOptions);
|
|
226
|
+
} else project=await measureBuildPhase('discovery',()=>scanProject(projectRoot,scanOptions));
|
|
215
227
|
if(dev&&config.experimental.lazyDynamicImports){
|
|
216
228
|
if(project.routes.some(route=>route.router!=='app'&&!route.internal))throw Error('experimental.lazyDynamicImports currently requires an App Router-only application.');
|
|
217
229
|
if(config.webpack||Object.keys(config.turbopack?.rules||{}).length)throw Error('experimental.lazyDynamicImports currently requires the native compiler without webpack hooks or custom loaders.');
|
|
@@ -222,12 +234,14 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
|
|
|
222
234
|
const stage = path.join(project.root, `.prnext-build-${randomUUID()}`);
|
|
223
235
|
const destination = await prepareBuildDirectory(project.root, config.distDir);
|
|
224
236
|
const backup = path.join(project.root, `.prnext-backup-${randomUUID()}`);
|
|
237
|
+
const recovery=await trackBuildStage(project.root,{stage,backup}).catch(()=>null);
|
|
238
|
+
try {
|
|
225
239
|
await mkdir(path.join(stage, '.entries'), { recursive: true });
|
|
226
240
|
await mkdir(path.join(stage, 'static'), { recursive: true });
|
|
227
241
|
await addBuiltinPagesErrors(project, stage);
|
|
228
242
|
const manifest = { version: 1, dev, buildId, cacheId: randomUUID(), previewModeId: randomUUID().replaceAll('-', ''), previewModeEncryptionKey: randomBytes(32).toString('hex'),
|
|
229
243
|
...(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: [],
|
|
244
|
+
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
245
|
...(project.pagesErrors ? { pagesErrors: project.pagesErrors } : {}) };
|
|
232
246
|
// Its build-unique name is known before bundling and avoids a hash cycle
|
|
233
247
|
// between page entry URLs and the manifest that lists those entries.
|
|
@@ -246,11 +260,10 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
|
|
|
246
260
|
const cssModules = createCssModules({ projectRoot: project.root, stage, assetBase: publicPaths.assetBase, sassOptions: config.sassOptions, cssEngine:config.cssEngine,dev,config });
|
|
247
261
|
configureCompiler({config,root:project.root,stage,buildId,dev,incremental,devInputs,tsconfig,assetBase:publicPaths.assetBase});
|
|
248
262
|
try {
|
|
249
|
-
await copyRuntimeFiles(stage,{development:dev});
|
|
263
|
+
await measureBuildPhase('runtime-inputs',()=>copyRuntimeFiles(stage,{development:dev}));
|
|
250
264
|
// Runtime copies must work even when dotenv is nested beneath the PRNext
|
|
251
265
|
// package instead of hoisted beside the application's dependencies.
|
|
252
266
|
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
267
|
manifest.config.serverActions = config.experimental.serverActions;
|
|
255
268
|
if(config.workerMemory && !dev)manifest.config.workerMemory=config.workerMemory;
|
|
256
269
|
if (config.workerRetirement) manifest.config.workerRetirement = config.workerRetirement;
|
|
@@ -280,14 +293,16 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
|
|
|
280
293
|
}
|
|
281
294
|
const common = { absWorkingDir: project.root, bundle: true, logLevel: 'silent', jsx: 'automatic', loader: assetLoaders, assetNames: '[name]-[hash]', publicPath: publicPaths.assetBase, metafile: true, define: { 'process.env.NODE_ENV': JSON.stringify(dev ? 'development' : 'production'), ...environment } };
|
|
282
295
|
if (Object.keys(serverEntries).length) {
|
|
283
|
-
const server = await bundle({ ...common, entryPoints: serverEntries, outdir: path.join(stage, 'server'), outExtension: { '.js': '.mjs' }, platform: 'node', format: 'esm', splitting: true, chunkNames: 'chunk-[hash]', banner: { js: "import {createRequire as __prnextCreateRequire} from 'node:module';const require=__prnextCreateRequire(import.meta.url);" }, target: 'node22', sourcemap: dev, plugins: [moduleResolutionPlugin(config.turbopack, project.root), images.plugin(), fonts.plugin(), compatibilityPlugin({ browser: false, pageFiles, projectRoot: project.root, documentFile: project.document, serverOnlyFiles, fonts, locations, reactCompiler }), npmPackagesPlugin({ pages: true, projectRoot: project.root, transpilePackages: config.transpilePackages, serverExternalPackages: config.serverExternalPackages }), cssModules.plugin(false)] });
|
|
296
|
+
const server = await measureBuildPhase('pages-server',()=>bundle({ ...common, write: false, entryPoints: serverEntries, outdir: path.join(stage, 'server'), outExtension: { '.js': '.mjs' }, platform: 'node', format: 'esm', splitting: true, chunkNames: 'chunk-[hash]', banner: { js: "import {createRequire as __prnextCreateRequire} from 'node:module';const require=__prnextCreateRequire(import.meta.url);" }, target: 'node22', sourcemap: dev, plugins: [moduleResolutionPlugin(config.turbopack, project.root), images.plugin(), fonts.plugin(), compatibilityPlugin({ browser: false, pageFiles, projectRoot: project.root, documentFile: project.document, serverOnlyFiles, fonts, locations, reactCompiler }), npmPackagesPlugin({ pages: true, projectRoot: project.root, transpilePackages: config.transpilePackages, serverExternalPackages: config.serverExternalPackages }), cssModules.plugin(false)] }));
|
|
284
297
|
fonts.record(server.metafile);
|
|
285
298
|
const serverChunks = new Set(Object.keys(server.metafile.outputs).filter(file => file.endsWith('.mjs')).map(file => path.basename(file)));
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
299
|
+
const importers = new Set(Object.entries(server.metafile.outputs).filter(([output, metadata])=>output.endsWith('.mjs')&&metadata.imports.some(item=>!item.external)).map(([output])=>path.resolve(project.root,output)));
|
|
300
|
+
// All buffers are transformed before their first write. Settle every owned
|
|
301
|
+
// writer before stage cleanup on failure, including external sourcemaps.
|
|
302
|
+
await measureBuildPhase('pages-output',async()=>{
|
|
303
|
+
const outputs = rewriteServerChunkOutputFiles(server.outputFiles,serverChunks,publicPaths.assetBase,importers);
|
|
304
|
+
await mapConcurrent(outputs,async file=>{await mkdir(path.dirname(file.path),{recursive:true});await writeFile(file.path,file.contents);},8);
|
|
305
|
+
});
|
|
291
306
|
// Image/font imports must resolve to the same public URLs on both render paths.
|
|
292
307
|
await mkdir(path.join(stage, 'assets'), { recursive: true });
|
|
293
308
|
for (const [output, metadata] of Object.entries(server.metafile.outputs)) {
|
|
@@ -295,7 +310,7 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
|
|
|
295
310
|
}
|
|
296
311
|
}
|
|
297
312
|
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)] });
|
|
313
|
+
const browser = await measureBuildPhase('pages-browser',()=>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
314
|
fonts.record(browser.metafile);
|
|
300
315
|
const byEntry = new Map(Object.entries(clientEntries).map(([id, file]) => [path.resolve(file), id]));
|
|
301
316
|
for (const [output, metadata] of Object.entries(browser.metafile.outputs)) {
|
|
@@ -309,10 +324,10 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
|
|
|
309
324
|
if (metadata.cssBundle) route.css.push(publicPaths.assetBase + '/' + path.basename(metadata.cssBundle));
|
|
310
325
|
}
|
|
311
326
|
}
|
|
312
|
-
await compileMiddleware({ project, stage, manifest, dev, moduleResolution: config.turbopack, defineEnvironment: environment });
|
|
327
|
+
await measureBuildPhase('middleware',()=>compileMiddleware({ project, stage, manifest, dev, moduleResolution: config.turbopack, defineEnvironment: environment }));
|
|
313
328
|
if (manifest.middleware) manifest.middleware.bodySizeLimit = config.experimental.proxyClientMaxBodySize;
|
|
314
|
-
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 });
|
|
329
|
+
await measureBuildPhase('metadata',()=>prepareMetadataRoutes(project, stage));
|
|
330
|
+
await measureBuildPhase('app',async()=>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
331
|
if(manifest.devLazy){
|
|
317
332
|
const css=Object.values(manifest.devLazy.outputs).flatMap(module=>module.css);
|
|
318
333
|
for(const route of manifest.routes)if(route.router==='app'&&route.kind==='page'){
|
|
@@ -328,24 +343,28 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
|
|
|
328
343
|
const selected=manifest.routes.find(route=>!route.internal&&route.pattern===pattern);
|
|
329
344
|
if(!selected||selected.cacheConfig?.runtime==='edge'||selected.handlerConfig?.runtime==='edge')throw new Error(`workerPreload route ${pattern} must be an existing Node route.`);
|
|
330
345
|
}
|
|
331
|
-
await compileInstrumentation({root:project.root,stage,manifest,config,dev,environment});
|
|
346
|
+
await measureBuildPhase('instrumentation',()=>compileInstrumentation({root:project.root,stage,manifest,config,dev,environment}));
|
|
332
347
|
if(deferredRoutes.length){
|
|
333
348
|
for(const route of deferredRoutes){
|
|
334
349
|
const module=`server/dev-deferred-${route.id}.mjs`;await writeFile(path.join(stage,module),'export {};\n');
|
|
335
350
|
manifest.routes.push({id:route.id,pattern:route.pattern,kind:route.kind,router:route.router,module,devDeferred:true,css:[],cacheConfig:{maxDuration:120}});
|
|
336
351
|
}
|
|
337
352
|
}
|
|
338
|
-
await checkTypeScript(project.root, config, { dev, tsconfig });
|
|
353
|
+
await measureBuildPhase('types',()=>checkTypeScript(project.root, config, { dev, tsconfig }));
|
|
339
354
|
fonts.attach(manifest);
|
|
340
355
|
expandLocales(manifest);
|
|
356
|
+
const imageResponse = await imageResponseRequirement(stage, {dev, manifest});
|
|
357
|
+
if (imageResponse.included) await prepareImageResponse(stage);
|
|
358
|
+
else await rm(path.join(stage, 'compat/og.cjs'), {force: true});
|
|
341
359
|
// All runtime preparation must finish before its shared modules are built;
|
|
342
360
|
// 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')});
|
|
361
|
+
if (!dev) await measureBuildPhase('runtime-bundle',()=>bundleRuntime(stage,{projectRoot:project.root,sourceDirectory:path.join(packageRoot,'runtime'),imageResponse:imageResponse.included}));
|
|
344
362
|
// Node caches CJS React by filename, not NODE_ENV. Once development metadata
|
|
345
363
|
// imports load it, subsequent production renders need a clean process too.
|
|
346
364
|
if(dev) developmentModulesLoaded = true;
|
|
347
|
-
if(!dev) Object.assign(manifest,await (incremental || developmentModulesLoaded ? isolatedPrerender : prerenderProduction)(project.root,stage,manifest,{prerender:config.prerender}));
|
|
348
|
-
else {await prerender(stage, manifest, dev);await prerenderApp(stage, manifest, dev);}
|
|
365
|
+
if(!dev) Object.assign(manifest,await measureBuildPhase('prerender',()=>(incremental || developmentModulesLoaded ? isolatedPrerender : prerenderProduction)(project.root,stage,manifest,{prerender:config.prerender})));
|
|
366
|
+
else await measureBuildPhase('prerender',async()=>{await prerender(stage, manifest, dev);await prerenderApp(stage, manifest, dev);});
|
|
367
|
+
if (!dev) await measureBuildPhase('native-ppr',()=>prepareNativePprShells(stage, manifest));
|
|
349
368
|
const pages = manifest.routes.filter(route => route.kind === 'page' && route.router !== 'app' && !route.internal && !route.devDeferred);
|
|
350
369
|
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
370
|
if (pages.length || manifest.pagesErrors) {
|
|
@@ -387,7 +406,7 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
|
|
|
387
406
|
}
|
|
388
407
|
// A report belongs to this successful staging generation, never to the
|
|
389
408
|
// runtime manifest or public assets. Publishing the stage is atomic below.
|
|
390
|
-
if(!dev)await retainDeploymentAssets({stage,destination,manifest,policy:config.deploymentRetention});
|
|
409
|
+
if(!dev)await measureBuildPhase('retention',()=>retainDeploymentAssets({stage,destination,manifest,policy:config.deploymentRetention}));
|
|
391
410
|
const compilationCache=compilationGraphCacheReport();
|
|
392
411
|
if(!dev)await writeFile(path.join(stage,'compilation-cache-report.json'),JSON.stringify(compilationCache,null,2)+'\n');
|
|
393
412
|
const compilerReport=await writeReactCompilerReport(stage,reactCompilerReport());
|
|
@@ -396,9 +415,9 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
|
|
|
396
415
|
await writeBuildOutput(stage, manifest);
|
|
397
416
|
// Offline compression is a deployment optimization. Recompressing source
|
|
398
417
|
// maps and every entry after a save stalls Fast Refresh for no local benefit.
|
|
399
|
-
if (!dev && config.compress) await precompressBuild(stage, manifest, {cacheDirectory:path.join(project.root,'.prnext-cache/precompress-v1')});
|
|
400
|
-
if(!dev)await writeBuildAnalysis({root:project.root,stage,destination,manifest,records:buildAnalysisRecords(),routeInputs:buildAnalysisRoutes(),precompressed:config.compress});
|
|
401
|
-
if (config.output === 'standalone' && !dev) await (await import('./standalone-runner.mjs')).prepareStandalone({ projectRoot: project.root, stage, manifest, config });
|
|
418
|
+
if (!dev && config.compress) await measureBuildPhase('precompression',()=>precompressBuild(stage, manifest, {cacheDirectory:path.join(project.root,'.prnext-cache/precompress-v1')}));
|
|
419
|
+
if(!dev)await measureBuildPhase('analysis',()=>writeBuildAnalysis({root:project.root,stage,destination,manifest,records:buildAnalysisRecords(),routeInputs:buildAnalysisRoutes(),precompressed:config.compress}));
|
|
420
|
+
if (config.output === 'standalone' && !dev) await measureBuildPhase('standalone',async()=>(await import('./standalone-runner.mjs')).prepareStandalone({ projectRoot: project.root, stage, manifest, config }));
|
|
402
421
|
const exporting = config.output === 'export' && !dev ? await import('./static-export.mjs') : null;
|
|
403
422
|
const exportSource = exporting && await exporting.prepareStaticExport({ project, stage, manifest, config });
|
|
404
423
|
let hadDestination = false;
|
|
@@ -420,12 +439,14 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
|
|
|
420
439
|
throw error;
|
|
421
440
|
} finally { await rm(pointerTemporary, {force:true}); }
|
|
422
441
|
await exported?.commit();
|
|
423
|
-
|
|
442
|
+
await recovery?.committed().catch(()=>{});
|
|
443
|
+
if(!recovery&&hadDestination)await rm(backup,{recursive:true,force:true});
|
|
424
444
|
if(dev&&!devLazyRequest)rememberLazyBuildContext(project.root,{manifest,config,environment,tsconfig,envMode});
|
|
425
445
|
return { ...manifest, outputDirectory: destination, durationMs: Math.round(performance.now() - started),reactCompilerReport:compilerReport,compilationCache };
|
|
426
446
|
} catch (error) {
|
|
427
|
-
await rm(stage, { recursive: true, force: true });
|
|
447
|
+
if(!recovery)await rm(stage, { recursive: true, force: true });
|
|
428
448
|
if (error.errors) error.message = error.errors.map(item => `${item.location ? `${item.location.file}:${item.location.line}: ` : ''}${item.text}`).join('\n');
|
|
429
449
|
throw error;
|
|
430
450
|
}
|
|
451
|
+
}finally{await recovery?.cleanup();}
|
|
431
452
|
}
|
package/build/inspect.mjs
CHANGED
|
@@ -70,6 +70,8 @@ export async function inspectProject(directory) {
|
|
|
70
70
|
notes: ['Enable measurements with prn start --inspect (or prn dev --inspect), visit routes, then run prn inspect again.',
|
|
71
71
|
'HTTP and fetch durations end when response headers are ready. Samples are bounded and can span server sessions; they are not full-response benchmarks.',
|
|
72
72
|
'Request-stage samples additionally separate admission, worker first byte, response headers and complete body consumption, with completion/cancellation/deadline outcomes. Stages overlap; do not add their durations. Body completion means the HTTP layer consumed the application body, not remote delivery.',
|
|
73
|
+
'Flight sizes are uncompressed application bytes observed while reading the stream. RSC and SSR observe the same payload separately: do not add their sizes or counts. Size quantiles include completed samples only; failed/cancelled samples may be partial.',
|
|
74
|
+
'Flight model-ready, shell-ready and duration timings include application work, queueing and asynchronous waits; they are not isolated serialization CPU. Browser execution, hydration, compressed transfer and remote delivery are not measured.',
|
|
73
75
|
'Fetch destinations omit queries and credentials. Runtime cache tags/paths are hashed; matching identifiers link reads to invalidations. No diagnostic HTTP endpoint is exposed.'] };
|
|
74
76
|
}
|
|
75
77
|
export function printInspection(report) {
|
|
@@ -77,6 +79,7 @@ export function printInspection(report) {
|
|
|
77
79
|
console.log(`Recorded sample: ${report.summary.requests} HTTP requests; p50 ${report.summary.p50Ms ?? 'n/a'} ms; p95 ${report.summary.p95Ms ?? 'n/a'} ms (response headers); ${report.summary.errors} server errors.`);
|
|
78
80
|
if(report.summary.wakeP95Ms !== null) console.log(`Worker startup p95: ${report.summary.wakeP95Ms} ms.`);
|
|
79
81
|
if(report.summary.workerRssBytes !== null) console.log(`Latest recorded worker RSS: ${(report.summary.workerRssBytes/1048576).toFixed(1)} MiB. Native server memory is not included.`);
|
|
82
|
+
for(const sample of report.flight||[])console.log(`FLIGHT ${sample.route} (${sample.phase}): ${sample.completed} completed sample(s), p95 ${sample.flightP95Bytes===null?'n/a':(sample.flightP95Bytes/1000).toFixed(1)+' kB'}; ${sample.failed} failed, ${sample.cancelled} cancelled, ${sample.timedOut} timed out; model ready p95 ${sample.modelReadyP95Ms??'n/a'} ms, shell ready p95 ${sample.shellReadyP95Ms??'n/a'} ms (includes waiting).`);
|
|
80
83
|
for(const stage of report.requestStages||[]) console.log(`STAGE ${stage.stage}: ${stage.samples} sample(s), p95 ${stage.p95Ms ?? 'n/a'} ms; ${stage.completed} completed, ${stage.failed} failed, ${stage.cancelled} cancelled, ${stage.timedOut} timed out.`);
|
|
81
84
|
for(const budget of report.budgets||[])console.log(`BUDGET ${budget.budget}: ${budget.used ?? 'n/a'}/${budget.limit ?? 'n/a'} ${budget.unit}, ${budget.waiting ?? 'n/a'} waiting; ${budget.rejected ?? 'n/a'} rejected, ${budget.timedOut ?? 'n/a'} timed out, ${budget.cancelledWaits ?? 'n/a'} cancelled waits.`);
|
|
82
85
|
for(const finding of report.findings)console.log(`${finding.level.toUpperCase()}: ${finding.message} ${finding.action}`);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {summarizeFlight,flightMetrics} from './flight-summary.mjs';
|
|
1
2
|
const quantile = (values, q) => values.length ? Math.round([...values].sort((a,b)=>a-b)[Math.ceil(values.length*q)-1]*100)/100 : null;
|
|
2
3
|
const durations = events => events.map(e=>e.durationMs).filter(v=>Number.isFinite(v)&&v>=0);
|
|
3
4
|
const stages=['admission','renderStart','bodyAdmission','bodyRead','workerQueue','workerFirstByte','workerHeaders','httpHeaders','responseBody','requestTotal'];
|
|
@@ -21,6 +22,7 @@ function currentBudgets(snapshot) {
|
|
|
21
22
|
});
|
|
22
23
|
}
|
|
23
24
|
export function summarizeDiagnostics(events, snapshots = []) {
|
|
25
|
+
const flightReport=summarizeFlight(events);
|
|
24
26
|
const http=events.filter(e=>e.type==='http'), times=durations(http);
|
|
25
27
|
const wake=durations(events.filter(e=>e.type==='wake'&&e.phase==='node-ready'));
|
|
26
28
|
const processes=new Map();
|
|
@@ -34,13 +36,16 @@ export function summarizeDiagnostics(events, snapshots = []) {
|
|
|
34
36
|
queuedJobs:current?last.workers?.queuedJobs??null:null,activeJobs:current?last.workers?.activeJobs??null:null, runtimeSampleAt:last?.unixMs??null,
|
|
35
37
|
completedResponseP95Ms:quantile(durations(stageSample.filter(e=>e.stage==='requestTotal'&&e.outcome==='completed')),.95),cancelledResponses:totals?.cancelled??null,timedOutResponses:totals?.timedOut??null};
|
|
36
38
|
const findings=[];
|
|
39
|
+
for(const route of flightReport.flight)if(route.completed>=3&&route.flightP95Bytes>=131072)findings.push({level:'info',code:'LARGE_FLIGHT',
|
|
40
|
+
message:`${route.route} (${route.phase}) has a sampled Flight p95 of ${(route.flightP95Bytes/1000).toFixed(1)} kB.`,
|
|
41
|
+
action:'Inspect the rendered model and repeated presentation markup. Compare explicit public-prop client boundaries with their browser JavaScript and hydration costs; private server code must stay on the server.'});
|
|
37
42
|
if(!http.length)findings.push({level:'info',code:'NO_SAMPLE',message:'No HTTP sample recorded.',action:'Start with --inspect, visit the site and run inspect again.'});
|
|
38
43
|
if(summary.queuedJobs>0)findings.push({level:'warning',code:'WORKER_QUEUE',message:`${summary.queuedJobs} jobs are waiting for a worker.`,action:'Check slow routes and available CPU/RAM before increasing concurrency.'});
|
|
39
44
|
if(summary.p95Ms>=1000)findings.push({level:'warning',code:'SLOW_HEADERS',message:'The slowest 5% of recorded responses take at least one second to prepare headers.',action:'Inspect route and fetch timings below; this includes queueing and startup.'});
|
|
40
45
|
if(summary.errors)findings.push({level:'warning',code:'SERVER_ERRORS',message:`${summary.errors} recorded requests returned a server error.`,action:'Check application logs for the same sample period.'});
|
|
41
46
|
if(summary.wakeP95Ms>=1000)findings.push({level:'info',code:'SLOW_WAKE',message:'Recorded worker startup p95 exceeds one second.',action:'Review startup imports or use keep-warm if the extra memory is acceptable.'});
|
|
42
47
|
if(summary.timedOutResponses>0)findings.push({level:'warning',code:'RESPONSE_DEADLINES',message:`${summary.timedOutResponses} recorded HTTP responses reached their request deadline.`,action:'Compare admission, worker and body timings before increasing the configured timeout.'});
|
|
43
|
-
return {summary,findings,requestStages,budgets:current?currentBudgets(last):null,stageSample,metricSample:http.filter(e=>Number.isFinite(e.durationMs)&&e.durationMs>=0&&e.durationMs<=86400000&&validTime(e.time)).map(e=>({route:typeof e.route==='string'?e.route.slice(0,256):'<unmatched>',method:/^(GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS|CONNECT|TRACE)$/.test(e.method)?e.method:'_OTHER',durationMs:e.durationMs,time:e.time}))};
|
|
48
|
+
return {summary,findings,requestStages,...flightReport,budgets:current?currentBudgets(last):null,stageSample,metricSample:http.filter(e=>Number.isFinite(e.durationMs)&&e.durationMs>=0&&e.durationMs<=86400000&&validTime(e.time)).map(e=>({route:typeof e.route==='string'?e.route.slice(0,256):'<unmatched>',method:/^(GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS|CONNECT|TRACE)$/.test(e.method)?e.method:'_OTHER',durationMs:e.durationMs,time:e.time}))};
|
|
44
49
|
}
|
|
45
50
|
|
|
46
51
|
/** OTLP/JSON Metrics export of a bounded local sample, never a complete traffic counter. */
|
|
@@ -66,5 +71,5 @@ export function inspectionMetrics(report) {
|
|
|
66
71
|
group.count++;group.sum+=value;group.min=Math.min(group.min,value);group.max=Math.max(group.max,value);group.start=Math.min(group.start,event.time);group.end=Math.max(group.end,event.time);group.buckets[index<0?bounds.length:index]++;
|
|
67
72
|
}
|
|
68
73
|
const stagePoints=[...stageGroups.values()].map(g=>({attributes:[{key:'prnext.request.stage',value:{stringValue:g.stage}},{key:'prnext.request.outcome',value:{stringValue:g.outcome}}],startTimeUnixNano:String(BigInt(g.start)*1000000n),timeUnixNano:String(BigInt(g.end)*1000000n),count:String(g.count),sum:g.sum,min:g.min,max:g.max,explicitBounds:bounds,bucketCounts:g.buckets.map(String)}));
|
|
69
|
-
return {resourceMetrics:[{resource:{attributes:[{key:'service.name',value:{stringValue:'prnext'}},{key:'prnext.inspect.sampled',value:{boolValue:true}}]},scopeMetrics:[{scope:{name:'prnext.inspect',version:'1'},metrics:[{name:'prnext.http.response.headers.duration',description:'Bounded diagnostic sample of time until response headers; excludes response body streaming.',unit:'s',histogram:{aggregationTemporality:1,dataPoints}},{name:'prnext.request.stage.duration',description:'Bounded diagnostic sample of request stages and terminal outcomes; stages overlap and must not be added together.',unit:'s',histogram:{aggregationTemporality:1,dataPoints:stagePoints}}]}]}]};
|
|
74
|
+
return {resourceMetrics:[{resource:{attributes:[{key:'service.name',value:{stringValue:'prnext'}},{key:'prnext.inspect.sampled',value:{boolValue:true}}]},scopeMetrics:[{scope:{name:'prnext.inspect',version:'1'},metrics:[{name:'prnext.http.response.headers.duration',description:'Bounded diagnostic sample of time until response headers; excludes response body streaming.',unit:'s',histogram:{aggregationTemporality:1,dataPoints}},{name:'prnext.request.stage.duration',description:'Bounded diagnostic sample of request stages and terminal outcomes; stages overlap and must not be added together.',unit:'s',histogram:{aggregationTemporality:1,dataPoints:stagePoints}},flightMetrics(report.flightSample||[])]}]}]};
|
|
70
75
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readdir, readFile, realpath } from 'node:fs/promises';
|
|
2
2
|
import { builtinModules, createRequire } from 'node:module';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import {
|
|
4
|
+
import { analyzeSourceSummary } from './source-analysis.mjs';
|
|
5
5
|
|
|
6
6
|
const builtins = new Set([...builtinModules, ...builtinModules.map(name => 'node:' + name)]);
|
|
7
7
|
|
|
@@ -15,6 +15,7 @@ export function createNativeDiscovery({ projectRoot, isClientSource }) {
|
|
|
15
15
|
const sourceImports = new Map(), entryNative = new Map(), declaredNative = new Map();
|
|
16
16
|
const resolvers = new Map();
|
|
17
17
|
let sourceBytes = 0;
|
|
18
|
+
let declaredVisits=0,declaredProofHits=0;
|
|
18
19
|
const isApplication = directory => {
|
|
19
20
|
const relative = path.relative(directory, projectRoot);
|
|
20
21
|
return relative === '' || (!relative.startsWith('..' + path.sep) && relative !== '..' && !path.isAbsolute(relative));
|
|
@@ -27,7 +28,7 @@ export function createNativeDiscovery({ projectRoot, isClientSource }) {
|
|
|
27
28
|
const size = Buffer.byteLength(source);
|
|
28
29
|
if (size > 8 * 1024 ** 2 || (sourceBytes += size) > 64 * 1024 ** 2) throw new Error('Native npm import analysis exceeds its 8 MiB file / 64 MiB source budget. Separate the native loader into a server-only package.');
|
|
29
30
|
// The compiler accepts JSX in JavaScript npm entries, including .js.
|
|
30
|
-
const { imports, computedRequire } =
|
|
31
|
+
const { imports, computedRequire } = analyzeSourceSummary(source,file);
|
|
31
32
|
// Keep edges, not the AST, so the shared parser can bound its retained cache.
|
|
32
33
|
return { imports, computedRequire };
|
|
33
34
|
})());
|
|
@@ -111,20 +112,30 @@ export function createNativeDiscovery({ projectRoot, isClientSource }) {
|
|
|
111
112
|
return dependencyRoots.get(key);
|
|
112
113
|
}
|
|
113
114
|
|
|
114
|
-
|
|
115
|
+
const nativePresent=new Set(),nativeAbsent=new Set();
|
|
116
|
+
async function nativeReachable(owner, visited) {
|
|
117
|
+
if(nativePresent.has(owner.directory)){declaredProofHits++;return true;}
|
|
118
|
+
if(nativeAbsent.has(owner.directory)){declaredProofHits++;return false;}
|
|
115
119
|
if (visited.has(owner.directory)) return false;
|
|
116
120
|
visited.add(owner.directory);
|
|
117
|
-
|
|
121
|
+
declaredVisits++;
|
|
122
|
+
if ((await filesFor(owner.directory)).native) {nativePresent.add(owner.directory);return true;}
|
|
118
123
|
const names = new Set([...Object.keys(owner.metadata.dependencies || {}), ...Object.keys(owner.metadata.optionalDependencies || {})]);
|
|
119
124
|
for (const name of names) {
|
|
120
125
|
const dependency = await resolveDependency(owner.directory, name);
|
|
121
|
-
if (dependency && await nativeReachable(dependency, visited)) return true;
|
|
126
|
+
if (dependency && await nativeReachable(dependency, visited)) {nativePresent.add(owner.directory);return true;}
|
|
122
127
|
}
|
|
123
128
|
return false;
|
|
124
129
|
}
|
|
125
130
|
|
|
126
131
|
function declaredNativeFor(owner) {
|
|
127
|
-
if (!declaredNative.has(owner.directory)) declaredNative.set(owner.directory,
|
|
132
|
+
if (!declaredNative.has(owner.directory)) declaredNative.set(owner.directory, (async()=>{
|
|
133
|
+
const visited=new Set(),result=await nativeReachable(owner,visited);
|
|
134
|
+
// A recursive false inside a cycle proves nothing by itself. Only a
|
|
135
|
+
// completely native-free root closure can certify all its visited nodes.
|
|
136
|
+
if(!result)for(const directory of visited)nativeAbsent.add(directory);
|
|
137
|
+
return result;
|
|
138
|
+
})());
|
|
128
139
|
return declaredNative.get(owner.directory);
|
|
129
140
|
}
|
|
130
141
|
|
|
@@ -172,6 +183,7 @@ export function createNativeDiscovery({ projectRoot, isClientSource }) {
|
|
|
172
183
|
}
|
|
173
184
|
|
|
174
185
|
return {
|
|
186
|
+
stats(){return {declaredVisits,declaredProofHits,nativePresent:nativePresent.size,nativeAbsent:nativeAbsent.size};},
|
|
175
187
|
setResolver(mode, resolve) { resolvers.set(mode, resolve); },
|
|
176
188
|
canExternalizeDirectory,
|
|
177
189
|
async externalOwner(file, mode) {
|
|
@@ -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/npm-packages.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import { builtinModules, createRequire } from 'node:module';
|
|
4
|
-
import {
|
|
4
|
+
import {analyzeSourceSummary} from './source-analysis.mjs';
|
|
5
5
|
import { createNativeDiscovery } from './native-discovery.mjs';
|
|
6
6
|
import {frameworkImportPattern} from './framework-imports.mjs';
|
|
7
7
|
|
|
@@ -25,7 +25,7 @@ export function npmPackagesPlugin({ pages = false, projectRoot, transpilePackage
|
|
|
25
25
|
sourceBytes += Buffer.byteLength(source);
|
|
26
26
|
if (sourceBytes > 64 * 1024 * 1024) throw new Error('npm compilation analysis exceeded 64 MiB of source. Separate large runtime dependencies using serverExternalPackages.');
|
|
27
27
|
if (Buffer.byteLength(source) > 8 * 1024 * 1024) throw new Error(`npm source ${file} exceeds the 8 MiB analysis limit. Configure serverExternalPackages for packages that do not use Next APIs.`);
|
|
28
|
-
const {imports,computedRequire,frameworkBoundary}=
|
|
28
|
+
const {imports,computedRequire,frameworkBoundary}=analyzeSourceSummary(source,file);
|
|
29
29
|
return {imports,computedRequire,frameworkBoundary};
|
|
30
30
|
})());
|
|
31
31
|
return sources.get(file);
|
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 });
|