@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/scan.mjs
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { access } from 'node:fs/promises';
|
|
2
|
+
import {readDirectory} from './directory-cache.mjs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { validateAppConfig, mergeAppConfig } from './app-config.mjs';
|
|
6
|
+
import { scanMiddleware } from './middleware.mjs';
|
|
7
|
+
import { scanMetadataRoutes } from './metadata-routes.mjs';
|
|
8
|
+
import { scanAdvancedAppRoutes } from './app-routing.mjs';
|
|
9
|
+
import { pageExtensionPattern, validatePageSource } from './page-extensions.mjs';
|
|
10
|
+
|
|
11
|
+
const sourceExtension = /\.(?:[cm]?js|jsx|tsx?)$/;
|
|
12
|
+
|
|
13
|
+
async function exists(file) {
|
|
14
|
+
try { await access(file); return true; } catch { return false; }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function walk(directory) {
|
|
18
|
+
const files = [];
|
|
19
|
+
for (const item of await readDirectory(directory)) {
|
|
20
|
+
if (item.name.startsWith('.')) continue;
|
|
21
|
+
const file = path.join(directory, item.name);
|
|
22
|
+
if (item.isDirectory()) files.push(...await walk(file));
|
|
23
|
+
else if (item.isFile()) files.push(file);
|
|
24
|
+
}
|
|
25
|
+
return files.sort();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function routePattern(relativeFile, extensionPattern = sourceExtension) {
|
|
29
|
+
const segments = relativeFile.replaceAll(path.sep, '/').replace(extensionPattern, '').split('/');
|
|
30
|
+
if (segments.at(-1) === 'index') segments.pop();
|
|
31
|
+
return validatePattern(segments, relativeFile);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function validatePattern(segments, relativeFile) {
|
|
35
|
+
const names = new Set();
|
|
36
|
+
for (const [index, segment] of segments.entries()) {
|
|
37
|
+
if (!segment.includes('[') && !segment.includes(']')) continue;
|
|
38
|
+
const match = /^(?:\[([A-Za-z_$][\w$]*)\]|\[\.\.\.([A-Za-z_$][\w$]*)\]|\[\[\.\.\.([A-Za-z_$][\w$]*)\]\])$/.exec(segment);
|
|
39
|
+
if (!match) throw new Error(`Unsupported route segment "${segment}" in ${relativeFile}. Use [id], [...slug], or [[...slug]].`);
|
|
40
|
+
const name = match[1] || match[2] || match[3];
|
|
41
|
+
if (names.has(name)) throw new Error(`Duplicate parameter "${name}" in ${relativeFile}.`);
|
|
42
|
+
names.add(name);
|
|
43
|
+
if ((match[2] || match[3]) && index !== segments.length - 1) {
|
|
44
|
+
throw new Error(`Catch-all parameter must be the final segment in ${relativeFile}.`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const pattern = '/' + segments.join('/');
|
|
48
|
+
if (pattern === '/_prnext' || pattern.startsWith('/_prnext/')) {
|
|
49
|
+
throw new Error(`Route ${pattern} uses the reserved /_prnext namespace.`);
|
|
50
|
+
}
|
|
51
|
+
return pattern;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function scanProject(projectRoot, { basePath = '', pageExtensions, i18n } = {}) {
|
|
55
|
+
const sourceExtension = pageExtensionPattern(pageExtensions);
|
|
56
|
+
const root = path.resolve(projectRoot);
|
|
57
|
+
const middleware = await scanMiddleware(root, { basePath, pageExtensions, i18n });
|
|
58
|
+
const roots = [];
|
|
59
|
+
for (const name of ['pages', 'src/pages']) if (await exists(path.join(root, name))) roots.push(path.join(root, name));
|
|
60
|
+
if (roots.length > 1) throw new Error('Both pages/ and src/pages/ exist. Keep exactly one Pages Router directory.');
|
|
61
|
+
const appRoots = [];
|
|
62
|
+
for (const name of ['app', 'src/app']) if (await exists(path.join(root, name))) appRoots.push(path.join(root, name));
|
|
63
|
+
if (appRoots.length > 1) throw new Error('Both app/ and src/app/ exist. Keep exactly one App Router directory.');
|
|
64
|
+
if (!roots.length && !appRoots.length) throw new Error('No pages/, src/pages/, app/, or src/app/ directory found. Create app/page.tsx or pages/index.tsx to start.');
|
|
65
|
+
const pageRoot = roots[0];
|
|
66
|
+
const files = (pageRoot ? await walk(pageRoot) : []).filter(file => sourceExtension.test(file) && !file.endsWith('.d.ts'));
|
|
67
|
+
const routes = [];
|
|
68
|
+
const patterns = new Map();
|
|
69
|
+
const pagesErrors = {};
|
|
70
|
+
let app;
|
|
71
|
+
let document;
|
|
72
|
+
for (const file of files) {
|
|
73
|
+
validatePageSource(file);
|
|
74
|
+
const relative = path.relative(pageRoot, file).replaceAll(path.sep, '/');
|
|
75
|
+
const name = relative.replace(sourceExtension, '');
|
|
76
|
+
if (name === '_app') {
|
|
77
|
+
if (app) throw new Error('Multiple _app files found. Keep only one.');
|
|
78
|
+
app = file;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (name === '_document') {
|
|
82
|
+
if (document) throw new Error('Multiple _document files found. Keep only one.');
|
|
83
|
+
document = file;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const pattern = routePattern(relative, sourceExtension);
|
|
87
|
+
const signature = pattern.replace(/\[\[\.\.\.[^\]]+\]\]/g, '[[...]]').replace(/\[\.\.\.[^\]]+\]/g, '[...]').replace(/\[[^\]]+\]/g, '[]');
|
|
88
|
+
if (patterns.has(signature)) throw new Error(`Conflicting routes: ${patterns.get(signature)} and ${relative} both match ${pattern}.`);
|
|
89
|
+
patterns.set(signature, relative);
|
|
90
|
+
const kind = relative.startsWith('api/') ? 'api' : 'page';
|
|
91
|
+
const id = `${kind}-${createHash('sha256').update(pattern).digest('hex').slice(0, 12)}`;
|
|
92
|
+
const errorKind = name === '_error' ? 'error' : pattern === '/404' ? 'notFound' : pattern === '/500' ? 'serverError' : undefined;
|
|
93
|
+
if (errorKind) pagesErrors[errorKind] = id;
|
|
94
|
+
routes.push({ id, pattern, kind, file,
|
|
95
|
+
...(errorKind === 'error' ? { internal: true } : errorKind ? { errorStatus: pattern === '/404' ? 404 : 500 } : {}),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
const appRoot = appRoots[0];
|
|
99
|
+
const { appNotFound, appGlobalError } = appRoot ? await scanAppRoutes({ root, appRoot, routes, patterns, sourceExtension }) : {};
|
|
100
|
+
const metadataManifest = appRoot ? await scanMetadataRoutes({ root, appRoot, files: await walk(appRoot), routes, patterns, validatePattern, basePath, pageExtensions, appNotFound }) : undefined;
|
|
101
|
+
if (metadataManifest) for (const route of routes) if (route.router === 'app' && route.kind === 'page') route.staticMetadata = { manifest: metadataManifest };
|
|
102
|
+
if (!routes.length && !appNotFound) throw new Error('No page or API route found in the Pages Router or App Router directories.');
|
|
103
|
+
return { root, pageRoot, appRoot, app, document, routes, ...(appNotFound ? { appNotFound } : {}), ...(appGlobalError ? { appGlobalError } : {}), ...(Object.keys(pagesErrors).length ? { pagesErrors } : {}), ...(middleware ? { middleware } : {}) };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const appConventions = new Map([
|
|
107
|
+
['page', 'page'], ['route', 'route'], ['layout', 'layout'], ['template', 'template'],
|
|
108
|
+
['loading', 'loading'], ['error', 'error'], ['not-found', 'notFound'], ['default', 'default'],
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
async function scanAppRoutes({ root, appRoot, routes, patterns, sourceExtension }) {
|
|
112
|
+
const directories = new Map();
|
|
113
|
+
const configs = new Map();
|
|
114
|
+
let appGlobalError;
|
|
115
|
+
for (const file of await walk(appRoot)) {
|
|
116
|
+
if (!sourceExtension.test(file) || file.endsWith('.d.ts')) continue;
|
|
117
|
+
const relative = path.relative(appRoot, file).replaceAll(path.sep, '/');
|
|
118
|
+
const parts = relative.split('/');
|
|
119
|
+
const filename = parts.pop().replace(sourceExtension, '');
|
|
120
|
+
if (parts.some(segment => segment.startsWith('_'))) continue;
|
|
121
|
+
if (filename === 'global-error') {
|
|
122
|
+
// This convention replaces the complete root document. A similarly named
|
|
123
|
+
// file inside a route group or segment is not a nested error boundary.
|
|
124
|
+
if (parts.length === 0) {
|
|
125
|
+
if (appGlobalError) throw new Error('Multiple global-error files in the App Router root. Keep only one.');
|
|
126
|
+
appGlobalError = file;
|
|
127
|
+
}
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (!appConventions.has(filename)) continue;
|
|
131
|
+
validatePageSource(file);
|
|
132
|
+
if (['page', 'layout', 'route'].includes(filename)) configs.set(file, await validateAppConfig(file));
|
|
133
|
+
const directory = parts.join('/');
|
|
134
|
+
const descriptor = directories.get(directory) || { directory, files: {} };
|
|
135
|
+
const name = appConventions.get(filename);
|
|
136
|
+
if (descriptor.files[name]) throw new Error(`Multiple ${filename} files in App Router directory ${directory || '/'}. Keep only one file for each convention.`);
|
|
137
|
+
descriptor.files[name] = file;
|
|
138
|
+
directories.set(directory, descriptor);
|
|
139
|
+
}
|
|
140
|
+
const advanced = [...directories.keys()].some(directory => directory.split('/').some(segment => segment.startsWith('@') || /^\(\.{1,3}\)/.test(segment)));
|
|
141
|
+
if (advanced) scanAdvancedAppRoutes({ root, appRoot, directories, configs, routes, patterns, validatePattern });
|
|
142
|
+
for (const { directory, files } of advanced ? [] : directories.values()) {
|
|
143
|
+
if (!files.page && !files.route) continue;
|
|
144
|
+
if (files.page && files.route) throw new Error(`Conflicting routes: App Router page and route handler both occupy ${directory || '/'}.`);
|
|
145
|
+
const parts = directory ? directory.split('/') : [];
|
|
146
|
+
const urlParts = parts.filter(segment => !/^\([^()]+\)$/.test(segment)).map(segment => segment.replace(/^%5f/i, '_'));
|
|
147
|
+
const pattern = validatePattern(urlParts, path.relative(root, files.page || files.route));
|
|
148
|
+
const signature = pattern.replace(/\[\[\.\.\.[^\]]+\]\]/g, '[[...]]').replace(/\[\.\.\.[^\]]+\]/g, '[...]').replace(/\[[^\]]+\]/g, '[]');
|
|
149
|
+
const label = path.relative(root, files.page || files.route);
|
|
150
|
+
if (patterns.has(signature)) throw new Error(`Conflicting routes: ${patterns.get(signature)} and ${label} both match ${pattern} (App Router).`);
|
|
151
|
+
patterns.set(signature, label);
|
|
152
|
+
const kind = files.page ? 'page' : 'api';
|
|
153
|
+
const id = `app-${kind}-${createHash('sha256').update(pattern).digest('hex').slice(0, 12)}`;
|
|
154
|
+
const segments = [];
|
|
155
|
+
for (let index = 0; index <= parts.length; index++) {
|
|
156
|
+
const directory = parts.slice(0, index).join('/');
|
|
157
|
+
const descriptor = directories.get(directory);
|
|
158
|
+
const { page, route, ...conventions } = descriptor?.files || {};
|
|
159
|
+
segments.push({ segment: index ? parts[index - 1] : '', path: directory, ...conventions,
|
|
160
|
+
...(conventions.layout ? { staticConfig: configs.get(conventions.layout) || {} } : {}),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
if (kind === 'page' && !segments.some(segment => segment.layout)) throw new Error(`App Router page ${label} needs a root layout. Create app/layout.tsx with <html> and <body>.`);
|
|
164
|
+
const configFiles = kind === 'page' ? [...segments.map(segment => segment.layout).filter(Boolean), files.page] : [files.route];
|
|
165
|
+
const cacheConfig = mergeAppConfig(configFiles.map(file => configs.get(file) || {}));
|
|
166
|
+
routes.push({ id, pattern, kind, router: 'app', file: files.page || files.route, cacheConfig,
|
|
167
|
+
...(kind === 'page' ? { segments, pageConfig: configs.get(files.page) || {} } : { handlerConfig: configs.get(files.route) || {} }),
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
const rootDescriptor = directories.get('');
|
|
171
|
+
if (rootDescriptor?.files.layout) {
|
|
172
|
+
if (routes.some(route => route.pattern === '/_not-found')) throw new Error('/_not-found is reserved for the App Router not-found entry.');
|
|
173
|
+
const { page, route, ...conventions } = rootDescriptor.files;
|
|
174
|
+
return { appGlobalError, appNotFound: { segments: [{ segment: '', path: '', ...conventions, staticConfig: configs.get(conventions.layout) || {} }],
|
|
175
|
+
cacheConfig: mergeAppConfig([configs.get(conventions.layout) || {}]), pageConfig: {} } };
|
|
176
|
+
}
|
|
177
|
+
return { appGlobalError };
|
|
178
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
|
|
5
|
+
/** Partytown is supplied by the application, just like Next's experimental integration. */
|
|
6
|
+
export async function prepareScriptWorkers({ projectRoot, stage, assetBase }) {
|
|
7
|
+
const resolve = createRequire(path.join(projectRoot, 'package.json'));
|
|
8
|
+
let integrationPath, utilitiesPath;
|
|
9
|
+
try {
|
|
10
|
+
integrationPath = resolve.resolve('@builder.io/partytown/integration');
|
|
11
|
+
utilitiesPath = resolve.resolve('@builder.io/partytown/utils');
|
|
12
|
+
} catch (cause) {
|
|
13
|
+
throw new Error('experimental.nextScriptWorkers requires @builder.io/partytown in the application. Install it with npm install --save-dev @builder.io/partytown, or disable this flag.', { cause });
|
|
14
|
+
}
|
|
15
|
+
const [integration, utilities] = await Promise.all([import(pathToFileURL(integrationPath).href), import(pathToFileURL(utilitiesPath).href)]);
|
|
16
|
+
const partytownSnippet = (integration.default || integration).partytownSnippet;
|
|
17
|
+
const copyLibFiles = (utilities.default || utilities).copyLibFiles;
|
|
18
|
+
if (typeof partytownSnippet !== 'function' || typeof copyLibFiles !== 'function') {
|
|
19
|
+
throw new Error('The installed @builder.io/partytown must export integration.partytownSnippet and utils.copyLibFiles.');
|
|
20
|
+
}
|
|
21
|
+
const snippet = partytownSnippet();
|
|
22
|
+
if (typeof snippet !== 'string' || !snippet) throw new Error('@builder.io/partytown returned an invalid bootstrap snippet.');
|
|
23
|
+
await copyLibFiles(path.join(stage, 'assets', '~partytown'));
|
|
24
|
+
return { lib: assetBase + '/~partytown/', snippet };
|
|
25
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export function validateServerActions(input) {
|
|
2
|
+
if (input === undefined || input === true) input = {};
|
|
3
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) throw new TypeError('experimental.serverActions must be an object');
|
|
4
|
+
for (const name of Object.keys(input)) if (!['allowedOrigins', 'bodySizeLimit'].includes(name)) throw new TypeError(`experimental.serverActions.${name} is not implemented`);
|
|
5
|
+
const origins = input.allowedOrigins ?? [];
|
|
6
|
+
if (!Array.isArray(origins) || origins.length > 128) throw new TypeError('serverActions.allowedOrigins must contain at most 128 host patterns');
|
|
7
|
+
for (const origin of origins) {
|
|
8
|
+
if (typeof origin !== 'string' || !origin || origin.length > 256 || /[^\x21-\x7e]|[/\\@?#]/.test(origin)) throw new TypeError('serverActions.allowedOrigins expects host names with optional ports, not URLs');
|
|
9
|
+
if (origin.startsWith('[')) {
|
|
10
|
+
try { const url = new URL('http://' + origin); if (url.host !== origin.toLowerCase() && url.port !== '') throw new Error(); }
|
|
11
|
+
catch { throw new TypeError(`Invalid serverActions.allowedOrigins host: ${origin}`); }
|
|
12
|
+
} else {
|
|
13
|
+
const match = /^([^:]+)(?::([0-9]+))?$/.exec(origin);
|
|
14
|
+
if (!match || match[2] && (Number(match[2]) > 65535 || Number(match[2]) < 1)) throw new TypeError(`Invalid serverActions.allowedOrigins host: ${origin}`);
|
|
15
|
+
const labels = match[1].split('.');
|
|
16
|
+
if (labels.some((part, index) => !/^[a-zA-Z0-9_-]+$/.test(part) && part !== '*' && !(part === '**' && index === 0)) || labels.length === 1 && labels[0].includes('*')) throw new TypeError(`Invalid serverActions.allowedOrigins pattern: ${origin}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
let bytes = input.bodySizeLimit ?? 1024 * 1024;
|
|
20
|
+
if (typeof bytes === 'string') {
|
|
21
|
+
const match = /^(\d+(?:\.\d+)?)\s*(b|kb|mb|gb|tb|pb)?$/i.exec(bytes.trim());
|
|
22
|
+
if (!match) throw new TypeError('serverActions.bodySizeLimit expects bytes or a size such as "2mb"');
|
|
23
|
+
bytes = Math.floor(Number(match[1]) * 1024 ** Math.max(0, ['b', 'kb', 'mb', 'gb', 'tb', 'pb'].indexOf((match[2] || 'b').toLowerCase())));
|
|
24
|
+
}
|
|
25
|
+
if (!Number.isSafeInteger(bytes) || bytes < 1 || bytes > 8 * 1024 * 1024) throw new TypeError('serverActions.bodySizeLimit must be between 1 byte and the 8 MiB request transport limit');
|
|
26
|
+
return { allowedOrigins: [...new Set(origins.map(origin => origin.toLowerCase()))], bodySizeLimit: bytes };
|
|
27
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import {AsyncLocalStorage} from 'node:async_hooks';
|
|
2
|
+
const scope = new AsyncLocalStorage();
|
|
3
|
+
export const withSourceMaps = callback => scope.run({enabled:false},callback);
|
|
4
|
+
export const configureSourceMaps = enabled => {if(scope.getStore())scope.getStore().enabled=!!enabled;};
|
|
5
|
+
export const sourceMapsEnabled = () => scope.getStore()?.enabled === true;
|
|
6
|
+
const directive = /(?:\/\/[#@]\s*sourceMappingURL=([^\s]+)|\/\*[#@]\s*sourceMappingURL=([^*]+)\*\/)/g;
|
|
7
|
+
|
|
8
|
+
export function extractSourceMap(source) {
|
|
9
|
+
let map;
|
|
10
|
+
const code=source.replace(directive,(comment,line,block)=>{
|
|
11
|
+
const url=(line || block).trim();
|
|
12
|
+
if(!url.startsWith('data:application/json'))return comment;
|
|
13
|
+
const comma=url.indexOf(',');
|
|
14
|
+
try {map=JSON.parse(url.slice(0,comma).includes(';base64')?Buffer.from(url.slice(comma+1),'base64').toString():decodeURIComponent(url.slice(comma+1)));}
|
|
15
|
+
catch {throw new Error('Invalid inline source map');}
|
|
16
|
+
// Keep original line coordinates while discarding the stale directive.
|
|
17
|
+
return line ? '' : comment.replace(/[^\r\n]/g,' ');
|
|
18
|
+
});
|
|
19
|
+
return {code,map};
|
|
20
|
+
}
|
|
21
|
+
export function inlineSourceMap(source,map) {
|
|
22
|
+
if(!map)return source;
|
|
23
|
+
const clean=extractSourceMap(source).code;
|
|
24
|
+
return clean+'\n//# sourceMappingURL=data:application/json;base64,'+Buffer.from(JSON.stringify(typeof map==='string'?JSON.parse(map):map)).toString('base64');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Babel composes the new locations directly with the previous transform map. */
|
|
28
|
+
export function generateMapped(generate,ast,options,source,file) {
|
|
29
|
+
if(!sourceMapsEnabled())return generate(ast,options,source).code;
|
|
30
|
+
const {code,map}=extractSourceMap(source);
|
|
31
|
+
const result=generate(ast,{...options,sourceMaps:true,sourceFileName:file,inputSourceMap:map,
|
|
32
|
+
shouldPrintComment:comment=>!/[#@]\s*sourceMappingURL=/.test(comment)},code);
|
|
33
|
+
return inlineSourceMap(result.code,result.map);
|
|
34
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
export function validateStandaloneConfig(config) {
|
|
4
|
+
if (config.output !== undefined && !['standalone', 'export'].includes(config.output)) throw new Error('output supports "standalone" or "export".');
|
|
5
|
+
if (config.outputFileTracingRoot !== undefined && (typeof config.outputFileTracingRoot !== 'string' || !path.isAbsolute(config.outputFileTracingRoot) || config.outputFileTracingRoot.includes('\0') || config.outputFileTracingRoot.length > 4096)) throw new Error('outputFileTracingRoot must be an absolute directory path.');
|
|
6
|
+
for (const key of ['outputFileTracingIncludes', 'outputFileTracingExcludes']) {
|
|
7
|
+
const rules = config[key];
|
|
8
|
+
if (rules === undefined) continue;
|
|
9
|
+
if (!rules || typeof rules !== 'object' || Array.isArray(rules) || Object.keys(rules).length > 256) throw new Error(`${key} must map at most 256 route globs to arrays of file globs.`);
|
|
10
|
+
let count = 0;
|
|
11
|
+
for (const [route, patterns] of Object.entries(rules)) {
|
|
12
|
+
if (!route.startsWith('/') || route.length > 4096 || route.includes('\0') || !Array.isArray(patterns)) throw new Error(`${key} must map URL route globs to arrays of relative file globs.`);
|
|
13
|
+
for (const pattern of patterns) {
|
|
14
|
+
if (typeof pattern !== 'string' || !pattern || pattern.length > 4096 || path.isAbsolute(pattern) || /[\0\\]/.test(pattern) || pattern.startsWith('!') || ++count > 1024) throw new Error(`${key} file globs must be relative paths; at most 1024 patterns are supported.`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return config;
|
|
19
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Worker } from 'node:worker_threads';
|
|
2
|
+
|
|
3
|
+
// Prerender installs Flight's webpack shims. NFT's native-loader discovery must
|
|
4
|
+
// see ordinary Node globals, so tracing runs in its own short-lived isolate.
|
|
5
|
+
export async function prepareStandalone(options) {
|
|
6
|
+
const { output, distDir, outputFileTracingRoot, outputFileTracingIncludes, outputFileTracingExcludes } = options.config;
|
|
7
|
+
const worker = new Worker(new URL('./standalone-worker.mjs', import.meta.url), {
|
|
8
|
+
workerData: { ...options, config: { output, distDir, outputFileTracingRoot, outputFileTracingIncludes, outputFileTracingExcludes } },
|
|
9
|
+
execArgv: process.execArgv.filter(argument => !argument.startsWith('--input-type')),
|
|
10
|
+
});
|
|
11
|
+
try {
|
|
12
|
+
await new Promise((resolve, reject) => {
|
|
13
|
+
worker.once('message', message => {
|
|
14
|
+
if (message.ok) resolve();
|
|
15
|
+
else { const error = new Error(message.error?.message || 'Standalone preparation failed'); if (message.error?.stack) error.stack = message.error.stack; reject(error); }
|
|
16
|
+
});
|
|
17
|
+
worker.once('error', reject);
|
|
18
|
+
worker.once('exit', code => reject(new Error(`Standalone preparation exited before completion (${code})`)));
|
|
19
|
+
});
|
|
20
|
+
} finally { await worker.terminate(); }
|
|
21
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { parentPort, workerData } from 'node:worker_threads';
|
|
2
|
+
import { createStandalone } from './standalone.mjs';
|
|
3
|
+
|
|
4
|
+
try { await createStandalone(workerData); parentPort.postMessage({ ok: true }); }
|
|
5
|
+
catch (error) { parentPort.postMessage({ error: { message: error.message, stack: error.stack } }); }
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { nodeFileTrace } from '@vercel/nft';
|
|
2
|
+
import resolver from '@vercel/nft/out/resolve-dependency.js';
|
|
3
|
+
import picomatch from 'picomatch';
|
|
4
|
+
import { mkdir, readFile, writeFile, readdir, realpath, readlink, stat, lstat, copyFile, chmod, symlink } from 'node:fs/promises';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { resolveNativeBinary } from '../native/resolve.mjs';
|
|
7
|
+
import { createHash } from 'node:crypto';
|
|
8
|
+
import { createRequire } from 'node:module';
|
|
9
|
+
|
|
10
|
+
const slash = value => value.split(path.sep).join('/');
|
|
11
|
+
const within = (root, file) => file === root || file.startsWith(root + path.sep);
|
|
12
|
+
const missing = error => error.code === 'ENOENT' || error.code === 'ENOTDIR';
|
|
13
|
+
const maxFiles = 100_000;
|
|
14
|
+
const maxBytes = 2 * 1024 ** 3;
|
|
15
|
+
|
|
16
|
+
function patternsFor(rules, route) {
|
|
17
|
+
return Object.entries(rules || {}).flatMap(([key, values]) => picomatch(key, { dot: true })(route) ? values : []);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function* walk(directory) {
|
|
21
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
const file = path.join(directory, entry.name);
|
|
24
|
+
if (entry.isDirectory()) yield* walk(file);
|
|
25
|
+
else yield file;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Resolve each configured pattern from its literal prefix, not by traversing a
|
|
30
|
+
// monorepo's entire dependency tree. Symlink directories are handled by NFT.
|
|
31
|
+
async function includedFiles(root, tracingRoot, patterns) {
|
|
32
|
+
const output = new Set();
|
|
33
|
+
for (const pattern of patterns) {
|
|
34
|
+
const absolute = path.resolve(root, pattern);
|
|
35
|
+
const parts = absolute.split(path.sep);
|
|
36
|
+
const wildcard = parts.findIndex(part => /[*?{[(!+@]/.test(part));
|
|
37
|
+
const prefix = wildcard < 0 ? absolute : parts.slice(0, wildcard).join(path.sep) || path.parse(root).root;
|
|
38
|
+
if (!within(tracingRoot, prefix)) throw new Error(`Tracing include ${pattern} leaves outputFileTracingRoot.`);
|
|
39
|
+
let info;
|
|
40
|
+
try { info = await stat(prefix); } catch (error) { if (missing(error)) continue; throw error; }
|
|
41
|
+
const match = picomatch(slash(absolute), { dot: true });
|
|
42
|
+
if (info.isDirectory()) {
|
|
43
|
+
let scanned = 0;
|
|
44
|
+
for await (const file of walk(prefix)) {
|
|
45
|
+
if (++scanned > maxFiles) throw new Error('Standalone include traverses more than 100000 files. Narrow its glob.');
|
|
46
|
+
if (match(slash(file))) output.add(file);
|
|
47
|
+
}
|
|
48
|
+
} else if (match(slash(prefix))) output.add(prefix);
|
|
49
|
+
}
|
|
50
|
+
return output;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function createStandalone({ projectRoot, stage, manifest, config }) {
|
|
54
|
+
if (config.output !== 'standalone' || manifest.dev) return;
|
|
55
|
+
projectRoot = await realpath(projectRoot);
|
|
56
|
+
stage = await realpath(stage);
|
|
57
|
+
const tracingRoot = await realpath(config.outputFileTracingRoot || projectRoot);
|
|
58
|
+
if (!within(tracingRoot, projectRoot)) throw new Error('outputFileTracingRoot must contain the application directory.');
|
|
59
|
+
const appRelative = path.relative(tracingRoot, projectRoot);
|
|
60
|
+
const output = path.join(stage, 'standalone');
|
|
61
|
+
const appOutput = path.join(output, 'app', appRelative);
|
|
62
|
+
const traceFiles = new Set();
|
|
63
|
+
const manualFiles = new Set();
|
|
64
|
+
const bridges = new Map();
|
|
65
|
+
const packageRoots = new Map();
|
|
66
|
+
const warnings = new Set();
|
|
67
|
+
let omittedWarnings = 0;
|
|
68
|
+
const cache = {};
|
|
69
|
+
let traceReads = 0;
|
|
70
|
+
let traceBytes = 0;
|
|
71
|
+
|
|
72
|
+
function relocated(file) {
|
|
73
|
+
if (within(stage, file)) return path.join(appOutput, config.distDir, path.relative(stage, file));
|
|
74
|
+
if (within(tracingRoot, file)) return path.join(output, 'app', path.relative(tracingRoot, file));
|
|
75
|
+
const offset = file.indexOf(`${path.sep}node_modules${path.sep}`);
|
|
76
|
+
if (offset >= 0) {
|
|
77
|
+
const anchor = file.slice(0, offset);
|
|
78
|
+
const key = createHash('sha256').update(anchor).digest('hex').slice(0, 16);
|
|
79
|
+
return path.join(output, 'dependencies', key, file.slice(offset + 1));
|
|
80
|
+
}
|
|
81
|
+
throw new Error(`Standalone trace ${file} is outside outputFileTracingRoot. Set that option to the shared workspace root.`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function packageRoot(filename, name) {
|
|
85
|
+
const key = filename + '\0' + name;
|
|
86
|
+
if (!packageRoots.has(key)) packageRoots.set(key, (async () => {
|
|
87
|
+
for (let dir = path.dirname(filename); dir !== path.dirname(dir); dir = path.dirname(dir)) {
|
|
88
|
+
try {
|
|
89
|
+
const metadata = JSON.parse(await readFile(path.join(dir, 'package.json'), 'utf8'));
|
|
90
|
+
if (metadata.name === name || dir.endsWith(path.sep + 'node_modules' + path.sep + name.split('/').join(path.sep))) return dir;
|
|
91
|
+
}
|
|
92
|
+
catch (error) { if (!missing(error) && !(error instanceof SyntaxError)) throw error; }
|
|
93
|
+
}
|
|
94
|
+
})());
|
|
95
|
+
return packageRoots.get(key);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function trace(entries, route) {
|
|
99
|
+
const excluded = patternsFor(config.outputFileTracingExcludes, route).map(value => picomatch(slash(path.resolve(projectRoot, value)), { dot: true }));
|
|
100
|
+
for (const conditions of [['node'], ['node', 'react-server']]) {
|
|
101
|
+
const result = await nodeFileTrace(entries, {
|
|
102
|
+
base: path.parse(projectRoot).root, processCwd: projectRoot, conditions, cache, fileIOConcurrency: 16,
|
|
103
|
+
ignore(file) { const absolute = path.resolve(path.parse(projectRoot).root, file); return !within(stage, absolute) && (excluded.some(match => match(slash(absolute))) || /^\.env(?:\.|$)/.test(path.basename(absolute))); },
|
|
104
|
+
async readFile(file) {
|
|
105
|
+
let info;
|
|
106
|
+
try { info = await stat(file); } catch (error) { if (missing(error)) return null; throw error; }
|
|
107
|
+
if (!info.isFile()) return null;
|
|
108
|
+
if (++traceReads > maxFiles || info.size > 32 * 1024 ** 2 || (traceBytes += info.size) > 512 * 1024 ** 2) throw new Error('Standalone tracing exceeded its 100000 reads / 32 MiB file / 512 MiB source budget. Narrow tracing inputs.');
|
|
109
|
+
return readFile(file);
|
|
110
|
+
},
|
|
111
|
+
async resolve(specifier, parent, job, cjs) {
|
|
112
|
+
const resolved = await resolver.default(specifier, parent, job, cjs);
|
|
113
|
+
if (!specifier.startsWith('.') && !specifier.startsWith('#') && !path.isAbsolute(specifier) && !specifier.startsWith('node:')) {
|
|
114
|
+
const name = specifier.split('/').slice(0, specifier.startsWith('@') ? 2 : 1).join('/');
|
|
115
|
+
for (const file of Array.isArray(resolved) ? resolved : [resolved]) {
|
|
116
|
+
if (file.startsWith('node:')) continue;
|
|
117
|
+
const owner = await packageRoot(file, name);
|
|
118
|
+
if (owner) bridges.set(path.join(path.dirname(parent), 'node_modules', name), owner);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return resolved;
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
for (const file of result.fileList) traceFiles.add(path.resolve(path.parse(projectRoot).root, file));
|
|
125
|
+
for (const warning of result.warnings) {
|
|
126
|
+
const message = warning.message.replaceAll(projectRoot, '<project>').slice(0, 2048);
|
|
127
|
+
if (warnings.has(message)) continue;
|
|
128
|
+
if (warnings.size < 128) warnings.add(message); else omittedWarnings++;
|
|
129
|
+
}
|
|
130
|
+
if (traceFiles.size > maxFiles) throw new Error('Standalone trace exceeds 100000 files.');
|
|
131
|
+
}
|
|
132
|
+
for (const file of await includedFiles(projectRoot, tracingRoot, patternsFor(config.outputFileTracingIncludes, route))) if (!excluded.some(match => match(slash(file)))) { traceFiles.add(file); manualFiles.add(file); }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Runtime imports are lazy and routes/client references are selected from the
|
|
136
|
+
// manifest. Give NFT all these roots explicitly, including both React modes.
|
|
137
|
+
const names = new Set(['worker', 'http']);
|
|
138
|
+
if (manifest.routes.some(route => route.router !== 'app' && route.kind === 'page')) names.add('render');
|
|
139
|
+
if (manifest.routes.some(route => route.kind === 'api')) { names.add('api'); names.add('route-static'); }
|
|
140
|
+
if (manifest.routes.some(route => route.router === 'app' && route.kind === 'page')) for (const name of ['app-render', 'app-static', 'rsc-worker']) names.add(name);
|
|
141
|
+
if (manifest.middleware) names.add('middleware');
|
|
142
|
+
if (manifest.config.cacheHandler) names.add('incremental-cache');
|
|
143
|
+
const runtimeRoots = [...names].map(name => path.join(stage, 'runtime', name + '.mjs'));
|
|
144
|
+
if (names.has('app-render')) {
|
|
145
|
+
// flightDecoder selects the production/development CJS decoder at runtime
|
|
146
|
+
// after require.resolve(). NFT cannot infer that computed filename.
|
|
147
|
+
const require = createRequire(path.join(stage, 'runtime/app-render.mjs'));
|
|
148
|
+
runtimeRoots.push(require.resolve('react-server-dom-webpack/client.node'));
|
|
149
|
+
}
|
|
150
|
+
await trace(runtimeRoots, '/*');
|
|
151
|
+
const groups = new Map();
|
|
152
|
+
for (const route of manifest.routes) {
|
|
153
|
+
const key = JSON.stringify([patternsFor(config.outputFileTracingIncludes, route.pattern), patternsFor(config.outputFileTracingExcludes, route.pattern)]);
|
|
154
|
+
if (!groups.has(key)) groups.set(key, { entries: [], pattern: route.pattern });
|
|
155
|
+
groups.get(key).entries.push(path.join(stage, route.module));
|
|
156
|
+
}
|
|
157
|
+
for (const { entries, pattern } of groups.values()) await trace(entries, pattern);
|
|
158
|
+
const dynamicRoots = [...Object.values(manifest.app?.clientModules || {}).map(value => value.ssrModule),
|
|
159
|
+
...Object.values(manifest.app?.actions || {}).map(value => value.module),
|
|
160
|
+
manifest.middleware?.module, manifest.config.cacheHandler, ...Object.values(manifest.config.cacheHandlers || {})]
|
|
161
|
+
.filter(Boolean).map(file => path.join(stage, file));
|
|
162
|
+
if (dynamicRoots.length) await trace(dynamicRoots, '/*');
|
|
163
|
+
|
|
164
|
+
// Copy immutable build assets and public files regardless of trace exclusions.
|
|
165
|
+
// Exclusions tune dependencies, never silently remove the server itself.
|
|
166
|
+
for await (const file of walk(stage)) traceFiles.add(file);
|
|
167
|
+
try { for await (const file of walk(path.join(projectRoot, 'public'))) { traceFiles.add(file); manualFiles.add(file); } }
|
|
168
|
+
catch (error) { if (!missing(error)) throw error; }
|
|
169
|
+
// Materialize symlink targets, including explicit includes/public directories
|
|
170
|
+
// which did not go through NFT. All output links point within the artifact.
|
|
171
|
+
for (const source of manualFiles) {
|
|
172
|
+
if (!(await lstat(source)).isSymbolicLink()) continue;
|
|
173
|
+
const target = await realpath(source);
|
|
174
|
+
relocated(target);
|
|
175
|
+
if ((await stat(target)).isDirectory()) {
|
|
176
|
+
for await (const file of walk(target)) {
|
|
177
|
+
traceFiles.add(file);
|
|
178
|
+
manualFiles.add(file);
|
|
179
|
+
if (traceFiles.size > maxFiles) throw new Error('Standalone trace exceeds 100000 files.');
|
|
180
|
+
}
|
|
181
|
+
} else traceFiles.add(target);
|
|
182
|
+
}
|
|
183
|
+
await mkdir(output, { recursive: true });
|
|
184
|
+
let copiedBytes = 0;
|
|
185
|
+
let copiedFiles = 0;
|
|
186
|
+
const pendingLinks = [];
|
|
187
|
+
for (const source of traceFiles) {
|
|
188
|
+
const target = relocated(source);
|
|
189
|
+
const info = await lstat(source);
|
|
190
|
+
if (++copiedFiles > maxFiles || (copiedBytes += info.size) > maxBytes) throw new Error('Standalone output exceeds 100000 files / 2 GiB. Narrow tracing includes.');
|
|
191
|
+
if (info.isSymbolicLink()) {
|
|
192
|
+
const destination = await realpath(source);
|
|
193
|
+
pendingLinks.push([target, relocated(destination)]);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (!info.isFile()) continue;
|
|
197
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
198
|
+
await copyFile(source, target);
|
|
199
|
+
}
|
|
200
|
+
const owners = new Set(bridges.values());
|
|
201
|
+
const includedOwners = new Set();
|
|
202
|
+
for (const file of traceFiles) for (let dir = path.dirname(file); dir !== path.dirname(dir); dir = path.dirname(dir)) if (owners.has(dir)) includedOwners.add(dir);
|
|
203
|
+
for (const [source, owner] of bridges) {
|
|
204
|
+
// An excluded optional dependency must not be brought back by a bridge.
|
|
205
|
+
if (!includedOwners.has(owner)) continue;
|
|
206
|
+
pendingLinks.push([relocated(source), relocated(owner)]);
|
|
207
|
+
}
|
|
208
|
+
for (const [link, target] of pendingLinks) {
|
|
209
|
+
if (link === target) continue;
|
|
210
|
+
await mkdir(path.dirname(link), { recursive: true });
|
|
211
|
+
try {
|
|
212
|
+
const current = await lstat(link);
|
|
213
|
+
if (current.isDirectory()) continue;
|
|
214
|
+
if (current.isSymbolicLink() && path.resolve(path.dirname(link), await readlink(link)) === target) continue;
|
|
215
|
+
throw new Error(`Conflicting standalone dependency at ${path.relative(output, link)}.`);
|
|
216
|
+
} catch (error) { if (!missing(error)) throw error; }
|
|
217
|
+
await symlink(path.relative(path.dirname(link), target), link, (await stat(target)).isDirectory() ? 'dir' : 'file');
|
|
218
|
+
}
|
|
219
|
+
const binaryName = process.platform === 'win32' ? 'prnext.exe' : 'prnext';
|
|
220
|
+
const binary = await resolveNativeBinary();
|
|
221
|
+
await mkdir(path.join(output, 'bin'), { recursive: true });
|
|
222
|
+
await copyFile(binary, path.join(output, 'bin', binaryName));
|
|
223
|
+
await chmod(path.join(output, 'bin', binaryName), 0o755);
|
|
224
|
+
await mkdir(appOutput, { recursive: true });
|
|
225
|
+
await writeFile(path.join(appOutput, '.prnext-output.json'), JSON.stringify({ distDir: config.distDir }) + '\n');
|
|
226
|
+
// Preserve package type, imports/exports, self references and application
|
|
227
|
+
// reads of its package metadata. Package scripts are never executed here.
|
|
228
|
+
let packageJson = '{"private":true}\n';
|
|
229
|
+
try { packageJson = await readFile(path.join(projectRoot, 'package.json'), 'utf8'); } catch (error) { if (!missing(error)) throw error; }
|
|
230
|
+
await writeFile(path.join(appOutput, 'package.json'), packageJson);
|
|
231
|
+
const relativeApp = slash(path.relative(output, appOutput));
|
|
232
|
+
await writeFile(path.join(output, 'package.json'), '{"private":true,"type":"commonjs"}\n');
|
|
233
|
+
await writeFile(path.join(output, 'server.js'), `'use strict';\nconst path=require('node:path');const {spawn}=require('node:child_process');\nconst root=path.join(__dirname,${JSON.stringify(relativeApp)});\nconst child=spawn(path.join(__dirname,'bin',${JSON.stringify(binaryName)}),['start',root,'--hostname',process.env.HOSTNAME||'0.0.0.0','--port',process.env.PORT||'3000','--workers',process.env.PRNEXT_WORKERS||'1','--node',process.execPath],{cwd:root,stdio:'inherit',env:{...process.env,NODE_ENV:'production'}});\nfor(const signal of ['SIGINT','SIGTERM'])process.on(signal,()=>child.kill(signal));\nchild.on('error',error=>{console.error(error.message);process.exitCode=1;});child.on('exit',(code,signal)=>{process.exitCode=code??(signal==='SIGINT'?130:signal==='SIGTERM'?143:1);});\n`);
|
|
234
|
+
if (process.platform !== 'win32') {
|
|
235
|
+
const quotedApp = "'" + relativeApp.replaceAll("'", "'\\''") + "'";
|
|
236
|
+
await writeFile(path.join(output, 'start'), `#!/bin/sh\nset -eu\nSELF=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)\ncd "$SELF/"${quotedApp}\nexec "$SELF/bin/prnext" start . --hostname "\${HOSTNAME:-0.0.0.0}" --port "\${PORT:-3000}" --workers "\${PRNEXT_WORKERS:-1}" --node "\${PRNEXT_NODE:-node}" "$@"\n`);
|
|
237
|
+
await chmod(path.join(output, 'start'), 0o755);
|
|
238
|
+
}
|
|
239
|
+
await writeFile(path.join(output, 'standalone.json'), JSON.stringify({ version: 1, app: relativeApp, distDir: config.distDir, platform: process.platform, arch: process.arch, node: process.versions.node, nodeModulesABI: process.versions.modules, files: copiedFiles, bytes: copiedBytes, warnings: [...warnings], ...(omittedWarnings ? { omittedWarnings } : {}) }, null, 2) + '\n');
|
|
240
|
+
}
|