@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/app-static.mjs
CHANGED
|
@@ -3,6 +3,9 @@ import { writeFile } from 'node:fs/promises';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { pathToFileURL } from 'node:url';
|
|
5
5
|
import { staticPath } from '../runtime/pages-paths.mjs';
|
|
6
|
+
import {mapConcurrent} from './concurrent.mjs';
|
|
7
|
+
import {mapPrerender,prerenderPath} from './prerender-pool.mjs';
|
|
8
|
+
import {validateHtmlOnlyPrerender} from './html-only-config.mjs';
|
|
6
9
|
|
|
7
10
|
const MAX_STATIC_PATHS = 10_000;
|
|
8
11
|
const MAX_RESPONSE_BYTES = 16 * 1024 * 1024;
|
|
@@ -48,7 +51,9 @@ export async function prerenderApp(stage, manifest, dev) {
|
|
|
48
51
|
? await import(pathToFileURL(path.join(stage, 'runtime/app-static.mjs')).href) : null;
|
|
49
52
|
const handlers = routes.some(route => route.kind === 'api')
|
|
50
53
|
? await import(pathToFileURL(path.join(stage, 'runtime/route-static.mjs')).href) : null;
|
|
51
|
-
|
|
54
|
+
const initialSeeds=[...manifest.prerendered];
|
|
55
|
+
const jobs=[],seeds=new Map(),partialRoutes=new Set();
|
|
56
|
+
await mapConcurrent(routes,async(route,routeIndex)=>{
|
|
52
57
|
const handler = route.kind === 'api';
|
|
53
58
|
const options = { modulePath: path.join(stage, route.module), distDir: stage, manifest, route, production: true };
|
|
54
59
|
// This fixed internal entry shares the root layout, but its parameter
|
|
@@ -59,14 +64,22 @@ export async function prerenderApp(stage, manifest, dev) {
|
|
|
59
64
|
if (handler) route.handlerMethods = inspected.methods;
|
|
60
65
|
const dynamic = route.pattern.includes('[');
|
|
61
66
|
const entries = appStaticEntries(route.pattern, dynamic ? inspected.params : [{}]);
|
|
67
|
+
if(route.htmlOnly && dynamic && (!inspected.generated || !entries.length || inspected.params.some(params=>appStaticEntries(route.pattern,[params]).length!==1))) {
|
|
68
|
+
throw new Error(`htmlOnly route ${route.pattern} requires nonempty complete generateStaticParams results.`);
|
|
69
|
+
}
|
|
62
70
|
const config = route.cacheConfig;
|
|
71
|
+
if (config.ensureStatic === 'navigation') {
|
|
72
|
+
if (config.runtime === 'edge' || config.dynamic === 'force-dynamic' || config.revalidate === 0 || config.forceNoStore) throw new Error(`Route ${route.pattern}: ensureStatic='navigation' conflicts with request-time rendering configuration.`);
|
|
73
|
+
if (dynamic && (!inspected.generated || !entries.length || inspected.params.some(params => appStaticEntries(route.pattern, [params]).length !== 1))) throw new Error(`Route ${route.pattern}: ensureStatic='navigation' requires generateStaticParams() with complete, nonempty parameter sets.`);
|
|
74
|
+
}
|
|
63
75
|
// Keep generated-path restrictions and method metadata for Edge routes,
|
|
64
76
|
// while separating dynamic rendering from the persistent Data Cache.
|
|
65
77
|
if (config.runtime === 'edge') {
|
|
66
78
|
if (dynamic && config.dynamicParams === false) route.allowedPaths = entries.map(entry => entry.path);
|
|
67
|
-
|
|
79
|
+
return;
|
|
68
80
|
}
|
|
69
81
|
const partial = !handler && manifest.config?.cacheComponents === true && (!config.dynamic || config.dynamic === 'auto');
|
|
82
|
+
if(partial)partialRoutes.add(route);
|
|
70
83
|
if (partial && dynamic && !inspected.generated) entries.length = 0;
|
|
71
84
|
if (partial && dynamic && inspected.generated && inspected.params.length === 0) {
|
|
72
85
|
throw new Error(`generateStaticParams for ${route.pattern} must return at least one parameter object when cacheComponents is enabled.`);
|
|
@@ -99,17 +112,20 @@ export async function prerenderApp(stage, manifest, dev) {
|
|
|
99
112
|
if (partial) route.pprFallback = config.dynamicParams !== false;
|
|
100
113
|
if (dynamic && config.dynamicParams === false) route.allowedPaths = entries.map(entry => entry.path);
|
|
101
114
|
if (handler) {
|
|
102
|
-
if (!inspected.staticEligible)
|
|
103
|
-
if (dynamic && !inspected.generated && !['force-static', 'error'].includes(config.dynamic))
|
|
115
|
+
if (!inspected.staticEligible) return;
|
|
116
|
+
if (dynamic && !inspected.generated && !['force-static', 'error'].includes(config.dynamic)) return;
|
|
104
117
|
} else {
|
|
105
|
-
if (config.dynamic === 'force-dynamic' || (config.dynamic !== 'force-static' && (config.revalidate === 0 || config.forceNoStore)))
|
|
106
|
-
if (dynamic && !inspected.generated && !partial && !['force-static', 'error'].includes(config.dynamic))
|
|
118
|
+
if (config.dynamic === 'force-dynamic' || (config.dynamic !== 'force-static' && (config.revalidate === 0 || config.forceNoStore))) return;
|
|
119
|
+
if (dynamic && !inspected.generated && !partial && !['force-static', 'error'].includes(config.dynamic)) return;
|
|
107
120
|
}
|
|
108
121
|
route.ssg = true;
|
|
109
122
|
route.fallback = config.dynamicParams === false ? false : 'blocking';
|
|
110
|
-
for (const entry of entries) {
|
|
123
|
+
for (const [entryIndex,entry] of entries.entries()) {
|
|
111
124
|
if (occupied.has(entry.path)) throw new Error(`Multiple pages prerender the same path: ${entry.path}.`);
|
|
112
|
-
|
|
125
|
+
occupied.add(entry.path);
|
|
126
|
+
jobs.push({route,routeIndex,entryIndex,path:entry.path,run:async()=>{
|
|
127
|
+
const rendered = await prerenderPath(handler?'handler':'app',{ ...options, path: entry.path, params: entry.params },handler?handlers.prerenderAppRouteHandler:pages.prerenderAppRoute);
|
|
128
|
+
if(route.htmlOnly)validateHtmlOnlyPrerender(route,rendered);
|
|
113
129
|
if (rendered.partial) {
|
|
114
130
|
const file = `static/partial-${createHash('sha256').update(entry.path).digest('hex').slice(0, 20)}.json`;
|
|
115
131
|
await writeFile(path.join(stage, file), JSON.stringify({ ...rendered.partial, ...rendered.metadata }));
|
|
@@ -117,7 +133,7 @@ export async function prerenderApp(stage, manifest, dev) {
|
|
|
117
133
|
if (!dynamic) { delete route.ssg; delete route.fallback; }
|
|
118
134
|
else (route.dynamicPaths ||= []).push(entry.path);
|
|
119
135
|
occupied.add(entry.path);
|
|
120
|
-
|
|
136
|
+
return;
|
|
121
137
|
}
|
|
122
138
|
if (rendered.dynamic) {
|
|
123
139
|
route.dynamicReasons = [...new Set([...(route.dynamicReasons || []), rendered.reason || 'request-time data'])];
|
|
@@ -129,32 +145,43 @@ export async function prerenderApp(stage, manifest, dev) {
|
|
|
129
145
|
// successful seeds survive, while ungenerated paths stay dynamic.
|
|
130
146
|
if (handler && config.dynamicParams !== false) route.fallback = 'dynamic';
|
|
131
147
|
}
|
|
132
|
-
|
|
148
|
+
return;
|
|
133
149
|
}
|
|
134
150
|
const body = Buffer.isBuffer(rendered.body) ? rendered.body : Buffer.from(rendered.body);
|
|
135
|
-
const flight = handler ? null : Buffer.isBuffer(rendered.flight) ? rendered.flight : Buffer.from(rendered.flight);
|
|
151
|
+
const flight = handler || route.htmlOnly ? null : Buffer.isBuffer(rendered.flight) ? rendered.flight : Buffer.from(rendered.flight);
|
|
136
152
|
if (body.byteLength > MAX_RESPONSE_BYTES || (flight && flight.byteLength > MAX_RESPONSE_BYTES)) throw new Error(`Static App route ${entry.path} exceeds the 16 MiB response limit.`);
|
|
137
153
|
const stem = `static/${handler ? 'route' : 'app'}-${createHash('sha256').update(entry.path).digest('hex').slice(0, 20)}`;
|
|
138
154
|
const file = `${stem}.${handler ? 'body' : 'html'}`;
|
|
139
155
|
// Flight is opaque text, not JSON; the native cache supplies text/x-component.
|
|
140
|
-
const dataFile = handler ? undefined : `${stem}.txt`;
|
|
156
|
+
const dataFile = handler || route.htmlOnly ? undefined : `${stem}.txt`;
|
|
141
157
|
await writeFile(path.join(stage, file), body);
|
|
142
158
|
if (dataFile) await writeFile(path.join(stage, dataFile), flight);
|
|
143
159
|
occupied.add(entry.path);
|
|
144
|
-
|
|
160
|
+
seeds.set(entry.path,{ routeId: route.id, path: entry.path, file, ...(dataFile ? { dataFile } : {}), status: rendered.status ?? 200,
|
|
145
161
|
headers: rendered.headers || {}, revalidate: rendered.revalidate ?? config.revalidate,
|
|
146
162
|
generatedAt: rendered.generatedAt, tags: rendered.tags || [], paths: rendered.paths || [],
|
|
147
163
|
});
|
|
164
|
+
}});
|
|
148
165
|
}
|
|
166
|
+
});
|
|
167
|
+
jobs.sort((a,b)=>a.routeIndex-b.routeIndex||a.entryIndex-b.entryIndex);
|
|
168
|
+
await mapPrerender(jobs,job=>job.run());
|
|
169
|
+
manifest.prerendered=[...initialSeeds,...jobs.flatMap(job=>seeds.has(job.path)?[seeds.get(job.path)]:[])];
|
|
170
|
+
for(const route of routes){
|
|
171
|
+
const routeJobs=jobs.filter(job=>job.route===route);
|
|
172
|
+
if(route.dynamicPaths)route.dynamicPaths=routeJobs.filter(job=>route.dynamicPaths.includes(job.path)).map(job=>job.path);
|
|
173
|
+
if(route.ppr)route.ppr=Object.fromEntries(routeJobs.filter(job=>Object.hasOwn(route.ppr,job.path)).map(job=>[job.path,route.ppr[job.path]]));
|
|
174
|
+
if(route.dynamicReasons)route.dynamicReasons=[...new Set(route.dynamicReasons)].sort();
|
|
149
175
|
// Unseen Cache Components paths are generated by the PPR runtime, which
|
|
150
176
|
// handles either complete HTML/Flight or a resumable shell atomically.
|
|
151
|
-
if (
|
|
177
|
+
if (partialRoutes.has(route) && route.pattern.includes('[')) {
|
|
152
178
|
if (manifest.prerendered.some(page => page.routeId === route.id)) {
|
|
153
179
|
// Concrete complete seeds use the native invalidation-aware cache.
|
|
154
180
|
// Other paths still reach the PPR runtime, including dynamic holes.
|
|
155
181
|
route.ssg = true; route.fallback = 'ppr';
|
|
156
182
|
} else { delete route.ssg; delete route.fallback; }
|
|
157
183
|
}
|
|
184
|
+
if(route.htmlOnly)route.fallback=false;
|
|
158
185
|
}
|
|
159
186
|
} finally { await closeAppRuntime(); }
|
|
160
187
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
const stylesheet = /\.(?:css|scss|sass)$/i;
|
|
4
|
+
const conventions = ['layout', 'template', 'loading', 'error', 'notFound', 'not-found', 'default', 'page'];
|
|
5
|
+
// Native esbuild identifies virtual inputs with absolute paths. The webpack
|
|
6
|
+
// bridge uses the same namespaces with project-relative paths instead. Only
|
|
7
|
+
// strip framework namespaces here; a Windows drive letter is part of the path.
|
|
8
|
+
const styleNamespace = /^prnext-(?:css-mapping|compiled-css|app-empty|edge-empty):/;
|
|
9
|
+
|
|
10
|
+
/** Join RSC references with their real client dependency graphs, without
|
|
11
|
+
* flattening CSS @import/composes dependencies into extra stylesheets. */
|
|
12
|
+
export function createAppStyleGraph(root) {
|
|
13
|
+
const graph = new Map();
|
|
14
|
+
const file = name => path.resolve(root, name.replace(styleNamespace, ''));
|
|
15
|
+
return {
|
|
16
|
+
alias(source, target) {
|
|
17
|
+
if (file(source) !== file(target)) graph.set(file(source), new Set([file(target)]));
|
|
18
|
+
},
|
|
19
|
+
record(metafile) {
|
|
20
|
+
for (const [name, input] of Object.entries(metafile?.inputs || {})) {
|
|
21
|
+
const key = file(name), dependencies = graph.get(key) || new Set();
|
|
22
|
+
for (const dependency of input.imports || []) if (!dependency.external) dependencies.add(file(dependency.path));
|
|
23
|
+
graph.set(key, dependencies);
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
collect(roots) {
|
|
27
|
+
const seen = new Set(), styles = new Set();
|
|
28
|
+
const pending = [...roots].reverse();
|
|
29
|
+
while (pending.length) {
|
|
30
|
+
const key = file(pending.pop());
|
|
31
|
+
if (seen.has(key)) continue;
|
|
32
|
+
seen.add(key);
|
|
33
|
+
if (stylesheet.test(key)) { styles.add(key); continue; }
|
|
34
|
+
const dependencies = [...graph.get(key) || []];
|
|
35
|
+
for (let index = dependencies.length - 1; index >= 0; index--) pending.push(dependencies[index]);
|
|
36
|
+
}
|
|
37
|
+
return [...styles];
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Layout scopes precede their descendants. Shared scopes produce the same
|
|
43
|
+
* content-addressed sheet on every route, while a page never owns a sibling's
|
|
44
|
+
* styles. A pruned parallel tree also includes its loading/error/default and
|
|
45
|
+
* intercepted branches so a retained slot cannot lose its stylesheet. */
|
|
46
|
+
export function appStyleEntries(route) {
|
|
47
|
+
const scopes = [];
|
|
48
|
+
function add(files, path) {
|
|
49
|
+
const roots = conventions.filter(name => name !== 'page' && name !== 'default').map(name => files?.[name]).filter(file => typeof file === 'string');
|
|
50
|
+
if (roots.length) scopes.push({ path, kind:'layout', roots });
|
|
51
|
+
for (const kind of ['default', 'page']) if (typeof files?.[kind] === 'string') scopes.push({ path, kind, roots:[files[kind]] });
|
|
52
|
+
}
|
|
53
|
+
if (route.routing) {
|
|
54
|
+
function visit(node) {
|
|
55
|
+
add(node.files, node.path || '');
|
|
56
|
+
for (const child of node.children || []) visit(child);
|
|
57
|
+
for (const name of Object.keys(node.slots || {}).sort()) visit(node.slots[name]);
|
|
58
|
+
}
|
|
59
|
+
visit(route.routing);
|
|
60
|
+
} else {
|
|
61
|
+
for (const segment of route.segments || []) add(segment, segment.path || '');
|
|
62
|
+
if (route.file) add({ page: route.file }, '');
|
|
63
|
+
}
|
|
64
|
+
return scopes;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const appStyleScopes = route => appStyleEntries(route).map(scope => scope.roots);
|
|
68
|
+
|
|
69
|
+
export function planAppStyleEntries(route, graph) {
|
|
70
|
+
const included = new Set(), groups = [];
|
|
71
|
+
for (const scope of appStyleEntries(route)) {
|
|
72
|
+
const styles = graph.collect(scope.roots).filter(file => !included.has(file));
|
|
73
|
+
for (const file of styles) included.add(file);
|
|
74
|
+
if (styles.length) groups.push({ ...scope, styles });
|
|
75
|
+
}
|
|
76
|
+
return groups;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const planAppStyles = (route, graph) => planAppStyleEntries(route, graph).map(scope => scope.styles);
|
package/build/app.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {frameworkImportName, frameworkRuntimeImportPattern, frameworkSubpathPattern} from './framework-imports.mjs';
|
|
1
|
+
import {applicationEntry, frameworkImportName, frameworkRuntimeImportPattern, frameworkSubpathPattern} from './framework-imports.mjs';
|
|
2
2
|
import { moduleResolutionPlugin } from './module-resolution.mjs';
|
|
3
|
-
import { build as bundle, compilerSource } from './compiler.mjs';
|
|
3
|
+
import { build as bundle, compilerSource,readCompilerFile } from './compiler.mjs';
|
|
4
4
|
import { readFile, writeFile, mkdir, cp, realpath } from 'node:fs/promises';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { createHash } from 'node:crypto';
|
|
@@ -14,16 +14,20 @@ import { createServerActions } from './actions.mjs';
|
|
|
14
14
|
import { expandActionExports } from './action-exports.mjs';
|
|
15
15
|
import { compileEdge } from './edge.mjs';
|
|
16
16
|
import { createNativePackages } from './native-packages.mjs';
|
|
17
|
-
import { createRefreshTransform, devSingletonsPlugin, devClientFile } from './dev.mjs';
|
|
17
|
+
import { createRefreshTransform, createSourceLocationTransform, devSingletonsPlugin, devClientFile } from './dev.mjs';
|
|
18
18
|
import { npmPackagesPlugin } from './npm-packages.mjs';
|
|
19
19
|
import { transformCacheComponents } from './cache-components.mjs';
|
|
20
|
+
import {createReactCompilerTransform} from './react-compiler.mjs';
|
|
21
|
+
import {createAppStyleGraph, appStyleScopes, planAppStyleEntries} from './app-styles.mjs';
|
|
22
|
+
import {selectHtmlOnlyRoutes} from './html-only-config.mjs';
|
|
23
|
+
import {createHtmlOnlyGraph} from './html-only-graph.mjs';
|
|
20
24
|
|
|
21
25
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
22
|
-
const extensions = { '.js': 'jsx', '.jsx': 'jsx', '.mjs': 'jsx', '.cjs': 'jsx', '.ts': 'ts', '.tsx': 'tsx' };
|
|
26
|
+
const extensions = { '.js': 'jsx', '.jsx': 'jsx', '.mjs': 'jsx', '.cjs': 'jsx', '.ts': 'ts', '.tsx': 'tsx', '.mts': 'ts', '.cts': 'ts' };
|
|
23
27
|
const assetLoaders = Object.fromEntries(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.avif', '.svg', '.ico', '.woff', '.woff2', '.ttf', '.eot'].map(ext => [ext, 'file']));
|
|
24
28
|
const builtins = new Set([...builtinModules, ...builtinModules.map(name => `node:${name}`)]);
|
|
25
|
-
const compatibility = new Set(['link', 'image', 'head', 'router', 'compat/router', 'next-router-context', 'next-app-router-context', 'navigation', 'headers', 'server', 'cache', 'dynamic', 'script', 'og']);
|
|
26
|
-
const forcedClient = new Set(['link.cjs', 'image.cjs', 'head.cjs', 'router.cjs', 'compat-router.cjs', 'next-router-context.cjs', 'next-app-router-context.cjs', 'app-context.cjs', 'script.cjs']);
|
|
29
|
+
const compatibility = new Set(['link', 'image', 'head', 'router', 'compat/router', 'next-router-context', 'next-app-router-context', 'navigation', 'headers', 'server', 'cache', 'dynamic', 'script', 'og', 'offline', 'redis-cache']);
|
|
30
|
+
const forcedClient = new Set(['link.cjs', 'image.cjs', 'head.cjs', 'router.cjs', 'compat-router.cjs', 'next-router-context.cjs', 'next-app-router-context.cjs', 'app-context.cjs', 'script.cjs', 'offline.cjs']);
|
|
27
31
|
|
|
28
32
|
export async function validateReactVersions(resolveFromProject) {
|
|
29
33
|
const names = ['react', 'react-dom', 'react-server-dom-webpack'];
|
|
@@ -39,7 +43,7 @@ export async function validateReactVersions(resolveFromProject) {
|
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
function sourceAst(source, filename) {
|
|
42
|
-
return parse(source, { sourceType: 'unambiguous', sourceFilename: filename, plugins: ['jsx', ...(/\.tsx?$/.test(filename) ? ['typescript'] : [])] });
|
|
46
|
+
return parse(source, { sourceType: 'unambiguous', sourceFilename: filename, plugins: ['jsx', ...(/\.[cm]?tsx?$/.test(filename) ? ['typescript'] : [])] });
|
|
43
47
|
}
|
|
44
48
|
function clientDirective(source, filename) {
|
|
45
49
|
if (!source.includes('use client')) return false;
|
|
@@ -140,7 +144,7 @@ function routeEntry(route, globalError, dev) {
|
|
|
140
144
|
if (globalError) imports.push(`export {default as GlobalError} from ${JSON.stringify(globalError)};`);
|
|
141
145
|
imports.push(`export {default as NavigationBoundary} from ${JSON.stringify(path.join(packageRoot, 'compat/app-navigation-boundary.cjs'))};`);
|
|
142
146
|
imports.push(`export {ClientPageRoot} from ${JSON.stringify(path.join(packageRoot, 'compat/app-client-page.cjs'))};`);
|
|
143
|
-
imports.push(`export {LayoutProvider,LayoutSlot} from ${JSON.stringify(path.join(packageRoot, 'compat/app-layout-context.cjs'))};`);
|
|
147
|
+
imports.push(`export {LayoutProvider,LayoutSlot,ActivitySlot} from ${JSON.stringify(path.join(packageRoot, 'compat/app-layout-context.cjs'))};`);
|
|
144
148
|
if (route.routing || route.segments.some(segment => segment.error)) imports.push(`export {default as ErrorBoundary} from ${JSON.stringify(path.join(packageRoot, 'compat/app-error-boundary.cjs'))};`);
|
|
145
149
|
if (route.routing) {
|
|
146
150
|
let index = 0;
|
|
@@ -173,11 +177,15 @@ function routeEntry(route, globalError, dev) {
|
|
|
173
177
|
}
|
|
174
178
|
|
|
175
179
|
/** Compile independent React Server and client graphs. The RSC graph contains only references to client boundaries. */
|
|
176
|
-
export async function compileApp({ project, stage, manifest, cssModules, fonts, images, dev, defineEnvironment = {}, productionBrowserSourceMaps = false, serverExternalPackages = [], moduleResolution = {} }) {
|
|
177
|
-
const refresh = dev ? createRefreshTransform(project.root) : undefined;
|
|
180
|
+
export async function compileApp({ project, stage, manifest, cssModules, fonts, images, dev, reactCompiler = false, defineEnvironment = {}, productionBrowserSourceMaps = false, serverExternalPackages = [], moduleResolution = {} }) {
|
|
181
|
+
const refresh = dev ? createRefreshTransform(project.root, {generation:manifest.cacheId,forceGeneration:manifest.devForceRefresh,forceFiles:manifest.devAffectedModules}) : undefined;
|
|
182
|
+
const locations = dev ? createSourceLocationTransform(project.root) : undefined;
|
|
183
|
+
const compileReact = reactCompiler ? createReactCompilerTransform(project.root, reactCompiler, {dev}) : undefined;
|
|
178
184
|
const { basePath = '', assetPrefix = '', assetBase = '/_prnext/assets', trailingSlash = false, skipTrailingSlashRedirect = false } = manifest.config || {};
|
|
179
185
|
const publicPath = assetBase + '/';
|
|
180
186
|
const routes = project.routes.filter(route => route.router === 'app');
|
|
187
|
+
const htmlOnly = dev ? new Set() : selectHtmlOnlyRoutes(project.routes, manifest.config.htmlOnly, manifest.config);
|
|
188
|
+
const htmlGraph = !dev && htmlOnly.size ? createHtmlOnlyGraph(project.root) : undefined;
|
|
181
189
|
if (project.appNotFound) {
|
|
182
190
|
const file = path.join(stage, '.entries', 'app-not-found.jsx');
|
|
183
191
|
await writeFile(file, `import {notFound} from 'next/navigation';export default function MissingRoute(){notFound()}`);
|
|
@@ -193,6 +201,7 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
|
|
|
193
201
|
}
|
|
194
202
|
if (!routes.length) return;
|
|
195
203
|
if (!manifest.config.cacheComponents && routes.some(route => route.cacheConfig?.instant !== undefined || route.pageConfig?.instant !== undefined || route.segments?.some(segment => segment.staticConfig?.instant !== undefined))) throw new Error('instant requires cacheComponents: true.');
|
|
204
|
+
if (!manifest.config.cacheComponents && routes.some(route => route.cacheConfig?.ensureStatic !== undefined || route.pageConfig?.ensureStatic !== undefined || route.segments?.some(segment => segment.staticConfig?.ensureStatic !== undefined))) throw new Error('ensureStatic requires cacheComponents: true.');
|
|
196
205
|
const actions = createServerActions({ projectRoot: project.root });
|
|
197
206
|
function actionTransform(source, file, mode, runtime = 'nodejs') {
|
|
198
207
|
const result = actions.transform(source, file, mode);
|
|
@@ -203,6 +212,7 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
|
|
|
203
212
|
}
|
|
204
213
|
const boundaries = new Map();
|
|
205
214
|
const cssFiles = new Set();
|
|
215
|
+
const styleGraph = createAppStyleGraph(project.root);
|
|
206
216
|
const globalErrorCssFiles = new Set();
|
|
207
217
|
const globalErrorOnlyCss = new Set();
|
|
208
218
|
const canonicalPaths = new Map();
|
|
@@ -228,29 +238,33 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
|
|
|
228
238
|
throw new Error('app/global-error must be a Client Component. Add the "use client" directive.');
|
|
229
239
|
}
|
|
230
240
|
|
|
231
|
-
function graphPlugin(mode, { styleTarget = cssFiles, omitGlobalStyles = true } = {}) {
|
|
241
|
+
function graphPlugin(mode, { styleTarget = cssFiles, omitGlobalStyles = true, omitAllStyles = false } = {}) {
|
|
232
242
|
const browser = mode === 'browser';
|
|
233
243
|
const rsc = mode === 'rsc';
|
|
234
244
|
return {
|
|
235
245
|
name: `prnext-app-${mode}`,
|
|
236
246
|
setup(esbuild) {
|
|
247
|
+
applicationEntry(esbuild,readCompilerFile,{transformTypedHelpers:!!compileReact && (browser || mode==='ssr')});
|
|
237
248
|
esbuild.onResolve({ filter: /^prnext-internal:use-cache$/ }, () => browser
|
|
238
249
|
? { errors: [{ text: "'use cache' cannot enter a Client Component graph" }] }
|
|
239
250
|
: { path: '../compat/use-cache.cjs', external: true });
|
|
240
251
|
if (browser && omitGlobalStyles) esbuild.onResolve({ filter: /\.(?:css|scss|sass)$/i }, async args => {
|
|
241
|
-
if (!globalErrorOnlyCss.size || /\.module\.(?:css|scss|sass)$/i.test(args.path) || args.pluginData?.prnextResolvingCss || args.pluginData?.prnextAppCss || args.kind === 'import-rule' || args.path.startsWith('prnext-css:')) return;
|
|
252
|
+
if ((!omitAllStyles && !globalErrorOnlyCss.size) || /\.module\.(?:css|scss|sass)$/i.test(args.path) || args.pluginData?.prnextResolvingCss || args.pluginData?.prnextAppCss || args.kind === 'import-rule' || args.path.startsWith('prnext-css:')) return;
|
|
242
253
|
const resolved = await esbuild.resolve(args.path, { resolveDir: args.resolveDir, kind: args.kind, pluginData: { prnextAppCss: true } });
|
|
243
|
-
if (!resolved.errors.length && !resolved.external && resolved.namespace === 'file' && globalErrorOnlyCss.has(await canonicalFile(resolved.path))) return { path: resolved.path, namespace: 'prnext-app-empty' };
|
|
254
|
+
if (!resolved.errors.length && !resolved.external && resolved.namespace === 'file' && (omitAllStyles || globalErrorOnlyCss.has(await canonicalFile(resolved.path)))) return { path: resolved.path, namespace: 'prnext-app-empty' };
|
|
244
255
|
});
|
|
245
256
|
esbuild.onResolve({ filter: /^prnext-internal:action-(?:crypto|client|ssr)$/ }, args => {
|
|
246
257
|
const helper = args.path.slice('prnext-internal:'.length);
|
|
247
258
|
if (browser && helper !== 'action-client') return { errors: [{ text: 'Server Action implementation cannot enter the browser graph.' }] };
|
|
248
259
|
return browser ? { path: path.join(packageRoot, 'runtime', helper + '.mjs') } : { path: '../runtime/' + helper + '.mjs', external: true };
|
|
249
260
|
});
|
|
261
|
+
esbuild.onResolve({ filter: /^prnext-internal:metadata-route$/ }, () => browser
|
|
262
|
+
? { errors: [{ text: 'Metadata route handlers are server-only.' }] }
|
|
263
|
+
: { path: '../runtime/metadata-route.mjs', external: true });
|
|
250
264
|
esbuild.onResolve({ filter: frameworkRuntimeImportPattern }, args => {
|
|
251
265
|
const name = frameworkImportName(args.path);
|
|
252
266
|
if (!compatibility.has(name)) return { errors: [{ text: `PRNext does not implement ${args.path} yet (imported by ${args.importer}).` }] };
|
|
253
|
-
if (browser && ['headers', 'server', 'cache', 'og'].includes(name)) return { errors: [{ text: `${args.path} is server-only and cannot be imported by a Client Component.` }] };
|
|
267
|
+
if (browser && ['headers', 'server', 'cache', 'og', 'redis-cache'].includes(name)) return { errors: [{ text: `${args.path} is server-only and cannot be imported by a Client Component.` }] };
|
|
254
268
|
const target = path.join(packageRoot, 'compat', (name === 'compat/router' ? 'compat-router' : name === 'dynamic' ? 'app-dynamic' : rsc && name === 'navigation' ? 'navigation-server' : name) + '.cjs');
|
|
255
269
|
if (!browser && (!rsc || !forcedClient.has(path.basename(target)))) return { path: '../compat/' + path.basename(target), external: true };
|
|
256
270
|
return { path: target };
|
|
@@ -294,9 +308,14 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
|
|
|
294
308
|
if (!/\.module\.(?:css|scss|sass)$/i.test(args.path)) return { path: file, namespace: 'prnext-app-empty' };
|
|
295
309
|
});
|
|
296
310
|
}
|
|
297
|
-
esbuild.onLoad({ filter: /\.(?:[cm]?
|
|
311
|
+
esbuild.onLoad({ filter: /\.(?:[cm]?[jt]s|[jt]sx)$/, namespace: 'file' }, async args => {
|
|
298
312
|
if (browser && args.path === project.document) return { errors: [{ text: 'pages/_document is server-only and cannot be imported by a Client Component.' }] };
|
|
299
|
-
const
|
|
313
|
+
const original=await readCompilerFile(args.path,args.prnextSource);
|
|
314
|
+
htmlGraph?.source(args.path, original);
|
|
315
|
+
// Only Client Component graphs use React's memo-cache runtime. Server
|
|
316
|
+
// Components, route handlers and Server Action graphs remain untouched.
|
|
317
|
+
const compiled=compileReact && (browser || mode==='ssr') ? await compileReact(original,args.path,{loaderOutput:args.prnextSource!==undefined}) : original;
|
|
318
|
+
const source = await fonts.transform(await expandActionExports(locations?await locations(compiled,args.path):compiled, args.path, async (specifier, importer) => {
|
|
300
319
|
const resolved = await esbuild.resolve(specifier, { resolveDir: path.dirname(importer), kind: 'import-statement', pluginData: { prnextExportScan: true } });
|
|
301
320
|
return resolved.errors.length || resolved.external || resolved.namespace !== 'file' ? undefined : resolved.path;
|
|
302
321
|
}), args.path);
|
|
@@ -305,6 +324,7 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
|
|
|
305
324
|
const actionSource = actionTransform(cachedSource, args.path, mode);
|
|
306
325
|
const builtin = path.dirname(args.path) === path.join(packageRoot, 'compat') && forcedClient.has(path.basename(args.path));
|
|
307
326
|
if (rsc && (builtin || clientDirective(source, args.path))) {
|
|
327
|
+
htmlGraph?.issue(args.path, 'a Client Component or interactive framework component');
|
|
308
328
|
let boundary = boundaries.get(args.path);
|
|
309
329
|
if (!boundary) {
|
|
310
330
|
const exports = await exportedNames(args.path, esbuild);
|
|
@@ -315,7 +335,7 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
|
|
|
315
335
|
return { contents: `import {registerClientReference} from 'react-server-dom-webpack/server';\n${references.join('\n')}`, loader: 'js', resolveDir: path.dirname(args.path) };
|
|
316
336
|
}
|
|
317
337
|
let contents = transformDynamicImports(actionSource, args.path, { projectRoot: project.root, mode: rsc ? 'rsc' : browser ? 'browser' : 'server' });
|
|
318
|
-
if (browser && refresh) contents = await refresh(contents, args.path);
|
|
338
|
+
if (browser && refresh && !actions.isServerModule(cachedSource,args.path)) contents = await refresh(contents, args.path);
|
|
319
339
|
return { contents, loader: extensions[path.extname(args.path)] || 'jsx', resolveDir: path.dirname(args.path) };
|
|
320
340
|
});
|
|
321
341
|
},
|
|
@@ -331,6 +351,7 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
|
|
|
331
351
|
const rsc = mode === 'rsc';
|
|
332
352
|
const framework = new Set(['link', 'image', 'head', 'router', 'compat-router', 'next-router-context', 'next-app-router-context', 'navigation', 'script', 'app-context', 'app-layout-context', 'app-navigation-boundary', 'app-client-page', 'app-error-boundary', 'app-dynamic']);
|
|
333
353
|
return { name: 'prnext-edge-app', setup(esbuild) {
|
|
354
|
+
applicationEntry(esbuild,readCompilerFile,{transformTypedHelpers:!!compileReact && mode==='ssr'});
|
|
334
355
|
esbuild.onResolve({filter:/^prnext-internal:action-(?:crypto|ssr)$/}, args => ({path:args.path.slice('prnext-internal:'.length),namespace:'prnext-edge-action'}));
|
|
335
356
|
esbuild.onLoad({filter:/.*/,namespace:'prnext-edge-action'}, args => ({loader:'js',contents:(args.path==='action-crypto'?['encryptBoundArgs','decryptBoundArgs','bindEncryptedReference']:['createServerReference']).map(name=>`export const ${name}=globalThis.__PRNEXT_EDGE_ACTIONS[${JSON.stringify(args.path)}][${JSON.stringify(name)}];`).join('\n')}));
|
|
336
357
|
esbuild.onResolve({ filter: frameworkSubpathPattern }, args => {
|
|
@@ -358,7 +379,7 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
|
|
|
358
379
|
const names = await exportedNames(args.path, esbuild), name = path.basename(args.path, '.cjs');
|
|
359
380
|
return { loader: 'js', contents: `const module=globalThis.__PRNEXT_EDGE_FRAMEWORK[${JSON.stringify(name)}];\n` + [...names].map((name, index) => `const value${index}=${name === 'default' ? '(module.default||module)' : `module[${JSON.stringify(name)}]`};export {value${index} as ${JSON.stringify(name)}};`).join('\n') };
|
|
360
381
|
});
|
|
361
|
-
if (rsc) esbuild.onLoad({ filter: /\.(?:[cm]?
|
|
382
|
+
if (rsc) esbuild.onLoad({ filter: /\.(?:[cm]?[jt]s|[jt]sx)$/, namespace: 'file' }, async args => {
|
|
362
383
|
const source = args.prnextSource ?? await readFile(args.path, 'utf8');
|
|
363
384
|
const builtin = path.dirname(args.path) === path.join(packageRoot, 'compat') && forcedClient.has(path.basename(args.path));
|
|
364
385
|
if (!builtin && !clientDirective(source, args.path)) return;
|
|
@@ -373,13 +394,18 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
|
|
|
373
394
|
} };
|
|
374
395
|
}
|
|
375
396
|
async function edgeAppBuild(file, outfile, mode, defaultExport = false) {
|
|
376
|
-
const result = await compileEdge({ file, outfile, projectRoot: project.root, dev, defineEnvironment, reactMode: mode, defaultExport,
|
|
397
|
+
const result = await compileEdge({ file, outfile, projectRoot: project.root, dev, defineEnvironment, reactMode: mode, defaultExport, transformTypedHelpers:!!compileReact && mode==='ssr',
|
|
377
398
|
plugins: [moduleResolutionPlugin(moduleResolution, project.root), edgeGraphPlugin(mode), images.plugin(), fonts.plugin(), cssModules.plugin(false)],
|
|
378
|
-
transformSource: async (source, file, builder
|
|
379
|
-
const
|
|
380
|
-
return
|
|
381
|
-
|
|
399
|
+
transformSource: async (source, file, builder, {loaderOutput=false}={}) => {
|
|
400
|
+
const compiled=compileReact && mode==='ssr' ? await compileReact(source,file,{loaderOutput}) : source;
|
|
401
|
+
return actionTransform(await fonts.transform(await expandActionExports(locations?await locations(compiled,file):compiled, file, async (specifier, importer) => {
|
|
402
|
+
const resolved = await builder.resolve(specifier, {resolveDir:path.dirname(importer),kind:'import-statement',pluginData:{prnextExportScan:true}});
|
|
403
|
+
return resolved.errors.length || resolved.external || resolved.namespace !== 'file' ? undefined : resolved.path;
|
|
404
|
+
}), file), file, mode, 'edge');
|
|
405
|
+
} });
|
|
382
406
|
fonts.record(result.metafile);
|
|
407
|
+
styleGraph.record(result.metafile);
|
|
408
|
+
htmlGraph?.record(result.metafile);
|
|
383
409
|
}
|
|
384
410
|
async function serverBuild(entryPoints, outdir, mode, styleTarget = cssFiles) {
|
|
385
411
|
if (!Object.keys(entryPoints).length) return;
|
|
@@ -407,6 +433,8 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
|
|
|
407
433
|
}
|
|
408
434
|
}
|
|
409
435
|
fonts.record(output.metafile);
|
|
436
|
+
styleGraph.record(output.metafile);
|
|
437
|
+
htmlGraph?.record(output.metafile);
|
|
410
438
|
for (const file of output.outputFiles) {
|
|
411
439
|
await mkdir(path.dirname(file.path), { recursive: true });
|
|
412
440
|
await writeFile(file.path, file.contents);
|
|
@@ -421,7 +449,6 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
|
|
|
421
449
|
}
|
|
422
450
|
|
|
423
451
|
async function collectBrowserStyles(browserEntries) {
|
|
424
|
-
if (!globalErrorId) return;
|
|
425
452
|
const revision = `${boundaries.size}:${actions.actions.size}:${nativePackages.version}`;
|
|
426
453
|
if (browserStyleRevision === revision) return;
|
|
427
454
|
// SSR intentionally erases ssr:false loaders. Discover their dependencies
|
|
@@ -431,6 +458,7 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
|
|
|
431
458
|
define: { 'process.env': '{}', ...common.define },
|
|
432
459
|
plugins: [moduleResolutionPlugin(moduleResolution, project.root), ...(dev ? [devSingletonsPlugin()] : []), images.plugin(), fonts.plugin(), graphPlugin('browser', { omitGlobalStyles: false }), nativePackages.plugin('browser'), cssModules.plugin(true)] });
|
|
433
460
|
fonts.record(discovery.metafile);
|
|
461
|
+
styleGraph.record(discovery.metafile);
|
|
434
462
|
const entryIds = new Map(await Promise.all(Object.entries(browserEntries).map(async ([id, file]) => [await canonicalFile(file), id])));
|
|
435
463
|
const globalRoots = [], normalRoots = [];
|
|
436
464
|
for (const output of Object.values(discovery.metafile.outputs)) {
|
|
@@ -473,6 +501,7 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
|
|
|
473
501
|
const collectInstant = node => {if(!node)return;instantConfigs.push(...Object.values(node.config || {}));for(const child of [...(node.children || []),...Object.values(node.slots || {})])collectInstant(child);};
|
|
474
502
|
collectInstant(route.routing);
|
|
475
503
|
manifest.routes.push({ id: route.id, pattern: route.pattern, kind: route.kind, router: 'app', module: `server/${route.id}.mjs`, css: [], ...(route.internal ? { internal: true } : {}),
|
|
504
|
+
...(!dev && htmlOnly.has(route.id) ? { htmlOnly:true } : {}),
|
|
476
505
|
...(route.parallel ? { parallel: true, interception: !!route.interception } : {}),
|
|
477
506
|
...(route.cacheConfig ? { cacheConfig: route.cacheConfig } : {}),
|
|
478
507
|
...(route.kind === 'page' ? { instantSamples: [route.pageConfig, ...[...route.segments].reverse().map(segment => segment.staticConfig)].find(config => config?.instant?.unstable_samples)?.instant.unstable_samples, instantBuild: instantConfigs.some(config => config?.instant?.level === 'experimental-error'), hasStaticParams: route.id !== manifest.appNotFound && !!(route.pageConfig?.generateStaticParams || route.segments.some(segment => segment.staticConfig?.generateStaticParams)) }
|
|
@@ -481,11 +510,16 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
|
|
|
481
510
|
}
|
|
482
511
|
manifest.app = { clientModules: {}, actions: {}, actionKey: actions.actionKey };
|
|
483
512
|
const actionRevision = () => [...actions.actions.values()].map(action=>action.id+':'+[...(action.runtimes || [])].sort().join(',')).join(';');
|
|
513
|
+
const boundaryRevision = () => [...boundaries.values()].map(boundary=>boundary.id+':'+!!boundary.edge).join(';');
|
|
514
|
+
const browserRevision = () => `${boundaryRevision()}:${cssFiles.size}:${globalErrorCssFiles.size}:${nativePackages.version}`;
|
|
484
515
|
// Route Handlers have a Node React environment and must not share RSC chunks
|
|
485
516
|
// that import registerServerReference or client-reference registration.
|
|
486
517
|
await serverBuild(handlerEntries, 'server', 'route');
|
|
487
518
|
for (;;) {
|
|
488
|
-
|
|
519
|
+
// Each graph consumes discoveries from the preceding one. Discovering a
|
|
520
|
+
// client boundary in RSC is normal: SSR and browser below already include
|
|
521
|
+
// it. Only discoveries made too late for a graph require another pass.
|
|
522
|
+
const emittedActions = actionRevision();
|
|
489
523
|
for (const action of actions.actions.values()) {
|
|
490
524
|
const entry = path.join(stage, '.entries', 'action-' + action.id + '.mjs');
|
|
491
525
|
await writeFile(entry, `export {${JSON.stringify(action.binding)} as invoke} from ${JSON.stringify(action.file)};`);
|
|
@@ -493,6 +527,8 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
|
|
|
493
527
|
if (!action.runtimes?.size || action.runtimes.has('nodejs')) entries['action-' + action.id] = entry;
|
|
494
528
|
}
|
|
495
529
|
await serverBuild(entries, 'server', 'rsc');
|
|
530
|
+
const serverNativeVersion = nativePackages.version;
|
|
531
|
+
const emittedBoundaries = boundaryRevision();
|
|
496
532
|
const ssrEntries = {};
|
|
497
533
|
const globalSsrEntries = {};
|
|
498
534
|
for (const boundary of boundaries.values()) {
|
|
@@ -512,6 +548,7 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
|
|
|
512
548
|
await collectBrowserStyles(browserEntries);
|
|
513
549
|
globalErrorOnlyCss.clear();
|
|
514
550
|
for (const file of globalErrorCssFiles) if (!cssFiles.has(file)) globalErrorOnlyCss.add(file);
|
|
551
|
+
const emittedBrowserInputs = browserRevision();
|
|
515
552
|
if (routes.some(route => route.kind === 'page')) {
|
|
516
553
|
let globalErrorCss = [];
|
|
517
554
|
if (globalErrorCssFiles.size) {
|
|
@@ -524,31 +561,62 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
|
|
|
524
561
|
}
|
|
525
562
|
if (globalErrorId) manifest.app.globalError = { id: globalErrorId, name: 'default', css: globalErrorCss };
|
|
526
563
|
const runtimeEntry = path.join(stage, '.entries', 'app-runtime.mjs');
|
|
564
|
+
const htmlOnlyRoutes = !dev && htmlOnly.size ? routes.filter(route => route.kind === 'page' && !route.internal).map(route => ({pattern:route.pattern,...(htmlOnly.has(route.id)?{htmlOnly:true}:{})})) : undefined;
|
|
527
565
|
const importers = Object.entries(browserEntries).map(([id, file]) => `${JSON.stringify(id)}:()=>import(${JSON.stringify(file)})`);
|
|
528
|
-
await writeFile(runtimeEntry, `${dev ? `import ${JSON.stringify(devClientFile)};\n` : ''}
|
|
566
|
+
await writeFile(runtimeEntry, `${dev ? `import ${JSON.stringify(devClientFile)};\n` : ''}\nimport {bootstrapApp} from ${JSON.stringify(path.join(packageRoot, 'runtime/app-client.mjs'))};\nbootstrapApp({clientModules:{${importers.join(',')}},...${JSON.stringify({ basePath, assetPrefix, assetBase, htmlOnlyRoutes, trailingSlash, skipTrailingSlashRedirect, deploymentId: dev ? undefined : manifest.cacheId, strictMode: manifest.config.reactStrictMode !== false, cacheComponents: manifest.config.cacheComponents === true, partialPrefetching: manifest.config.partialPrefetching === true, navigationCache: manifest.config.navigationCache, ...(globalErrorId ? { globalErrorId, globalErrorCss } : {}), ...(dev ? { dev: { buildId: manifest.buildId, clientManifest: manifest.devClient, onDemand:manifest.devOnDemand===true, sequence:manifest.devSequence, session:manifest.devSession, indicator:manifest.devIndicators, ...(manifest.devTools ? {tools:manifest.devTools} : {}) } } : {}) })}});\n`);
|
|
529
567
|
const clientEntries = { 'app-runtime': runtimeEntry, ...browserEntries };
|
|
530
568
|
const browser = await bundle({ ...common, entryPoints: clientEntries, outdir: path.join(stage, 'assets'), entryNames: '[name]-[hash]', chunkNames: 'app-chunk-[hash]', platform: 'browser', format: 'esm', target: ['es2022'], splitting: true, minify: !dev, sourcemap: dev || productionBrowserSourceMaps,
|
|
531
|
-
define: { 'process.env': '{}', ...common.define }, plugins: [moduleResolutionPlugin(moduleResolution, project.root), ...(dev ? [devSingletonsPlugin()] : []), images.plugin(), fonts.plugin(), graphPlugin('browser'), nativePackages.plugin('browser'), cssModules.plugin(true, { emitStyles:
|
|
569
|
+
define: { 'process.env': '{}', ...common.define, __PRNEXT_CACHE_COMPONENTS__: JSON.stringify(manifest.config.cacheComponents === true) }, plugins: [moduleResolutionPlugin(moduleResolution, project.root), ...(dev ? [devSingletonsPlugin()] : []), images.plugin(), fonts.plugin(), graphPlugin('browser', { omitAllStyles: true }), nativePackages.plugin('browser'), cssModules.plugin(true, { emitStyles: () => false })] });
|
|
532
570
|
fonts.record(browser.metafile);
|
|
533
571
|
const entryIds = new Map(Object.entries(clientEntries).map(([id, file]) => [file, id]));
|
|
534
572
|
const edgeBoundaries = new Set([...boundaries.values()].filter(boundary => boundary.edge).map(boundary => boundary.id));
|
|
535
|
-
const styles = new Set();
|
|
536
573
|
let client;
|
|
537
574
|
for (const [output, metadata] of Object.entries(browser.metafile.outputs)) {
|
|
538
|
-
if (metadata.cssBundle) styles.add(publicPath + path.basename(metadata.cssBundle));
|
|
539
575
|
if (!metadata.entryPoint || !output.endsWith('.js')) continue;
|
|
540
576
|
const id = entryIds.get(path.resolve(project.root, metadata.entryPoint));
|
|
541
577
|
if (id === 'app-runtime') { client = publicPath + path.basename(output); continue; }
|
|
542
578
|
if (!id) continue;
|
|
543
579
|
manifest.app.clientModules[id] = { id, chunks: [id, publicPath + path.basename(output)], name: '*', ssrModule: `app-ssr/${id}.mjs`, ...(edgeBoundaries.has(id) ? { edgeSsrModule: `app-ssr-edge/${id}.mjs` } : {}), browserModule: publicPath + path.basename(output) };
|
|
544
580
|
}
|
|
545
|
-
|
|
581
|
+
const styleBundles = new Map();
|
|
582
|
+
for (const route of routes.filter(route => route.kind === 'page')) {
|
|
583
|
+
// All esbuild graphs canonicalize their inputs, including symlinked
|
|
584
|
+
// convention files. Match those identities before querying reachability.
|
|
585
|
+
for (const files of appStyleScopes(route)) for (const file of files) styleGraph.alias(file, await canonicalFile(file));
|
|
586
|
+
const plan = planAppStyleEntries(route, styleGraph);
|
|
587
|
+
const styles = [];
|
|
588
|
+
const cssScopes = [];
|
|
589
|
+
for (const { styles:files, path:scopePath, kind } of plan) {
|
|
590
|
+
const signature = JSON.stringify(files);
|
|
591
|
+
if (!styleBundles.has(signature)) {
|
|
592
|
+
const id = 'app-style-' + createHash('sha256').update(files.map(file => path.relative(project.root, file).replaceAll(path.sep, '/')).join('\0')).digest('hex').slice(0, 16);
|
|
593
|
+
const styleEntry = path.join(stage, '.entries', id + '.mjs');
|
|
594
|
+
await writeFile(styleEntry, files.map(file => `import ${JSON.stringify(file)};`).join('\n'));
|
|
595
|
+
const output = await bundle({ ...common, entryPoints: { [id]: styleEntry }, outdir: path.join(stage, 'assets'), entryNames: '[name]-[hash]', platform: 'browser', format: 'esm', target: ['es2022'], minify: !dev, sourcemap: dev || productionBrowserSourceMaps,
|
|
596
|
+
plugins: [moduleResolutionPlugin(moduleResolution, project.root), images.plugin(), fonts.plugin(), graphPlugin('browser', { omitGlobalStyles: false }), cssModules.plugin(true)] });
|
|
597
|
+
styleBundles.set(signature, Object.values(output.metafile.outputs).filter(output => output.cssBundle).map(output => publicPath + path.basename(output.cssBundle)));
|
|
598
|
+
}
|
|
599
|
+
const css = styleBundles.get(signature);
|
|
600
|
+
styles.push(...css);
|
|
601
|
+
if (route.parallel) cssScopes.push({ path:scopePath, kind, css });
|
|
602
|
+
}
|
|
603
|
+
const emitted = manifest.routes.find(item => item.id === route.id);
|
|
604
|
+
if (!emitted.htmlOnly) emitted.client = client;
|
|
605
|
+
emitted.css = [...new Set(styles)];
|
|
606
|
+
if (route.parallel) emitted.cssScopes = cssScopes;
|
|
607
|
+
}
|
|
546
608
|
}
|
|
547
609
|
manifest.app.actions = Object.fromEntries([...actions.actions.values()].map(action => [action.id, {
|
|
548
610
|
module: `server/action-${action.runtimes?.has('nodejs')?'':'edge-'}${action.id}.mjs`, export: 'invoke',
|
|
549
611
|
...(action.runtimes?.has('edge') ? {edgeModule:`server/action-edge-${action.id}.mjs`} : {}),
|
|
550
612
|
}]));
|
|
551
|
-
if (
|
|
613
|
+
if (emittedActions === actionRevision() && emittedBoundaries === boundaryRevision() &&
|
|
614
|
+
serverNativeVersion === nativePackages.version && emittedBrowserInputs === browserRevision()) break;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
if (htmlGraph) for (const route of routes) if (htmlOnly.has(route.id)) {
|
|
618
|
+
for (const file of [...appStyleScopes(route).flat(), ...(route.metadataFiles || []).map(item=>item.file)].filter(Boolean)) htmlGraph.alias(file, await canonicalFile(file));
|
|
619
|
+
htmlGraph.validate(route);
|
|
552
620
|
}
|
|
553
621
|
|
|
554
622
|
}
|
package/build/build-lock.mjs
CHANGED
|
@@ -11,7 +11,7 @@ function alive(pid) {
|
|
|
11
11
|
}
|
|
12
12
|
async function owner(directory) {
|
|
13
13
|
const info = await lstat(directory);
|
|
14
|
-
if (!info.isDirectory() || info.isSymbolicLink()) throw busy(
|
|
14
|
+
if (!info.isDirectory() || info.isSymbolicLink()) throw busy(`The project lock is not a real directory. Check ${path.basename(directory)} before retrying.`);
|
|
15
15
|
const file = await open(path.join(directory, 'owner.json'), 'r');
|
|
16
16
|
try {
|
|
17
17
|
if ((await file.stat()).size > 4096) throw new Error('oversized owner');
|
|
@@ -22,8 +22,8 @@ async function owner(directory) {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/** One publisher per canonical project, covering React and direct static imports. */
|
|
25
|
-
export async function withBuildLock(directory, action) {
|
|
26
|
-
const root = await realpath(directory), lock = path.join(root, LOCK);
|
|
25
|
+
export async function withBuildLock(directory, action, {development = false} = {}) {
|
|
26
|
+
const root = await realpath(directory), lock = path.join(root, development ? '.prnext-dev.lock' : LOCK);
|
|
27
27
|
const identity = { pid: process.pid, host: hostname(), token: randomUUID(), createdAt: new Date().toISOString() };
|
|
28
28
|
const prepared = path.join(root, `.prnext-lock-${identity.token}`);
|
|
29
29
|
await mkdir(prepared, { mode: 0o700 });
|
|
@@ -37,16 +37,16 @@ export async function withBuildLock(directory, action) {
|
|
|
37
37
|
try { previous = await owner(lock); }
|
|
38
38
|
catch (cause) {
|
|
39
39
|
if (cause.code === 'ENOENT' && attempt < 8) continue;
|
|
40
|
-
throw busy(
|
|
40
|
+
throw busy(`Cannot verify the project lock. Check ${path.basename(lock)} before retrying.`);
|
|
41
41
|
}
|
|
42
|
-
if (previous.host !== identity.host || alive(previous.pid)) throw busy(`A build is already running for this project (PID ${previous.pid}). Wait for it to finish.`);
|
|
42
|
+
if (previous.host !== identity.host || alive(previous.pid)) throw busy(development ? `A development server is already running for this project (PID ${previous.pid}). Stop it before starting another.` : `A build is already running for this project (PID ${previous.pid}). Wait for it to finish.`);
|
|
43
43
|
// Exactly one contender can recover this generation. Never delete a lock
|
|
44
44
|
// merely because it is old: long builds and suspended processes are valid.
|
|
45
45
|
let claim;
|
|
46
46
|
try { claim = await open(path.join(lock, 'recovery'), 'wx', 0o600); }
|
|
47
47
|
catch (cause) {
|
|
48
48
|
if (cause.code === 'ENOENT' && attempt < 8) continue;
|
|
49
|
-
throw busy(
|
|
49
|
+
throw busy(`Project lock recovery is in progress. Retry shortly; if recovery was interrupted, check ${path.basename(lock)} manually.`);
|
|
50
50
|
}
|
|
51
51
|
const retired = path.join(root, `.prnext-lock-retired-${identity.token}`);
|
|
52
52
|
try {
|