@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/app.mjs
CHANGED
|
@@ -6,9 +6,9 @@ import path from 'node:path';
|
|
|
6
6
|
import { createHash } from 'node:crypto';
|
|
7
7
|
import { createRequire, builtinModules } from 'node:module';
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
|
-
import { parse } from '@babel/parser';
|
|
10
9
|
import * as t from '@babel/types';
|
|
11
|
-
import {
|
|
10
|
+
import {analyzeSourceSummary} from './source-analysis.mjs';
|
|
11
|
+
import { rewriteServerChunkOutputFiles } from './transform.mjs';
|
|
12
12
|
import { transformDynamicImports } from './dynamic.mjs';
|
|
13
13
|
import { createServerActions } from './actions.mjs';
|
|
14
14
|
import { expandActionExports } from './action-exports.mjs';
|
|
@@ -22,6 +22,8 @@ import {createAppStyleGraph, appStyleScopes, planAppStyleEntries} from './app-st
|
|
|
22
22
|
import {selectHtmlOnlyRoutes} from './html-only-config.mjs';
|
|
23
23
|
import {createHtmlOnlyGraph} from './html-only-graph.mjs';
|
|
24
24
|
import {createLazyDynamicImports} from './lazy-dynamic-imports.mjs';
|
|
25
|
+
import {developmentActionMetadata} from './dev-action-graph.mjs';
|
|
26
|
+
import {immutableStylePlan, executeStylePlan, createStyleOutputWriter} from './parallel-plan.mjs';
|
|
25
27
|
|
|
26
28
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
27
29
|
const extensions = { '.js': 'jsx', '.jsx': 'jsx', '.mjs': 'jsx', '.cjs': 'jsx', '.ts': 'ts', '.tsx': 'tsx', '.mts': 'ts', '.cts': 'ts' };
|
|
@@ -43,77 +45,28 @@ export async function validateReactVersions(resolveFromProject) {
|
|
|
43
45
|
}
|
|
44
46
|
}
|
|
45
47
|
|
|
46
|
-
function sourceAst(source, filename) {
|
|
47
|
-
return parse(source, { sourceType: 'unambiguous', sourceFilename: filename, plugins: ['jsx', ...(/\.[cm]?tsx?$/.test(filename) ? ['typescript'] : [])] });
|
|
48
|
-
}
|
|
49
48
|
function clientDirective(source, filename) {
|
|
50
49
|
if (!source.includes('use client')) return false;
|
|
51
|
-
return
|
|
50
|
+
return analyzeSourceSummary(source,filename).directives.includes('use client');
|
|
52
51
|
}
|
|
53
52
|
|
|
54
53
|
function frameworkDirective(source, filename) {
|
|
55
54
|
if (clientDirective(source, filename)) return true;
|
|
56
55
|
if (!source.includes('use server')) return false;
|
|
57
|
-
|
|
58
|
-
if (!node || typeof node !== 'object') return false;
|
|
59
|
-
if (t.isDirective(node) && node.value.value === 'use server') return true;
|
|
60
|
-
return (t.VISITOR_KEYS[node.type] || []).some(key => Array.isArray(node[key]) ? node[key].some(contains) : contains(node[key]));
|
|
61
|
-
}
|
|
62
|
-
return contains(sourceAst(source, filename));
|
|
56
|
+
return analyzeSourceSummary(source,filename).hasServerDirective;
|
|
63
57
|
}
|
|
64
58
|
|
|
65
59
|
/** Enumerate runtime ESM exports without evaluating client code in the server build. */
|
|
66
|
-
async function exportedNames(file, esbuild, seen = new Set()) {
|
|
60
|
+
export async function exportedNames(file, esbuild, seen = new Set()) {
|
|
67
61
|
if (seen.has(file)) return new Set();
|
|
68
62
|
seen.add(file);
|
|
69
63
|
const source = await compilerSource(file);
|
|
70
|
-
const
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (node.exportKind === 'type') continue;
|
|
75
|
-
if (t.isExportNamedDeclaration(node)) {
|
|
76
|
-
if (node.declaration && !t.isTSInterfaceDeclaration(node.declaration) && !t.isTSTypeAliasDeclaration(node.declaration) && !node.declaration.declare) {
|
|
77
|
-
for (const name of Object.keys(t.getOuterBindingIdentifiers(node.declaration))) names.add(name);
|
|
78
|
-
}
|
|
79
|
-
for (const specifier of node.specifiers) if (specifier.exportKind !== 'type') names.add(specifier.exported.name ?? specifier.exported.value);
|
|
80
|
-
}
|
|
81
|
-
if (t.isExportAllDeclaration(node)) {
|
|
82
|
-
const resolved = await esbuild.resolve(node.source.value, { resolveDir: path.dirname(file), kind: 'import-statement', pluginData: { prnextExportScan: true } });
|
|
83
|
-
if (resolved.errors.length || !resolved.path || resolved.external) throw new Error(`Cannot enumerate client boundary exports from ${node.source.value} in ${file}. Use explicit named re-exports.`);
|
|
64
|
+
const analysis=analyzeSourceSummary(source,file),names=new Set(analysis.runtimeExports);
|
|
65
|
+
for (const specifier of analysis.exportStars) {
|
|
66
|
+
const resolved = await esbuild.resolve(specifier, { resolveDir: path.dirname(file), kind: 'import-statement', pluginData: { prnextExportScan: true } });
|
|
67
|
+
if (resolved.errors.length || !resolved.path || resolved.external) throw new Error(`Cannot enumerate client boundary exports from ${specifier} in ${file}. Use explicit named re-exports.`);
|
|
84
68
|
for (const name of await exportedNames(resolved.path, esbuild, seen)) if (name !== 'default') names.add(name);
|
|
85
|
-
}
|
|
86
69
|
}
|
|
87
|
-
// CommonJS is supported for npm and PRNext compatibility components. The default
|
|
88
|
-
// is module.exports; statically assigned properties are named exports as in Node.
|
|
89
|
-
function visit(node) {
|
|
90
|
-
if (!node || typeof node !== 'object') return;
|
|
91
|
-
if (t.isCallExpression(node) && t.isMemberExpression(node.callee) && t.isIdentifier(node.callee.object, { name: 'Object' }) && t.isIdentifier(node.callee.property, { name: 'defineProperty' }) && t.isIdentifier(node.arguments[0], { name: 'exports' }) && t.isStringLiteral(node.arguments[1]) && node.arguments[1].value !== '__esModule') {
|
|
92
|
-
names.add('default');
|
|
93
|
-
names.add(node.arguments[1].value);
|
|
94
|
-
}
|
|
95
|
-
if (t.isAssignmentExpression(node) && t.isMemberExpression(node.left)) {
|
|
96
|
-
const left = node.left;
|
|
97
|
-
const property = left.computed ? left.property.value : left.property.name;
|
|
98
|
-
if (t.isIdentifier(left.object, { name: 'module' }) && property === 'exports') {
|
|
99
|
-
names.add('default');
|
|
100
|
-
if (t.isObjectExpression(node.right)) for (const item of node.right.properties) {
|
|
101
|
-
const key = item.computed ? item.key?.value : item.key?.name ?? item.key?.value;
|
|
102
|
-
if (typeof key === 'string') names.add(key);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
if (t.isIdentifier(left.object, { name: 'exports' }) || (t.isMemberExpression(left.object) && t.isIdentifier(left.object.object, { name: 'module' }) && left.object.property.name === 'exports')) {
|
|
106
|
-
names.add('default');
|
|
107
|
-
if (typeof property === 'string') names.add(property);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
for (const key of t.VISITOR_KEYS[node.type] || []) {
|
|
111
|
-
const child = node[key];
|
|
112
|
-
if (Array.isArray(child)) child.forEach(visit); else visit(child);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
visit(ast.program);
|
|
116
|
-
names.delete('__esModule');
|
|
117
70
|
return names;
|
|
118
71
|
}
|
|
119
72
|
|
|
@@ -178,7 +131,7 @@ function routeEntry(route, globalError, dev) {
|
|
|
178
131
|
}
|
|
179
132
|
|
|
180
133
|
/** Compile independent React Server and client graphs. The RSC graph contains only references to client boundaries. */
|
|
181
|
-
export async function compileApp({ project, stage, manifest, cssModules, fonts, images, dev, devActionSecret, reactCompiler = false, defineEnvironment = {}, productionBrowserSourceMaps = false, serverExternalPackages = [], moduleResolution = {},lazyDynamicImports=false,devLazyRequest,devLazyOnly=false,devLazyActivated=[] }) {
|
|
134
|
+
export async function compileApp({ project, stage, manifest, cssModules, fonts, images, dev, devActionSecret, reactCompiler = false, defineEnvironment = {}, productionBrowserSourceMaps = false, serverExternalPackages = [], moduleResolution = {},lazyDynamicImports=false,devLazyRequest,devLazyOnly=false,devLazyActivated=[], parallelBuilds=false }) {
|
|
182
135
|
if(devLazyOnly&&(!dev||!lazyDynamicImports||!devLazyRequest))throw Error('Deferred-only compilation requires a registered development import');
|
|
183
136
|
const refresh = dev ? createRefreshTransform(project.root, {generation:manifest.cacheId,forceGeneration:manifest.devForceRefresh,forceFiles:manifest.devAffectedModules}) : undefined;
|
|
184
137
|
const locations = dev ? createSourceLocationTransform(project.root) : undefined;
|
|
@@ -229,6 +182,7 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
|
|
|
229
182
|
for (const segment of route.segments || []) if (segment.layout) cacheSegments.set(await canonicalFile(segment.layout), 'layout');
|
|
230
183
|
}
|
|
231
184
|
let browserStyleRevision;
|
|
185
|
+
const writeParallelStyles = parallelBuilds ? createStyleOutputWriter(path.join(stage, 'assets')) : undefined;
|
|
232
186
|
const resolveFromProject = createRequire(path.join(project.root, 'package.json'));
|
|
233
187
|
await validateReactVersions(resolveFromProject);
|
|
234
188
|
const nativePackages = createNativePackages(project.root, { isClientSource: frameworkDirective });
|
|
@@ -317,8 +271,11 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
|
|
|
317
271
|
htmlGraph?.source(args.path, original);
|
|
318
272
|
// Only Client Component graphs use React's memo-cache runtime. Server
|
|
319
273
|
// Components, route handlers and Server Action graphs remain untouched.
|
|
320
|
-
const
|
|
321
|
-
const
|
|
274
|
+
const compile = compileReact && (browser || mode==='ssr');
|
|
275
|
+
const locateFirst = !!(compile && reactCompiler.backend==='oxc' && locations);
|
|
276
|
+
const input = locateFirst ? await locations(original,args.path) : original;
|
|
277
|
+
const compiled = compile ? await compileReact(input,args.path,{loaderOutput:args.prnextSource!==undefined}) : input;
|
|
278
|
+
const source = await fonts.transform(await expandActionExports(locations && !locateFirst ? await locations(compiled,args.path) : compiled, args.path, async (specifier, importer) => {
|
|
322
279
|
const resolved = await esbuild.resolve(specifier, { resolveDir: path.dirname(importer), kind: 'import-statement', pluginData: { prnextExportScan: true } });
|
|
323
280
|
return resolved.errors.length || resolved.external || resolved.namespace !== 'file' ? undefined : resolved.path;
|
|
324
281
|
}), args.path);
|
|
@@ -401,8 +358,11 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
|
|
|
401
358
|
const result = await compileEdge({ file, outfile, projectRoot: project.root, dev, defineEnvironment, reactMode: mode, defaultExport, transformTypedHelpers:!!compileReact && mode==='ssr',
|
|
402
359
|
plugins: [moduleResolutionPlugin(moduleResolution, project.root), edgeGraphPlugin(mode), images.plugin(), fonts.plugin(), cssModules.plugin(false)],
|
|
403
360
|
transformSource: async (source, file, builder, {loaderOutput=false}={}) => {
|
|
404
|
-
const
|
|
405
|
-
|
|
361
|
+
const compile = compileReact && mode==='ssr';
|
|
362
|
+
const locateFirst = !!(compile && reactCompiler.backend==='oxc' && locations);
|
|
363
|
+
const input = locateFirst ? await locations(source,file) : source;
|
|
364
|
+
const compiled = compile ? await compileReact(input,file,{loaderOutput}) : input;
|
|
365
|
+
return actionTransform(await fonts.transform(await expandActionExports(locations && !locateFirst ? await locations(compiled,file) : compiled, file, async (specifier, importer) => {
|
|
406
366
|
const resolved = await builder.resolve(specifier, {resolveDir:path.dirname(importer),kind:'import-statement',pluginData:{prnextExportScan:true}});
|
|
407
367
|
return resolved.errors.length || resolved.external || resolved.namespace !== 'file' ? undefined : resolved.path;
|
|
408
368
|
}), file), file, mode, 'edge', source);
|
|
@@ -439,14 +399,15 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
|
|
|
439
399
|
fonts.record(output.metafile);
|
|
440
400
|
styleGraph.record(output.metafile);
|
|
441
401
|
htmlGraph?.record(output.metafile);
|
|
402
|
+
const chunks = new Set(Object.keys(output.metafile.outputs).filter(file => file.endsWith('.mjs')).map(file => path.basename(file)));
|
|
403
|
+
const importers = new Set(Object.entries(output.metafile.outputs).filter(([file,metadata])=>file.endsWith('.mjs')&&metadata.imports.some(item=>!item.external)).map(([file])=>path.resolve(project.root,file)));
|
|
404
|
+
output={...output,outputFiles:rewriteServerChunkOutputFiles(output.outputFiles,chunks,assetBase,importers)};
|
|
442
405
|
for (const file of output.outputFiles) {
|
|
443
406
|
await mkdir(path.dirname(file.path), { recursive: true });
|
|
444
407
|
await writeFile(file.path, file.contents);
|
|
445
408
|
}
|
|
446
|
-
const chunks = new Set(Object.keys(output.metafile.outputs).filter(file => file.endsWith('.mjs')).map(file => path.basename(file)));
|
|
447
409
|
for (const [file, metadata] of Object.entries(output.metafile.outputs)) {
|
|
448
410
|
const absolute = path.resolve(project.root, file);
|
|
449
|
-
if (file.endsWith('.mjs') && metadata.imports.some(item => !item.external)) await rewriteServerChunks(absolute, chunks, assetBase);
|
|
450
411
|
if (!metadata.entryPoint && !file.endsWith('.map') && !file.endsWith('.mjs')) await cp(absolute, path.join(stage, 'assets', path.basename(file)));
|
|
451
412
|
}
|
|
452
413
|
return output;
|
|
@@ -597,24 +558,30 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
|
|
|
597
558
|
if (!id) continue;
|
|
598
559
|
manifest.app.clientModules[id] = { id, chunks: [id, publicPath + path.basename(output)], name: '*', ssrModule: `app-ssr/${id}.mjs`, ...(edgeBoundaries.has(id) ? { edgeSsrModule: `app-ssr-edge/${id}.mjs` } : {}), browserModule: publicPath + path.basename(output) };
|
|
599
560
|
}
|
|
600
|
-
const
|
|
601
|
-
for (const route of
|
|
561
|
+
const styledRoutes = routes.filter(route => route.kind === 'page');
|
|
562
|
+
for (const route of styledRoutes) {
|
|
602
563
|
// All esbuild graphs canonicalize their inputs, including symlinked
|
|
603
564
|
// convention files. Match those identities before querying reachability.
|
|
604
565
|
for (const files of appStyleScopes(route)) for (const file of files) styleGraph.alias(file, await canonicalFile(file));
|
|
605
|
-
|
|
566
|
+
}
|
|
567
|
+
// Freeze discovery before any independent emission starts. Deduplication
|
|
568
|
+
// owns each output path once, and linking follows source order even when
|
|
569
|
+
// builds complete out of order. Failure drains all started jobs first.
|
|
570
|
+
const plan = immutableStylePlan(styledRoutes, route => planAppStyleEntries(route, styleGraph));
|
|
571
|
+
const {outputs: styleBundles, concurrency} = await executeStylePlan(plan, async ({styles: files}) => {
|
|
572
|
+
const id = 'app-style-' + createHash('sha256').update(files.map(file => path.relative(project.root, file).replaceAll(path.sep, '/')).join('\0')).digest('hex').slice(0, 16);
|
|
573
|
+
const styleEntry = path.join(stage, '.entries', id + '.mjs');
|
|
574
|
+
await writeFile(styleEntry, files.map(file => `import ${JSON.stringify(file)};`).join('\n'));
|
|
575
|
+
const output = await bundle({ ...common, ...(writeParallelStyles ? {write: false} : {}), entryPoints: { [id]: styleEntry }, outdir: path.join(stage, 'assets'), entryNames: '[name]-[hash]', platform: 'browser', format: 'esm', target: ['es2022'], minify: !dev, sourcemap: dev || productionBrowserSourceMaps,
|
|
576
|
+
plugins: [moduleResolutionPlugin(moduleResolution, project.root), images.plugin(), fonts.plugin(), graphPlugin('browser', { omitGlobalStyles: false }), cssModules.plugin(true)] }, { role: 'styles' });
|
|
577
|
+
if (writeParallelStyles) await writeParallelStyles(output.outputFiles);
|
|
578
|
+
return Object.values(output.metafile.outputs).filter(output => output.cssBundle).map(output => publicPath + path.basename(output.cssBundle));
|
|
579
|
+
}, dev ? false : parallelBuilds);
|
|
580
|
+
if (!dev && parallelBuilds) manifest.buildParallelism = {version: 1, phase: 'app-styles', jobs: plan.jobs.length, concurrency};
|
|
581
|
+
for (const route of plan.routes) {
|
|
606
582
|
const styles = [];
|
|
607
583
|
const cssScopes = [];
|
|
608
|
-
for (const {
|
|
609
|
-
const signature = JSON.stringify(files);
|
|
610
|
-
if (!styleBundles.has(signature)) {
|
|
611
|
-
const id = 'app-style-' + createHash('sha256').update(files.map(file => path.relative(project.root, file).replaceAll(path.sep, '/')).join('\0')).digest('hex').slice(0, 16);
|
|
612
|
-
const styleEntry = path.join(stage, '.entries', id + '.mjs');
|
|
613
|
-
await writeFile(styleEntry, files.map(file => `import ${JSON.stringify(file)};`).join('\n'));
|
|
614
|
-
const output = await bundle({ ...common, entryPoints: { [id]: styleEntry }, outdir: path.join(stage, 'assets'), entryNames: '[name]-[hash]', platform: 'browser', format: 'esm', target: ['es2022'], minify: !dev, sourcemap: dev || productionBrowserSourceMaps,
|
|
615
|
-
plugins: [moduleResolutionPlugin(moduleResolution, project.root), images.plugin(), fonts.plugin(), graphPlugin('browser', { omitGlobalStyles: false }), cssModules.plugin(true)] }, { role: 'styles' });
|
|
616
|
-
styleBundles.set(signature, Object.values(output.metafile.outputs).filter(output => output.cssBundle).map(output => publicPath + path.basename(output.cssBundle)));
|
|
617
|
-
}
|
|
584
|
+
for (const { signature, path:scopePath, kind } of route.scopes) {
|
|
618
585
|
const css = styleBundles.get(signature);
|
|
619
586
|
styles.push(...css);
|
|
620
587
|
if (route.parallel) cssScopes.push({ path:scopePath, kind, css });
|
|
@@ -634,6 +601,7 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
|
|
|
634
601
|
}
|
|
635
602
|
|
|
636
603
|
lazy?.finish();
|
|
604
|
+
if (dev) manifest.devActions = developmentActionMetadata(project.root, actions.actions.values());
|
|
637
605
|
|
|
638
606
|
if (htmlGraph) for (const route of routes) if (htmlOnly.has(route.id)) {
|
|
639
607
|
for (const file of [...appStyleScopes(route).flat(), ...(route.metadataFiles || []).map(item=>item.file)].filter(Boolean)) htmlGraph.alias(file, await canonicalFile(file));
|
package/build/build-lock.mjs
CHANGED
|
@@ -1,71 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { hostname } from 'node:os';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
|
|
6
|
-
const LOCK = '.prnext-build.lock';
|
|
7
|
-
const busy = message => Object.assign(new Error(message), { code: 'PRNEXT_BUILD_LOCKED' });
|
|
8
|
-
function alive(pid) {
|
|
9
|
-
try { process.kill(pid, 0); return true; }
|
|
10
|
-
catch (error) { return error.code !== 'ESRCH'; }
|
|
11
|
-
}
|
|
12
|
-
async function owner(directory) {
|
|
13
|
-
const info = await lstat(directory);
|
|
14
|
-
if (!info.isDirectory() || info.isSymbolicLink()) throw busy(`The project lock is not a real directory. Check ${path.basename(directory)} before retrying.`);
|
|
15
|
-
const file = await open(path.join(directory, 'owner.json'), 'r');
|
|
16
|
-
try {
|
|
17
|
-
if ((await file.stat()).size > 4096) throw new Error('oversized owner');
|
|
18
|
-
const value = JSON.parse(await file.readFile('utf8'));
|
|
19
|
-
if (!Number.isSafeInteger(value.pid) || value.pid < 1 || typeof value.host !== 'string' || !/^[a-f0-9-]{36}$/.test(value.token)) throw new Error('invalid owner');
|
|
20
|
-
return value;
|
|
21
|
-
} finally { await file.close(); }
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** One publisher per canonical project, covering React and direct static imports. */
|
|
25
|
-
export async function withBuildLock(directory, action, {development = false} = {}) {
|
|
26
|
-
const root = await realpath(directory), lock = path.join(root, development ? '.prnext-dev.lock' : LOCK);
|
|
27
|
-
const identity = { pid: process.pid, host: hostname(), token: randomUUID(), createdAt: new Date().toISOString() };
|
|
28
|
-
const prepared = path.join(root, `.prnext-lock-${identity.token}`);
|
|
29
|
-
await mkdir(prepared, { mode: 0o700 });
|
|
30
|
-
try {
|
|
31
|
-
await writeFile(path.join(prepared, 'owner.json'), JSON.stringify(identity), { flag: 'wx', mode: 0o600 });
|
|
32
|
-
for (let attempt = 0; ; attempt++) {
|
|
33
|
-
try { await rename(prepared, lock); break; }
|
|
34
|
-
catch (error) {
|
|
35
|
-
if (!['EEXIST', 'ENOTEMPTY', 'EPERM', 'EACCES'].includes(error.code)) throw error;
|
|
36
|
-
let previous;
|
|
37
|
-
try { previous = await owner(lock); }
|
|
38
|
-
catch (cause) {
|
|
39
|
-
if (cause.code === 'ENOENT' && attempt < 8) continue;
|
|
40
|
-
throw busy(`Cannot verify the project lock. Check ${path.basename(lock)} before retrying.`);
|
|
41
|
-
}
|
|
42
|
-
if (previous.host !== identity.host || alive(previous.pid)) throw busy(development ? `A development server is already running for this project (PID ${previous.pid}). Stop it before starting another.` : `A build is already running for this project (PID ${previous.pid}). Wait for it to finish.`);
|
|
43
|
-
// Exactly one contender can recover this generation. Never delete a lock
|
|
44
|
-
// merely because it is old: long builds and suspended processes are valid.
|
|
45
|
-
let claim;
|
|
46
|
-
try { claim = await open(path.join(lock, 'recovery'), 'wx', 0o600); }
|
|
47
|
-
catch (cause) {
|
|
48
|
-
if (cause.code === 'ENOENT' && attempt < 8) continue;
|
|
49
|
-
throw busy(`Project lock recovery is in progress. Retry shortly; if recovery was interrupted, check ${path.basename(lock)} manually.`);
|
|
50
|
-
}
|
|
51
|
-
const retired = path.join(root, `.prnext-lock-retired-${identity.token}`);
|
|
52
|
-
try {
|
|
53
|
-
await claim.writeFile(JSON.stringify(identity)); await claim.close(); claim = null;
|
|
54
|
-
const current = await owner(lock);
|
|
55
|
-
if (current.token !== previous.token) {
|
|
56
|
-
// Do not unlink through this path: the new owner can release and
|
|
57
|
-
// another generation can appear between our read and the unlink.
|
|
58
|
-
throw busy('The build lock changed during recovery. Retry the build.');
|
|
59
|
-
}
|
|
60
|
-
await rename(lock, retired);
|
|
61
|
-
await rm(retired, { recursive: true });
|
|
62
|
-
} finally { await claim?.close(); }
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
try { return await action(root); }
|
|
66
|
-
finally {
|
|
67
|
-
// Ownership must still match before releasing a lock.
|
|
68
|
-
if ((await owner(lock)).token === identity.token) await rm(lock, { recursive: true });
|
|
69
|
-
}
|
|
70
|
-
} finally { await rm(prepared, { recursive: true, force: true }); }
|
|
71
|
-
}
|
|
1
|
+
// Compiler entry point for the shared publisher and maintenance lock protocol.
|
|
2
|
+
export {withBuildLock} from '../runtime/project-lock.mjs';
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// Recovery journal for framework-owned build stages. Uncommitted backups can
|
|
2
|
+
// be the only working build after a crash: keep them for explicit recovery.
|
|
3
|
+
import {mkdir,open,opendir,lstat,rename,unlink,rm,realpath} from 'node:fs/promises';
|
|
4
|
+
import {constants} from 'node:fs';
|
|
5
|
+
import {randomUUID} from 'node:crypto';
|
|
6
|
+
import {hostname} from 'node:os';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import {captureStorageDirectory,verifyStorageDirectories,sameStoragePath} from '../runtime/storage-path.mjs';
|
|
9
|
+
const uuid='[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}';
|
|
10
|
+
const recordName=new RegExp('^'+uuid+'\\.json$'),stageName=new RegExp('^\\.prnext-build-'+uuid+'$'),backupName=new RegExp('^\\.prnext-backup-'+uuid+'$');
|
|
11
|
+
const MAX_JOURNAL_NAMES=32768,MAX_PAGE=128;
|
|
12
|
+
async function container(root){
|
|
13
|
+
const directory=path.join(root,'.prnext-gc');
|
|
14
|
+
await mkdir(directory,{mode:0o700}).catch(error=>{if(error.code!=='EEXIST')throw error;});
|
|
15
|
+
const info=await lstat(directory);if(!info.isDirectory()||info.isSymbolicLink())throw Error('Build recovery journal must be a real directory.');
|
|
16
|
+
return directory;
|
|
17
|
+
}
|
|
18
|
+
async function readValue(file){
|
|
19
|
+
let handle;
|
|
20
|
+
try{
|
|
21
|
+
const info=await lstat(file);if(!info.isFile()||info.isSymbolicLink()||info.size>4096)return null;
|
|
22
|
+
handle=await open(file,constants.O_RDONLY|(constants.O_NOFOLLOW||0));if((await handle.stat()).size>4096)return null;
|
|
23
|
+
return JSON.parse(await handle.readFile('utf8'));
|
|
24
|
+
}catch{return null;}finally{await handle?.close().catch(()=>{});}
|
|
25
|
+
}
|
|
26
|
+
async function read(file){
|
|
27
|
+
const value=await readValue(file);
|
|
28
|
+
if(!value||value.version!==1||!Number.isSafeInteger(value.pid)||value.pid<1||typeof value.host!=='string'||!Number.isSafeInteger(value.createdAt)||!stageName.test(value.stage)||!backupName.test(value.backup)||typeof value.committed!=='boolean')return null;
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
async function save(file,value){
|
|
32
|
+
const temporary=file+'.'+randomUUID()+'.tmp';
|
|
33
|
+
try{const handle=await open(temporary,'wx',0o600);try{await handle.writeFile(JSON.stringify(value));}finally{await handle.close();}await rename(temporary,file);}
|
|
34
|
+
finally{await unlink(temporary).catch(()=>{});}
|
|
35
|
+
}
|
|
36
|
+
function alive(pid){try{process.kill(pid,0);return true;}catch(error){return error.code!=='ESRCH';}}
|
|
37
|
+
async function remove(root,name,pattern){
|
|
38
|
+
if(!pattern.test(name))return false;
|
|
39
|
+
const target=path.resolve(root,name);if(path.dirname(target)!==root)return false;
|
|
40
|
+
const rootGuard=await captureStorageDirectory(root);if(!rootGuard)return false;
|
|
41
|
+
const info=await lstat(target).catch(error=>{if(error.code!=='ENOENT')throw error;});
|
|
42
|
+
if(!info)return 'missing';if(!info.isDirectory()||info.isSymbolicLink())return false;
|
|
43
|
+
const targetGuard=await captureStorageDirectory(target);
|
|
44
|
+
if(!targetGuard||!await verifyStorageDirectories([rootGuard,targetGuard]))return false;
|
|
45
|
+
// Root and basename were checked immediately before this scoped removal.
|
|
46
|
+
await rm(target,{recursive:true,force:true,maxRetries:3,retryDelay:50});return 'removed';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Caller holds withBuildLock for this canonical root. No age-only deletion. */
|
|
50
|
+
export async function reclaimBuildStages(root,{now=Date.now(),graceMs=3600000,maxEntries=MAX_PAGE,maxListingEntries=MAX_JOURNAL_NAMES}={}){
|
|
51
|
+
const report={scanned:0,listed:0,removedStages:0,removedBackups:0,retainedRecoveryBackups:0,scanComplete:true,deferred:false};
|
|
52
|
+
const pageSize=Number.isSafeInteger(maxEntries)&&maxEntries>0?Math.min(maxEntries,MAX_PAGE):MAX_PAGE;
|
|
53
|
+
const listingLimit=Number.isSafeInteger(maxListingEntries)&&maxListingEntries>0?Math.min(maxListingEntries,MAX_JOURNAL_NAMES):MAX_JOURNAL_NAMES;
|
|
54
|
+
try{
|
|
55
|
+
root=path.resolve(root);const rootGuard=await captureStorageDirectory(root);
|
|
56
|
+
if(!rootGuard)return {...report,scanComplete:false,deferred:true,reason:'The build recovery root changed.'};
|
|
57
|
+
const directory=await container(root);
|
|
58
|
+
const guards=[rootGuard,await captureStorageDirectory(directory)];
|
|
59
|
+
if(!await verifyStorageDirectories(guards))return {...report,scanComplete:false,deferred:true,reason:'The build recovery directory changed.'};
|
|
60
|
+
const cursorFile=path.join(directory,'cursor.json');
|
|
61
|
+
const cursorInfo=await lstat(cursorFile).catch(error=>{if(error.code!=='ENOENT')throw error;});
|
|
62
|
+
if(cursorInfo&&(!cursorInfo.isFile()||cursorInfo.isSymbolicLink()))return {...report,scanComplete:false,deferred:true,reason:'The build recovery cursor is not an ordinary file.'};
|
|
63
|
+
const saved=await readValue(cursorFile);
|
|
64
|
+
const after=saved?.version===1&&typeof saved.after==='string'&&recordName.test(saved.after)?saved.after:null;
|
|
65
|
+
const names=[];
|
|
66
|
+
for await(const entry of await opendir(directory)){
|
|
67
|
+
if(report.listed>=listingLimit)return {...report,scanComplete:false,deferred:true,reason:'The build recovery journal listing limit was reached; no stages were removed.'};
|
|
68
|
+
report.listed++;
|
|
69
|
+
if(recordName.test(entry.name))names.push(entry.name);
|
|
70
|
+
}
|
|
71
|
+
names.sort();
|
|
72
|
+
let start=after?names.findIndex(name=>name>after):0;if(start<0)start=0;
|
|
73
|
+
const page=names.slice(start,start+pageSize);
|
|
74
|
+
report.scanComplete=start+page.length>=names.length;
|
|
75
|
+
report.deferred=!report.scanComplete;
|
|
76
|
+
if(report.deferred)report.reason='More build recovery journals remain; the next build resumes this scan.';
|
|
77
|
+
for(const name of page){
|
|
78
|
+
if(!await verifyStorageDirectories(guards))return {...report,scanComplete:false,deferred:true,reason:'The build recovery directory changed.'};
|
|
79
|
+
report.scanned++;
|
|
80
|
+
const file=path.join(directory,name),owner=await read(file);
|
|
81
|
+
if(!owner||owner.host!==hostname()||alive(owner.pid)||now-owner.createdAt<graceMs)continue;
|
|
82
|
+
let stageGone=false,backupGone=false;
|
|
83
|
+
try{stageGone=await remove(root,owner.stage,stageName);if(stageGone==='removed')report.removedStages++;}catch{}
|
|
84
|
+
if(owner.committed){try{backupGone=await remove(root,owner.backup,backupName);if(backupGone==='removed')report.removedBackups++;}catch{}}
|
|
85
|
+
else{
|
|
86
|
+
const backup=await lstat(path.join(root,owner.backup)).catch(()=>null);
|
|
87
|
+
backupGone=!backup;if(backup)report.retainedRecoveryBackups++;
|
|
88
|
+
}
|
|
89
|
+
if(stageGone&&backupGone&&await verifyStorageDirectories(guards))await unlink(file).catch(()=>{});
|
|
90
|
+
}
|
|
91
|
+
if(!await verifyStorageDirectories(guards))return {...report,scanComplete:false,deferred:true,reason:'The build recovery directory changed.'};
|
|
92
|
+
// The caller owns the publisher lock, so a single persistent cursor advances
|
|
93
|
+
// across fresh CLI processes without renaming any live owner's journals.
|
|
94
|
+
await save(cursorFile,{version:1,after:report.scanComplete?null:page.at(-1)||null});
|
|
95
|
+
}catch{report.scanComplete=false;report.deferred=true;report.reason='Build recovery maintenance is unavailable; retry on a later build.';}
|
|
96
|
+
return report;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function trackBuildStage(root,{stage,backup}){
|
|
100
|
+
root=path.resolve(root);const rootGuard=await captureStorageDirectory(root);
|
|
101
|
+
if(!rootGuard)throw Error('Build recovery root must be a canonical physical directory.');
|
|
102
|
+
const value={version:1,pid:process.pid,host:hostname(),createdAt:Date.now(),stage:path.basename(stage),backup:path.basename(backup),committed:false};
|
|
103
|
+
if(!stageName.test(value.stage)||!backupName.test(value.backup)||!sameStoragePath(await realpath(path.dirname(path.resolve(stage))),root)||!sameStoragePath(await realpath(path.dirname(path.resolve(backup))),root))throw Error('Build recovery paths must be owned stages inside the project.');
|
|
104
|
+
const file=path.join(await container(root),randomUUID()+'.json');
|
|
105
|
+
const guards=[rootGuard,await captureStorageDirectory(path.dirname(file))];
|
|
106
|
+
if(!await verifyStorageDirectories(guards))throw Error('Build recovery directory changed before publication.');
|
|
107
|
+
await save(file,value);
|
|
108
|
+
return {
|
|
109
|
+
async committed(){value.committed=true;if(await verifyStorageDirectories(guards))await save(file,value);},
|
|
110
|
+
async cleanup(){
|
|
111
|
+
try{
|
|
112
|
+
const stageGone=await remove(root,value.stage,stageName);
|
|
113
|
+
// An uncommitted backup is kept until rollback has restored it.
|
|
114
|
+
const backupGone=value.committed?await remove(root,value.backup,backupName):!await lstat(path.join(root,value.backup)).catch(()=>null);
|
|
115
|
+
if(stageGone&&backupGone&&await verifyStorageDirectories(guards))await unlink(file);
|
|
116
|
+
}catch{/* Keep the journal so a later build can retry Windows busy files. */}
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import {AsyncLocalStorage} from 'node:async_hooks';
|
|
2
|
+
|
|
3
|
+
const storage = new AsyncLocalStorage();
|
|
4
|
+
const names = new Set(['configuration', 'discovery', 'runtime-inputs', 'pages-server', 'pages-browser', 'pages-output', 'middleware', 'metadata', 'app', 'instrumentation', 'types', 'runtime-bundle', 'prerender', 'native-ppr', 'retention', 'precompression', 'analysis', 'standalone', 'compiler-node', 'compiler-browser', 'compiler-edge']);
|
|
5
|
+
|
|
6
|
+
/** Wall times include awaited native/compiler work; nested phases are not additive. */
|
|
7
|
+
export async function withBuildTimings(run, clock = () => performance.now()) {
|
|
8
|
+
const state = {clock, started: clock(), phases: new Map()};
|
|
9
|
+
return storage.run(state, async () => {
|
|
10
|
+
try { return {result: await run(), timings: report(state)}; }
|
|
11
|
+
catch (error) {
|
|
12
|
+
// Diagnostics must never replace the original application/build failure.
|
|
13
|
+
try { if (error && typeof error === 'object') Object.defineProperty(error, 'buildTimings', {value: report(state), configurable: true}); } catch {}
|
|
14
|
+
throw error;
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function measureBuildPhase(name, run) {
|
|
20
|
+
if (!names.has(name)) throw new TypeError('Unknown build timing phase');
|
|
21
|
+
const state = storage.getStore();
|
|
22
|
+
if (!state) return run();
|
|
23
|
+
const started = state.clock();
|
|
24
|
+
let failed = false;
|
|
25
|
+
try { return await run(); }
|
|
26
|
+
catch (error) { failed = true; throw error; }
|
|
27
|
+
finally {
|
|
28
|
+
const row = state.phases.get(name) || {calls: 0, failedCalls: 0, milliseconds: 0};
|
|
29
|
+
row.calls++; row.failedCalls += Number(failed); row.milliseconds += Math.max(0, state.clock() - started);
|
|
30
|
+
state.phases.set(name, row);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function report(state) {
|
|
35
|
+
return {version: 1, scope: 'build-wall-time-excluding-module-startup', nestedPhasesAreAdditive: false,
|
|
36
|
+
totalMilliseconds: Math.max(0, state.clock() - state.started),
|
|
37
|
+
phases: Object.fromEntries([...state.phases].map(([name, value]) => [name, {...value}]))};
|
|
38
|
+
}
|
package/build/bundle-runtime.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import {createHash} from 'node:crypto';
|
|
5
5
|
import {version as esbuildVersion} from 'esbuild';
|
|
6
6
|
import {privateCacheDirectory,readCacheRecord,writeCacheRecord} from './persistent-cache.mjs';
|
|
7
|
+
import {mapConcurrent} from './concurrent.mjs';
|
|
7
8
|
|
|
8
9
|
// Paths used by the native worker, generated application modules, prerender,
|
|
9
10
|
// standalone tracing, compatibility modules and the runtime lifecycle contract.
|
|
@@ -15,7 +16,7 @@ export const runtimeEntryNames = Object.freeze([
|
|
|
15
16
|
].sort());
|
|
16
17
|
|
|
17
18
|
/** Run once, after compiling application/Edge graphs and before any prerender. */
|
|
18
|
-
export async function bundleRuntime(stage,{projectRoot,sourceDirectory}={}) {
|
|
19
|
+
export async function bundleRuntime(stage,{projectRoot,sourceDirectory,imageResponse=true}={}) {
|
|
19
20
|
const resolvedStage = path.resolve(stage);
|
|
20
21
|
if (!path.basename(resolvedStage).startsWith('.prnext-build-')) {
|
|
21
22
|
throw new Error('Runtime bundling requires an unpublished PRNext build stage.');
|
|
@@ -35,24 +36,24 @@ export async function bundleRuntime(stage,{projectRoot,sourceDirectory}={}) {
|
|
|
35
36
|
if (originals.some(entry => !entry.isFile() || entry.isSymbolicLink())) {
|
|
36
37
|
throw new Error('Runtime source modules must be ordinary files.');
|
|
37
38
|
}
|
|
38
|
-
|
|
39
|
+
const selectedEntries = imageResponse ? runtimeEntryNames : runtimeEntryNames.filter(name => name !== 'og-loader');
|
|
40
|
+
for (const name of selectedEntries) {
|
|
39
41
|
if (!originals.some(entry => entry.name === `${name}.mjs`)) throw new Error(`Missing runtime entry ${name}.mjs`);
|
|
40
42
|
}
|
|
41
|
-
const sources=new Map();
|
|
42
43
|
// Production compiles the installed sources directly. Only generated stage
|
|
43
44
|
// overrides (currently the bundled env loader) replace an installed module.
|
|
44
45
|
// Hash and compile the same captured bytes; do not trust mtimes or skip app
|
|
45
46
|
// graph callbacks. This avoids copying hundreds of modules into every stage.
|
|
46
|
-
|
|
47
|
+
const sources=new Map(await mapConcurrent(originals,async entry=>{
|
|
47
48
|
let file=path.join(input,entry.name);
|
|
48
49
|
if(sourceDirectory){const override=path.join(runtime,entry.name);const info=await lstat(override).catch(e=>{if(e.code!=='ENOENT')throw e;});if(info){if(!info.isFile()||info.isSymbolicLink())throw Error('Runtime overrides must be ordinary files');file=override;}}
|
|
49
|
-
|
|
50
|
-
}
|
|
50
|
+
return [path.join(input,entry.name),await readFile(file)];
|
|
51
|
+
},8));
|
|
51
52
|
let directory,key,cached;
|
|
52
53
|
if(projectRoot){
|
|
53
54
|
directory=await privateCacheDirectory(projectRoot,'runtime-bundle-v2');
|
|
54
55
|
if(directory){
|
|
55
|
-
const hash=createHash('sha256').update(esbuildVersion).update(JSON.stringify(
|
|
56
|
+
const hash=createHash('sha256').update(esbuildVersion).update(JSON.stringify(selectedEntries)).update(await readFile(new URL('./bundle-runtime.mjs',import.meta.url)));
|
|
56
57
|
for(const entry of [...originals].sort((a,b)=>a.name.localeCompare(b.name)))hash.update(JSON.stringify([entry.name,createHash('sha256').update(sources.get(path.join(input,entry.name))).digest('hex')]));
|
|
57
58
|
key=hash.digest('hex');cached=await readCacheRecord(directory,key);
|
|
58
59
|
}
|
|
@@ -61,10 +62,10 @@ export async function bundleRuntime(stage,{projectRoot,sourceDirectory}={}) {
|
|
|
61
62
|
// no files change if parsing or resolution fails. The virtual output is a
|
|
62
63
|
// sibling so all relative paths have the same depth as the final runtime.
|
|
63
64
|
const virtualOutput = path.join(resolvedStage, '.runtime-bundle');
|
|
64
|
-
const validCache=cached&&Array.isArray(cached.files)&&cached.files.length<=128&&cached.files.every(item=>Array.isArray(item)&&item.length===2&&/^[\w-]+\.mjs$/.test(item[0])&&typeof item[1]==='string')&&new Set(cached.files.map(([name])=>name)).size===cached.files.length&&
|
|
65
|
+
const validCache=cached&&Array.isArray(cached.files)&&cached.files.length<=128&&cached.files.every(item=>Array.isArray(item)&&item.length===2&&/^[\w-]+\.mjs$/.test(item[0])&&typeof item[1]==='string')&&new Set(cached.files.map(([name])=>name)).size===cached.files.length&&selectedEntries.every(name=>cached.files.some(([file])=>file===name+'.mjs'))&& (imageResponse || !cached.files.some(([file])=>file==='og-loader.mjs'));
|
|
65
66
|
const result = validCache?null:await build({
|
|
66
67
|
absWorkingDir: input,
|
|
67
|
-
entryPoints:
|
|
68
|
+
entryPoints: selectedEntries.map(name => path.join(input, `${name}.mjs`)),
|
|
68
69
|
outdir: virtualOutput,
|
|
69
70
|
outbase: input,
|
|
70
71
|
entryNames: '[name]',
|
|
@@ -78,7 +79,7 @@ export async function bundleRuntime(stage,{projectRoot,sourceDirectory}={}) {
|
|
|
78
79
|
packages: 'external',
|
|
79
80
|
// Edge's prepared VM is already a self-contained CJS artifact. Keeping it
|
|
80
81
|
// external preserves its delayed import and original execution boundary.
|
|
81
|
-
external: ['../compat/*', './profiles.json', './*.cjs'],
|
|
82
|
+
external: ['../compat/*', './profiles.json', './*.cjs', './og/*'],
|
|
82
83
|
minify: false,
|
|
83
84
|
sourcemap: false,
|
|
84
85
|
metafile: true,
|
|
@@ -94,17 +95,17 @@ export async function bundleRuntime(stage,{projectRoot,sourceDirectory}={}) {
|
|
|
94
95
|
}
|
|
95
96
|
emitted.set(path.basename(file.path), file.contents);
|
|
96
97
|
}
|
|
97
|
-
if (
|
|
98
|
+
if (selectedEntries.some(name => !emitted.has(`${name}.mjs`))) throw new Error('Runtime bundle omitted a required entry.');
|
|
98
99
|
if(directory&&!validCache)await writeCacheRecord(directory,key,{files:[...emitted].map(([name,bytes])=>[name,Buffer.from(bytes).toString()])});
|
|
99
100
|
// This is an unpublished, disposable stage. On any write failure buildProject
|
|
100
101
|
// discards the stage and preserves the previous published build unchanged.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if(!entry.name.endsWith('.mjs')||!entry.isFile())
|
|
102
|
+
await mapConcurrent(emitted,async([name,contents])=>writeFile(path.join(runtime,name),contents),8);
|
|
103
|
+
await mapConcurrent(await readdir(runtime,{withFileTypes:true}),async entry=>{
|
|
104
|
+
if(!entry.name.endsWith('.mjs')||!entry.isFile())return;
|
|
104
105
|
if (!emitted.has(entry.name)) await unlink(path.join(runtime, entry.name));
|
|
105
|
-
}
|
|
106
|
+
},8);
|
|
106
107
|
// JSON, CJS, license files and the OG assets directory are never traversed or
|
|
107
108
|
// removed. The caller writes the normal runtime contract after prerender.
|
|
108
|
-
return { entryCount:
|
|
109
|
+
return { entryCount: selectedEntries.length, moduleCount: emitted.size,cached:!!validCache,
|
|
109
110
|
bytes: [...emitted.values()].reduce((sum, contents) => sum + contents.length, 0) };
|
|
110
111
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import {inspectCompilationStorage,pruneCompilationStorage,validateCompilationCache} from './compilation-storage.mjs';
|
|
3
|
+
|
|
4
|
+
export async function cacheCommand(args){
|
|
5
|
+
if(args.length===1&&['--help','-h'].includes(args[0])){
|
|
6
|
+
console.log('prnext cache [directory] [--json]\nprnext cache [directory] --prune [--dry-run] [--max-bytes N] [--json]\n\nInspect compiler caches without executing application config. The budget comes\nfrom the last build (default 256 MiB). --max-bytes overrides this purge only.\nRuntime data, SQLite, page/image caches and application files are excluded.\nStop development before purging. Active worker caches remain pinned.');return;
|
|
7
|
+
}
|
|
8
|
+
let directory,prune=false,dryRun=false,json=false,maxDiskSize;
|
|
9
|
+
const seen=new Set();
|
|
10
|
+
for(let i=0;i<args.length;i++){
|
|
11
|
+
const arg=args[i];
|
|
12
|
+
if(arg.startsWith('--')){if(seen.has(arg))throw Error(`Duplicate cache option: ${arg}`);seen.add(arg);}
|
|
13
|
+
if(arg==='--prune')prune=true;
|
|
14
|
+
else if(arg==='--dry-run')dryRun=true;
|
|
15
|
+
else if(arg==='--json')json=true;
|
|
16
|
+
else if(arg==='--max-bytes'){
|
|
17
|
+
const value=args[++i];if(!/^\d+$/.test(value||''))throw Error('--max-bytes requires a byte count from 0 to 4294967296.');
|
|
18
|
+
maxDiskSize=validateCompilationCache({maxDiskSize:Number(value)}).maxDiskSize;
|
|
19
|
+
}else if(arg.startsWith('-')||directory!==undefined)throw Error('cache accepts one directory, --prune, --dry-run, --max-bytes N and --json.');
|
|
20
|
+
else directory=arg;
|
|
21
|
+
}
|
|
22
|
+
if((dryRun||maxDiskSize!==undefined)&&!prune)throw Error('--dry-run and --max-bytes require --prune.');
|
|
23
|
+
const root=path.resolve(directory||'.');
|
|
24
|
+
const report=prune?await pruneCompilationStorage(root,{maxDiskSize,dryRun}):await inspectCompilationStorage(root);
|
|
25
|
+
if(json){console.log(JSON.stringify(report,null,2));return;}
|
|
26
|
+
const size=value=>(value/1024/1024).toFixed(2)+' MiB';
|
|
27
|
+
console.log(`Compilation caches: ${size(report.bytes)} / ${size(report.maxDiskSize)}`);
|
|
28
|
+
for(const category of report.categories)console.log(` ${category.name}: ${size(category.bytes)}, ${category.files} files${category.pinned?' (active worker)':''}`);
|
|
29
|
+
if(prune)console.log(report.deferred?report.reason:`${dryRun?'Would reclaim':'Reclaimed'} ${size(report.removedBytes)} in ${report.removedFiles} files; ${size(report.afterBytes)} retained.${report.overBudget?' Active or busy files remain above the budget.':''}`);
|
|
30
|
+
else if(!report.scanComplete)console.log('The scan limit was reached; totals are a lower bound.');
|
|
31
|
+
}
|