@thomas.f/prnext 0.1.0-alpha.1
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 +54 -0
- package/build/action-exports.mjs +107 -0
- package/build/actions.mjs +214 -0
- package/build/app-config.mjs +138 -0
- package/build/app-routing.mjs +119 -0
- package/build/app-static.mjs +158 -0
- package/build/app.mjs +536 -0
- package/build/cache-components.mjs +91 -0
- package/build/cache-handlers.mjs +54 -0
- package/build/check.mjs +40 -0
- package/build/compiler.mjs +222 -0
- package/build/config.mjs +193 -0
- package/build/css-modules.mjs +114 -0
- package/build/custom-routes.mjs +363 -0
- package/build/dev-worker.mjs +18 -0
- package/build/dev.mjs +76 -0
- package/build/directory-cache.mjs +29 -0
- package/build/dynamic.mjs +125 -0
- package/build/edge.mjs +141 -0
- package/build/font-data.LICENSE.txt +25 -0
- package/build/font-data.json +1 -0
- package/build/font-files.mjs +221 -0
- package/build/fonts.mjs +131 -0
- package/build/framework-imports.mjs +17 -0
- package/build/i18n.mjs +39 -0
- package/build/images-config.mjs +62 -0
- package/build/images.mjs +86 -0
- package/build/incremental.mjs +76 -0
- package/build/index.mjs +356 -0
- package/build/loader-cache.mjs +69 -0
- package/build/metadata-images.mjs +72 -0
- package/build/metadata-routes.mjs +77 -0
- package/build/middleware.mjs +231 -0
- package/build/migrate.mjs +289 -0
- package/build/module-resolution.mjs +78 -0
- package/build/native-discovery.mjs +134 -0
- package/build/native-packages.mjs +105 -0
- package/build/npm-packages.mjs +110 -0
- package/build/og.mjs +14 -0
- package/build/page-extensions.mjs +12 -0
- package/build/passthrough-loader.cjs +4 -0
- package/build/precompress.mjs +122 -0
- package/build/scan.mjs +178 -0
- package/build/script-workers.mjs +25 -0
- package/build/server-actions-config.mjs +27 -0
- package/build/source-maps.mjs +34 -0
- package/build/standalone-config.mjs +19 -0
- package/build/standalone-runner.mjs +21 -0
- package/build/standalone-worker.mjs +5 -0
- package/build/standalone.mjs +240 -0
- package/build/static-export.mjs +93 -0
- package/build/styles.mjs +174 -0
- package/build/transform-cache.mjs +50 -0
- package/build/transform.mjs +155 -0
- package/build/turbopack-conditions.mjs +59 -0
- package/build/webpack-bridge.cjs +3 -0
- package/build/webpack-graph.mjs +256 -0
- package/build/webpack-loader.cjs +9 -0
- package/cli.mjs +210 -0
- package/compat/app-client-page.cjs +32 -0
- package/compat/app-context.cjs +40 -0
- package/compat/app-context.d.cts +20 -0
- package/compat/app-dynamic.cjs +34 -0
- package/compat/app-error-boundary.cjs +53 -0
- package/compat/app-global-error-boundary.cjs +34 -0
- package/compat/app-layout-context.cjs +27 -0
- package/compat/app-navigation-boundary.cjs +47 -0
- package/compat/app.cjs +33 -0
- package/compat/app.d.cts +9 -0
- package/compat/cache-handlers.cjs +223 -0
- package/compat/cache-life.cjs +34 -0
- package/compat/cache.cjs +99 -0
- package/compat/cache.d.cts +36 -0
- package/compat/compat/router.d.cts +2 -0
- package/compat/compat-router.cjs +7 -0
- package/compat/constants.cjs +8 -0
- package/compat/constants.d.cts +7 -0
- package/compat/cookies.cjs +129 -0
- package/compat/data-cache.cjs +389 -0
- package/compat/dist/shared/lib/app-router-context.shared-runtime.d.cts +3 -0
- package/compat/dist/shared/lib/router-context.d.cts +1 -0
- package/compat/dist/shared/lib/router-context.shared-runtime.d.cts +3 -0
- package/compat/document.cjs +73 -0
- package/compat/document.d.cts +41 -0
- package/compat/draft.cjs +28 -0
- package/compat/dynamic-bailout.cjs +13 -0
- package/compat/dynamic.cjs +119 -0
- package/compat/dynamic.d.cts +32 -0
- package/compat/error.cjs +36 -0
- package/compat/error.d.cts +15 -0
- package/compat/fetch.d.cts +10 -0
- package/compat/font/google.d.cts +1 -0
- package/compat/font/local.d.cts +2 -0
- package/compat/font-google.cjs +2 -0
- package/compat/font-google.d.cts +1944 -0
- package/compat/font-local.cjs +5 -0
- package/compat/font-local.d.cts +3 -0
- package/compat/font-types.d.cts +25 -0
- package/compat/head.cjs +180 -0
- package/compat/head.d.cts +3 -0
- package/compat/headers.cjs +89 -0
- package/compat/headers.d.cts +26 -0
- package/compat/image-config.cjs +3 -0
- package/compat/image-imports.d.ts +12 -0
- package/compat/image-shared.cjs +62 -0
- package/compat/image.cjs +64 -0
- package/compat/image.d.cts +35 -0
- package/compat/incremental-cache.cjs +186 -0
- package/compat/index.d.ts +159 -0
- package/compat/instant-samples.cjs +39 -0
- package/compat/instant.cjs +13 -0
- package/compat/link.cjs +68 -0
- package/compat/link.d.cts +17 -0
- package/compat/locale.cjs +30 -0
- package/compat/metadata.d.cts +77 -0
- package/compat/navigation-controls.cjs +27 -0
- package/compat/navigation-server.cjs +25 -0
- package/compat/navigation.cjs +52 -0
- package/compat/navigation.d.cts +28 -0
- package/compat/next-app-router-context.cjs +6 -0
- package/compat/next-router-context.cjs +3 -0
- package/compat/og.cjs +32 -0
- package/compat/og.d.cts +1 -0
- package/compat/paths.cjs +33 -0
- package/compat/preview.cjs +41 -0
- package/compat/rewrite.cjs +41 -0
- package/compat/route-request.cjs +64 -0
- package/compat/router.cjs +137 -0
- package/compat/router.d.cts +58 -0
- package/compat/script-context.cjs +5 -0
- package/compat/script-loader.cjs +135 -0
- package/compat/script.cjs +69 -0
- package/compat/script.d.cts +17 -0
- package/compat/server-primitives.cjs +109 -0
- package/compat/server.cjs +13 -0
- package/compat/server.d.cts +37 -0
- package/compat/static-generation.cjs +108 -0
- package/compat/use-cache.cjs +129 -0
- package/index.mjs +1 -0
- package/native/platforms.json +6 -0
- package/native/resolve.mjs +81 -0
- package/package.json +127 -0
- package/release/verify.mjs +59 -0
- package/runtime/action-client.mjs +19 -0
- package/runtime/action-crypto.mjs +138 -0
- package/runtime/action-reference.mjs +2 -0
- package/runtime/action-server.mjs +161 -0
- package/runtime/action-ssr.mjs +5 -0
- package/runtime/api.mjs +326 -0
- package/runtime/app-client.mjs +539 -0
- package/runtime/app-content.mjs +8 -0
- package/runtime/app-errors.mjs +12 -0
- package/runtime/app-instant.mjs +30 -0
- package/runtime/app-metadata.mjs +410 -0
- package/runtime/app-partial-model.mjs +228 -0
- package/runtime/app-partial.mjs +130 -0
- package/runtime/app-prefetch.mjs +34 -0
- package/runtime/app-recovery.mjs +87 -0
- package/runtime/app-render.mjs +544 -0
- package/runtime/app-routing-access.mjs +143 -0
- package/runtime/app-routing-context.mjs +35 -0
- package/runtime/app-routing-state.mjs +61 -0
- package/runtime/app-routing.mjs +323 -0
- package/runtime/app-segments.mjs +22 -0
- package/runtime/app-static-params.mjs +120 -0
- package/runtime/app-static.mjs +86 -0
- package/runtime/app-stream-channel.mjs +92 -0
- package/runtime/app-stream-html.mjs +279 -0
- package/runtime/build-directory.mjs +48 -0
- package/runtime/cache-arguments.mjs +33 -0
- package/runtime/cache-components-rsc.mjs +64 -0
- package/runtime/cache-request.mjs +23 -0
- package/runtime/client-navigation.mjs +40 -0
- package/runtime/dev-client.mjs +129 -0
- package/runtime/dev-file-changes.mjs +110 -0
- package/runtime/document.mjs +96 -0
- package/runtime/edge.mjs +105 -0
- package/runtime/env.mjs +68 -0
- package/runtime/fetch-cache.mjs +379 -0
- package/runtime/font-preload.mjs +4 -0
- package/runtime/http.mjs +190 -0
- package/runtime/incremental-cache.mjs +83 -0
- package/runtime/metadata-route.mjs +83 -0
- package/runtime/middleware.mjs +186 -0
- package/runtime/module-loader.mjs +21 -0
- package/runtime/navigation.mjs +16 -0
- package/runtime/og-loader.mjs +17 -0
- package/runtime/pages-client.mjs +197 -0
- package/runtime/pages-error-props.mjs +6 -0
- package/runtime/pages-errors.mjs +85 -0
- package/runtime/pages-initial-props.mjs +43 -0
- package/runtime/pages-navigation.mjs +565 -0
- package/runtime/pages-paths.mjs +80 -0
- package/runtime/pages-revalidate.mjs +33 -0
- package/runtime/partial-artifact-cache.mjs +37 -0
- package/runtime/partial-template.mjs +174 -0
- package/runtime/profile.mjs +9 -0
- package/runtime/profiles.json +47 -0
- package/runtime/render.mjs +304 -0
- package/runtime/request-transport.mjs +41 -0
- package/runtime/request-work.mjs +7 -0
- package/runtime/route-static.mjs +117 -0
- package/runtime/rsc-worker.mjs +426 -0
- package/runtime/script-html.mjs +61 -0
- package/runtime/stream-select.mjs +25 -0
- package/runtime/stream-utils.mjs +127 -0
- package/runtime/transport.mjs +108 -0
- package/runtime/watch-project.mjs +130 -0
- package/runtime/worker-artifacts.mjs +22 -0
- package/runtime/worker-metadata.mjs +31 -0
- package/runtime/worker.mjs +197 -0
package/build/app.mjs
ADDED
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
import {frameworkImportName, frameworkRuntimeImportPattern, frameworkSubpathPattern} from './framework-imports.mjs';
|
|
2
|
+
import { moduleResolutionPlugin } from './module-resolution.mjs';
|
|
3
|
+
import { build as bundle, compilerSource } from './compiler.mjs';
|
|
4
|
+
import { readFile, writeFile, mkdir, cp, realpath } from 'node:fs/promises';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
7
|
+
import { createRequire, builtinModules } from 'node:module';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import { parse } from '@babel/parser';
|
|
10
|
+
import * as t from '@babel/types';
|
|
11
|
+
import { rewriteServerChunks } from './transform.mjs';
|
|
12
|
+
import { transformDynamicImports } from './dynamic.mjs';
|
|
13
|
+
import { createServerActions } from './actions.mjs';
|
|
14
|
+
import { expandActionExports } from './action-exports.mjs';
|
|
15
|
+
import { compileEdge } from './edge.mjs';
|
|
16
|
+
import { createNativePackages } from './native-packages.mjs';
|
|
17
|
+
import { createRefreshTransform, devSingletonsPlugin, devClientFile } from './dev.mjs';
|
|
18
|
+
import { npmPackagesPlugin } from './npm-packages.mjs';
|
|
19
|
+
import { transformCacheComponents } from './cache-components.mjs';
|
|
20
|
+
|
|
21
|
+
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' };
|
|
23
|
+
const assetLoaders = Object.fromEntries(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.avif', '.svg', '.ico', '.woff', '.woff2', '.ttf', '.eot'].map(ext => [ext, 'file']));
|
|
24
|
+
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']);
|
|
27
|
+
|
|
28
|
+
export async function validateReactVersions(resolveFromProject) {
|
|
29
|
+
const names = ['react', 'react-dom', 'react-server-dom-webpack'];
|
|
30
|
+
const ownPackage = JSON.parse(await readFile(path.join(packageRoot, 'package.json'), 'utf8'));
|
|
31
|
+
const expected = ownPackage.peerDependencies['react-server-dom-webpack'];
|
|
32
|
+
const versions = await Promise.all(names.map(async name => {
|
|
33
|
+
try { return JSON.parse(await readFile(resolveFromProject.resolve(`${name}/package.json`), 'utf8')).version; }
|
|
34
|
+
catch { return 'missing or inaccessible'; }
|
|
35
|
+
}));
|
|
36
|
+
if (versions.some(version => version !== expected)) {
|
|
37
|
+
throw new Error(`PRNext App Router requires exactly matching React protocol versions: ${names.map(name => `${name}@${expected}`).join(', ')}. Resolved ${names.map((name, index) => `${name}@${versions[index]}`).join(', ')}. Run npm install --save-exact ${names.map(name => `${name}@${expected}`).join(' ')} in your application. React Server Components bundler APIs are version-specific.`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function sourceAst(source, filename) {
|
|
42
|
+
return parse(source, { sourceType: 'unambiguous', sourceFilename: filename, plugins: ['jsx', ...(/\.tsx?$/.test(filename) ? ['typescript'] : [])] });
|
|
43
|
+
}
|
|
44
|
+
function clientDirective(source, filename) {
|
|
45
|
+
if (!source.includes('use client')) return false;
|
|
46
|
+
return sourceAst(source, filename).program.directives.some(directive => directive.value.value === 'use client');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function frameworkDirective(source, filename) {
|
|
50
|
+
if (clientDirective(source, filename)) return true;
|
|
51
|
+
if (!source.includes('use server')) return false;
|
|
52
|
+
function contains(node) {
|
|
53
|
+
if (!node || typeof node !== 'object') return false;
|
|
54
|
+
if (t.isDirective(node) && node.value.value === 'use server') return true;
|
|
55
|
+
return (t.VISITOR_KEYS[node.type] || []).some(key => Array.isArray(node[key]) ? node[key].some(contains) : contains(node[key]));
|
|
56
|
+
}
|
|
57
|
+
return contains(sourceAst(source, filename));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Enumerate runtime ESM exports without evaluating client code in the server build. */
|
|
61
|
+
async function exportedNames(file, esbuild, seen = new Set()) {
|
|
62
|
+
if (seen.has(file)) return new Set();
|
|
63
|
+
seen.add(file);
|
|
64
|
+
const source = await compilerSource(file);
|
|
65
|
+
const ast = sourceAst(source, file);
|
|
66
|
+
const names = new Set();
|
|
67
|
+
for (const node of ast.program.body) {
|
|
68
|
+
if (t.isExportDefaultDeclaration(node)) { names.add('default'); continue; }
|
|
69
|
+
if (node.exportKind === 'type') continue;
|
|
70
|
+
if (t.isExportNamedDeclaration(node)) {
|
|
71
|
+
if (node.declaration && !t.isTSInterfaceDeclaration(node.declaration) && !t.isTSTypeAliasDeclaration(node.declaration) && !node.declaration.declare) {
|
|
72
|
+
for (const name of Object.keys(t.getOuterBindingIdentifiers(node.declaration))) names.add(name);
|
|
73
|
+
}
|
|
74
|
+
for (const specifier of node.specifiers) if (specifier.exportKind !== 'type') names.add(specifier.exported.name ?? specifier.exported.value);
|
|
75
|
+
}
|
|
76
|
+
if (t.isExportAllDeclaration(node)) {
|
|
77
|
+
const resolved = await esbuild.resolve(node.source.value, { resolveDir: path.dirname(file), kind: 'import-statement', pluginData: { prnextExportScan: true } });
|
|
78
|
+
if (resolved.errors.length || !resolved.path || resolved.external) throw new Error(`Cannot enumerate client boundary exports from ${node.source.value} in ${file}. Use explicit named re-exports.`);
|
|
79
|
+
for (const name of await exportedNames(resolved.path, esbuild, seen)) if (name !== 'default') names.add(name);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// CommonJS is supported for npm and PRNext compatibility components. The default
|
|
83
|
+
// is module.exports; statically assigned properties are named exports as in Node.
|
|
84
|
+
function visit(node) {
|
|
85
|
+
if (!node || typeof node !== 'object') return;
|
|
86
|
+
if (t.isCallExpression(node) && t.isMemberExpression(node.callee) && t.isIdentifier(node.callee.object, { name: 'Object' }) && t.isIdentifier(node.callee.property, { name: 'defineProperty' }) && t.isIdentifier(node.arguments[0], { name: 'exports' }) && t.isStringLiteral(node.arguments[1]) && node.arguments[1].value !== '__esModule') {
|
|
87
|
+
names.add('default');
|
|
88
|
+
names.add(node.arguments[1].value);
|
|
89
|
+
}
|
|
90
|
+
if (t.isAssignmentExpression(node) && t.isMemberExpression(node.left)) {
|
|
91
|
+
const left = node.left;
|
|
92
|
+
const property = left.computed ? left.property.value : left.property.name;
|
|
93
|
+
if (t.isIdentifier(left.object, { name: 'module' }) && property === 'exports') {
|
|
94
|
+
names.add('default');
|
|
95
|
+
if (t.isObjectExpression(node.right)) for (const item of node.right.properties) {
|
|
96
|
+
const key = item.computed ? item.key?.value : item.key?.name ?? item.key?.value;
|
|
97
|
+
if (typeof key === 'string') names.add(key);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (t.isIdentifier(left.object, { name: 'exports' }) || (t.isMemberExpression(left.object) && t.isIdentifier(left.object.object, { name: 'module' }) && left.object.property.name === 'exports')) {
|
|
101
|
+
names.add('default');
|
|
102
|
+
if (typeof property === 'string') names.add(property);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
for (const key of t.VISITOR_KEYS[node.type] || []) {
|
|
106
|
+
const child = node[key];
|
|
107
|
+
if (Array.isArray(child)) child.forEach(visit); else visit(child);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
visit(ast.program);
|
|
111
|
+
names.delete('__esModule');
|
|
112
|
+
return names;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function boundaryEntry(boundary) {
|
|
116
|
+
// Explicit ESM exports also preserve named exports from CommonJS npm clients;
|
|
117
|
+
// a raw CJS entry would expose only `default` to the Flight module loader.
|
|
118
|
+
return `import * as client from ${JSON.stringify(boundary.file)};\n` + boundary.exports.map((name, index) => `const ref${index}=client[${JSON.stringify(name)}];export {ref${index} as ${JSON.stringify(name)}};`).join('\n');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function routeEntry(route, globalError) {
|
|
122
|
+
if (route.kind === 'api') return `export * from ${JSON.stringify(route.file)};`;
|
|
123
|
+
const imports = [`import * as page from ${JSON.stringify(route.file)};`, 'export {page};', `export const pageConfig=${JSON.stringify(route.pageConfig || {})};`];
|
|
124
|
+
const rootLayout = route.segments.find(segment => segment.layout)?.layout;
|
|
125
|
+
imports.push(`export const rootLayout=${JSON.stringify(rootLayout ? createHash('sha256').update(rootLayout).digest('hex').slice(0, 20) : undefined)};`);
|
|
126
|
+
if (route.staticMetadata) imports.push(`export const staticMetadata=${JSON.stringify(route.staticMetadata)};`);
|
|
127
|
+
if (route.metadataFiles?.length) {
|
|
128
|
+
const files = route.metadataFiles.map((item, index) => {
|
|
129
|
+
const { file, exports: names, ...descriptor } = item;
|
|
130
|
+
if (!file || !names.length) return JSON.stringify(descriptor);
|
|
131
|
+
const fields = names.map((name, field) => {
|
|
132
|
+
const binding = `metadata_${index}_${field}`;
|
|
133
|
+
imports.push(`import {${name} as ${binding}} from ${JSON.stringify(file)};`);
|
|
134
|
+
return `${name}:${binding}`;
|
|
135
|
+
});
|
|
136
|
+
return `{...${JSON.stringify(descriptor)},module:{${fields.join(',')}}}`;
|
|
137
|
+
});
|
|
138
|
+
imports.push(`export const metadataFiles=[${files.join(',')}];`);
|
|
139
|
+
}
|
|
140
|
+
if (globalError) imports.push(`export {default as GlobalError} from ${JSON.stringify(globalError)};`);
|
|
141
|
+
imports.push(`export {default as NavigationBoundary} from ${JSON.stringify(path.join(packageRoot, 'compat/app-navigation-boundary.cjs'))};`);
|
|
142
|
+
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'))};`);
|
|
144
|
+
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
|
+
if (route.routing) {
|
|
146
|
+
let index = 0;
|
|
147
|
+
function serialize(value) {
|
|
148
|
+
const files = [];
|
|
149
|
+
for (const [name, file] of Object.entries(value.files)) {
|
|
150
|
+
const binding = `routing_${index++}`;
|
|
151
|
+
imports.push(`import * as ${binding} from ${JSON.stringify(file)};`);
|
|
152
|
+
files.push(`${JSON.stringify(name)}:${binding}`);
|
|
153
|
+
}
|
|
154
|
+
return `{path:${JSON.stringify(value.path)},segment:${JSON.stringify(value.segment)},pattern:${JSON.stringify(value.pattern)},layoutId:${JSON.stringify(value.layoutId)},config:${JSON.stringify(value.config || {})},interception:${JSON.stringify(value.interception || null)},files:{${files.join(',')}},children:[${value.children.map(serialize).join(',')}],slots:{${Object.entries(value.slots).map(([name, child]) => `${JSON.stringify(name)}:${serialize(child)}`).join(',')}}}`;
|
|
155
|
+
}
|
|
156
|
+
const routing = serialize(route.routing);
|
|
157
|
+
imports.push(`export const routing=${routing};export const interceptionOnly=${!!route.interceptionOnly};`);
|
|
158
|
+
}
|
|
159
|
+
const segments = route.segments.map((segment, index) => {
|
|
160
|
+
const fields = [`segment:${JSON.stringify(segment.segment)}`, `path:${JSON.stringify(segment.path)}`, `staticConfig:${JSON.stringify(segment.staticConfig || {})}`];
|
|
161
|
+
for (const [name, file] of Object.entries(segment)) {
|
|
162
|
+
if (name === 'segment' || name === 'path' || name === 'staticConfig') continue;
|
|
163
|
+
const binding = `segment_${index}_${name}`;
|
|
164
|
+
imports.push(`import * as ${binding} from ${JSON.stringify(file)};`);
|
|
165
|
+
fields.push(`${name}:${binding}`);
|
|
166
|
+
}
|
|
167
|
+
return `{${fields.join(',')}}`;
|
|
168
|
+
});
|
|
169
|
+
return imports.join('\n') + `\nexport const segments=[${segments.join(',')}];\n`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Compile independent React Server and client graphs. The RSC graph contains only references to client boundaries. */
|
|
173
|
+
export async function compileApp({ project, stage, manifest, cssModules, fonts, images, dev, defineEnvironment = {}, productionBrowserSourceMaps = false, serverExternalPackages = [], moduleResolution = {} }) {
|
|
174
|
+
const refresh = dev ? createRefreshTransform(project.root) : undefined;
|
|
175
|
+
const { basePath = '', assetPrefix = '', assetBase = '/_prnext/assets', trailingSlash = false, skipTrailingSlashRedirect = false } = manifest.config || {};
|
|
176
|
+
const publicPath = assetBase + '/';
|
|
177
|
+
const routes = project.routes.filter(route => route.router === 'app');
|
|
178
|
+
if (project.appNotFound) {
|
|
179
|
+
const file = path.join(stage, '.entries', 'app-not-found.jsx');
|
|
180
|
+
await writeFile(file, `import {notFound} from 'next/navigation';export default function MissingRoute(){notFound()}`);
|
|
181
|
+
const segments = project.appNotFound.segments.map(segment => ({ ...segment }));
|
|
182
|
+
if (!segments[0].notFound) {
|
|
183
|
+
const fallback = path.join(stage, '.entries', 'app-default-not-found.jsx');
|
|
184
|
+
await writeFile(fallback, `export default function NotFound(){return <main style={{fontFamily:'system-ui,sans-serif',height:'100vh',display:'flex',alignItems:'center',justifyContent:'center'}}><title>404: This page could not be found.</title><h1 style={{fontSize:24,fontWeight:500,paddingRight:23,marginRight:20,borderRight:'1px solid #888'}}>404</h1><h2 style={{fontSize:14,fontWeight:400}}>This page could not be found.</h2></main>}`);
|
|
185
|
+
segments[0].notFound = fallback;
|
|
186
|
+
}
|
|
187
|
+
const id = 'app-page-internal-not-found';
|
|
188
|
+
routes.push({ ...project.appNotFound, segments, id, file, pattern: '/_not-found', kind: 'page', router: 'app', internal: true });
|
|
189
|
+
manifest.appNotFound = id;
|
|
190
|
+
}
|
|
191
|
+
if (!routes.length) return;
|
|
192
|
+
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.');
|
|
193
|
+
const actions = createServerActions({ projectRoot: project.root });
|
|
194
|
+
function actionTransform(source, file, mode, runtime = 'nodejs') {
|
|
195
|
+
const result = actions.transform(source, file, mode);
|
|
196
|
+
if (mode !== 'browser') for (const action of actions.actions.values()) if (action.file === file) {
|
|
197
|
+
(action.runtimes ||= new Set()).add(runtime);
|
|
198
|
+
}
|
|
199
|
+
return result;
|
|
200
|
+
}
|
|
201
|
+
const boundaries = new Map();
|
|
202
|
+
const cssFiles = new Set();
|
|
203
|
+
const globalErrorCssFiles = new Set();
|
|
204
|
+
const globalErrorOnlyCss = new Set();
|
|
205
|
+
const canonicalPaths = new Map();
|
|
206
|
+
const canonicalFile = file => {
|
|
207
|
+
if (!canonicalPaths.has(file)) canonicalPaths.set(file, realpath(file));
|
|
208
|
+
return canonicalPaths.get(file);
|
|
209
|
+
};
|
|
210
|
+
const cacheSegments = new Map();
|
|
211
|
+
for (const route of routes) {
|
|
212
|
+
if (route.kind === 'page') cacheSegments.set(await canonicalFile(route.file), 'page');
|
|
213
|
+
for (const segment of route.segments || []) if (segment.layout) cacheSegments.set(await canonicalFile(segment.layout), 'layout');
|
|
214
|
+
}
|
|
215
|
+
let browserStyleRevision;
|
|
216
|
+
const resolveFromProject = createRequire(path.join(project.root, 'package.json'));
|
|
217
|
+
await validateReactVersions(resolveFromProject);
|
|
218
|
+
const nativePackages = createNativePackages(project.root, { isClientSource: frameworkDirective });
|
|
219
|
+
const clientId = file => 'client-' + createHash('sha256').update(path.relative(project.root, file).replaceAll(path.sep, '/')).digest('hex').slice(0, 16);
|
|
220
|
+
// esbuild canonicalizes file paths before registering client boundaries.
|
|
221
|
+
// Use that same identity when the project or convention file is a symlink.
|
|
222
|
+
const globalError = routes.some(route => route.kind === 'page') && project.appGlobalError ? await canonicalFile(project.appGlobalError) : undefined;
|
|
223
|
+
const globalErrorId = globalError ? clientId(globalError) : undefined;
|
|
224
|
+
if (globalError && !clientDirective(await readFile(globalError, 'utf8'), globalError)) {
|
|
225
|
+
throw new Error('app/global-error must be a Client Component. Add the "use client" directive.');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function graphPlugin(mode, { styleTarget = cssFiles, omitGlobalStyles = true } = {}) {
|
|
229
|
+
const browser = mode === 'browser';
|
|
230
|
+
const rsc = mode === 'rsc';
|
|
231
|
+
return {
|
|
232
|
+
name: `prnext-app-${mode}`,
|
|
233
|
+
setup(esbuild) {
|
|
234
|
+
esbuild.onResolve({ filter: /^prnext-internal:use-cache$/ }, () => browser
|
|
235
|
+
? { errors: [{ text: "'use cache' cannot enter a Client Component graph" }] }
|
|
236
|
+
: { path: '../compat/use-cache.cjs', external: true });
|
|
237
|
+
if (browser && omitGlobalStyles) esbuild.onResolve({ filter: /\.(?:css|scss|sass)$/i }, async args => {
|
|
238
|
+
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;
|
|
239
|
+
const resolved = await esbuild.resolve(args.path, { resolveDir: args.resolveDir, kind: args.kind, pluginData: { prnextAppCss: true } });
|
|
240
|
+
if (!resolved.errors.length && !resolved.external && resolved.namespace === 'file' && globalErrorOnlyCss.has(await canonicalFile(resolved.path))) return { path: resolved.path, namespace: 'prnext-app-empty' };
|
|
241
|
+
});
|
|
242
|
+
esbuild.onResolve({ filter: /^prnext-internal:action-(?:crypto|client|ssr)$/ }, args => {
|
|
243
|
+
const helper = args.path.slice('prnext-internal:'.length);
|
|
244
|
+
if (browser && helper !== 'action-client') return { errors: [{ text: 'Server Action implementation cannot enter the browser graph.' }] };
|
|
245
|
+
return browser ? { path: path.join(packageRoot, 'runtime', helper + '.mjs') } : { path: '../runtime/' + helper + '.mjs', external: true };
|
|
246
|
+
});
|
|
247
|
+
esbuild.onResolve({ filter: frameworkRuntimeImportPattern }, args => {
|
|
248
|
+
const name = frameworkImportName(args.path);
|
|
249
|
+
if (!compatibility.has(name)) return { errors: [{ text: `PRNext does not implement ${args.path} yet (imported by ${args.importer}).` }] };
|
|
250
|
+
if (browser && ['headers', 'server', 'cache', 'og'].includes(name)) return { errors: [{ text: `${args.path} is server-only and cannot be imported by a Client Component.` }] };
|
|
251
|
+
const target = path.join(packageRoot, 'compat', (name === 'compat/router' ? 'compat-router' : name === 'dynamic' ? 'app-dynamic' : rsc && name === 'navigation' ? 'navigation-server' : name) + '.cjs');
|
|
252
|
+
if (!browser && (!rsc || !forcedClient.has(path.basename(target)))) return { path: '../compat/' + path.basename(target), external: true };
|
|
253
|
+
return { path: target };
|
|
254
|
+
});
|
|
255
|
+
// A single compatibility context must be shared with the normal React SSR
|
|
256
|
+
// runtime. Never bundle a second copy into each client SSR entry.
|
|
257
|
+
if (mode === 'ssr') esbuild.onResolve({ filter: /.*/ }, args => {
|
|
258
|
+
if (path.isAbsolute(args.path) && path.dirname(args.path) === path.join(packageRoot, 'compat')) return { path: '../compat/' + path.basename(args.path), external: true };
|
|
259
|
+
});
|
|
260
|
+
esbuild.onResolve({ filter: /^(?:server-only|client-only)$/ }, args => {
|
|
261
|
+
if (args.path === 'server-only' && browser) return { errors: [{ text: `A server-only module is reachable from a Client Component (${args.importer}).` }] };
|
|
262
|
+
if (args.path === 'client-only' && rsc) return { errors: [{ text: `A client-only module is reachable from a Server Component (${args.importer}). Add a 'use client' boundary.` }] };
|
|
263
|
+
return { path: args.path, namespace: 'prnext-app-empty' };
|
|
264
|
+
});
|
|
265
|
+
esbuild.onLoad({ filter: /.*/, namespace: 'prnext-app-empty' }, () => ({ contents: '', loader: 'js' }));
|
|
266
|
+
esbuild.onResolve({ filter: /^(?:react|react-dom|react-server-dom-webpack)(?:\/|$)/ }, args => {
|
|
267
|
+
if (args.pluginData?.prnextExportScan) return;
|
|
268
|
+
return browser ? { path: resolveFromProject.resolve(args.path) } : { path: args.path, external: true };
|
|
269
|
+
});
|
|
270
|
+
if (!browser) {
|
|
271
|
+
esbuild.onResolve({ filter: /.*/ }, args => {
|
|
272
|
+
if (builtins.has(args.path)) return { path: args.path, external: true };
|
|
273
|
+
});
|
|
274
|
+
esbuild.onResolve({ filter: /\.(?:css|scss|sass)$/i }, async args => {
|
|
275
|
+
if (args.pluginData?.prnextResolvingCss || args.pluginData?.prnextAppCss) return;
|
|
276
|
+
const resolved = await esbuild.resolve(args.path, { resolveDir: args.resolveDir, kind: args.kind, pluginData: { prnextAppCss: true, prnextResolvingCss: true } });
|
|
277
|
+
if (resolved.errors.length) return { errors: resolved.errors };
|
|
278
|
+
let file = resolved.path;
|
|
279
|
+
if (resolved.external) file = createRequire(path.join(args.resolveDir, '__prnext_css.cjs')).resolve(args.path);
|
|
280
|
+
file = await canonicalFile(file);
|
|
281
|
+
styleTarget.add(file);
|
|
282
|
+
if (!/\.module\.(?:css|scss|sass)$/i.test(args.path)) return { path: file, namespace: 'prnext-app-empty' };
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
esbuild.onLoad({ filter: /\.(?:[cm]?js|jsx|tsx?)$/, namespace: 'file' }, async args => {
|
|
286
|
+
if (browser && args.path === project.document) return { errors: [{ text: 'pages/_document is server-only and cannot be imported by a Client Component.' }] };
|
|
287
|
+
const source = await fonts.transform(await expandActionExports(args.prnextSource ?? await readFile(args.path, 'utf8'), args.path, async (specifier, importer) => {
|
|
288
|
+
const resolved = await esbuild.resolve(specifier, { resolveDir: path.dirname(importer), kind: 'import-statement', pluginData: { prnextExportScan: true } });
|
|
289
|
+
return resolved.errors.length || resolved.external || resolved.namespace !== 'file' ? undefined : resolved.path;
|
|
290
|
+
}), args.path);
|
|
291
|
+
const cacheSegment = cacheSegments.get(await canonicalFile(args.path)) || '';
|
|
292
|
+
const cachedSource = transformCacheComponents(source, args.path, { enabled: manifest.config.cacheComponents, handlers: manifest.config.cacheHandlers, projectRoot: project.root, mode, buildId: manifest.cacheId, segment: cacheSegment });
|
|
293
|
+
const actionSource = actionTransform(cachedSource, args.path, mode);
|
|
294
|
+
const builtin = path.dirname(args.path) === path.join(packageRoot, 'compat') && forcedClient.has(path.basename(args.path));
|
|
295
|
+
if (rsc && (builtin || clientDirective(source, args.path))) {
|
|
296
|
+
let boundary = boundaries.get(args.path);
|
|
297
|
+
if (!boundary) {
|
|
298
|
+
const exports = await exportedNames(args.path, esbuild);
|
|
299
|
+
boundary = { id: clientId(args.path), file: args.path, exports: [...exports].sort() };
|
|
300
|
+
boundaries.set(args.path, boundary);
|
|
301
|
+
}
|
|
302
|
+
const references = boundary.exports.map((name, index) => builtin && path.basename(args.path) === 'image.cjs' && name === 'getImageProps' ? `export {getImageProps} from ${JSON.stringify(path.join(packageRoot, 'compat/image-shared.cjs'))};` : `const ref${index}=registerClientReference(function(){throw new Error(${JSON.stringify(`Cannot call client export ${name} from a Server Component (${path.relative(project.root, args.path)}).`)});},${JSON.stringify(boundary.id)},${JSON.stringify(name)});export {ref${index} as ${JSON.stringify(name)}};`);
|
|
303
|
+
return { contents: `import {registerClientReference} from 'react-server-dom-webpack/server';\n${references.join('\n')}`, loader: 'js', resolveDir: path.dirname(args.path) };
|
|
304
|
+
}
|
|
305
|
+
let contents = transformDynamicImports(actionSource, args.path, { projectRoot: project.root, mode: rsc ? 'rsc' : browser ? 'browser' : 'server' });
|
|
306
|
+
if (browser && refresh) contents = await refresh(contents, args.path);
|
|
307
|
+
return { contents, loader: extensions[path.extname(args.path)] || 'jsx', resolveDir: path.dirname(args.path) };
|
|
308
|
+
});
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const common = {
|
|
314
|
+
absWorkingDir: project.root, bundle: true, logLevel: 'silent', jsx: 'automatic', loader: assetLoaders,
|
|
315
|
+
assetNames: '[name]-[hash]', publicPath: publicPath.slice(0, -1), metafile: true,
|
|
316
|
+
define: { 'process.env.NODE_ENV': JSON.stringify(dev ? 'development' : 'production'), ...defineEnvironment },
|
|
317
|
+
};
|
|
318
|
+
function edgeGraphPlugin(mode) {
|
|
319
|
+
const rsc = mode === 'rsc';
|
|
320
|
+
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']);
|
|
321
|
+
return { name: 'prnext-edge-app', setup(esbuild) {
|
|
322
|
+
esbuild.onResolve({filter:/^prnext-internal:action-(?:crypto|ssr)$/}, args => ({path:args.path.slice('prnext-internal:'.length),namespace:'prnext-edge-action'}));
|
|
323
|
+
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')}));
|
|
324
|
+
esbuild.onResolve({ filter: frameworkSubpathPattern }, args => {
|
|
325
|
+
let name = frameworkImportName(args.path);
|
|
326
|
+
if (name === 'dynamic') name = 'app-dynamic';
|
|
327
|
+
if (name === 'compat/router') name = 'compat-router';
|
|
328
|
+
if (!framework.has(name) || (rsc && name === 'navigation')) return;
|
|
329
|
+
return { path: path.join(packageRoot, 'compat', name + '.cjs'), ...(!rsc || name === 'app-dynamic' ? { namespace: 'prnext-edge-framework' } : {}) };
|
|
330
|
+
});
|
|
331
|
+
esbuild.onResolve({ filter: /.*/ }, args => {
|
|
332
|
+
if (!rsc && path.dirname(args.path) === path.join(packageRoot, 'compat') && framework.has(path.basename(args.path, '.cjs'))) return { path: args.path, namespace: 'prnext-edge-framework' };
|
|
333
|
+
});
|
|
334
|
+
esbuild.onResolve({ filter: /\.(?:css|scss|sass)$/i }, async args => {
|
|
335
|
+
if (args.pluginData?.prnextResolvingCss) return;
|
|
336
|
+
const resolved = await esbuild.resolve(args.path, { resolveDir: args.resolveDir, kind: args.kind, pluginData: { prnextResolvingCss: true } });
|
|
337
|
+
if (resolved.errors.length) return { errors: resolved.errors };
|
|
338
|
+
cssFiles.add(await canonicalFile(resolved.path));
|
|
339
|
+
// Server-only Edge components have no browser module that would import
|
|
340
|
+
// their CSS. Publish that stylesheet too, then let the module plugin
|
|
341
|
+
// supply the same class map to the VM and the browser stylesheet.
|
|
342
|
+
if (/\.module\.(?:css|scss|sass)$/i.test(args.path)) return;
|
|
343
|
+
return { path: resolved.path, namespace: 'prnext-edge-empty' };
|
|
344
|
+
});
|
|
345
|
+
esbuild.onLoad({ filter: /.*/, namespace: 'prnext-edge-framework' }, async args => {
|
|
346
|
+
const names = await exportedNames(args.path, esbuild), name = path.basename(args.path, '.cjs');
|
|
347
|
+
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') };
|
|
348
|
+
});
|
|
349
|
+
if (rsc) esbuild.onLoad({ filter: /\.(?:[cm]?js|jsx|tsx?)$/, namespace: 'file' }, async args => {
|
|
350
|
+
const source = args.prnextSource ?? await readFile(args.path, 'utf8');
|
|
351
|
+
const builtin = path.dirname(args.path) === path.join(packageRoot, 'compat') && forcedClient.has(path.basename(args.path));
|
|
352
|
+
if (!builtin && !clientDirective(source, args.path)) return;
|
|
353
|
+
let boundary = boundaries.get(args.path);
|
|
354
|
+
if (!boundary) {
|
|
355
|
+
boundary = { id: clientId(args.path), file: args.path, exports: [...await exportedNames(args.path, esbuild)].sort() };
|
|
356
|
+
boundaries.set(args.path, boundary);
|
|
357
|
+
}
|
|
358
|
+
boundary.edge = true;
|
|
359
|
+
return { loader: 'js', contents: `import {registerClientReference} from 'react-server-dom-webpack/server';\n` + boundary.exports.map((name, index) => `const ref${index}=registerClientReference(function(){throw new Error('Cannot call a Client Component export from the Edge server graph')},${JSON.stringify(boundary.id)},${JSON.stringify(name)});export {ref${index} as ${JSON.stringify(name)}};`).join('\n') };
|
|
360
|
+
});
|
|
361
|
+
} };
|
|
362
|
+
}
|
|
363
|
+
async function edgeAppBuild(file, outfile, mode, defaultExport = false) {
|
|
364
|
+
const result = await compileEdge({ file, outfile, projectRoot: project.root, dev, defineEnvironment, reactMode: mode, defaultExport,
|
|
365
|
+
plugins: [moduleResolutionPlugin(moduleResolution, project.root), edgeGraphPlugin(mode), images.plugin(), fonts.plugin(), cssModules.plugin(false)],
|
|
366
|
+
transformSource: async (source, file, builder) => actionTransform(await fonts.transform(await expandActionExports(source, file, async (specifier, importer) => {
|
|
367
|
+
const resolved = await builder.resolve(specifier, {resolveDir:path.dirname(importer),kind:'import-statement',pluginData:{prnextExportScan:true}});
|
|
368
|
+
return resolved.errors.length || resolved.external || resolved.namespace !== 'file' ? undefined : resolved.path;
|
|
369
|
+
}), file), file, mode, 'edge') });
|
|
370
|
+
fonts.record(result.metafile);
|
|
371
|
+
}
|
|
372
|
+
async function serverBuild(entryPoints, outdir, mode, styleTarget = cssFiles) {
|
|
373
|
+
if (!Object.keys(entryPoints).length) return;
|
|
374
|
+
let output;
|
|
375
|
+
for (;;) {
|
|
376
|
+
const nativeVersion = nativePackages.version;
|
|
377
|
+
try {
|
|
378
|
+
output = await bundle({ ...common, write: false, entryPoints, outdir: path.join(stage, outdir), outExtension: { '.js': '.mjs' }, platform: 'node', format: 'esm', splitting: true,
|
|
379
|
+
define:{...common.define,'process.env.NEXT_RUNTIME':'"nodejs"'},
|
|
380
|
+
chunkNames: 'chunk-[hash]', banner: { js: "import {createRequire as __prnextCreateRequire} from 'node:module';const require=__prnextCreateRequire(import.meta.url);" },
|
|
381
|
+
target: 'node22', conditions: mode === 'rsc' ? ['react-server', 'node'] : ['node'], sourcemap: dev,
|
|
382
|
+
plugins: [moduleResolutionPlugin(moduleResolution, project.root), images.plugin(), fonts.plugin(), graphPlugin(mode, { styleTarget }), npmPackagesPlugin({ projectRoot: project.root, serverExternalPackages }), nativePackages.plugin(mode), cssModules.plugin(false)],
|
|
383
|
+
});
|
|
384
|
+
if (nativePackages.version !== nativeVersion) {
|
|
385
|
+
nativePackages.assertCompatibleCss(cssFiles);
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
break;
|
|
389
|
+
} catch (error) {
|
|
390
|
+
if (nativePackages.version === nativeVersion) throw error;
|
|
391
|
+
nativePackages.assertCompatibleCss(cssFiles);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
fonts.record(output.metafile);
|
|
395
|
+
for (const file of output.outputFiles) {
|
|
396
|
+
await mkdir(path.dirname(file.path), { recursive: true });
|
|
397
|
+
await writeFile(file.path, file.contents);
|
|
398
|
+
}
|
|
399
|
+
const chunks = new Set(Object.keys(output.metafile.outputs).filter(file => file.endsWith('.mjs')).map(file => path.basename(file)));
|
|
400
|
+
for (const [file, metadata] of Object.entries(output.metafile.outputs)) {
|
|
401
|
+
const absolute = path.resolve(project.root, file);
|
|
402
|
+
if (file.endsWith('.mjs') && metadata.imports.some(item => !item.external)) await rewriteServerChunks(absolute, chunks, assetBase);
|
|
403
|
+
if (!metadata.entryPoint && !file.endsWith('.map') && !file.endsWith('.mjs')) await cp(absolute, path.join(stage, 'assets', path.basename(file)));
|
|
404
|
+
}
|
|
405
|
+
return output;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async function collectBrowserStyles(browserEntries) {
|
|
409
|
+
if (!globalErrorId) return;
|
|
410
|
+
const revision = `${boundaries.size}:${actions.actions.size}:${nativePackages.version}`;
|
|
411
|
+
if (browserStyleRevision === revision) return;
|
|
412
|
+
// SSR intentionally erases ssr:false loaders. Discover their dependencies
|
|
413
|
+
// in one browser graph, then classify styles by reachability from each root.
|
|
414
|
+
const discovery = await bundle({ ...common, write: false, entryPoints: browserEntries, outdir: path.join(stage, '.browser-styles'),
|
|
415
|
+
platform: 'browser', format: 'esm', target: ['es2022'], splitting: true,
|
|
416
|
+
define: { 'process.env': '{}', ...common.define },
|
|
417
|
+
plugins: [moduleResolutionPlugin(moduleResolution, project.root), ...(dev ? [devSingletonsPlugin()] : []), images.plugin(), fonts.plugin(), graphPlugin('browser', { omitGlobalStyles: false }), nativePackages.plugin('browser'), cssModules.plugin(true)] });
|
|
418
|
+
fonts.record(discovery.metafile);
|
|
419
|
+
const entryIds = new Map(await Promise.all(Object.entries(browserEntries).map(async ([id, file]) => [await canonicalFile(file), id])));
|
|
420
|
+
const globalRoots = [], normalRoots = [];
|
|
421
|
+
for (const output of Object.values(discovery.metafile.outputs)) {
|
|
422
|
+
if (!output.entryPoint) continue;
|
|
423
|
+
const id = entryIds.get(await canonicalFile(path.resolve(project.root, output.entryPoint)));
|
|
424
|
+
if (id) (id === globalErrorId ? globalRoots : normalRoots).push(output.entryPoint);
|
|
425
|
+
}
|
|
426
|
+
async function collect(roots, target) {
|
|
427
|
+
const seen = new Set(), pending = [...roots];
|
|
428
|
+
while (pending.length) {
|
|
429
|
+
const file = pending.pop();
|
|
430
|
+
if (seen.has(file)) continue;
|
|
431
|
+
seen.add(file);
|
|
432
|
+
const input = discovery.metafile.inputs[file];
|
|
433
|
+
if (!input) continue;
|
|
434
|
+
if (/\.(?:css|scss|sass)$/i.test(file)) {
|
|
435
|
+
const source = file.replace(/^prnext-(?:css-mapping|compiled-css):/, '');
|
|
436
|
+
target.add(await canonicalFile(path.resolve(project.root, source)));
|
|
437
|
+
// Keep @import/composes ordering inside its owning stylesheet, rather
|
|
438
|
+
// than flattening its children into additional standalone imports.
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
for (const dependency of input.imports) if (!dependency.external) pending.push(dependency.path);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
await Promise.all([collect(globalRoots, globalErrorCssFiles), collect(normalRoots, cssFiles)]);
|
|
445
|
+
browserStyleRevision = revision;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
await mkdir(path.join(stage, 'assets'), { recursive: true });
|
|
449
|
+
const entries = {};
|
|
450
|
+
for (const route of routes) {
|
|
451
|
+
const entry = path.join(stage, '.entries', route.id + '.server.mjs');
|
|
452
|
+
if ((route.handlerConfig?.runtime === 'edge' || route.cacheConfig?.runtime === 'edge') && manifest.config?.cacheComponents) throw new Error(`Cache Components cannot run in the Edge Runtime (${route.file}).`);
|
|
453
|
+
await writeFile(entry, routeEntry(route, globalError));
|
|
454
|
+
if (route.cacheConfig?.runtime === 'edge' && route.kind === 'page') await edgeAppBuild(entry, path.join(stage, 'server', route.id + '.mjs'), 'rsc');
|
|
455
|
+
else if (route.handlerConfig?.runtime === 'edge') await compileEdge({ file: route.file, outfile: path.join(stage, 'server', route.id + '.mjs'), projectRoot: project.root, dev, defineEnvironment });
|
|
456
|
+
else entries[route.id] = entry;
|
|
457
|
+
const instantConfigs = [route.pageConfig, ...(route.segments || []).map(segment=>segment.staticConfig)];
|
|
458
|
+
const collectInstant = node => {if(!node)return;instantConfigs.push(...Object.values(node.config || {}));for(const child of [...(node.children || []),...Object.values(node.slots || {})])collectInstant(child);};
|
|
459
|
+
collectInstant(route.routing);
|
|
460
|
+
manifest.routes.push({ id: route.id, pattern: route.pattern, kind: route.kind, router: 'app', module: `server/${route.id}.mjs`, css: [], ...(route.internal ? { internal: true } : {}),
|
|
461
|
+
...(route.parallel ? { parallel: true, interception: !!route.interception } : {}),
|
|
462
|
+
...(route.cacheConfig ? { cacheConfig: route.cacheConfig } : {}),
|
|
463
|
+
...(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)) }
|
|
464
|
+
: { handlerConfig: route.handlerConfig || {}, hasStaticParams: !!route.handlerConfig?.generateStaticParams }),
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
manifest.app = { clientModules: {}, actions: {}, actionKey: actions.actionKey };
|
|
468
|
+
const actionRevision = () => [...actions.actions.values()].map(action=>action.id+':'+[...(action.runtimes || [])].sort().join(',')).join(';');
|
|
469
|
+
for (;;) {
|
|
470
|
+
const revision = `${actionRevision()}:${boundaries.size}:${cssFiles.size}:${globalErrorCssFiles.size}`;
|
|
471
|
+
for (const action of actions.actions.values()) {
|
|
472
|
+
const entry = path.join(stage, '.entries', 'action-' + action.id + '.mjs');
|
|
473
|
+
await writeFile(entry, `export {${JSON.stringify(action.binding)} as invoke} from ${JSON.stringify(action.file)};`);
|
|
474
|
+
if (action.runtimes?.has('edge')) await edgeAppBuild(entry, path.join(stage, 'server', 'action-edge-' + action.id + '.mjs'), 'rsc');
|
|
475
|
+
if (!action.runtimes?.size || action.runtimes.has('nodejs')) entries['action-' + action.id] = entry;
|
|
476
|
+
}
|
|
477
|
+
await serverBuild(entries, 'server', 'rsc');
|
|
478
|
+
const ssrEntries = {};
|
|
479
|
+
const globalSsrEntries = {};
|
|
480
|
+
for (const boundary of boundaries.values()) {
|
|
481
|
+
const entry = path.join(stage, '.entries', boundary.id + '.ssr.mjs');
|
|
482
|
+
await writeFile(entry, boundaryEntry(boundary));
|
|
483
|
+
(boundary.id === globalErrorId ? globalSsrEntries : ssrEntries)[boundary.id] = entry;
|
|
484
|
+
}
|
|
485
|
+
await serverBuild(ssrEntries, 'app-ssr', 'ssr');
|
|
486
|
+
for (const boundary of boundaries.values()) if (boundary.edge) await edgeAppBuild(path.join(stage, '.entries', boundary.id + '.ssr.mjs'), path.join(stage, 'app-ssr-edge', boundary.id + '.mjs'), 'ssr', boundary.exports.includes('default'));
|
|
487
|
+
await serverBuild(globalSsrEntries, 'app-ssr', 'ssr', globalErrorCssFiles);
|
|
488
|
+
const browserEntries = {};
|
|
489
|
+
for (const boundary of boundaries.values()) {
|
|
490
|
+
const entry = path.join(stage, '.entries', boundary.id + '.browser.mjs');
|
|
491
|
+
await writeFile(entry, boundaryEntry(boundary));
|
|
492
|
+
browserEntries[boundary.id] = entry;
|
|
493
|
+
}
|
|
494
|
+
await collectBrowserStyles(browserEntries);
|
|
495
|
+
globalErrorOnlyCss.clear();
|
|
496
|
+
for (const file of globalErrorCssFiles) if (!cssFiles.has(file)) globalErrorOnlyCss.add(file);
|
|
497
|
+
if (routes.some(route => route.kind === 'page')) {
|
|
498
|
+
let globalErrorCss = [];
|
|
499
|
+
if (globalErrorCssFiles.size) {
|
|
500
|
+
const styleEntry = path.join(stage, '.entries', 'app-global-error-style.mjs');
|
|
501
|
+
await writeFile(styleEntry, [...globalErrorCssFiles].map(file => `import ${JSON.stringify(file)};`).join('\n'));
|
|
502
|
+
const styles = await bundle({ ...common, entryPoints: { 'app-global-error-style': styleEntry }, outdir: path.join(stage, 'assets'), entryNames: '[name]-[hash]',
|
|
503
|
+
platform: 'browser', format: 'esm', target: ['es2022'], minify: !dev, sourcemap: dev || productionBrowserSourceMaps,
|
|
504
|
+
plugins: [moduleResolutionPlugin(moduleResolution, project.root), images.plugin(), fonts.plugin(), graphPlugin('browser', { omitGlobalStyles: false }), cssModules.plugin(true)] });
|
|
505
|
+
globalErrorCss = Object.values(styles.metafile.outputs).filter(output => output.cssBundle).map(output => publicPath + path.basename(output.cssBundle));
|
|
506
|
+
}
|
|
507
|
+
if (globalErrorId) manifest.app.globalError = { id: globalErrorId, name: 'default', css: globalErrorCss };
|
|
508
|
+
const runtimeEntry = path.join(stage, '.entries', 'app-runtime.mjs');
|
|
509
|
+
const importers = Object.entries(browserEntries).map(([id, file]) => `${JSON.stringify(id)}:()=>import(${JSON.stringify(file)})`);
|
|
510
|
+
await writeFile(runtimeEntry, `${dev ? `import ${JSON.stringify(devClientFile)};\n` : ''}${[...cssFiles].map(file => `import ${JSON.stringify(file)};`).join('\n')}\nimport {bootstrapApp} from ${JSON.stringify(path.join(packageRoot, 'runtime/app-client.mjs'))};\nbootstrapApp({clientModules:{${importers.join(',')}},...${JSON.stringify({ basePath, assetPrefix, assetBase, trailingSlash, skipTrailingSlashRedirect, strictMode: manifest.config.reactStrictMode !== false, cacheComponents: manifest.config.cacheComponents === true, ...(globalErrorId ? { globalErrorId, globalErrorCss } : {}), ...(dev ? { dev: { buildId: manifest.buildId, clientManifest: manifest.devClient } } : {}) })}});\n`);
|
|
511
|
+
const clientEntries = { 'app-runtime': runtimeEntry, ...browserEntries };
|
|
512
|
+
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,
|
|
513
|
+
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: file => !globalErrorOnlyCss.has(file) })] });
|
|
514
|
+
fonts.record(browser.metafile);
|
|
515
|
+
const entryIds = new Map(Object.entries(clientEntries).map(([id, file]) => [file, id]));
|
|
516
|
+
const edgeBoundaries = new Set([...boundaries.values()].filter(boundary => boundary.edge).map(boundary => boundary.id));
|
|
517
|
+
const styles = new Set();
|
|
518
|
+
let client;
|
|
519
|
+
for (const [output, metadata] of Object.entries(browser.metafile.outputs)) {
|
|
520
|
+
if (metadata.cssBundle) styles.add(publicPath + path.basename(metadata.cssBundle));
|
|
521
|
+
if (!metadata.entryPoint || !output.endsWith('.js')) continue;
|
|
522
|
+
const id = entryIds.get(path.resolve(project.root, metadata.entryPoint));
|
|
523
|
+
if (id === 'app-runtime') { client = publicPath + path.basename(output); continue; }
|
|
524
|
+
if (!id) continue;
|
|
525
|
+
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) };
|
|
526
|
+
}
|
|
527
|
+
for (const route of manifest.routes) if (route.router === 'app' && route.kind === 'page') { route.client = client; route.css = [...styles]; }
|
|
528
|
+
}
|
|
529
|
+
manifest.app.actions = Object.fromEntries([...actions.actions.values()].map(action => [action.id, {
|
|
530
|
+
module: `server/action-${action.runtimes?.has('nodejs')?'':'edge-'}${action.id}.mjs`, export: 'invoke',
|
|
531
|
+
...(action.runtimes?.has('edge') ? {edgeModule:`server/action-edge-${action.id}.mjs`} : {}),
|
|
532
|
+
}]));
|
|
533
|
+
if (revision === `${actionRevision()}:${boundaries.size}:${cssFiles.size}:${globalErrorCssFiles.size}`) break;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import {generateMapped} from './source-maps.mjs';
|
|
2
|
+
import { parse } from '@babel/parser';
|
|
3
|
+
import traverseModule from '@babel/traverse';
|
|
4
|
+
import generateModule from '@babel/generator';
|
|
5
|
+
import * as t from '@babel/types';
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { cachedTransform } from './transform-cache.mjs';
|
|
9
|
+
const traverse = traverseModule.default || traverseModule;
|
|
10
|
+
const generate = generateModule.default || generateModule;
|
|
11
|
+
const cacheDirective = directive => /^use cache(?:$|:)/.test(directive.value.value);
|
|
12
|
+
|
|
13
|
+
export function transformCacheComponents(source, file, { enabled, projectRoot, mode = 'rsc', buildId, segment: configuredSegment, handlers = {} }) {
|
|
14
|
+
if (!source.includes('use cache')) return source;
|
|
15
|
+
return cachedTransform('cache-components',source,[file,enabled,projectRoot,mode,buildId,configuredSegment,handlers],()=>transformUncached(source,file,{enabled,projectRoot,mode,buildId,configuredSegment,handlers}));
|
|
16
|
+
}
|
|
17
|
+
function transformUncached(source,file,{enabled,projectRoot,mode,buildId,configuredSegment,handlers}) {
|
|
18
|
+
const ast = parse(source, { sourceType: 'unambiguous', sourceFilename: file, plugins: ['jsx', ...(/\.tsx?$/.test(file) ? ['typescript'] : [])] });
|
|
19
|
+
let program;
|
|
20
|
+
traverse(ast, { Program(value) { program = value; value.stop(); } });
|
|
21
|
+
const moduleDirective = ast.program.directives.find(cacheDirective);
|
|
22
|
+
const client = ast.program.directives.some(item => item.value.value === 'use client');
|
|
23
|
+
const exported = new Set();
|
|
24
|
+
const defaultExport = ast.program.body.find(node => t.isExportDefaultDeclaration(node))?.declaration;
|
|
25
|
+
const defaultName = t.isIdentifier(defaultExport) ? defaultExport.name : undefined;
|
|
26
|
+
if (moduleDirective && defaultName) exported.add(defaultName);
|
|
27
|
+
if (moduleDirective) for (const statement of ast.program.body) {
|
|
28
|
+
if (t.isExportNamedDeclaration(statement)) {
|
|
29
|
+
for (const name of Object.keys(t.getOuterBindingIdentifiers(statement.declaration || {}))) exported.add(name);
|
|
30
|
+
for (const specifier of statement.specifiers) if (specifier.local) exported.add(specifier.local.name);
|
|
31
|
+
if (statement.source && statement.exportKind !== 'type') throw new Error(`A 'use cache' module must define its cached exports locally (${file}).`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (moduleDirective) {
|
|
35
|
+
for (const name of exported) {
|
|
36
|
+
const binding = program.scope.getBinding(name);
|
|
37
|
+
let node = binding?.path.node;
|
|
38
|
+
if (t.isVariableDeclarator(node)) node = node.init;
|
|
39
|
+
if (!t.isFunction(node) || !node.async) throw new Error(`A 'use cache' module must export async functions (${name} in ${file}).`);
|
|
40
|
+
}
|
|
41
|
+
if (defaultExport && !defaultName && (!t.isFunction(defaultExport) || !defaultExport.async)) throw new Error(`A 'use cache' module must export async functions (${file}).`);
|
|
42
|
+
}
|
|
43
|
+
const helper = program.scope.generateUidIdentifier('prnextCache');
|
|
44
|
+
let transformed = false;
|
|
45
|
+
traverse(ast, { Function: { exit(fn) {
|
|
46
|
+
const own = fn.node.body.directives?.find(cacheDirective);
|
|
47
|
+
const name = fn.node.id?.name || (fn.parentPath.isVariableDeclarator() ? fn.parentPath.node.id.name : undefined);
|
|
48
|
+
const directive = own || (moduleDirective && (exported.has(name) || fn.parentPath.isExportDefaultDeclaration()) ? moduleDirective : null);
|
|
49
|
+
if (!directive) return;
|
|
50
|
+
if (!enabled) throw new Error(`'use cache' requires cacheComponents:true in the project configuration (${file}).`);
|
|
51
|
+
if (client || mode === 'browser' || mode === 'ssr') throw new Error(`'use cache' is server-only and cannot be defined in a Client Component (${file}).`);
|
|
52
|
+
if (!fn.node.async || fn.node.generator || fn.isObjectMethod() || fn.isClassMethod() || fn.isClassPrivateMethod()) throw new Error(`'use cache' requires an async function (${file}).`);
|
|
53
|
+
const kind = directive.value.value === 'use cache' ? 'default' : directive.value.value.slice('use cache:'.length).trim();
|
|
54
|
+
if (!['default', 'remote', 'private'].includes(kind) && !Object.hasOwn(handlers, kind)) throw new Error(`Unsupported cache directive ${directive.value.value}; configure cacheHandlers.${kind} (${file}).`);
|
|
55
|
+
const captures = new Map();
|
|
56
|
+
fn.traverse({ ReferencedIdentifier(ref) {
|
|
57
|
+
if (ref.node.name === 'arguments' && fn.isArrowFunctionExpression()) {
|
|
58
|
+
let owner = ref.getFunctionParent();
|
|
59
|
+
while (owner?.isArrowFunctionExpression()) owner = owner.getFunctionParent();
|
|
60
|
+
if (!owner || !owner.findParent(parent => parent === fn)) throw new Error(`'use cache' arrows cannot capture lexical arguments; pass an explicit array parameter (${file}).`);
|
|
61
|
+
}
|
|
62
|
+
const binding = ref.scope.getBinding(ref.node.name);
|
|
63
|
+
if (!binding || binding.scope === program.scope || binding.scope === fn.scope || binding.path === fn || binding.scope.path.findParent(parent => parent === fn)) return;
|
|
64
|
+
captures.set(ref.node.name, binding);
|
|
65
|
+
}, ThisExpression() { throw new Error(`'use cache' functions cannot depend on this (${file}).`); },
|
|
66
|
+
'AssignmentExpression|UpdateExpression'(ref) {
|
|
67
|
+
for (const name of Object.keys(t.getBindingIdentifiers(ref.node.left || ref.node.argument))) {
|
|
68
|
+
const binding = ref.scope.getBinding(name);
|
|
69
|
+
if (binding && binding.scope !== fn.scope && binding.scope !== program.scope && !binding.scope.path.findParent(parent => parent === fn)) throw new Error(`'use cache' cannot mutate captured variable ${name} (${file}).`);
|
|
70
|
+
}
|
|
71
|
+
} });
|
|
72
|
+
if (fn.node.body.directives) fn.node.body.directives = fn.node.body.directives.filter(item => !cacheDirective(item));
|
|
73
|
+
const id = createHash('sha256').update(JSON.stringify([buildId, path.relative(projectRoot, file), fn.node.start, kind])).digest('hex');
|
|
74
|
+
const args = fn.scope.generateUidIdentifier('cacheArgs');
|
|
75
|
+
const decoded = fn.scope.generateUidIdentifier('decodedArgs');
|
|
76
|
+
const captured = fn.scope.generateUidIdentifier('capturedArgs');
|
|
77
|
+
const original = t.functionExpression(null, fn.node.params, t.isBlockStatement(fn.node.body) ? fn.node.body : t.blockStatement([t.returnStatement(fn.node.body)]), false, true);
|
|
78
|
+
const segment = configuredSegment ?? /(?:^|[/\\])(page|layout)\.[cm]?[jt]sx?$/.exec(file)?.[1];
|
|
79
|
+
const pageFunction = segment && (fn.parentPath.isExportDefaultDeclaration() || name && name === defaultName);
|
|
80
|
+
const body = [];
|
|
81
|
+
if (captures.size) body.push(t.variableDeclaration('const', [t.variableDeclarator(t.arrayPattern([...captures.keys()].map(name => t.identifier(name))), captured)]));
|
|
82
|
+
body.push(t.returnStatement(t.callExpression(t.memberExpression(original, t.identifier('apply')), [t.identifier('undefined'), decoded])));
|
|
83
|
+
fn.node.params = [t.restElement(args)];
|
|
84
|
+
fn.node.body = t.blockStatement([t.returnStatement(t.callExpression(helper, [t.stringLiteral(id), t.stringLiteral(kind), t.arrayExpression([...captures.keys()].map(name => t.identifier(name))), args, t.arrowFunctionExpression([captured, decoded], t.blockStatement(body), true), t.stringLiteral(pageFunction ? segment : '')]))]);
|
|
85
|
+
transformed = true;
|
|
86
|
+
} } });
|
|
87
|
+
if (!transformed) return source;
|
|
88
|
+
ast.program.directives = ast.program.directives.filter(item => !cacheDirective(item));
|
|
89
|
+
ast.program.body.unshift(t.importDeclaration([t.importSpecifier(helper, t.identifier('invokeCache'))], t.stringLiteral('prnext-internal:use-cache')));
|
|
90
|
+
return generateMapped(generate, ast, { retainLines: true }, source, file);
|
|
91
|
+
}
|