@thomas.f/prnext 0.1.16-alpha → 0.1.18-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AI.md +63 -0
- package/README.md +35 -503
- package/build/ADAPTERS.md +27 -2
- package/build/actions.mjs +6 -1
- package/build/analyze.mjs +340 -0
- package/build/app-config.mjs +20 -8
- package/build/app-static.mjs +41 -14
- package/build/app-styles.mjs +79 -0
- package/build/app.mjs +99 -31
- package/build/build-lock.mjs +6 -6
- package/build/bundle-runtime.mjs +39 -11
- package/build/cache-components.mjs +1 -1
- package/build/cache-handlers.mjs +1 -1
- package/build/check.mjs +56 -11
- package/build/compact-css.mjs +53 -0
- package/build/compilation-cache.mjs +269 -0
- package/build/compilation-store.mjs +142 -0
- package/build/compiler.mjs +51 -4
- package/build/concurrent.mjs +12 -0
- package/build/config.mjs +50 -13
- package/build/create.mjs +239 -0
- package/build/css-modules.mjs +65 -10
- package/build/deployment-assets.mjs +89 -0
- package/build/deployment-config.mjs +18 -0
- package/build/dev-compiler-health.mjs +11 -0
- package/build/dev-css-update.mjs +85 -0
- package/build/dev-dependencies.mjs +44 -0
- package/build/dev-discovery.mjs +28 -0
- package/build/dev-worker.mjs +14 -5
- package/build/dev.mjs +64 -6
- package/build/dynamic.mjs +1 -1
- package/build/edge.mjs +5 -4
- package/build/fonts.mjs +1 -1
- package/build/framework-imports.mjs +50 -0
- package/build/html-only-config.mjs +53 -0
- package/build/html-only-graph.mjs +67 -0
- package/build/incremental.mjs +8 -4
- package/build/index.mjs +140 -112
- package/build/inspect.mjs +3 -0
- package/build/inspection-summary.mjs +37 -4
- package/build/instrumentation.mjs +30 -0
- package/build/loader-cache.mjs +53 -26
- package/build/metadata-routes.mjs +3 -4
- package/build/migrate.mjs +60 -57
- package/build/migration-audit.mjs +1 -1
- package/build/native-type-config.mjs +37 -0
- package/build/npm-packages.mjs +3 -18
- package/build/package-manager.mjs +308 -0
- package/build/persistent-cache.mjs +112 -0
- package/build/precompress.mjs +62 -30
- package/build/prerender-cache.mjs +70 -0
- package/build/prerender-config.mjs +29 -0
- package/build/prerender-pages.mjs +100 -0
- package/build/prerender-pool.mjs +165 -0
- package/build/prerender-process-worker.mjs +58 -0
- package/build/prerender-worker.mjs +10 -0
- package/build/production-builder.mjs +129 -0
- package/build/react-compiler-report.mjs +225 -0
- package/build/react-compiler.mjs +139 -0
- package/build/runtime-contract.mjs +2 -2
- package/build/scan.mjs +4 -4
- package/build/source-analysis.mjs +42 -0
- package/build/static-adapter.mjs +13 -5
- package/build/static-export.mjs +4 -4
- package/build/static-input.mjs +48 -0
- package/build/styles.mjs +17 -3
- package/build/transform-cache.mjs +37 -4
- package/build/transform.mjs +1 -1
- package/build/type-command.mjs +41 -0
- package/build/typed-routes.mjs +95 -0
- package/build/typescript-dev-worker.mjs +18 -0
- package/build/typescript-dev.mjs +49 -0
- package/build/typescript-native-worker.mjs +55 -0
- package/build/typescript-native.mjs +18 -0
- package/build/typescript-toolchain.mjs +19 -0
- package/build/typescript-watch.mjs +22 -0
- package/build/typescript.mjs +11 -8
- package/build/webpack-graph.mjs +170 -30
- package/build/worker-memory.mjs +9 -0
- package/build/worker-preload.mjs +9 -0
- package/cli.mjs +162 -122
- package/compat/activity-cache.cjs +26 -0
- package/compat/after.cjs +6 -4
- package/compat/app-layout-context.cjs +52 -2
- package/compat/base64.cjs +10 -0
- package/compat/cache-handlers.cjs +12 -2
- package/compat/cache-wire.cjs +28 -0
- package/compat/cache.cjs +2 -1
- package/compat/cache.d.cts +4 -0
- package/compat/cookies.cjs +21 -5
- package/compat/data-cache.cjs +12 -8
- package/compat/flight-delta.cjs +150 -0
- package/compat/headers.cjs +4 -2
- package/compat/incremental-cache.cjs +20 -7
- package/compat/index.d.ts +89 -10
- package/compat/link.cjs +12 -9
- package/compat/link.d.cts +4 -3
- package/compat/navigation-authorization.cjs +9 -0
- package/compat/navigation-stages.cjs +19 -0
- package/compat/navigation.d.cts +4 -3
- package/compat/offline-store.cjs +11 -0
- package/compat/offline.cjs +5 -0
- package/compat/offline.d.cts +2 -0
- package/compat/redis-cache.cjs +201 -0
- package/compat/redis-cache.d.cts +37 -0
- package/compat/router.d.cts +4 -3
- package/compat/server-primitives.cjs +31 -11
- package/compat/server.d.cts +15 -2
- package/compat/static-generation.cjs +7 -0
- package/compat/typed-route.d.cts +17 -0
- package/compat/use-cache.cjs +3 -0
- package/docs/ai/compatibility.md +275 -0
- package/docs/ai/data.md +288 -0
- package/docs/ai/deployment.md +269 -0
- package/docs/ai/manifest.json +11 -0
- package/docs/ai/quickstart.md +171 -0
- package/docs/ai/routing.md +214 -0
- package/docs/ai/troubleshooting.md +155 -0
- package/index.mjs +2 -0
- package/native/diagnostics.mjs +34 -0
- package/native/resolve.mjs +7 -2
- package/package.json +44 -7
- package/release/ai-docs.mjs +48 -0
- package/release/verify.mjs +2 -0
- package/runtime/action-client.mjs +10 -4
- package/runtime/action-server.mjs +2 -0
- package/runtime/ai-docs.mjs +202 -0
- package/runtime/app-client.mjs +226 -68
- package/runtime/app-content.mjs +4 -3
- package/runtime/app-partial-client.mjs +3 -0
- package/runtime/app-partial.mjs +1 -1
- package/runtime/app-prefetch-constants.mjs +4 -0
- package/runtime/app-prefetch.mjs +2 -3
- package/runtime/app-render-key.mjs +5 -0
- package/runtime/app-render.mjs +100 -28
- package/runtime/app-routing.mjs +22 -9
- package/runtime/app-static.mjs +18 -8
- package/runtime/app-stream-html.mjs +10 -53
- package/runtime/app-style-selection.mjs +34 -0
- package/runtime/app-styles.mjs +154 -0
- package/runtime/cache-components-rsc.mjs +4 -1
- package/runtime/cache-request.mjs +2 -1
- package/runtime/cloud/archive.mjs +5 -6
- package/runtime/compile-cache.mjs +8 -0
- package/runtime/deployment-client.mjs +19 -0
- package/runtime/dev-ai-brands.mjs +26 -0
- package/runtime/dev-ai-style.mjs +99 -0
- package/runtime/dev-build-reasons.mjs +25 -0
- package/runtime/dev-cache-control.mjs +53 -0
- package/runtime/dev-client.mjs +245 -46
- package/runtime/dev-compiler-broker.mjs +42 -0
- package/runtime/dev-console-copy.mjs +19 -0
- package/runtime/dev-console.mjs +106 -0
- package/runtime/dev-dependency-graph.mjs +40 -0
- package/runtime/dev-element-picker-style.mjs +27 -0
- package/runtime/dev-element-picker.mjs +213 -0
- package/runtime/dev-error-overlay-style.mjs +41 -0
- package/runtime/dev-error-overlay.mjs +167 -0
- package/runtime/dev-error-report.mjs +121 -0
- package/runtime/dev-file-changes.mjs +1 -1
- package/runtime/dev-gateway.mjs +44 -0
- package/runtime/dev-host-relay.mjs +66 -0
- package/runtime/dev-indicator.mjs +191 -0
- package/runtime/dev-issue-motion.mjs +214 -0
- package/runtime/dev-lazy-tool.mjs +34 -0
- package/runtime/dev-logo.mjs +2 -0
- package/runtime/dev-notification.mjs +81 -0
- package/runtime/dev-popout.mjs +34 -0
- package/runtime/dev-position.mjs +107 -0
- package/runtime/dev-preview-touch.mjs +122 -0
- package/runtime/dev-runtime-pool.mjs +45 -0
- package/runtime/dev-runtime-spare.mjs +57 -0
- package/runtime/dev-tools-client.mjs +155 -0
- package/runtime/dev-tools-mcp.mjs +126 -0
- package/runtime/dev-tools-metrics.mjs +31 -0
- package/runtime/dev-tools-model.mjs +42 -0
- package/runtime/dev-tools-panel.mjs +341 -0
- package/runtime/dev-tools-result.mjs +37 -0
- package/runtime/dev-tools-server.mjs +256 -0
- package/runtime/dev-tools-source.mjs +104 -0
- package/runtime/dev-tooltip.mjs +94 -0
- package/runtime/dev-ui-icons.mjs +8 -0
- package/runtime/dev-workbench-style.mjs +106 -0
- package/runtime/dev-workbench.mjs +428 -0
- package/runtime/dev-worker-logs.mjs +31 -0
- package/runtime/development.mjs +324 -0
- package/runtime/document.mjs +1 -1
- package/runtime/edge.mjs +6 -5
- package/runtime/env.mjs +2 -1
- package/runtime/fetch-cache.mjs +10 -8
- package/runtime/flight-delta.mjs +50 -0
- package/runtime/flight-tap.mjs +28 -0
- package/runtime/html-only.mjs +27 -0
- package/runtime/http.mjs +2 -0
- package/runtime/incremental-cache.mjs +25 -4
- package/runtime/instrumentation.mjs +49 -0
- package/runtime/navigation-recovery.mjs +36 -0
- package/runtime/navigation.mjs +7 -0
- package/runtime/node-html-stream.mjs +47 -0
- package/runtime/pages-client.mjs +24 -8
- package/runtime/pages-navigation.mjs +54 -64
- package/runtime/persistent/client.mjs +6 -5
- package/runtime/persistent/daemon.mjs +66 -56
- package/runtime/persistent/native-proxy.mjs +63 -0
- package/runtime/prefetch-scheduler.mjs +110 -0
- package/runtime/preload.mjs +12 -0
- package/runtime/profile.mjs +14 -0
- package/runtime/profiles.json +2 -2
- package/runtime/render.mjs +3 -0
- package/runtime/route-pattern.mjs +44 -0
- package/runtime/rsc-worker.mjs +41 -12
- package/runtime/trace.mjs +78 -0
- package/runtime/typed-route.mjs +19 -0
- package/runtime/watch-project.mjs +29 -18
- package/runtime/worker.mjs +102 -24
package/build/bundle-runtime.mjs
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { build } from 'esbuild';
|
|
2
|
-
import { lstat, realpath, readdir, writeFile, unlink } from 'node:fs/promises';
|
|
2
|
+
import { lstat, realpath, readdir, writeFile, unlink,readFile } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import {createHash} from 'node:crypto';
|
|
5
|
+
import {version as esbuildVersion} from 'esbuild';
|
|
6
|
+
import {privateCacheDirectory,readCacheRecord,writeCacheRecord} from './persistent-cache.mjs';
|
|
4
7
|
|
|
5
8
|
// Paths used by the native worker, generated application modules, prerender,
|
|
6
9
|
// standalone tracing, compatibility modules and the runtime lifecycle contract.
|
|
@@ -12,7 +15,7 @@ export const runtimeEntryNames = Object.freeze([
|
|
|
12
15
|
].sort());
|
|
13
16
|
|
|
14
17
|
/** Run once, after compiling application/Edge graphs and before any prerender. */
|
|
15
|
-
export async function bundleRuntime(stage) {
|
|
18
|
+
export async function bundleRuntime(stage,{projectRoot,sourceDirectory}={}) {
|
|
16
19
|
const resolvedStage = path.resolve(stage);
|
|
17
20
|
if (!path.basename(resolvedStage).startsWith('.prnext-build-')) {
|
|
18
21
|
throw new Error('Runtime bundling requires an unpublished PRNext build stage.');
|
|
@@ -26,23 +29,44 @@ export async function bundleRuntime(stage) {
|
|
|
26
29
|
if (path.relative(canonicalStage, canonicalRuntime) !== 'runtime') {
|
|
27
30
|
throw new Error('Runtime bundling escaped the build stage.');
|
|
28
31
|
}
|
|
29
|
-
const
|
|
30
|
-
const
|
|
32
|
+
const input=sourceDirectory?await realpath(sourceDirectory):runtime;
|
|
33
|
+
const entries = await readdir(input, { withFileTypes: true });
|
|
34
|
+
const originals = entries.filter(entry => entry.name.endsWith('.mjs')&&!entry.name.endsWith('.test.mjs'));
|
|
31
35
|
if (originals.some(entry => !entry.isFile() || entry.isSymbolicLink())) {
|
|
32
36
|
throw new Error('Runtime source modules must be ordinary files.');
|
|
33
37
|
}
|
|
34
38
|
for (const name of runtimeEntryNames) {
|
|
35
39
|
if (!originals.some(entry => entry.name === `${name}.mjs`)) throw new Error(`Missing runtime entry ${name}.mjs`);
|
|
36
40
|
}
|
|
41
|
+
const sources=new Map();
|
|
42
|
+
// Production compiles the installed sources directly. Only generated stage
|
|
43
|
+
// overrides (currently the bundled env loader) replace an installed module.
|
|
44
|
+
// Hash and compile the same captured bytes; do not trust mtimes or skip app
|
|
45
|
+
// graph callbacks. This avoids copying hundreds of modules into every stage.
|
|
46
|
+
for(const entry of originals){
|
|
47
|
+
let file=path.join(input,entry.name);
|
|
48
|
+
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
|
+
sources.set(path.join(input,entry.name),await readFile(file));
|
|
50
|
+
}
|
|
51
|
+
let directory,key,cached;
|
|
52
|
+
if(projectRoot){
|
|
53
|
+
directory=await privateCacheDirectory(projectRoot,'runtime-bundle-v2');
|
|
54
|
+
if(directory){
|
|
55
|
+
const hash=createHash('sha256').update(esbuildVersion).update(JSON.stringify(runtimeEntryNames)).update(await readFile(new URL('./bundle-runtime.mjs',import.meta.url)));
|
|
56
|
+
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
|
+
key=hash.digest('hex');cached=await readCacheRecord(directory,key);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
37
60
|
// write:false makes compilation atomic with respect to the source runtime:
|
|
38
61
|
// no files change if parsing or resolution fails. The virtual output is a
|
|
39
62
|
// sibling so all relative paths have the same depth as the final runtime.
|
|
40
63
|
const virtualOutput = path.join(resolvedStage, '.runtime-bundle');
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
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&&runtimeEntryNames.every(name=>cached.files.some(([file])=>file===name+'.mjs'));
|
|
65
|
+
const result = validCache?null:await build({
|
|
66
|
+
absWorkingDir: input,
|
|
67
|
+
entryPoints: runtimeEntryNames.map(name => path.join(input, `${name}.mjs`)),
|
|
44
68
|
outdir: virtualOutput,
|
|
45
|
-
outbase:
|
|
69
|
+
outbase: input,
|
|
46
70
|
entryNames: '[name]',
|
|
47
71
|
chunkNames: '_shared-[hash]',
|
|
48
72
|
outExtension: { '.js': '.mjs' },
|
|
@@ -60,23 +84,27 @@ export async function bundleRuntime(stage) {
|
|
|
60
84
|
metafile: true,
|
|
61
85
|
write: false,
|
|
62
86
|
logLevel: 'silent',
|
|
87
|
+
plugins:[{name:'prnext-runtime-snapshot',setup(build){build.onLoad({filter:/\.mjs$/},args=>sources.has(args.path)?{contents:sources.get(args.path),loader:'js',resolveDir:path.dirname(args.path)}:undefined);}}],
|
|
63
88
|
});
|
|
64
89
|
const emitted = new Map();
|
|
65
|
-
for
|
|
90
|
+
if(validCache)for(const [name,contents]of cached.files)emitted.set(name,Buffer.from(contents));
|
|
91
|
+
for (const file of result?.outputFiles||[]) {
|
|
66
92
|
if (path.dirname(file.path) !== virtualOutput || !file.path.endsWith('.mjs')) {
|
|
67
93
|
throw new Error('Runtime bundler emitted a file outside its flat output directory.');
|
|
68
94
|
}
|
|
69
95
|
emitted.set(path.basename(file.path), file.contents);
|
|
70
96
|
}
|
|
71
97
|
if (runtimeEntryNames.some(name => !emitted.has(`${name}.mjs`))) throw new Error('Runtime bundle omitted a required entry.');
|
|
98
|
+
if(directory&&!validCache)await writeCacheRecord(directory,key,{files:[...emitted].map(([name,bytes])=>[name,Buffer.from(bytes).toString()])});
|
|
72
99
|
// This is an unpublished, disposable stage. On any write failure buildProject
|
|
73
100
|
// discards the stage and preserves the previous published build unchanged.
|
|
74
101
|
for (const [name, contents] of emitted) await writeFile(path.join(runtime, name), contents);
|
|
75
|
-
for (const entry of
|
|
102
|
+
for (const entry of await readdir(runtime,{withFileTypes:true})) {
|
|
103
|
+
if(!entry.name.endsWith('.mjs')||!entry.isFile())continue;
|
|
76
104
|
if (!emitted.has(entry.name)) await unlink(path.join(runtime, entry.name));
|
|
77
105
|
}
|
|
78
106
|
// JSON, CJS, license files and the OG assets directory are never traversed or
|
|
79
107
|
// removed. The caller writes the normal runtime contract after prerender.
|
|
80
|
-
return { entryCount: runtimeEntryNames.length, moduleCount: emitted.size,
|
|
108
|
+
return { entryCount: runtimeEntryNames.length, moduleCount: emitted.size,cached:!!validCache,
|
|
81
109
|
bytes: [...emitted.values()].reduce((sum, contents) => sum + contents.length, 0) };
|
|
82
110
|
}
|
|
@@ -15,7 +15,7 @@ export function transformCacheComponents(source, file, { enabled, projectRoot, m
|
|
|
15
15
|
return cachedTransform('cache-components',source,[file,enabled,projectRoot,mode,buildId,configuredSegment,handlers],()=>transformUncached(source,file,{enabled,projectRoot,mode,buildId,configuredSegment,handlers}));
|
|
16
16
|
}
|
|
17
17
|
function transformUncached(source,file,{enabled,projectRoot,mode,buildId,configuredSegment,handlers}) {
|
|
18
|
-
const ast = parse(source, { sourceType: 'unambiguous', sourceFilename: file, plugins: ['jsx', ...(/\.tsx?$/.test(file) ? ['typescript'] : [])] });
|
|
18
|
+
const ast = parse(source, { sourceType: 'unambiguous', sourceFilename: file, plugins: ['jsx', ...(/\.[cm]?tsx?$/.test(file) ? ['typescript'] : [])] });
|
|
19
19
|
let program;
|
|
20
20
|
traverse(ast, { Program(value) { program = value; value.stop(); } });
|
|
21
21
|
const moduleDirective = ast.program.directives.find(cacheDirective);
|
package/build/cache-handlers.mjs
CHANGED
|
@@ -47,7 +47,7 @@ export async function prepareCacheHandlers({ config, projectRoot, stage, environ
|
|
|
47
47
|
platform: 'node', target: 'node22', format: 'esm', packages: 'external', logLevel: 'silent', define: environment,
|
|
48
48
|
banner: { js: "import {createRequire as __prnextCreateRequire} from 'node:module';const require=__prnextCreateRequire(import.meta.url);" },
|
|
49
49
|
plugins: [{ name: 'cache-handler-compatibility', setup(builder) {
|
|
50
|
-
builder.onResolve({ filter: /^(?:next|prnext|@thomas\.f\/prnext)\/(cache
|
|
50
|
+
builder.onResolve({ filter: /^(?:next|prnext|@thomas\.f\/prnext)\/(cache(?:\/redis)?|headers|server)(?:\.js)?$/ }, ({ path: specifier }) => ({ path: `../compat/${frameworkImportName(specifier)}.cjs`, external: true }));
|
|
51
51
|
} }],
|
|
52
52
|
});
|
|
53
53
|
result[name] = output;
|
package/build/check.mjs
CHANGED
|
@@ -7,34 +7,79 @@ import { scanProject } from './scan.mjs';
|
|
|
7
7
|
import { compileCustomRoutes } from './custom-routes.mjs';
|
|
8
8
|
import { validateReactVersions } from './app.mjs';
|
|
9
9
|
import { snapshotEnvConfig } from '../runtime/env.mjs';
|
|
10
|
+
import { environmentDiagnostics } from '../native/diagnostics.mjs';
|
|
11
|
+
import { packageManifest } from '../native/resolve.mjs';
|
|
12
|
+
import { projectTsconfig } from './typescript.mjs';
|
|
10
13
|
|
|
11
14
|
/** Preflight an existing project without rewriting sources, dependencies or build output. */
|
|
12
|
-
export async function checkProject(directory, { dev = false, validateDependencies = true } = {}) {
|
|
15
|
+
export async function checkProject(directory, { dev = false, validateDependencies = true, validateEnvironment = validateDependencies } = {}, services = {}) {
|
|
13
16
|
const root = path.resolve(directory);
|
|
14
|
-
const report = { root, ok: false, routes: [], versions: {}, errors: [], notes: [
|
|
17
|
+
const report = { root, ok: false, routes: [], versions: {}, errors: [], diagnostics: [], notes: [
|
|
15
18
|
'Preflight validates configuration, route conventions and installed React versions. A successful production build and browser tests are still required.',
|
|
16
19
|
'Webpack plugin hooks are validated during compilation; preflight only validates plugin shape and translated configuration.',
|
|
17
20
|
'Rust handles HTTP, routing, static files, images and persistent caches. React and npm JavaScript execute on Node/V8.',
|
|
18
21
|
] };
|
|
22
|
+
const diagnostic = (id, error, action) => {
|
|
23
|
+
const item = { id, severity: 'error', message: error.message, action };
|
|
24
|
+
report.diagnostics.push(item);
|
|
25
|
+
report.errors.push(item.message);
|
|
26
|
+
};
|
|
19
27
|
const restore = snapshotEnvConfig();
|
|
20
28
|
try {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
29
|
+
let config, project;
|
|
30
|
+
try { config = await loadProjectConfig(root, {dev}); }
|
|
31
|
+
catch (error) { diagnostic('config', error, 'Correct the project configuration and run check again.'); }
|
|
32
|
+
if (config) {
|
|
33
|
+
if (config.onDemandEntries) report.notes.push('Development compiles requested routes and retains open browser routes. onDemandEntries controls inactive route retention; middleware, rewrites and parallel/intercepting routing use the complete graph.');
|
|
34
|
+
try { await compileCustomRoutes(config); }
|
|
35
|
+
catch (error) { diagnostic('custom-routes', error, 'Correct redirects, headers or rewrites in the configuration.'); }
|
|
36
|
+
try { project = await scanProject(root, {basePath:config.basePath,pageExtensions:config.pageExtensions,i18n:config.i18n}); }
|
|
37
|
+
catch (error) { diagnostic('routes', error, 'Correct the conflicting route files or route conventions.'); }
|
|
38
|
+
try { await validateCompilerConfig(config,root,{dev,edge:project?.middleware?.runtime==='edge'||project?.routes.some(route=>route.cacheConfig?.runtime==='edge'||route.handlerConfig?.runtime==='edge')}); }
|
|
39
|
+
catch (error) { diagnostic('compiler', error, 'Correct the compiler options or install the required compiler plugins.'); }
|
|
40
|
+
try {
|
|
41
|
+
const tsconfig = await projectTsconfig(root, config);
|
|
42
|
+
if (tsconfig) {
|
|
43
|
+
let ts;
|
|
44
|
+
try { ts = createRequire(path.join(root, 'package.json'))('typescript'); }
|
|
45
|
+
catch {
|
|
46
|
+
const required = config.typedRoutes || config.typescript && !config.typescript.ignoreBuildErrors;
|
|
47
|
+
const item = { id: 'typescript', severity: required ? 'error' : 'warning', message: 'TypeScript configuration exists, but TypeScript is not installed in the application.', action: 'Install typescript as an application dev dependency.' };
|
|
48
|
+
report.diagnostics.push(item); if (required) report.errors.push(item.message);
|
|
49
|
+
}
|
|
50
|
+
if (ts) {
|
|
51
|
+
const parsed = ts.getParsedCommandLineOfConfigFile(tsconfig, { noEmit: true }, { ...ts.sys, onUnRecoverableConfigFileDiagnostic: value => { throw new Error(ts.flattenDiagnosticMessageText(value.messageText, '\n')); } });
|
|
52
|
+
if (!parsed || parsed.errors.length) throw new Error(parsed ? ts.formatDiagnostics(parsed.errors, { getCanonicalFileName: name => name, getCurrentDirectory: () => root, getNewLine: () => '\n' }) : 'TypeScript configuration could not be parsed.');
|
|
53
|
+
report.versions.typescript = ts.version;
|
|
54
|
+
report.diagnostics.push({ id: 'typescript', severity: 'info', message: `TypeScript ${ts.version} configuration is valid. Semantic errors require prnext typecheck.` });
|
|
55
|
+
}
|
|
56
|
+
} else if (config.typedRoutes || config.typescript && !config.typescript.ignoreBuildErrors) throw new Error('TypeScript is enabled, but tsconfig.json does not exist.');
|
|
57
|
+
} catch (error) { diagnostic('typescript', error, 'Correct typescript.tsconfigPath or the selected TypeScript configuration.'); }
|
|
58
|
+
}
|
|
59
|
+
if (project) {
|
|
26
60
|
report.routes = project.routes.filter(route=>!route.internal).map(({pattern,kind,router})=>({pattern,kind,router:router||'pages'}));
|
|
61
|
+
}
|
|
27
62
|
const resolve = createRequire(path.join(root,'package.json'));
|
|
28
63
|
for (const name of ['next','react','react-dom','react-server-dom-webpack']) {
|
|
29
64
|
try { report.versions[name] = JSON.parse(await readFile(resolve.resolve(`${name}/package.json`),'utf8')).version; }
|
|
30
65
|
catch { report.versions[name] = null; }
|
|
31
66
|
}
|
|
32
67
|
if (validateDependencies) {
|
|
33
|
-
|
|
34
|
-
|
|
68
|
+
try {
|
|
69
|
+
if (project?.routes.some(route=>route.router==='app')) await validateReactVersions(resolve);
|
|
70
|
+
else {
|
|
71
|
+
const names = ['react', 'react-dom'];
|
|
72
|
+
if (names.some(name => report.versions[name] !== packageManifest.peerDependencies[name])) throw new Error(`Pages routes require the declared React peer versions. Resolved ${names.map(name => `${name}@${report.versions[name] || 'missing'}`).join(', ')}. Install ${names.map(name => `${name}@${packageManifest.peerDependencies[name]}`).join(' ')} in the application.`);
|
|
73
|
+
}
|
|
74
|
+
} catch (error) { diagnostic('react', error, 'Install the matching React protocol packages shown in this error.'); }
|
|
35
75
|
} else report.notes.push('Installed React versions have not been validated yet; migration checks them after dependency installation.');
|
|
36
|
-
|
|
37
|
-
|
|
76
|
+
if (validateEnvironment) {
|
|
77
|
+
const diagnostics = await (services.environmentDiagnostics || environmentDiagnostics)();
|
|
78
|
+
report.diagnostics.push(...diagnostics);
|
|
79
|
+
report.errors.push(...diagnostics.filter(item => item.severity === 'error').map(item => item.message));
|
|
80
|
+
}
|
|
81
|
+
report.ok = report.errors.length === 0;
|
|
82
|
+
} catch (error) { diagnostic('preflight', error, 'Resolve the reported file or installation error and retry.'); }
|
|
38
83
|
finally { restore(); }
|
|
39
84
|
return report;
|
|
40
85
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import postcss from 'postcss';
|
|
2
|
+
import selectors from 'postcss-selector-parser';
|
|
3
|
+
import values from 'postcss-value-parser';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
|
|
6
|
+
// Names are independent of compilation order and of which routes are built.
|
|
7
|
+
// A collision fails the build instead of changing one of two published maps.
|
|
8
|
+
export function compactCssModule(css, exports, {globals = new Set(), hash = name => 'p' + createHash('sha256').update(name).digest('base64url').slice(0, 10)} = {}) {
|
|
9
|
+
const names = new Map(), reverse = new Map();
|
|
10
|
+
for (const list of Object.values(exports)) for (const name of list.split(/\s+/).filter(Boolean)) {
|
|
11
|
+
if (names.has(name) || globals.has(name)) continue;
|
|
12
|
+
const short = hash(name);
|
|
13
|
+
if (!/^[a-zA-Z_][\w-]*$/.test(short) || globals.has(short) && short!==name || reverse.has(short) && reverse.get(short) !== name) throw new Error('CSS Module compact-name collision.');
|
|
14
|
+
names.set(name, short); reverse.set(short, name);
|
|
15
|
+
}
|
|
16
|
+
const root = postcss.parse(css);
|
|
17
|
+
root.walkRules(rule => {
|
|
18
|
+
rule.selector = selectors(ast => ast.walkClasses(node => {
|
|
19
|
+
if (names.has(node.value)) node.value = names.get(node.value);
|
|
20
|
+
})).processSync(rule.selector);
|
|
21
|
+
});
|
|
22
|
+
root.walkAtRules(/^(?:-webkit-)?keyframes$/i, rule => {
|
|
23
|
+
const parsed = values(rule.params);
|
|
24
|
+
parsed.walk(node => { if (node.type === 'word' && names.has(node.value)) node.value = names.get(node.value); });
|
|
25
|
+
rule.params = parsed.toString();
|
|
26
|
+
});
|
|
27
|
+
root.walkDecls(/^(?:-webkit-)?animation(?:-name)?$/i, declaration => {
|
|
28
|
+
const parsed = values(declaration.value);
|
|
29
|
+
parsed.walk(node => { if (node.type === 'word' && names.has(node.value)) node.value = names.get(node.value); });
|
|
30
|
+
declaration.value = parsed.toString();
|
|
31
|
+
});
|
|
32
|
+
return { css: root.toString(), names:[...names], mapping: Object.fromEntries(Object.entries(exports).map(([key, list]) => [key, list.split(/\s+/).map(name => names.get(name) || name).join(' ')])) };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function globalCssNames(css, {local = true} = {}, result = new Set()) {
|
|
36
|
+
const root = postcss.parse(css);
|
|
37
|
+
root.walkRules(rule => {
|
|
38
|
+
selectors(ast => ast.each(selector => {
|
|
39
|
+
let global = !local;
|
|
40
|
+
selector.each(node => {
|
|
41
|
+
if (node.type === 'pseudo' && [':global',':local'].includes(node.value)) {
|
|
42
|
+
if (node.nodes?.length) { if (node.value === ':global') node.walkClasses(item=>result.add(item.value)); }
|
|
43
|
+
else global = node.value === ':global';
|
|
44
|
+
} else if (global) { if(node.type==='class') result.add(node.value); node.walkClasses?.(item=>result.add(item.value)); }
|
|
45
|
+
});
|
|
46
|
+
})).processSync(rule.selector);
|
|
47
|
+
});
|
|
48
|
+
root.walkDecls(/^composes$/i, declaration => {
|
|
49
|
+
const words = values(declaration.value).nodes.filter(node=>node.type==='word').map(node=>node.value);
|
|
50
|
+
if (words.at(-1)==='global' && words.at(-2)==='from') for(const name of words.slice(0,-2)) result.add(name);
|
|
51
|
+
});
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { lstat, readFile, opendir, realpath, readlink, mkdir, writeFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { version as esbuildVersion } from 'esbuild';
|
|
5
|
+
import { buildCacheIdentity, cacheHash, privateCacheDirectory, readCacheRecord, writeCacheRecord } from './persistent-cache.mjs';
|
|
6
|
+
import {mapConcurrent} from './concurrent.mjs';
|
|
7
|
+
import { readCompilationRecord, writeCompilationRecord } from './compilation-store.mjs';
|
|
8
|
+
|
|
9
|
+
// Graph outputs have a narrower contract than source transforms. In particular,
|
|
10
|
+
// application graph callbacks discover actions, boundaries and font assets. They
|
|
11
|
+
// must run on every generation and are never skipped by this cache.
|
|
12
|
+
const reports = new AsyncLocalStorage();
|
|
13
|
+
const MAX_FILES = 4096, MAX_DIRECTORIES = 8192, MAX_NAMES = 32768;
|
|
14
|
+
const MAX_FILE_BYTES = 16 * 1024 * 1024, MAX_BYTES = 48 * 1024 * 1024;
|
|
15
|
+
const RECORD_LIMIT = 64 * 1024 * 1024;
|
|
16
|
+
const SHA = /^[a-f0-9]{64}$/;
|
|
17
|
+
const frameworkDirectory=name=>['.git','.prnext-cache','.prnext','.prnext-incremental-stage'].includes(name)||/^\.prnext-(?:build|backup|config|postcss)-/.test(name);
|
|
18
|
+
const frameworkPointer=name=>/^\.prnext-output(?:\.json|-.+\.tmp)$/.test(name);
|
|
19
|
+
const ignored = entry => entry.isDirectory() && (entry.name==='node_modules'||frameworkDirectory(entry.name)) || frameworkPointer(entry.name);
|
|
20
|
+
const safePath = file => typeof file === 'string' && file.length < 32768 && path.isAbsolute(file) && !file.includes('\0');
|
|
21
|
+
const inside = (root, file) => { const relative = path.relative(root, file); return relative === '' || !path.isAbsolute(relative) && relative !== '..' && !relative.startsWith('..' + path.sep); };
|
|
22
|
+
export const withCompilationGraphCacheReporting = run => reports.run({ kinds: new Map(), identities:new Map(), listings:new Map(),listingNames:0 }, run);
|
|
23
|
+
export function compilationGraphCacheReport() {
|
|
24
|
+
const kinds = [...(reports.getStore()?.kinds || [])].map(([kind, value]) => ({ kind, ...value, reasons: { ...value.reasons } }));
|
|
25
|
+
return { version: 1, scope: 'eligible-production-compilation-graphs', kinds,
|
|
26
|
+
hits: kinds.reduce((sum, item) => sum + item.hits, 0), misses: kinds.reduce((sum, item) => sum + item.misses, 0),
|
|
27
|
+
bypasses: kinds.reduce((sum, item) => sum + item.bypasses, 0), reusedBytes: kinds.reduce((sum, item) => sum + item.reusedBytes, 0) };
|
|
28
|
+
}
|
|
29
|
+
function record(kind, outcome, reason, bytes = 0) {
|
|
30
|
+
const state = reports.getStore(); if (!state) return;
|
|
31
|
+
if (!state.kinds.has(kind)) state.kinds.set(kind, { hits: 0, misses: 0, bypasses: 0, writes: 0, reusedBytes: 0, reasons: {} });
|
|
32
|
+
const item = state.kinds.get(kind); item[outcome]++; item.reusedBytes += bytes;
|
|
33
|
+
if (reason) item.reasons[reason] = (item.reasons[reason] || 0) + 1;
|
|
34
|
+
}
|
|
35
|
+
function serializable(value) {
|
|
36
|
+
if (value === null || ['string', 'number', 'boolean'].includes(typeof value)) return true;
|
|
37
|
+
if (Array.isArray(value)) return value.every(serializable);
|
|
38
|
+
return value && Object.getPrototypeOf(value) === Object.prototype && Object.values(value).every(item => item === undefined || serializable(item));
|
|
39
|
+
}
|
|
40
|
+
const directorySignature=info=>[info.dev,info.ino,info.mode,info.mtimeNs,info.ctimeNs].join(':');
|
|
41
|
+
async function directoryEntries(directory,budget){
|
|
42
|
+
const state=reports.getStore(),before=await lstat(directory,{bigint:true});
|
|
43
|
+
if(!before.isDirectory()||before.isSymbolicLink())throw Error('unstable-resolution');
|
|
44
|
+
const signature=directorySignature(before),saved=state?.listings.get(directory);
|
|
45
|
+
if(saved?.signature===signature){
|
|
46
|
+
const after=await lstat(directory,{bigint:true});
|
|
47
|
+
if(directorySignature(after)===signature){budget.names+=saved.entries.length;if(budget.names>MAX_NAMES)throw Error('resolution-limit');return {entries:saved.entries,info:after,stable:true};}
|
|
48
|
+
}
|
|
49
|
+
const entries=[];
|
|
50
|
+
for await(const entry of await opendir(directory)){if(++budget.names>MAX_NAMES)throw Error('resolution-limit');entries.push(entry);}
|
|
51
|
+
const after=await lstat(directory,{bigint:true}),stable=signature===directorySignature(after);
|
|
52
|
+
if(state&&stable){
|
|
53
|
+
if(state.listings.has(directory)){state.listingNames-=state.listings.get(directory).entries.length;state.listings.delete(directory);}
|
|
54
|
+
while(state.listings.size&&(state.listings.size>=MAX_DIRECTORIES||state.listingNames+entries.length>MAX_NAMES)){
|
|
55
|
+
const [oldest,value]=state.listings.entries().next().value;state.listings.delete(oldest);state.listingNames-=value.entries.length;
|
|
56
|
+
}
|
|
57
|
+
if(entries.length<=MAX_NAMES){state.listings.set(directory,{signature,entries});state.listingNames+=entries.length;}
|
|
58
|
+
}
|
|
59
|
+
return {entries,info:after,stable};
|
|
60
|
+
}
|
|
61
|
+
async function fileDigest(file, started = Infinity, budget = {bytes:0}) {
|
|
62
|
+
const before = await lstat(file);
|
|
63
|
+
if (!before.isFile() || before.isSymbolicLink() || before.size > MAX_FILE_BYTES || before.mtimeMs > started || before.ctimeMs > started) throw Error('unstable-input');
|
|
64
|
+
budget.bytes+=before.size;if(budget.bytes>MAX_BYTES)throw Error('dependency-limit');
|
|
65
|
+
const bytes = await readFile(file), after = await lstat(file);
|
|
66
|
+
if (bytes.length !== before.size || before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs) throw Error('unstable-input');
|
|
67
|
+
return cacheHash(bytes);
|
|
68
|
+
}
|
|
69
|
+
async function directoryDigest(directory, budget, started = Infinity, lookup = false, excluded = []) {
|
|
70
|
+
let listing;
|
|
71
|
+
try{listing=await directoryEntries(directory,budget);}catch(error){if(error.code==='ENOENT')return 'missing';throw error;}
|
|
72
|
+
const {info}=listing;
|
|
73
|
+
if(started!==Infinity&&(Number(info.mtimeNs)/1e6>started||Number(info.ctimeNs)/1e6>started))throw Error('unstable-resolution');
|
|
74
|
+
const namesAt=async listing=>{
|
|
75
|
+
const entries=listing.entries.filter(entry=>lookup?['package.json','tsconfig.json','jsconfig.json','.browserslistrc','browserslist','node_modules'].includes(entry.name):!ignored(entry)&&!excluded.includes(path.join(directory,entry.name)));
|
|
76
|
+
entries.sort((a,b)=>a.name.localeCompare(b.name));
|
|
77
|
+
return mapConcurrent(entries,async entry=>[entry.name,entry.isDirectory()?'directory':entry.isSymbolicLink()?['link',await readlink(path.join(directory,entry.name))]:entry.isFile()?'file':'other']);
|
|
78
|
+
};
|
|
79
|
+
const names=await namesAt(listing),digest=cacheHash(JSON.stringify(names));
|
|
80
|
+
if(!listing.stable&&cacheHash(JSON.stringify(await namesAt(await directoryEntries(directory,budget))))!==digest)throw Error('unstable-resolution');
|
|
81
|
+
return digest;
|
|
82
|
+
}
|
|
83
|
+
async function snapshot(root, dependencies, { started = Infinity, imports = [], rejectTsconfig = false, styleTargets=false, baseline, excluded = [] } = {}) {
|
|
84
|
+
if (!Array.isArray(dependencies) || dependencies.length > MAX_FILES || dependencies.some(file => !safePath(file))) throw Error('dependency-limit');
|
|
85
|
+
const files = new Set(dependencies), directories = new Set(), trees = new Set([root]), fullDirectories = new Set();
|
|
86
|
+
if(dependencies.some(file=>path.dirname(file).split(path.sep).some(frameworkDirectory)||frameworkPointer(path.basename(file))||excluded.some(directory=>inside(directory,file))))throw Error('generated-input');
|
|
87
|
+
for(const name of ['package.json','package-lock.json','npm-shrinkwrap.json','pnpm-lock.yaml','yarn.lock','bun.lock','bun.lockb','.env','.env.local','.env.production','.env.production.local','.env.development','.env.development.local','.env.test','.env.test.local']){
|
|
88
|
+
const file=path.join(root,name);try{const info=await lstat(file);if(!info.isFile()||info.isSymbolicLink())throw Error('unsafe-config');files.add(file);}catch(error){if(error.code!=='ENOENT')throw error;}
|
|
89
|
+
}
|
|
90
|
+
const addParents = async file => {
|
|
91
|
+
let directory = path.dirname(file);
|
|
92
|
+
while (true) {
|
|
93
|
+
directories.add(directory);
|
|
94
|
+
// Node's package search includes missing nearer node_modules directories.
|
|
95
|
+
directories.add(path.join(directory, 'node_modules'));
|
|
96
|
+
for (const name of ['package.json', 'tsconfig.json', 'jsconfig.json', ...(styleTargets?['.browserslistrc','browserslist']:[])]) {
|
|
97
|
+
const candidate = path.join(directory, name);
|
|
98
|
+
try {
|
|
99
|
+
const info = await lstat(candidate);
|
|
100
|
+
if(name==='browserslist'&&info.isDirectory())continue;
|
|
101
|
+
if (rejectTsconfig && /^(?:ts|js)config\.json$/.test(name)) throw Error('untracked-tsconfig');
|
|
102
|
+
if (!info.isFile() || info.isSymbolicLink() || info.size>MAX_FILE_BYTES) throw Error('unsafe-config');
|
|
103
|
+
if(styleTargets&&(name==='.browserslistrc'||name==='browserslist'||name==='package.json')){
|
|
104
|
+
const source=await readFile(candidate,'utf8');
|
|
105
|
+
const targets=name==='package.json'?JSON.stringify(JSON.parse(source).browserslist):source;
|
|
106
|
+
if(typeof targets==='string'&&/\b(?:extends|stats)\b/i.test(targets))throw Error('custom-browser-targets');
|
|
107
|
+
}
|
|
108
|
+
files.add(candidate);
|
|
109
|
+
} catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
110
|
+
}
|
|
111
|
+
const parent = path.dirname(directory); if (parent === directory) break; directory = parent;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
for (const file of dependencies) {
|
|
115
|
+
await addParents(file);
|
|
116
|
+
// Package export/browser maps may choose an absent file anywhere inside the
|
|
117
|
+
// package. Directory names, including absence, participate in invalidation.
|
|
118
|
+
let directory = path.dirname(file);
|
|
119
|
+
while (true) {
|
|
120
|
+
try { if ((await lstat(path.join(directory, 'package.json'))).isFile()) { trees.add(directory); break; } }
|
|
121
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
122
|
+
const parent = path.dirname(directory); if (parent === directory) break; directory = parent;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
for (const [importer, specifier] of imports) {
|
|
126
|
+
if (typeof specifier !== 'string' || /^(?:node:|[./\\]|[A-Za-z]:)/.test(specifier)) continue;
|
|
127
|
+
const parts = specifier.split('/'), name = parts.slice(0, specifier.startsWith('@') ? 2 : 1).join('/');
|
|
128
|
+
if (!name || name.includes('\0') || name.split('/').some(part => part === '..')) throw Error('unsafe-resolution');
|
|
129
|
+
let directory = path.dirname(importer);
|
|
130
|
+
while (true) {
|
|
131
|
+
const candidate = path.join(directory, 'node_modules', name);
|
|
132
|
+
try { const info = await lstat(candidate); if (info.isSymbolicLink()) throw Error('linked-package'); if (info.isDirectory()) trees.add(candidate); }
|
|
133
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
134
|
+
const parent = path.dirname(directory); if (parent === directory) break; directory = parent;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const walked={names:0};
|
|
138
|
+
const walk = async (directory, depth = 0) => {
|
|
139
|
+
if (depth > 32 || directories.size > MAX_DIRECTORIES) throw Error('resolution-limit');
|
|
140
|
+
const listing=await directoryEntries(directory,walked);
|
|
141
|
+
directories.add(directory); fullDirectories.add(directory);
|
|
142
|
+
for (const entry of listing.entries) {
|
|
143
|
+
if (ignored(entry) || excluded.includes(path.join(directory,entry.name))) continue;
|
|
144
|
+
if (entry.isSymbolicLink()) throw Error('linked-input');
|
|
145
|
+
if (entry.isDirectory()) await walk(path.join(directory, entry.name), depth + 1);
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
for (const tree of trees) await walk(tree);
|
|
149
|
+
if (files.size > MAX_FILES || directories.size > MAX_DIRECTORIES) throw Error('dependency-limit');
|
|
150
|
+
const budget = { names: 0,bytes:0 };
|
|
151
|
+
const knownFiles = new Map(baseline?.files || []), knownDirectories = new Map((baseline?.directories || []).map(item => [item[0], item]));
|
|
152
|
+
const result = { excluded, files: await mapConcurrent([...files].sort(),async file => [file, await fileDigest(file, knownFiles.has(file) ? Infinity : started,budget)]),
|
|
153
|
+
directories: await mapConcurrent([...directories].sort(),async directory => {
|
|
154
|
+
const lookup = !fullDirectories.has(directory) && path.basename(directory) !== 'node_modules';
|
|
155
|
+
const known = knownDirectories.get(directory), comparable = known && known[2] === lookup;
|
|
156
|
+
return [directory, await directoryDigest(directory, budget, comparable ? Infinity : started, lookup, excluded), lookup];
|
|
157
|
+
}) };
|
|
158
|
+
for (const [file,digest] of result.files) if (knownFiles.has(file) && knownFiles.get(file) !== digest) throw Error('unstable-input');
|
|
159
|
+
for (const [directory,digest,lookup] of result.directories) {
|
|
160
|
+
const known = knownDirectories.get(directory);
|
|
161
|
+
if (known && known[2] === lookup && known[1] !== digest) throw Error('unstable-resolution');
|
|
162
|
+
}
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
async function validSnapshot(value) {
|
|
166
|
+
const pairs = (list, missing) => Array.isArray(list) && list.length <= (missing ? MAX_DIRECTORIES : MAX_FILES)
|
|
167
|
+
&& new Set(list.map(item => item?.[0])).size === list.length
|
|
168
|
+
&& list.every(item => Array.isArray(item) && item.length === (missing ? 3 : 2) && safePath(item[0]) && (SHA.test(item[1]) || missing && item[1] === 'missing') && (!missing || typeof item[2] === 'boolean'));
|
|
169
|
+
if (!value || !Array.isArray(value.excluded) || value.excluded.length>32 || !value.excluded.every(safePath) || !pairs(value.files, false) || !pairs(value.directories, true)) return false;
|
|
170
|
+
const budget = { names: 0,bytes:0 };
|
|
171
|
+
for (const [file, digest] of value.files) if (await fileDigest(file,Infinity,budget) !== digest) return false;
|
|
172
|
+
for (const [directory, digest, lookup] of value.directories) if (await directoryDigest(directory, budget, Infinity, lookup, value.excluded) !== digest) return false;
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
/** Framework-owned graph producers explicitly return dependencies and payloads. */
|
|
176
|
+
export async function cachedCompilation({ root, kind, settings, inputs = [], excluded = [], enabled = true, reason, validate, produce, bytes = () => 0, rejectTsconfig = false,styleTargets=false, binary = false }) {
|
|
177
|
+
if (!enabled || process.env.PRNEXT_BUILD_CACHE === '0' || !serializable(settings)) {
|
|
178
|
+
record(kind, 'bypasses', process.env.PRNEXT_BUILD_CACHE === '0' ? 'disabled' : reason || 'unsupported-configuration'); return (await produce()).value;
|
|
179
|
+
}
|
|
180
|
+
let directory, key;
|
|
181
|
+
try {
|
|
182
|
+
root = await realpath(root); directory = await privateCacheDirectory(root, binary ? 'compilation-graphs-v2' : 'compilation-graphs-v1');
|
|
183
|
+
if(!directory)throw Error('unavailable');
|
|
184
|
+
// Only metadata for the same current environment is shared. Individual
|
|
185
|
+
// graph snapshots still re-hash source/config bytes and validate listings.
|
|
186
|
+
const state=reports.getStore(),environment=cacheHash(JSON.stringify(Object.entries(process.env).sort())),identityKey=root+'\0'+environment;
|
|
187
|
+
if(state&&!state.identities.has(identityKey)){
|
|
188
|
+
if(state.identities.size>=8)state.identities.delete(state.identities.keys().next().value);
|
|
189
|
+
state.identities.set(identityKey,buildCacheIdentity(root));
|
|
190
|
+
}
|
|
191
|
+
const identity = directory && await (state?.identities.get(identityKey)||buildCacheIdentity(root));
|
|
192
|
+
if (!identity) throw Error('unavailable');
|
|
193
|
+
if(excluded.length>32||!excluded.every(file=>safePath(file)&&inside(root,file)&&file!==root))throw Error('unsafe-output-directory');
|
|
194
|
+
key = cacheHash(JSON.stringify([identity, esbuildVersion, kind, settings, excluded]));
|
|
195
|
+
const cached = binary ? await readCompilationRecord(directory, key) : await readCacheRecord(directory, key, RECORD_LIMIT);
|
|
196
|
+
if (cached?.format === 1 && validate(cached.value) && await validSnapshot(cached.snapshot)) {
|
|
197
|
+
record(kind, 'hits', undefined, bytes(cached.value)); return cached.value;
|
|
198
|
+
}
|
|
199
|
+
} catch { /* Corrupt, excessive or unavailable caches are optional. */ }
|
|
200
|
+
record(kind, 'misses');
|
|
201
|
+
let baseline, baselineError;
|
|
202
|
+
if(directory && key)try{baseline=await snapshot(root,inputs,{rejectTsconfig,excluded,styleTargets});}catch(error){baselineError=error;}
|
|
203
|
+
const started = Date.now(), produced = await produce();
|
|
204
|
+
if (directory && key && validate(produced.value)) try {
|
|
205
|
+
if(!baseline)throw baselineError || Error('snapshot-unavailable');
|
|
206
|
+
const current = await snapshot(root, produced.dependencies, { started, imports: produced.imports, rejectTsconfig,styleTargets, baseline, excluded });
|
|
207
|
+
const saved = { format: 1, snapshot: current, value: produced.value };
|
|
208
|
+
if (await (binary ? writeCompilationRecord(directory, key, saved) : writeCacheRecord(directory, key, saved, RECORD_LIMIT,{maxFiles:1024}))) record(kind, 'writes');
|
|
209
|
+
} catch (error) { record(kind, 'bypasses', ['unstable-input', 'unstable-resolution', 'untracked-tsconfig', 'linked-package', 'linked-input', 'resolution-limit', 'dependency-limit','generated-input','custom-browser-targets'].includes(error.message) ? error.message : 'snapshot-unavailable'); }
|
|
210
|
+
else if(!validate(produced.value))record(kind,'bypasses','generation-specific-output');
|
|
211
|
+
return produced.value;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function validMetafile(value){
|
|
215
|
+
const object=value=>value&&typeof value==='object'&&!Array.isArray(value),text=value=>typeof value==='string'&&value.length<32768&&!value.includes('\0');
|
|
216
|
+
const count=value=>Number.isSafeInteger(value)&&value>=0;
|
|
217
|
+
const imports=value=>Array.isArray(value)&&value.length<=MAX_FILES&&value.every(item=>object(item)&&text(item.path)&&text(item.kind)&&(item.external===undefined||typeof item.external==='boolean'));
|
|
218
|
+
return object(value)&&object(value.inputs)&&object(value.outputs)&&Object.keys(value.inputs).length<=MAX_FILES&&Object.keys(value.outputs).length<=MAX_FILES
|
|
219
|
+
&&Object.entries(value.inputs).every(([file,input])=>text(file)&&object(input)&&count(input.bytes)&&imports(input.imports))
|
|
220
|
+
&&Object.entries(value.outputs).every(([file,output])=>text(file)&&object(output)&&count(output.bytes)&&imports(output.imports)
|
|
221
|
+
&&(output.entryPoint===undefined||text(output.entryPoint))&&(output.cssBundle===undefined||text(output.cssBundle))
|
|
222
|
+
&&Array.isArray(output.exports)&&output.exports.length<=MAX_FILES&&output.exports.every(text)&&object(output.inputs)
|
|
223
|
+
&&Object.keys(output.inputs).length<=MAX_FILES&&Object.entries(output.inputs).every(([file,input])=>text(file)&&object(input)&&count(input.bytesInOutput)));
|
|
224
|
+
}
|
|
225
|
+
function validGraph(value) {
|
|
226
|
+
return value && Array.isArray(value.files) && value.files.length > 0 && value.files.length <= MAX_FILES
|
|
227
|
+
&& value.files.every(file => Array.isArray(file) && file.length === 3 && typeof file[0] === 'string' && !path.isAbsolute(file[0])
|
|
228
|
+
&& !/[\0<>:"|?*]/.test(file[0]) && file[0].split(/[\\/]/).every(part => part && part !== '.' && part !== '..') && file[1] instanceof Uint8Array && file[1].byteLength <= MAX_FILE_BYTES && typeof file[2] === 'string')
|
|
229
|
+
&& new Set(value.files.map(file => file[0])).size === value.files.length
|
|
230
|
+
&& value.files.reduce((sum, file) => sum + file[1].byteLength, 0) <= MAX_BYTES
|
|
231
|
+
&& validMetafile(value.metafile);
|
|
232
|
+
}
|
|
233
|
+
const deepMap = (value, convert) => typeof value === 'string' ? convert(value) : Array.isArray(value) ? value.map(item => deepMap(item, convert))
|
|
234
|
+
: value && typeof value === 'object' ? Object.fromEntries(Object.entries(value).map(([key, item]) => [convert(key), deepMap(item, convert)])) : value;
|
|
235
|
+
/** Cache native esbuild graphs only; plugins and generated entries need replay. */
|
|
236
|
+
export async function cachedGraphBuild(options, { root, stage, dev = false, config = {} }, compile) {
|
|
237
|
+
const output = options.outdir || options.outfile && path.dirname(options.outfile);
|
|
238
|
+
const permitted = !dev && !(options.plugins?.length) && options.bundle === true && output && stage && inside(stage, path.resolve(output))
|
|
239
|
+
&& !options.stdin && !options.tsconfig && !options.tsconfigRaw && options.mangleCache===undefined && !config.webpack && !Object.keys(config.turbopack?.rules || {}).length;
|
|
240
|
+
if (!permitted) { record('javascript', 'bypasses', dev ? 'development' : options.plugins?.length ? 'graph-side-effects' : 'unsupported-configuration'); return compile(options); }
|
|
241
|
+
const token = '__PRNEXT_COMPILATION_STAGE__', toSaved = value => value.replaceAll(stage,()=>token).replaceAll(path.basename(stage),()=>token), fromSaved = value => value.replaceAll(token,()=>stage);
|
|
242
|
+
const rawSignature = { ...options, plugins: undefined, write: false, metafile: true };
|
|
243
|
+
if(!serializable(rawSignature)){record('javascript','bypasses','unsupported-configuration');return compile(options);}
|
|
244
|
+
const signature = deepMap(rawSignature, toSaved);
|
|
245
|
+
if (!serializable(signature)) { record('javascript', 'bypasses', 'unsupported-configuration'); return compile(options); }
|
|
246
|
+
let fresh;
|
|
247
|
+
const entries = Array.isArray(options.entryPoints) ? options.entryPoints.map(entry=>typeof entry==='string'?entry:entry.in) : Object.values(options.entryPoints || {});
|
|
248
|
+
const excluded = [path.resolve(root,config.distDir || '.prnext'),...(config.output==='export'?[path.join(root,'out')]:[])];
|
|
249
|
+
const saved = await cachedCompilation({ root, kind: 'javascript', settings: signature, inputs:entries.map(file=>path.resolve(options.absWorkingDir || process.cwd(),file)), excluded, validate: validGraph, rejectTsconfig: true, binary: true,
|
|
250
|
+
bytes: value => value.files.reduce((sum, file) => sum + file[1].byteLength, 0),
|
|
251
|
+
produce: async () => {
|
|
252
|
+
fresh = await compile({ ...options, write: false, metafile: true });
|
|
253
|
+
// Warnings may carry plugin details/locations; do not replay approximate diagnostics.
|
|
254
|
+
if (fresh.warnings?.length) return { value: null, dependencies: [] };
|
|
255
|
+
if(JSON.stringify(fresh.metafile).includes(token))return {value:null,dependencies:[]};
|
|
256
|
+
const dependencies = Object.keys(fresh.metafile.inputs).map(file => path.resolve(options.absWorkingDir || process.cwd(), file));
|
|
257
|
+
if (dependencies.some(file => inside(stage, file))) return { value: null, dependencies: [] };
|
|
258
|
+
const files = fresh.outputFiles.map(file => [path.relative(output, file.path), file.contents, file.hash]);
|
|
259
|
+
if (fresh.outputFiles.some(file => Buffer.from(file.contents).includes(Buffer.from(stage)) || Buffer.from(file.contents).includes(Buffer.from(path.basename(stage))))) return { value: null, dependencies: [] };
|
|
260
|
+
const imports = Object.entries(fresh.metafile.inputs).flatMap(([file, input]) => input.imports.filter(item => item.external).map(item => [path.resolve(options.absWorkingDir || process.cwd(), file), item.path]));
|
|
261
|
+
return { value: { files, metafile: deepMap(fresh.metafile, toSaved) }, dependencies, imports };
|
|
262
|
+
} });
|
|
263
|
+
const outputFiles = saved ? saved.files.map(([name, data, hash]) => ({ path: path.join(output, name), contents: Buffer.from(data.buffer, data.byteOffset, data.byteLength), hash, get text() { return this.contents.toString(); } })) : fresh.outputFiles;
|
|
264
|
+
if (options.write !== false) for (const file of outputFiles) { await mkdir(path.dirname(file.path), { recursive: true }); await writeFile(file.path, file.contents); }
|
|
265
|
+
return { ...(fresh || { errors: [], warnings: [] }), ...(options.metafile ? { metafile: saved ? deepMap(saved.metafile, fromSaved) : fresh.metafile } : { metafile: undefined }),
|
|
266
|
+
outputFiles: options.write === false ? outputFiles : undefined };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export const compilationCacheLimits = Object.freeze({ files: MAX_FILES, directories: MAX_DIRECTORIES, names: MAX_NAMES, fileBytes: MAX_FILE_BYTES, bytes: MAX_BYTES });
|