@thomas.f/prnext 0.1.17-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/README.md +26 -869
- package/build/actions.mjs +1 -1
- package/build/analyze.mjs +180 -24
- package/build/app-config.mjs +13 -0
- package/build/app-static.mjs +34 -12
- package/build/app-styles.mjs +79 -0
- package/build/app.mjs +81 -26
- package/build/bundle-runtime.mjs +22 -9
- package/build/cache-components.mjs +1 -1
- package/build/cache-handlers.mjs +1 -1
- 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 +31 -8
- package/build/config.mjs +30 -1
- package/build/create.mjs +239 -0
- package/build/css-modules.mjs +56 -12
- package/build/deployment-assets.mjs +89 -0
- package/build/deployment-config.mjs +18 -0
- package/build/dev-css-update.mjs +4 -2
- package/build/dev-dependencies.mjs +1 -1
- package/build/dev-discovery.mjs +3 -3
- package/build/dev.mjs +15 -8
- package/build/dynamic.mjs +1 -1
- package/build/edge.mjs +3 -3
- package/build/fonts.mjs +1 -1
- package/build/framework-imports.mjs +15 -5
- package/build/html-only-config.mjs +53 -0
- package/build/html-only-graph.mjs +67 -0
- package/build/index.mjs +59 -26
- package/build/instrumentation.mjs +30 -0
- package/build/metadata-routes.mjs +1 -1
- package/build/migration-audit.mjs +1 -1
- package/build/native-type-config.mjs +37 -0
- package/build/persistent-cache.mjs +18 -3
- package/build/precompress.mjs +1 -1
- package/build/prerender-cache.mjs +70 -0
- package/build/prerender-config.mjs +29 -0
- package/build/prerender-pages.mjs +19 -9
- package/build/prerender-pool.mjs +165 -0
- package/build/prerender-process-worker.mjs +58 -0
- package/build/prerender-worker.mjs +5 -5
- package/build/production-builder.mjs +58 -9
- package/build/react-compiler-report.mjs +225 -0
- package/build/react-compiler.mjs +139 -0
- package/build/runtime-contract.mjs +2 -2
- package/build/static-export.mjs +4 -4
- package/build/styles.mjs +6 -4
- package/build/transform.mjs +1 -1
- package/build/typed-routes.mjs +4 -0
- package/build/typescript-dev.mjs +35 -8
- 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.mjs +6 -4
- package/build/webpack-graph.mjs +6 -3
- package/build/worker-memory.mjs +9 -0
- package/cli.mjs +60 -4
- 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 +68 -3
- package/compat/link.cjs +12 -9
- package/compat/navigation-authorization.cjs +9 -0
- package/compat/navigation-stages.cjs +19 -0
- 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/server-primitives.cjs +31 -11
- package/compat/server.d.cts +15 -2
- package/compat/static-generation.cjs +7 -0
- package/compat/use-cache.cjs +3 -0
- package/docs/ai/compatibility.md +171 -2
- package/docs/ai/data.md +165 -5
- package/docs/ai/deployment.md +158 -0
- package/docs/ai/quickstart.md +29 -4
- package/docs/ai/routing.md +82 -0
- package/docs/ai/troubleshooting.md +37 -2
- package/package.json +26 -7
- package/runtime/action-client.mjs +10 -4
- package/runtime/action-server.mjs +2 -0
- package/runtime/app-client.mjs +212 -65
- 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 +88 -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/deployment-client.mjs +19 -0
- package/runtime/dev-client.mjs +7 -2
- package/runtime/dev-compiler-broker.mjs +42 -0
- package/runtime/dev-indicator.mjs +8 -3
- package/runtime/dev-lazy-tool.mjs +16 -0
- package/runtime/development.mjs +29 -12
- package/runtime/document.mjs +1 -1
- package/runtime/edge.mjs +6 -5
- 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 +14 -7
- package/runtime/pages-navigation.mjs +48 -63
- package/runtime/prefetch-scheduler.mjs +110 -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 +34 -12
- package/runtime/trace.mjs +78 -0
- package/runtime/watch-project.mjs +19 -15
- package/runtime/worker.mjs +70 -26
|
@@ -4,6 +4,7 @@ import {readFile} from 'node:fs/promises';
|
|
|
4
4
|
import {analyzeSource} from './source-analysis.mjs';
|
|
5
5
|
import {cachedTransform} from './transform-cache.mjs';
|
|
6
6
|
const adapters = new Map([
|
|
7
|
+
['cache/redis','redis-cache'],
|
|
7
8
|
['dist/shared/lib/router-context.shared-runtime','next-router-context'],
|
|
8
9
|
['dist/shared/lib/router-context','next-router-context'],
|
|
9
10
|
['dist/shared/lib/app-router-context.shared-runtime','next-app-router-context'],
|
|
@@ -23,33 +24,42 @@ export function frameworkImportName(specifier) {
|
|
|
23
24
|
|
|
24
25
|
// Application graphs must not traverse the programmatic build API's native tools.
|
|
25
26
|
// Outside a compiled application, the package root retains its full Node API.
|
|
26
|
-
export function applicationEntry(builder,readSource=(file,override)=>override??readFile(file,'utf8')){
|
|
27
|
-
//
|
|
27
|
+
export function applicationEntry(builder,readSource=(file,override)=>override??readFile(file,'utf8'),{transformTypedHelpers=false}={}){
|
|
28
|
+
// Server/client module boundaries in .mts/.cts are not supported. Never
|
|
28
29
|
// silently bundle a server implementation or misclassify a client boundary.
|
|
29
30
|
builder.onLoad({filter:/\.[cm]ts$/i,namespace:'file'},async args=>{
|
|
30
31
|
// Loader bridges dispatch framework callbacks independently of their filters.
|
|
31
32
|
if(!/\.[cm]ts$/i.test(args.path))return;
|
|
32
33
|
const source=await readSource(args.path,args.prnextSource);
|
|
33
34
|
const original=args.prnextSource===undefined?source:await readSource(args.path);
|
|
35
|
+
let nativeDecorators=false;
|
|
34
36
|
// A loader may remove a directive as well as introduce one. Both versions
|
|
35
37
|
// must be safe before its output can become an ordinary browser module.
|
|
36
38
|
for(const candidate of new Set([original,source])){
|
|
37
|
-
const unsupported=cachedTransform('unsupported-framework-directives',candidate,[args.path,'
|
|
39
|
+
const unsupported=cachedTransform('unsupported-framework-directives',candidate,[args.path,'v4-native-decorators'],()=>{
|
|
38
40
|
const analysis=analyzeSource(candidate,args.path);
|
|
39
41
|
if(analysis.frameworkBoundary)return 'yes';
|
|
40
|
-
const pending=[analysis.program];
|
|
42
|
+
const pending=[analysis.program];let decorators=false;
|
|
41
43
|
while(pending.length){
|
|
42
44
|
const node=pending.pop();
|
|
43
45
|
if(node.type==='ExpressionStatement'&&node.directive==='use server')return 'yes';
|
|
46
|
+
if(node.type==='Decorator'||node.decorators?.length)decorators=true;
|
|
44
47
|
for(const value of Object.values(node)){
|
|
45
48
|
if(Array.isArray(value)){for(const item of value)if(item?.type)pending.push(item);}
|
|
46
49
|
else if(value&&typeof value==='object'&&value.type)pending.push(value);
|
|
47
50
|
}
|
|
48
51
|
}
|
|
49
|
-
return 'no';
|
|
52
|
+
return decorators?'decorated':'no';
|
|
50
53
|
});
|
|
51
54
|
if(unsupported==='yes')throw new Error(`React server/client directives are not supported in ${args.path}. Rename this file to .ts or .tsx.`);
|
|
55
|
+
nativeDecorators ||= unsupported==='decorated';
|
|
52
56
|
}
|
|
57
|
+
// Compiler-enabled render graphs can transform ordinary typed helpers after
|
|
58
|
+
// this boundary check. Other consumers keep the historical esbuild path.
|
|
59
|
+
// Decorated typed helpers already have a native esbuild implementation.
|
|
60
|
+
// Keep that path instead of widening the syntax accepted by every Babel
|
|
61
|
+
// transform, while their importing React components can still be compiled.
|
|
62
|
+
if(transformTypedHelpers && !nativeDecorators)return;
|
|
53
63
|
return {contents:source,loader:args.prnextSource===undefined?'ts':'tsx',resolveDir:dirname(args.path)};
|
|
54
64
|
});
|
|
55
65
|
builder.onResolve({filter:/^(?:prnext|@thomas\.f\/prnext)(?:\/route(?:\.js)?)?$/},()=>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import {assertHtmlOnlyDocument} from '../runtime/html-only.mjs';
|
|
2
|
+
const MAX_PATTERNS = 128;
|
|
3
|
+
|
|
4
|
+
export function validateHtmlOnly(value) {
|
|
5
|
+
if (value === undefined) return [];
|
|
6
|
+
if (!Array.isArray(value) || value.length > MAX_PATTERNS) throw new TypeError('htmlOnly must be an array of at most 128 App route patterns.');
|
|
7
|
+
for (const pattern of value) {
|
|
8
|
+
if (typeof pattern !== 'string' || pattern.length > 512 || !pattern.startsWith('/') || pattern.startsWith('//') ||
|
|
9
|
+
/[\s\\?#\u0000-\u001f\u007f]/.test(pattern) || /%(?![\da-f]{2})/i.test(pattern)) throw new TypeError('htmlOnly entries must be clean absolute route patterns without queries, fragments, whitespace or backslashes.');
|
|
10
|
+
const pathname = pattern.endsWith('/**') ? pattern.slice(0, -3) || '/' : pattern;
|
|
11
|
+
if (pathname.includes('*') || (pathname !== '/' && pathname.endsWith('/')) || pathname.slice(1).includes('//')) throw new TypeError('htmlOnly supports exact registered route patterns and a final /** subtree suffix only.');
|
|
12
|
+
for (const segment of pathname.split('/').slice(1)) {
|
|
13
|
+
let decoded;
|
|
14
|
+
try { decoded = decodeURIComponent(segment); } catch { throw new TypeError('htmlOnly patterns must use valid URL encoding.'); }
|
|
15
|
+
if (decoded === '.' || decoded === '..' || /[\\/\u0000-\u001f\u007f]/.test(decoded)) throw new TypeError('htmlOnly patterns cannot contain dot segments or encoded path separators.');
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return [...new Set(value)];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function matchesHtmlOnly(pattern, routePattern) {
|
|
22
|
+
if (!pattern.endsWith('/**')) return pattern === routePattern;
|
|
23
|
+
const prefix = pattern.slice(0, -3);
|
|
24
|
+
return !prefix || routePattern === prefix || routePattern.startsWith(prefix + '/');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function selectHtmlOnlyRoutes(routes, patterns, { cacheComponents = false } = {}) {
|
|
28
|
+
const selected = new Set();
|
|
29
|
+
for (const pattern of validateHtmlOnly(patterns)) {
|
|
30
|
+
const matches = routes.filter(route => !route.internal && matchesHtmlOnly(pattern, route.pattern));
|
|
31
|
+
if (!matches.length) throw new Error(`htmlOnly pattern ${JSON.stringify(pattern)} does not match a registered route.`);
|
|
32
|
+
for (const route of matches) {
|
|
33
|
+
if (route.router !== 'app' || route.kind !== 'page') throw new Error(`htmlOnly route ${route.pattern} must be an App Router page; Pages Router and Route Handlers are not supported.`);
|
|
34
|
+
const config = route.cacheConfig || {};
|
|
35
|
+
if (cacheComponents || config.runtime === 'edge' || config.dynamic === 'force-dynamic' || config.forceNoStore || (config.revalidate !== undefined && config.revalidate !== false)) {
|
|
36
|
+
throw new Error(`htmlOnly route ${route.pattern} must be fully prerendered without Edge, Cache Components/PPR, dynamic rendering or ISR.`);
|
|
37
|
+
}
|
|
38
|
+
if (route.parallel || route.interception) throw new Error(`htmlOnly route ${route.pattern} does not support parallel or intercepted routing.`);
|
|
39
|
+
if (route.pattern.includes('[') && config.dynamicParams !== false) throw new Error(`htmlOnly route ${route.pattern} requires dynamicParams: false and complete generateStaticParams results.`);
|
|
40
|
+
selected.add(route.id);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return selected;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function validateHtmlOnlyPrerender(route, rendered) {
|
|
47
|
+
if (!route.htmlOnly) return;
|
|
48
|
+
if (rendered.dynamic || rendered.partial || rendered.revalidate !== false || (rendered.status ?? 200) !== 200) {
|
|
49
|
+
throw new Error(`htmlOnly route ${route.pattern} must produce a complete successful static HTML document without dynamic data, partial rendering, redirects or ISR.`);
|
|
50
|
+
}
|
|
51
|
+
const html = Buffer.isBuffer(rendered.body) ? rendered.body.toString('utf8') : rendered.body;
|
|
52
|
+
assertHtmlOnlyDocument(html, route.pattern);
|
|
53
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { builtinModules } from 'node:module';
|
|
3
|
+
import { analyzeSource } from './source-analysis.mjs';
|
|
4
|
+
import { frameworkImportName, frameworkImportPattern } from './framework-imports.mjs';
|
|
5
|
+
|
|
6
|
+
const builtins = new Set([...builtinModules, ...builtinModules.map(name => 'node:' + name)]);
|
|
7
|
+
const interactive = new Set(['link','image','head','script','dynamic','router','compat/router','next-router-context','next-app-router-context']);
|
|
8
|
+
const namespace = /^prnext-(?:css-mapping|compiled-css|app-empty|edge-empty):/;
|
|
9
|
+
|
|
10
|
+
export function createHtmlOnlyGraph(root) {
|
|
11
|
+
const graph = new Map(), issues = new Map();
|
|
12
|
+
const file = value => path.resolve(root, value.replace(namespace, ''));
|
|
13
|
+
function issue(filename, reason) {
|
|
14
|
+
const key = file(filename);
|
|
15
|
+
if (!issues.has(key)) issues.set(key, reason);
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
issue,
|
|
19
|
+
alias(source, target) {
|
|
20
|
+
if (file(source) !== file(target)) graph.set(file(source), [{ path:file(target) }]);
|
|
21
|
+
},
|
|
22
|
+
source(filename, source) {
|
|
23
|
+
const { program, directives, computedRequire, imports } = analyzeSource(source, filename);
|
|
24
|
+
if (directives.includes('use client')) issue(filename, 'a Client Component (use client)');
|
|
25
|
+
if (computedRequire) issue(filename, 'a computed require() whose dependencies cannot be validated');
|
|
26
|
+
for (const dependency of imports) if (frameworkImportPattern.test(dependency.path) && interactive.has(frameworkImportName(dependency.path))) issue(filename, `interactive framework import ${dependency.path}; use ordinary HTML elements instead`);
|
|
27
|
+
const pending = [program];
|
|
28
|
+
while (pending.length) {
|
|
29
|
+
const node = pending.pop();
|
|
30
|
+
if (node.directive === 'use server') issue(filename, 'a Server Action (use server)');
|
|
31
|
+
if (node.type === 'ImportExpression' && typeof node.source?.value !== 'string') issue(filename, 'a computed import() whose dependencies cannot be validated');
|
|
32
|
+
if (node.type === 'JSXOpeningElement' && node.name?.type === 'JSXIdentifier' && ['script','iframe','object','embed'].includes(node.name.name.toLowerCase())) issue(filename, `a <${node.name.name}> element`);
|
|
33
|
+
if (node.type === 'JSXAttribute' && node.name?.type === 'JSXIdentifier' && /^on[A-Z]/.test(node.name.name)) issue(filename, `interactive event property ${node.name.name}`);
|
|
34
|
+
for (const value of Object.values(node)) {
|
|
35
|
+
if (Array.isArray(value)) { for (const child of value) if (child?.type) pending.push(child); }
|
|
36
|
+
else if (value && typeof value === 'object' && value.type) pending.push(value);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
record(metafile) {
|
|
41
|
+
for (const [name, input] of Object.entries(metafile?.inputs || {})) {
|
|
42
|
+
const key=file(name), dependencies=graph.get(key) || [];
|
|
43
|
+
for (const dependency of input.imports || []) {
|
|
44
|
+
if (!dependency.external) dependencies.push({path:file(dependency.path)});
|
|
45
|
+
else if (!builtins.has(dependency.path) && !/^(?:react|react-dom|react-server-dom-webpack)(?:\/|$)/.test(dependency.path) && !/^\.\.\/compat\/(?:navigation|headers|cache|server|font-local|font-google|og)\.cjs$/.test(dependency.path)) {
|
|
46
|
+
dependencies.push({external:dependency.path});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
graph.set(key,dependencies);
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
validate(route) {
|
|
53
|
+
const roots = new Set([route.file, ...Object.values(route.segments || {}).flatMap(segment => Object.values(segment).filter(value => typeof value === 'string' && /\.[cm]?[jt]sx?$/.test(value))), ...(route.metadataFiles || []).map(item => item.file)].filter(Boolean));
|
|
54
|
+
const pending=[...roots].map(file), seen=new Set();
|
|
55
|
+
while (pending.length) {
|
|
56
|
+
const current=pending.pop();
|
|
57
|
+
if (seen.has(current)) continue;
|
|
58
|
+
seen.add(current);
|
|
59
|
+
if (issues.has(current)) throw new Error(`htmlOnly route ${route.pattern} reaches ${issues.get(current)} in ${path.relative(root,current)}.`);
|
|
60
|
+
for (const dependency of graph.get(current) || []) {
|
|
61
|
+
if (dependency.external) throw new Error(`htmlOnly route ${route.pattern} reaches unverified external dependency ${dependency.external} from ${path.relative(root,current)}. Bundle a static server dependency or remove it from this HTML-only route.`);
|
|
62
|
+
pending.push(dependency.path);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
package/build/index.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import {compileInstrumentation} from './instrumentation.mjs';
|
|
1
2
|
import {prerenderPages as prerender} from './prerender-pages.mjs';
|
|
2
|
-
import {isolatedPrerender} from './production-builder.mjs';
|
|
3
|
+
import {isolatedPrerender,prerenderProduction} from './production-builder.mjs';
|
|
3
4
|
import { rename } from '../runtime/fs.mjs';
|
|
4
5
|
import { writeRuntimeContract } from './runtime-contract.mjs';
|
|
5
6
|
import { writeBuildOutput } from './output-contract.mjs';
|
|
@@ -7,11 +8,13 @@ import { bundleRuntime } from './bundle-runtime.mjs';
|
|
|
7
8
|
import {withPersistentTransforms} from './transform-cache.mjs';
|
|
8
9
|
import {applicationEntry, frameworkImportName, frameworkRuntimeImportPattern} from './framework-imports.mjs';
|
|
9
10
|
import { expandLocales, localizedStaticPaths } from './i18n.mjs';
|
|
10
|
-
import { build as bundle, withCompilation, configureCompiler, compilerSource,buildAnalysisRecords,buildAnalysisRoutes,devDependencies,devStylePlans,devStyleDependencies,devModules,readCompilerFile } from './compiler.mjs';
|
|
11
|
+
import { build as bundle, withCompilation, configureCompiler, compilerSource,buildAnalysisRecords,buildAnalysisRoutes,devDependencies,devStylePlans,devStyleDependencies,devModules,readCompilerFile,compilationGraphCacheReport } from './compiler.mjs';
|
|
12
|
+
import {retainDeploymentAssets} from './deployment-assets.mjs';
|
|
13
|
+
import {selectHtmlOnlyRoutes} from './html-only-config.mjs';
|
|
11
14
|
import {writeBuildAnalysis} from './analyze.mjs';
|
|
12
15
|
import {routeFiles} from './dev-discovery.mjs';
|
|
13
16
|
import {writeTypedRoutes} from './typed-routes.mjs';
|
|
14
|
-
import { mkdir, readFile, writeFile, rm, cp, copyFile, stat, readdir } from 'node:fs/promises';
|
|
17
|
+
import { mkdir, readFile, writeFile, rm, cp, copyFile, stat, readdir, realpath } from 'node:fs/promises';
|
|
15
18
|
import path from 'node:path';
|
|
16
19
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
17
20
|
import { createRequire } from 'node:module';
|
|
@@ -40,20 +43,22 @@ import { createRefreshTransform, createSourceLocationTransform, devSingletonsPlu
|
|
|
40
43
|
import { snapshotEnvConfig } from '../runtime/env.mjs';
|
|
41
44
|
import { validateStaticPaths } from '../runtime/pages-paths.mjs';
|
|
42
45
|
import { projectTsconfig, checkTypeScript } from './typescript.mjs';
|
|
46
|
+
import {createReactCompilerTransform} from './react-compiler.mjs';
|
|
47
|
+
import {writeReactCompilerReport,reactCompilerReport} from './react-compiler-report.mjs';
|
|
43
48
|
export { staticPath } from '../runtime/pages-paths.mjs';
|
|
44
49
|
|
|
45
50
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
46
|
-
const compatibleModules = new Set(['link', 'head', 'router', 'compat/router', 'next-router-context', 'next-app-router-context', 'image', 'cache', 'dynamic', 'error', 'document', 'app', 'script', 'og']);
|
|
47
|
-
const extensions = { '.js': 'jsx', '.jsx': 'jsx', '.mjs': 'jsx', '.cjs': 'jsx', '.ts': 'ts', '.tsx': 'tsx' };
|
|
51
|
+
const compatibleModules = new Set(['link', 'head', 'router', 'compat/router', 'next-router-context', 'next-app-router-context', 'image', 'cache', 'dynamic', 'error', 'document', 'app', 'script', 'og', 'offline', 'redis-cache']);
|
|
52
|
+
const extensions = { '.js': 'jsx', '.jsx': 'jsx', '.mjs': 'jsx', '.cjs': 'jsx', '.ts': 'ts', '.tsx': 'tsx', '.mts': 'ts', '.cts': 'ts' };
|
|
48
53
|
const assetLoaders = Object.fromEntries(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.avif', '.svg', '.ico', '.woff', '.woff2', '.ttf', '.eot'].map(ext => [ext, 'file']));
|
|
49
54
|
|
|
50
55
|
// These framework files have already been enumerated: copyFile avoids cp's
|
|
51
56
|
// recursive traversal/stat work for every file. Bound I/O on large installations.
|
|
52
|
-
async function copyRuntimeFiles(stage) {
|
|
57
|
+
async function copyRuntimeFiles(stage,{development=true}={}) {
|
|
53
58
|
const files = (await Promise.all(['runtime', 'compat'].map(async directory => {
|
|
54
59
|
await mkdir(path.join(stage, directory), { recursive: true });
|
|
55
60
|
return (await readdir(path.join(packageRoot, directory)))
|
|
56
|
-
.filter(name => directory === 'runtime' ? name === 'profiles.json' || name.endsWith('.mjs') && !name.endsWith('.test.mjs') : name.endsWith('.cjs'))
|
|
61
|
+
.filter(name => directory === 'runtime' ? name === 'profiles.json' || development&&name.endsWith('.mjs') && !name.endsWith('.test.mjs') : name.endsWith('.cjs'))
|
|
57
62
|
.map(name => path.join(directory, name));
|
|
58
63
|
}))).flat();
|
|
59
64
|
let next = 0;
|
|
@@ -68,18 +73,18 @@ async function copyRuntimeFiles(stage) {
|
|
|
68
73
|
if (failed) throw failed.reason;
|
|
69
74
|
}
|
|
70
75
|
|
|
71
|
-
function compatibilityPlugin({ browser, pageFiles, projectRoot, documentFile, fonts, refresh, locations }) {
|
|
76
|
+
function compatibilityPlugin({ browser, pageFiles, projectRoot, documentFile, serverOnlyFiles, fonts, refresh, locations, reactCompiler }) {
|
|
72
77
|
return {
|
|
73
78
|
name: 'prnext-pages-compatibility',
|
|
74
79
|
setup(esbuild) {
|
|
75
|
-
applicationEntry(esbuild,readCompilerFile);
|
|
80
|
+
applicationEntry(esbuild,readCompilerFile,{transformTypedHelpers:browser && !!reactCompiler});
|
|
76
81
|
esbuild.onResolve({ filter: /\.(?:css|scss|sass)$/i }, args => args.importer === documentFile
|
|
77
82
|
? { errors: [{ text: 'CSS cannot be imported within pages/_document. Move global styles to pages/_app, or return collected styles from Document.getInitialProps.' }] } : undefined);
|
|
78
83
|
esbuild.onResolve({ filter: frameworkRuntimeImportPattern }, args => {
|
|
79
84
|
const name = frameworkImportName(args.path);
|
|
80
85
|
if (!compatibleModules.has(name)) return { errors: [{ text: `PRNext does not implement ${args.path} yet (imported by ${args.importer}). Supported runtime imports: next/link, next/head, next/router, next/image, next/cache, next/dynamic, next/error, next/document, next/app, next/script.` }] };
|
|
81
86
|
if (browser && name === 'document') return { errors: [{ text: `${args.path} is server-only. Import Document, Html, Head, Main and NextScript from pages/_document, not from a browser page or component.` }] };
|
|
82
|
-
if (browser && ['cache', 'og'].includes(name)) return { errors: [{ text: `${args.path} is server-only and cannot be imported by a browser component.` }] };
|
|
87
|
+
if (browser && ['cache', 'og', 'redis-cache'].includes(name)) return { errors: [{ text: `${args.path} is server-only and cannot be imported by a browser component.` }] };
|
|
83
88
|
const file = name === 'compat/router' ? 'compat-router' : name;
|
|
84
89
|
return browser ? { path: path.join(packageRoot, 'compat', file + '.cjs') } : { path: `../compat/${file}.cjs`, external: true };
|
|
85
90
|
});
|
|
@@ -95,10 +100,15 @@ function compatibilityPlugin({ browser, pageFiles, projectRoot, documentFile, fo
|
|
|
95
100
|
});
|
|
96
101
|
esbuild.onLoad({ filter: /.*/, namespace: 'prnext-empty' }, () => ({ contents: '', loader: 'js' }));
|
|
97
102
|
}
|
|
98
|
-
esbuild.onLoad({ filter: /\.(?:[cm]?
|
|
103
|
+
esbuild.onLoad({ filter: /\.(?:[cm]?[jt]s|[jt]sx)$/, namespace: 'file' }, async args => {
|
|
99
104
|
if (browser && args.path === documentFile) return { errors: [{ text: 'pages/_document is server-only and cannot be imported by a browser page or component.' }] };
|
|
100
105
|
const original=await readCompilerFile(args.path,args.prnextSource);
|
|
101
|
-
|
|
106
|
+
// Pages shares a server graph with API handlers, Document and data
|
|
107
|
+
// loaders. Keep that entire graph original: an uppercase JSX utility
|
|
108
|
+
// may be called normally there, outside a React render. Browser render
|
|
109
|
+
// graphs receive memoization before instrumentation and JSX lowering.
|
|
110
|
+
const compiled=browser && reactCompiler && args.path!==documentFile && !serverOnlyFiles.has(args.path) ? await reactCompiler(original,args.path,{loaderOutput:args.prnextSource!==undefined}) : original;
|
|
111
|
+
const source = await fonts.transform(locations?await locations(compiled,args.path):compiled, args.path);
|
|
102
112
|
assertSupportedSource(source, args.path);
|
|
103
113
|
const dynamicSource = transformDynamicImports(source, args.path, { projectRoot, mode: browser ? 'browser' : 'server' });
|
|
104
114
|
let contents = browser && pageFiles.has(args.path) ? stripServerCode(dynamicSource, args.path) : dynamicSource;
|
|
@@ -183,13 +193,15 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
|
|
|
183
193
|
const customRoutes = await compileCustomRoutes(config);
|
|
184
194
|
const buildId = await generateBuildId(config, { dev });
|
|
185
195
|
const scanOptions={basePath:config.basePath,pageExtensions:config.pageExtensions,i18n:config.i18n};
|
|
186
|
-
let project;
|
|
196
|
+
let project,deferredRoutes=[];
|
|
187
197
|
if(dev && Array.isArray(devRoutes)) {
|
|
188
198
|
const discovery=await scanProject(projectRoot,{...scanOptions,devAppFiles:new Set()});
|
|
189
199
|
project=await scanProject(projectRoot,{...scanOptions,devAppFiles:routeFiles(discovery,devRoutes)});
|
|
190
200
|
await writeTypedRoutes(projectRoot,discovery,config);
|
|
201
|
+
deferredRoutes=discovery.routes.filter(route=>!devRoutes.includes(route.pattern)&&!route.internal&&!route.errorStatus);
|
|
191
202
|
project.routes=project.routes.filter(route=>devRoutes.includes(route.pattern)||route.internal||route.errorStatus);
|
|
192
203
|
} else project=await scanProject(projectRoot,scanOptions);
|
|
204
|
+
if(!dev&&config.htmlOnly.length)selectHtmlOnlyRoutes(project.routes,config.htmlOnly,config);
|
|
193
205
|
if(!dev || !Array.isArray(devRoutes))await writeTypedRoutes(projectRoot,project,config);
|
|
194
206
|
const stage = path.join(project.root, `.prnext-build-${randomUUID()}`);
|
|
195
207
|
const destination = await prepareBuildDirectory(project.root, config.distDir);
|
|
@@ -199,25 +211,32 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
|
|
|
199
211
|
await addBuiltinPagesErrors(project, stage);
|
|
200
212
|
const manifest = { version: 1, dev, buildId, cacheId: randomUUID(), previewModeId: randomUUID().replaceAll('-', ''), previewModeEncryptionKey: randomBytes(32).toString('hex'),
|
|
201
213
|
...(dev && Array.isArray(devRoutes) ? {devOnDemand:true} : {}), ...(dev && devSession ? {devSequence,devSession} : {}), ...(dev ? {devForceRefresh,devAffectedModules,devIndicators:config.devIndicators,...(devTools?{devTools}:{})} : {}),
|
|
202
|
-
config: { i18n: config.i18n, skipMiddlewareUrlNormalize: config.skipMiddlewareUrlNormalize, compress: config.compress, poweredByHeader: config.poweredByHeader, reactStrictMode: config.reactStrictMode, cacheComponents: config.cacheComponents, cacheLife: config.cacheLife, images: config.images, ...publicPaths }, customRoutes, routes: [], prerendered: [],
|
|
214
|
+
config: { i18n: config.i18n, skipMiddlewareUrlNormalize: config.skipMiddlewareUrlNormalize, compress: config.compress, poweredByHeader: config.poweredByHeader, reactStrictMode: config.reactStrictMode, cacheComponents: config.cacheComponents, partialPrefetching: config.partialPrefetching, navigationCache: config.navigationCache, cacheLife: config.cacheLife, htmlOnly:config.htmlOnly, images: config.images, ...publicPaths }, customRoutes, routes: [], prerendered: [],
|
|
203
215
|
...(project.pagesErrors ? { pagesErrors: project.pagesErrors } : {}) };
|
|
204
216
|
// Its build-unique name is known before bundling and avoids a hash cycle
|
|
205
217
|
// between page entry URLs and the manifest that lists those entries.
|
|
218
|
+
if(!dev)manifest.config.deploymentId=manifest.cacheId;
|
|
206
219
|
const pagesManifestUrl = `${publicPaths.assetBase}/pages-manifest-${manifest.cacheId}.json`;
|
|
207
220
|
if (dev) manifest.devClient = `${publicPaths.assetBase}/dev-manifest-${manifest.cacheId}.json`;
|
|
208
221
|
const development = dev ? { buildId, clientManifest: manifest.devClient, onDemand:manifest.devOnDemand===true, sequence:devSequence, session:devSession, indicator:config.devIndicators,...(devTools?{tools:devTools}:{}) } : undefined;
|
|
209
222
|
const refresh = dev ? createRefreshTransform(project.root, {generation:manifest.cacheId,forceGeneration:devForceRefresh,forceFiles:devAffectedModules}) : undefined;
|
|
210
223
|
const locations = dev ? createSourceLocationTransform(project.root) : undefined;
|
|
224
|
+
const reactCompiler = config.reactCompiler ? createReactCompilerTransform(project.root, config.reactCompiler, {dev}) : undefined;
|
|
225
|
+
const serverOnlyFiles = new Set();
|
|
226
|
+
if (reactCompiler) for (const file of [...project.routes.filter(route=>route.kind==='api').map(route=>route.file),...(project.document?[project.document]:[])]) {
|
|
227
|
+
serverOnlyFiles.add(file);serverOnlyFiles.add(await realpath(file));
|
|
228
|
+
}
|
|
211
229
|
const fonts = createFonts({ projectRoot: project.root, stage, assetBase: publicPaths.assetBase });
|
|
212
|
-
const cssModules = createCssModules({ projectRoot: project.root, stage, assetBase: publicPaths.assetBase, sassOptions: config.sassOptions, cssEngine:config.cssEngine });
|
|
230
|
+
const cssModules = createCssModules({ projectRoot: project.root, stage, assetBase: publicPaths.assetBase, sassOptions: config.sassOptions, cssEngine:config.cssEngine,dev,config });
|
|
213
231
|
configureCompiler({config,root:project.root,stage,buildId,dev,incremental,devInputs,tsconfig,assetBase:publicPaths.assetBase});
|
|
214
232
|
try {
|
|
215
|
-
await copyRuntimeFiles(stage);
|
|
233
|
+
await copyRuntimeFiles(stage,{development:dev});
|
|
216
234
|
// Runtime copies must work even when dotenv is nested beneath the PRNext
|
|
217
235
|
// package instead of hoisted beside the application's dependencies.
|
|
218
236
|
await bundle({ entryPoints: [path.join(packageRoot, 'runtime/env.mjs')], outfile: path.join(stage, 'runtime/env.mjs'), bundle: true, platform: 'node', format: 'esm', target: 'node22', logLevel: 'silent', banner: { js: "import {createRequire as __prnextCreateRequire} from 'node:module';const require=__prnextCreateRequire(import.meta.url);" } });
|
|
219
237
|
await prepareImageResponse(stage);
|
|
220
238
|
manifest.config.serverActions = config.experimental.serverActions;
|
|
239
|
+
if(config.workerMemory && !dev)manifest.config.workerMemory=config.workerMemory;
|
|
221
240
|
if (config.workerRetirement) manifest.config.workerRetirement = config.workerRetirement;
|
|
222
241
|
if(config.workerPreload&&!dev)manifest.config.workerPreload=config.workerPreload;
|
|
223
242
|
const cacheHandlers = await prepareCacheHandlers({ config, projectRoot: project.root, stage, environment });
|
|
@@ -236,7 +255,7 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
|
|
|
236
255
|
serverEntries[route.id] = serverEntry;
|
|
237
256
|
if (route.kind === 'page') {
|
|
238
257
|
const clientEntry = path.join(stage, '.entries', route.id + '.client.jsx');
|
|
239
|
-
await writeFile(clientEntry, browserEntry(route, project.app, pagesManifestUrl, { ...publicPaths, strictMode: config.reactStrictMode === true }, development));
|
|
258
|
+
await writeFile(clientEntry, browserEntry(route, project.app, pagesManifestUrl, { ...publicPaths, deploymentId:dev?undefined:manifest.cacheId, strictMode: config.reactStrictMode === true }, development));
|
|
240
259
|
clientEntries[route.id] = clientEntry;
|
|
241
260
|
}
|
|
242
261
|
manifest.routes.push({ id: route.id, pattern: route.pattern, kind: route.kind, module: `server/${route.id}.mjs`, css: [],
|
|
@@ -245,7 +264,7 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
|
|
|
245
264
|
}
|
|
246
265
|
const common = { absWorkingDir: project.root, bundle: true, logLevel: 'silent', jsx: 'automatic', loader: assetLoaders, assetNames: '[name]-[hash]', publicPath: publicPaths.assetBase, metafile: true, define: { 'process.env.NODE_ENV': JSON.stringify(dev ? 'development' : 'production'), ...environment } };
|
|
247
266
|
if (Object.keys(serverEntries).length) {
|
|
248
|
-
const server = await bundle({ ...common, entryPoints: serverEntries, outdir: path.join(stage, 'server'), outExtension: { '.js': '.mjs' }, platform: 'node', format: 'esm', splitting: true, chunkNames: 'chunk-[hash]', banner: { js: "import {createRequire as __prnextCreateRequire} from 'node:module';const require=__prnextCreateRequire(import.meta.url);" }, target: 'node22', sourcemap: dev, plugins: [moduleResolutionPlugin(config.turbopack, project.root), images.plugin(), fonts.plugin(), compatibilityPlugin({ browser: false, pageFiles, projectRoot: project.root, documentFile: project.document, fonts, locations }), npmPackagesPlugin({ pages: true, projectRoot: project.root, transpilePackages: config.transpilePackages, serverExternalPackages: config.serverExternalPackages }), cssModules.plugin(false)] });
|
|
267
|
+
const server = await bundle({ ...common, entryPoints: serverEntries, outdir: path.join(stage, 'server'), outExtension: { '.js': '.mjs' }, platform: 'node', format: 'esm', splitting: true, chunkNames: 'chunk-[hash]', banner: { js: "import {createRequire as __prnextCreateRequire} from 'node:module';const require=__prnextCreateRequire(import.meta.url);" }, target: 'node22', sourcemap: dev, plugins: [moduleResolutionPlugin(config.turbopack, project.root), images.plugin(), fonts.plugin(), compatibilityPlugin({ browser: false, pageFiles, projectRoot: project.root, documentFile: project.document, serverOnlyFiles, fonts, locations, reactCompiler }), npmPackagesPlugin({ pages: true, projectRoot: project.root, transpilePackages: config.transpilePackages, serverExternalPackages: config.serverExternalPackages }), cssModules.plugin(false)] });
|
|
249
268
|
fonts.record(server.metafile);
|
|
250
269
|
const serverChunks = new Set(Object.keys(server.metafile.outputs).filter(file => file.endsWith('.mjs')).map(file => path.basename(file)));
|
|
251
270
|
for (const [output, metadata] of Object.entries(server.metafile.outputs)) {
|
|
@@ -260,7 +279,7 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
|
|
|
260
279
|
}
|
|
261
280
|
}
|
|
262
281
|
if (Object.keys(clientEntries).length) {
|
|
263
|
-
const browser = await bundle({ ...common, entryPoints: clientEntries, outdir: path.join(stage, 'assets'), entryNames: '[name]-[hash]', chunkNames: 'chunk-[hash]', platform: 'browser', format: 'esm', target: ['es2022'], splitting: true, minify: !dev, sourcemap: dev || config.productionBrowserSourceMaps, define: { 'process.env': '{}', ...common.define }, plugins: [moduleResolutionPlugin(config.turbopack, project.root), ...(dev ? [devSingletonsPlugin()] : []), images.plugin(), fonts.plugin(), compatibilityPlugin({ browser: true, pageFiles, projectRoot: project.root, documentFile: project.document, fonts, refresh, locations }), cssModules.plugin(true)] });
|
|
282
|
+
const browser = await bundle({ ...common, entryPoints: clientEntries, outdir: path.join(stage, 'assets'), entryNames: '[name]-[hash]', chunkNames: 'chunk-[hash]', platform: 'browser', format: 'esm', target: ['es2022'], splitting: true, minify: !dev, sourcemap: dev || config.productionBrowserSourceMaps, define: { 'process.env': '{}', ...common.define }, plugins: [moduleResolutionPlugin(config.turbopack, project.root), ...(dev ? [devSingletonsPlugin()] : []), images.plugin(), fonts.plugin(), compatibilityPlugin({ browser: true, pageFiles, projectRoot: project.root, documentFile: project.document, serverOnlyFiles, fonts, refresh, locations, reactCompiler }), cssModules.plugin(true)] });
|
|
264
283
|
fonts.record(browser.metafile);
|
|
265
284
|
const byEntry = new Map(Object.entries(clientEntries).map(([id, file]) => [path.resolve(file), id]));
|
|
266
285
|
for (const [output, metadata] of Object.entries(browser.metafile.outputs)) {
|
|
@@ -277,27 +296,35 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
|
|
|
277
296
|
await compileMiddleware({ project, stage, manifest, dev, moduleResolution: config.turbopack, defineEnvironment: environment });
|
|
278
297
|
if (manifest.middleware) manifest.middleware.bodySizeLimit = config.experimental.proxyClientMaxBodySize;
|
|
279
298
|
await prepareMetadataRoutes(project, stage);
|
|
280
|
-
await compileApp({ project, stage, manifest, moduleResolution: config.turbopack, cssModules, fonts, images, dev, defineEnvironment: environment, productionBrowserSourceMaps: config.productionBrowserSourceMaps, serverExternalPackages: config.serverExternalPackages });
|
|
299
|
+
await compileApp({ project, stage, manifest, reactCompiler: config.reactCompiler, moduleResolution: config.turbopack, cssModules, fonts, images, dev, defineEnvironment: environment, productionBrowserSourceMaps: config.productionBrowserSourceMaps, serverExternalPackages: config.serverExternalPackages });
|
|
281
300
|
if(manifest.config.workerPreload)for(const pattern of config.workerPreload.routes){
|
|
282
301
|
const selected=manifest.routes.find(route=>!route.internal&&route.pattern===pattern);
|
|
283
302
|
if(!selected||selected.cacheConfig?.runtime==='edge'||selected.handlerConfig?.runtime==='edge')throw new Error(`workerPreload route ${pattern} must be an existing Node route.`);
|
|
284
303
|
}
|
|
304
|
+
await compileInstrumentation({root:project.root,stage,manifest,config,dev,environment});
|
|
305
|
+
if(deferredRoutes.length){
|
|
306
|
+
for(const route of deferredRoutes){
|
|
307
|
+
const module=`server/dev-deferred-${route.id}.mjs`;await writeFile(path.join(stage,module),'export {};\n');
|
|
308
|
+
manifest.routes.push({id:route.id,pattern:route.pattern,kind:route.kind,router:route.router,module,devDeferred:true,css:[],cacheConfig:{maxDuration:120}});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
285
311
|
await checkTypeScript(project.root, config, { dev, tsconfig });
|
|
286
312
|
fonts.attach(manifest);
|
|
287
313
|
expandLocales(manifest);
|
|
288
314
|
// All runtime preparation must finish before its shared modules are built;
|
|
289
315
|
// no staged renderer has been imported before this production-only point.
|
|
290
|
-
if (!dev) await bundleRuntime(stage,{projectRoot:project.root});
|
|
316
|
+
if (!dev) await bundleRuntime(stage,{projectRoot:project.root,sourceDirectory:path.join(packageRoot,'runtime')});
|
|
291
317
|
// Node caches CJS React by filename, not NODE_ENV. Once development metadata
|
|
292
318
|
// imports load it, subsequent production renders need a clean process too.
|
|
293
319
|
if(dev) developmentModulesLoaded = true;
|
|
294
|
-
if(!dev
|
|
320
|
+
if(!dev) Object.assign(manifest,await (incremental || developmentModulesLoaded ? isolatedPrerender : prerenderProduction)(project.root,stage,manifest,{prerender:config.prerender}));
|
|
295
321
|
else {await prerender(stage, manifest, dev);await prerenderApp(stage, manifest, dev);}
|
|
296
|
-
const pages = manifest.routes.filter(route => route.kind === 'page' && route.router !== 'app' && !route.internal);
|
|
322
|
+
const pages = manifest.routes.filter(route => route.kind === 'page' && route.router !== 'app' && !route.internal && !route.devDeferred);
|
|
297
323
|
const publicPage = route => ({ ...(route.locale ? {locale:route.locale,originalPattern:route.originalPattern} : {}), id: route.id, pattern: route.pattern, client: route.client, css: route.css, ...(route.fonts ? { fonts: route.fonts } : {}), ssg: !!route.ssg, ssp: !!route.ssp, gip: !!route.gip, appGip: !!route.appGip });
|
|
298
324
|
if (pages.length || manifest.pagesErrors) {
|
|
299
325
|
const publicPages = {
|
|
300
326
|
buildId: manifest.buildId,
|
|
327
|
+
...(manifest.config.deploymentId?{deploymentId:manifest.config.deploymentId}:{}),
|
|
301
328
|
...publicPaths,
|
|
302
329
|
// Header rules cannot select a different page. Other server routing
|
|
303
330
|
// stays authoritative, including middleware with conditional behavior.
|
|
@@ -327,10 +354,16 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
|
|
|
327
354
|
const appRoute = manifest.routes.find(route => route.router === 'app' && route.kind === 'page' && route.client);
|
|
328
355
|
await writeFile(path.join(stage, 'assets', path.basename(manifest.devClient)), JSON.stringify({ buildId,
|
|
329
356
|
pages: manifest.routes.filter(route => route.router !== 'app' && route.client).map(publicPage),
|
|
330
|
-
...(appRoute ? { app: { client: appRoute.client, css: appRoute.css } } : {}),
|
|
357
|
+
...(appRoute ? { app: { client: appRoute.client, css: appRoute.css, routes: manifest.routes.filter(route=>route.router==='app'&&route.kind==='page'&&route.client).map(({id,pattern,css,cssScopes})=>({id,pattern,css,cssScopes:cssScopes||[]})) } } : {}),
|
|
331
358
|
}));
|
|
332
359
|
}
|
|
333
|
-
|
|
360
|
+
// A report belongs to this successful staging generation, never to the
|
|
361
|
+
// runtime manifest or public assets. Publishing the stage is atomic below.
|
|
362
|
+
if(!dev)await retainDeploymentAssets({stage,destination,manifest,policy:config.deploymentRetention});
|
|
363
|
+
const compilationCache=compilationGraphCacheReport();
|
|
364
|
+
if(!dev)await writeFile(path.join(stage,'compilation-cache-report.json'),JSON.stringify(compilationCache,null,2)+'\n');
|
|
365
|
+
const compilerReport=await writeReactCompilerReport(stage,reactCompilerReport());
|
|
366
|
+
await writeRuntimeContract(stage,{deployment:!dev,htmlOnly:!dev&&manifest.routes.some(route=>route.htmlOnly),workerMemory:Boolean(manifest.config.workerMemory),incrementalCache:Boolean(manifest.config.cacheHandler)});
|
|
334
367
|
await writeFile(path.join(stage, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n');
|
|
335
368
|
await writeBuildOutput(stage, manifest);
|
|
336
369
|
// Offline compression is a deployment optimization. Recompressing source
|
|
@@ -360,7 +393,7 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
|
|
|
360
393
|
} finally { await rm(pointerTemporary, {force:true}); }
|
|
361
394
|
await exported?.commit();
|
|
362
395
|
if (hadDestination) await rm(backup, { recursive: true, force: true });
|
|
363
|
-
return { ...manifest, outputDirectory: destination, durationMs: Math.round(performance.now() - started) };
|
|
396
|
+
return { ...manifest, outputDirectory: destination, durationMs: Math.round(performance.now() - started),reactCompilerReport:compilerReport,compilationCache };
|
|
364
397
|
} catch (error) {
|
|
365
398
|
await rm(stage, { recursive: true, force: true });
|
|
366
399
|
if (error.errors) error.message = error.errors.map(item => `${item.location ? `${item.location.file}:${item.location.line}: ` : ''}${item.text}`).join('\n');
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import {access,writeFile,mkdir} from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {build} from './compiler.mjs';
|
|
4
|
+
import {compileEdge} from './edge.mjs';
|
|
5
|
+
import {moduleResolutionPlugin} from './module-resolution.mjs';
|
|
6
|
+
|
|
7
|
+
export async function compileInstrumentation({root,stage,manifest,config,dev,environment}) {
|
|
8
|
+
const found=[];
|
|
9
|
+
for(const directory of ['', 'src'])for(const extension of ['ts','js','mjs','cjs']){
|
|
10
|
+
const file=path.join(root,directory,'instrumentation.'+extension);
|
|
11
|
+
try{await access(file);found.push(file);}catch(error){if(error.code!=='ENOENT')throw error;}
|
|
12
|
+
}
|
|
13
|
+
if(!found.length)return;
|
|
14
|
+
if(found.length>1)throw Error('Only one instrumentation file is allowed, in the project root or src directory.');
|
|
15
|
+
const file=found[0],entry=path.join(stage,'.entries/instrumentation.mjs');manifest.instrumentation={};
|
|
16
|
+
await mkdir(path.dirname(entry),{recursive:true});
|
|
17
|
+
await writeFile(entry,`import * as hooks from ${JSON.stringify(file)};export const register=hooks.register??hooks.default?.register;export const onRequestError=hooks.onRequestError??hooks.default?.onRequestError;`);
|
|
18
|
+
for(const runtime of ['node','rsc']){
|
|
19
|
+
const module=`server/instrumentation-${runtime}.mjs`;
|
|
20
|
+
await build({absWorkingDir:root,entryPoints:[entry],outfile:path.join(stage,module),bundle:true,packages:'external',platform:'node',format:'esm',target:'node22',banner:{js:"import {createRequire as __prnextCreateRequire} from 'node:module';const require=__prnextCreateRequire(import.meta.url);"},conditions:runtime==='rsc'?['react-server']:[],sourcemap:dev,logLevel:'silent',
|
|
21
|
+
define:{...environment,'process.env.NODE_ENV':JSON.stringify(dev?'development':'production'),'process.env.NEXT_RUNTIME':'"nodejs"'},
|
|
22
|
+
plugins:[moduleResolutionPlugin(config.turbopack,root)]});
|
|
23
|
+
manifest.instrumentation[runtime]=module;
|
|
24
|
+
}
|
|
25
|
+
if(manifest.middleware?.runtime==='edge'||manifest.routes.some(route=>route.cacheConfig?.runtime==='edge'||route.handlerConfig?.runtime==='edge')){
|
|
26
|
+
const module='server/instrumentation-edge.mjs';
|
|
27
|
+
await compileEdge({file:entry,outfile:path.join(stage,module),projectRoot:root,dev,defineEnvironment:environment});
|
|
28
|
+
manifest.instrumentation.edge=module;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -60,7 +60,7 @@ export async function prepareMetadataRoutes(project, stage) {
|
|
|
60
60
|
let source;
|
|
61
61
|
if (dynamic) {
|
|
62
62
|
const config = await validateAppConfig(route.file);
|
|
63
|
-
source = `import * as source from ${JSON.stringify(route.file)};import {metadataHandler,metadataStaticParams} from '
|
|
63
|
+
source = `import * as source from ${JSON.stringify(route.file)};import {metadataHandler,metadataStaticParams} from 'prnext-internal:metadata-route';export * from ${JSON.stringify(route.file)};\n`;
|
|
64
64
|
if (config.revalidate === undefined) source += 'export const revalidate=false;\n';
|
|
65
65
|
const options = JSON.stringify({ kind, image, multiple, contentType });
|
|
66
66
|
source += `export const GET=metadataHandler(source,${options});\n`;
|
|
@@ -6,7 +6,7 @@ import { checkProject } from './check.mjs';
|
|
|
6
6
|
import { frameworkImportName } from './framework-imports.mjs';
|
|
7
7
|
import { packageManifest } from '../native/resolve.mjs';
|
|
8
8
|
|
|
9
|
-
const supported = new Set([...Object.keys(packageManifest.exports).map(name => name.slice(2)), 'font/google', 'font/local', 'next-router-context', 'next-app-router-context']);
|
|
9
|
+
const supported = new Set([...Object.keys(packageManifest.exports).map(name => frameworkImportName(name.slice(2))), 'font/google', 'font/local', 'next-router-context', 'next-app-router-context']);
|
|
10
10
|
const ignored = new Set(['node_modules', '.git', '.next', '.prnext', '.prnext-cache', 'dist', 'build', 'out', 'coverage', 'test-results', '__tests__']);
|
|
11
11
|
const code = /\.(?:[cm]?[jt]s|[jt]sx)$/;
|
|
12
12
|
function walkAst(node, visit) {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import {readFile} from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {fileURLToPath} from 'node:url';
|
|
4
|
+
import {parseTsconfig,createPathsMatcher} from 'get-tsconfig';
|
|
5
|
+
import {parse,modify,applyEdits} from 'jsonc-parser';
|
|
6
|
+
|
|
7
|
+
// TypeScript 7 has no stable legacy compiler API. Parse configuration only with
|
|
8
|
+
// the dedicated JSONC/config resolvers; semantic checking stays in native tsc.
|
|
9
|
+
export async function nativeTypeConfig(filename,helper){
|
|
10
|
+
let source=await readFile(filename,'utf8');const errors=[];
|
|
11
|
+
const own=parse(source,errors,{allowTrailingComma:true});
|
|
12
|
+
if(errors.length||!own||typeof own!=='object'||Array.isArray(own))throw new Error('Cannot configure typed routes: invalid tsconfig.');
|
|
13
|
+
const config=parseTsconfig(filename),directory=path.dirname(filename);
|
|
14
|
+
const format={insertSpaces:true,tabSize:2,eol:source.includes('\r\n')?'\r\n':'\n'};
|
|
15
|
+
const update=(location,value)=>{source=applyEdits(source,modify(source,location,value,{formattingOptions:format}));};
|
|
16
|
+
const files=own.files??config.files??[];
|
|
17
|
+
if(!Array.isArray(files)||files.some(file=>typeof file!=='string'))throw new Error('tsconfig.files must be an array.');
|
|
18
|
+
if(!files.some(file=>path.resolve(directory,file)===helper))update(['files'],[...files,path.relative(directory,helper).replaceAll(path.sep,'/')]);
|
|
19
|
+
const options=config.compilerOptions||{},paths=options.paths||{};
|
|
20
|
+
const missing=['next','next/*'].filter(name=>!Object.hasOwn(paths,name));
|
|
21
|
+
if(!missing.length)return source;
|
|
22
|
+
const base=options.baseUrl?path.resolve(directory,options.baseUrl):directory;
|
|
23
|
+
const relative=filename=>{const value=path.relative(base,filename).replaceAll(path.sep,'/');return value.startsWith('.')?value:'./'+value;};
|
|
24
|
+
const resolved=own.compilerOptions?.paths?{...paths}:{};
|
|
25
|
+
if(!own.compilerOptions?.paths){
|
|
26
|
+
const matcher=createPathsMatcher({path:filename,config});
|
|
27
|
+
for(const pattern of Object.keys(paths)){
|
|
28
|
+
const probe='__prnext_type_path__',values=matcher?.(pattern.replace('*',probe));
|
|
29
|
+
if(!values?.length)throw new Error('Cannot resolve inherited TypeScript path: '+pattern);
|
|
30
|
+
resolved[pattern]=values.map(value=>relative(value).replaceAll(probe,'*'));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const compat=fileURLToPath(new URL('../compat/',import.meta.url));
|
|
34
|
+
for(const name of missing)resolved[name]=[relative(path.join(compat,name==='next'?'index.d.ts':'*.d.cts'))];
|
|
35
|
+
update(['compilerOptions','paths'],resolved);
|
|
36
|
+
return source;
|
|
37
|
+
}
|
|
@@ -17,7 +17,21 @@ async function frameworkHash(){
|
|
|
17
17
|
const hash=createHash('sha256');
|
|
18
18
|
for(const f of files)hash.update(f).update(await readFile(path.join(packageRoot,'build',f)));
|
|
19
19
|
hash.update(await readFile(path.join(packageRoot,'package.json')));
|
|
20
|
-
for(const name of ['@babel/core','@babel/parser','@babel/traverse','@babel/generator','@babel/types','esbuild'])hash.update(name+':'+require(name+'/package.json').version);
|
|
20
|
+
for(const name of ['@babel/core','@babel/parser','@babel/traverse','@babel/generator','@babel/types','babel-plugin-react-compiler','esbuild'])hash.update(name+':'+require(name+'/package.json').version);
|
|
21
|
+
// CSS graph outputs also depend on the installed style engine and browser
|
|
22
|
+
// database, including nested/non-hoisted installations with different pins.
|
|
23
|
+
for(const name of ['lightningcss','browserslist','caniuse-lite','electron-to-chromium','postcss','postcss-value-parser','postcss-flexbugs-fixes','postcss-preset-env']){
|
|
24
|
+
let filename;
|
|
25
|
+
try{filename=require.resolve(name+'/package.json');}
|
|
26
|
+
catch{
|
|
27
|
+
let directory=path.dirname(require.resolve(name));
|
|
28
|
+
while(true){
|
|
29
|
+
try{const candidate=path.join(directory,'package.json'),value=JSON.parse(await readFile(candidate,'utf8'));if(value.name===name){filename=candidate;break;}}catch{}
|
|
30
|
+
const parent=path.dirname(directory);if(parent===directory)throw new Error(`Cannot fingerprint ${name}`);directory=parent;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
hash.update(name).update(await readFile(filename));
|
|
34
|
+
}
|
|
21
35
|
return hash.digest('hex');
|
|
22
36
|
})();
|
|
23
37
|
return frameworkIdentity;
|
|
@@ -78,8 +92,9 @@ export async function readCacheRecord(directory,key,limit=16*1024*1024){
|
|
|
78
92
|
return JSON.parse(record.payload);
|
|
79
93
|
}catch{return null;}finally{await file?.close().catch(()=>{});}
|
|
80
94
|
}
|
|
81
|
-
export async function writeCacheRecord(directory,key,value,limit=16*1024*1024){
|
|
95
|
+
export async function writeCacheRecord(directory,key,value,limit=16*1024*1024,{maxFiles=64}={}){
|
|
82
96
|
if(!directory||!/^[a-f0-9]{64}$/.test(key))return false;
|
|
97
|
+
if(!Number.isInteger(maxFiles)||maxFiles<1||maxFiles>4096)return false;
|
|
83
98
|
const filename=path.join(directory,key+'.json'),temporary=filename+'.'+randomUUID()+'.tmp';
|
|
84
99
|
try{
|
|
85
100
|
const payload=JSON.stringify(value),body=JSON.stringify({version:1,key,checksum:cacheHash(payload),payload});
|
|
@@ -91,7 +106,7 @@ export async function writeCacheRecord(directory,key,value,limit=16*1024*1024){
|
|
|
91
106
|
const stat=await lstat(path.join(directory,name)).catch(()=>null);if(stat?.isFile()&&!stat.isSymbolicLink())records.push({name,size:stat.size,time:stat.mtimeMs});
|
|
92
107
|
}
|
|
93
108
|
records.sort((a,b)=>b.time-a.time);let total=0;
|
|
94
|
-
for(let i=0;i<records.length;i++){total+=records[i].size;if(i>=
|
|
109
|
+
for(let i=0;i<records.length;i++){total+=records[i].size;if(i>=maxFiles||total>MAX_DISK)await unlink(path.join(directory,records[i].name)).catch(()=>{});}
|
|
95
110
|
return true;
|
|
96
111
|
}catch{return false;}finally{await unlink(temporary).catch(()=>{});}
|
|
97
112
|
}
|
package/build/precompress.mjs
CHANGED
|
@@ -20,7 +20,7 @@ async function digest(stream, limit = Infinity) {
|
|
|
20
20
|
// this bounded index and serialize publication/eviction, including overwrites.
|
|
21
21
|
// Cached bytes are still decoded and hashed before reuse below.
|
|
22
22
|
async function compressionCache(directory) {
|
|
23
|
-
if (!directory) return;
|
|
23
|
+
if (!directory || process.env.PRNEXT_BUILD_CACHE === '0') return;
|
|
24
24
|
try {
|
|
25
25
|
await mkdir(directory,{recursive:true});
|
|
26
26
|
const info=await lstat(directory);
|