@thomas.f/prnext 0.1.12-alpha → 0.1.13-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +262 -33
- package/build/actions.mjs +4 -0
- package/build/app-config.mjs +2 -0
- package/build/app.mjs +20 -5
- package/build/bundle-runtime.mjs +82 -0
- package/build/compiler.mjs +3 -1
- package/build/config.mjs +9 -2
- package/build/font-cache.mjs +50 -0
- package/build/font-files.mjs +5 -3
- package/build/fonts.mjs +1 -1
- package/build/index.mjs +5 -0
- package/build/metadata-conflicts.mjs +13 -0
- package/build/metadata-images.mjs +3 -3
- package/build/metadata-routes.mjs +3 -3
- package/build/native-packages.mjs +5 -1
- package/build/npm-packages.mjs +2 -1
- package/build/optimize-imports.mjs +112 -0
- package/build/proxy-body-config.mjs +10 -0
- package/build/scan.mjs +9 -7
- package/cli.mjs +16 -0
- package/compat/after.cjs +51 -0
- package/compat/index.d.ts +3 -1
- package/compat/server.cjs +2 -1
- package/compat/server.d.cts +2 -0
- package/native/launch.mjs +19 -0
- package/package.json +7 -7
- package/runtime/api.mjs +15 -5
- package/runtime/app-render.mjs +78 -33
- package/runtime/app-route-plan.mjs +45 -0
- package/runtime/app-routing-state.mjs +101 -21
- package/runtime/app-routing.mjs +37 -18
- package/runtime/app-segments.mjs +25 -11
- package/runtime/app-static.mjs +1 -1
- package/runtime/app-stream-html.mjs +2 -1
- package/runtime/cache-request.mjs +10 -2
- package/runtime/cloud/archive.mjs +316 -0
- package/runtime/cloud/client.mjs +658 -0
- package/runtime/cloud/environment.mjs +60 -0
- package/runtime/cloud/state.mjs +141 -0
- package/runtime/cloud/upload.mjs +48 -0
- package/runtime/html-envelope.mjs +70 -7
- package/runtime/http.mjs +7 -4
- package/runtime/middleware.mjs +3 -3
- package/runtime/request-transport.mjs +1 -1
- package/runtime/route-static.mjs +1 -1
- package/runtime/rsc-worker.mjs +27 -4
- package/runtime/stream-utils.mjs +22 -3
- package/runtime/transport.mjs +16 -0
- package/runtime/worker-metadata.mjs +23 -0
- package/runtime/worker.mjs +36 -9
package/build/config.mjs
CHANGED
|
@@ -17,6 +17,8 @@ import { validateI18n } from './i18n.mjs';
|
|
|
17
17
|
import { frameworkPackages } from './framework-imports.mjs';
|
|
18
18
|
import { validateTypeScript } from './typescript.mjs';
|
|
19
19
|
import { validateWorkerRetirement } from './worker-retirement.mjs';
|
|
20
|
+
import { validateOptimizePackageImports } from './optimize-imports.mjs';
|
|
21
|
+
import { validateProxyBodySize } from './proxy-body-config.mjs';
|
|
20
22
|
|
|
21
23
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
22
24
|
const configName = /^(?:next|prnext)\.config\.(?:js|mjs|cjs|ts)$/;
|
|
@@ -29,6 +31,7 @@ supported.add('turbopack');
|
|
|
29
31
|
supported.add('i18n');
|
|
30
32
|
supported.add('webpack');
|
|
31
33
|
supported.add('onDemandEntries');
|
|
34
|
+
supported.add('devIndicators');
|
|
32
35
|
for (const key of ['reactCompiler', 'typedRoutes', 'typescript']) supported.add(key);
|
|
33
36
|
for (const key of ['output', 'outputFileTracingRoot', 'outputFileTracingIncludes', 'outputFileTracingExcludes']) supported.add(key);
|
|
34
37
|
|
|
@@ -89,6 +92,7 @@ export function validateProjectConfig(input, filename = 'project config') {
|
|
|
89
92
|
}
|
|
90
93
|
config.typescript = validateTypeScript(config.typescript);
|
|
91
94
|
config.workerRetirement = validateWorkerRetirement(config.workerRetirement);
|
|
95
|
+
if (config.devIndicators !== undefined && config.devIndicators !== false) throw new TypeError('devIndicators currently supports only false. PRNext provides development errors and Fast Refresh, but no Next.js development indicator menu.');
|
|
92
96
|
if (config.onDemandEntries !== undefined) {
|
|
93
97
|
const entries = config.onDemandEntries;
|
|
94
98
|
if (!entries || typeof entries !== 'object' || Array.isArray(entries)) throw new TypeError('onDemandEntries must be an object');
|
|
@@ -117,14 +121,17 @@ export function validateProjectConfig(input, filename = 'project config') {
|
|
|
117
121
|
config[key] = [...new Set(names)];
|
|
118
122
|
}
|
|
119
123
|
for (const name of config.transpilePackages) if (config.serverExternalPackages.includes(name)) throw new Error(`${filename}: ${name} cannot be in both transpilePackages and serverExternalPackages.`);
|
|
124
|
+
// Like Next, retain pg's Node module boundary: its optional pg-native getter
|
|
125
|
+
// must resolve lazily, not be traversed by the RSC/server bundler at build.
|
|
126
|
+
if (!config.transpilePackages.includes('pg') && !config.serverExternalPackages.includes('pg')) config.serverExternalPackages.push('pg');
|
|
120
127
|
if (config.sassOptions !== undefined && (!config.sassOptions || typeof config.sassOptions !== 'object' || Array.isArray(config.sassOptions))) throw new Error(`${filename}: sassOptions must be an object.`);
|
|
121
128
|
if (!config.experimental || typeof config.experimental !== 'object' || Array.isArray(config.experimental)) throw new Error(`${filename}: experimental must be an object.`);
|
|
122
129
|
for (const key of Object.keys(config.experimental)) {
|
|
123
|
-
if (!['nextScriptWorkers', 'serverActions'].includes(key)) throw new Error(`${filename}: experimental.${key} is not supported by PRNext. It cannot be silently ignored.`);
|
|
130
|
+
if (!['nextScriptWorkers', 'serverActions', 'optimizePackageImports', 'proxyClientMaxBodySize'].includes(key)) throw new Error(`${filename}: experimental.${key} is not supported by PRNext. It cannot be silently ignored.`);
|
|
124
131
|
}
|
|
125
132
|
const nextScriptWorkers = config.experimental.nextScriptWorkers === undefined ? false : config.experimental.nextScriptWorkers;
|
|
126
133
|
if (typeof nextScriptWorkers !== 'boolean') throw new Error(`${filename}: experimental.nextScriptWorkers must be a boolean.`);
|
|
127
|
-
config.experimental = { nextScriptWorkers, serverActions: validateServerActions(config.experimental.serverActions) };
|
|
134
|
+
config.experimental = { nextScriptWorkers, serverActions: validateServerActions(config.experimental.serverActions), optimizePackageImports: validateOptimizePackageImports(config.experimental.optimizePackageImports), proxyClientMaxBodySize: validateProxyBodySize(config.experimental.proxyClientMaxBodySize) };
|
|
128
135
|
for (const key of ['basePath', 'assetPrefix']) {
|
|
129
136
|
if (typeof config[key] !== 'string' || Buffer.byteLength(config[key]) > 4096) throw new Error(`${filename}: ${key} must be a string of at most 4096 bytes.`);
|
|
130
137
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdir, readFile, writeFile, rename, rm, readdir, lstat } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
const hash = value => createHash('sha256').update(value).digest('hex');
|
|
6
|
+
const MAX_TOTAL = 64 * 1024 * 1024;
|
|
7
|
+
let writes = Promise.resolve();
|
|
8
|
+
|
|
9
|
+
/** Project-local build inputs, never responses/user data. Clearing this directory
|
|
10
|
+
* refreshes Google assets. A warm cache also permits an offline rebuild.
|
|
11
|
+
*/
|
|
12
|
+
export async function cachedFontResource(root, url, maximum, accept, download) {
|
|
13
|
+
if (!root) return download();
|
|
14
|
+
const directory = path.join(root, '.prnext-cache', 'fonts-v1');
|
|
15
|
+
const file = path.join(directory, hash(JSON.stringify([url, accept])) + '.bin');
|
|
16
|
+
try {
|
|
17
|
+
const metadata = await lstat(file);
|
|
18
|
+
if (metadata.isFile() && metadata.size > 65 && metadata.size <= maximum + 65) {
|
|
19
|
+
const stored = await readFile(file);
|
|
20
|
+
const bytes = stored.subarray(65);
|
|
21
|
+
if (stored[64] === 10 && hash(bytes) === stored.toString('ascii', 0, 64)) return bytes;
|
|
22
|
+
}
|
|
23
|
+
} catch (error) { if (!['ENOENT', 'ENOTDIR', 'EACCES'].includes(error.code)) throw error; }
|
|
24
|
+
const bytes = await download();
|
|
25
|
+
if (!bytes.length || bytes.length > maximum) throw new Error('Font resource exceeds its cache input limit');
|
|
26
|
+
const task = writes.catch(() => {}).then(async () => {
|
|
27
|
+
await mkdir(directory, { recursive: true });
|
|
28
|
+
const temporary = file + '.' + randomUUID() + '.tmp';
|
|
29
|
+
try {
|
|
30
|
+
await writeFile(temporary, Buffer.concat([Buffer.from(hash(bytes) + '\n'), bytes]), { flag: 'wx' });
|
|
31
|
+
await rename(temporary, file);
|
|
32
|
+
} finally { await rm(temporary, { force: true }); }
|
|
33
|
+
const entries = [];
|
|
34
|
+
for (const item of await readdir(directory, { withFileTypes: true })) {
|
|
35
|
+
if (!item.isFile() || !/^[a-f0-9]{64}\.bin$/.test(item.name)) continue;
|
|
36
|
+
try { entries.push({ file: path.join(directory, item.name), ...await lstat(path.join(directory, item.name)) }); }
|
|
37
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
38
|
+
}
|
|
39
|
+
entries.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
40
|
+
let total = 0;
|
|
41
|
+
for (let index = 0; index < entries.length; index++) {
|
|
42
|
+
total += entries[index].size;
|
|
43
|
+
if (index >= 256 || total > MAX_TOTAL) await rm(entries[index].file, { force: true });
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
writes = task;
|
|
47
|
+
try { await task; }
|
|
48
|
+
catch (error) { if (!['EACCES', 'EPERM', 'EROFS', 'ENOSPC', 'ENOENT'].includes(error.code)) throw error; }
|
|
49
|
+
return bytes;
|
|
50
|
+
}
|
package/build/font-files.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { readFile, writeFile, mkdir, stat } from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
4
|
import googleFonts from './font-data.json' with { type: 'json' };
|
|
5
|
+
import { cachedFontResource } from './font-cache.mjs';
|
|
5
6
|
|
|
6
7
|
const formats = { woff: 'woff', woff2: 'woff2', ttf: 'truetype', otf: 'opentype', eot: 'embedded-opentype' };
|
|
7
8
|
const types = { woff: 'font/woff', woff2: 'font/woff2', ttf: 'font/ttf', otf: 'font/otf', eot: 'application/vnd.ms-fontobject' };
|
|
@@ -36,7 +37,7 @@ function validate(kind, options) {
|
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
async function responseBytes(url, maximum, accept) {
|
|
39
|
-
const response = await fetch(url, { signal: AbortSignal.timeout(15_000), headers: { accept,
|
|
40
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(15_000), redirect: 'error', headers: { accept,
|
|
40
41
|
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36' } });
|
|
41
42
|
if (!response.ok) { await response.body?.cancel(); fail(`Unable to download ${url}: HTTP ${response.status}.`); }
|
|
42
43
|
if (Number(response.headers.get('content-length')) > maximum) { await response.body?.cancel(); fail(`Font resource exceeds ${maximum} bytes.`); }
|
|
@@ -145,7 +146,8 @@ function googleOptions(name, options) {
|
|
|
145
146
|
|
|
146
147
|
async function google(name, options, context) {
|
|
147
148
|
const normalized = googleOptions(name, options);
|
|
148
|
-
const
|
|
149
|
+
const resourceBytes = (url, maximum, accept) => cachedFontResource(context.projectRoot, url, maximum, accept, () => responseBytes(url, maximum, accept).catch(error => { throw new Error(`next/font: download failed for ${url}: ${error.message}. An initial build needs access to Google Fonts; subsequent builds can reuse .prnext-cache/fonts-v1.`, { cause: error }); }));
|
|
150
|
+
const css = (await resourceBytes(normalized.url, 1024 * 1024, 'text/css')).toString('utf8');
|
|
149
151
|
const faces = [], downloaded = new Map();
|
|
150
152
|
let total = 0;
|
|
151
153
|
const facePattern = /(?:\/\*\s*([^*]+?)\s*\*\/\s*)?@font-face\s*\{([^}]+)\}/g;
|
|
@@ -161,7 +163,7 @@ async function google(name, options, context) {
|
|
|
161
163
|
if (url.protocol !== 'https:' || url.hostname !== 'fonts.gstatic.com' || url.username || url.password) fail('Google font CSS referenced an unexpected font origin.');
|
|
162
164
|
if (!downloaded.has(url.href)) {
|
|
163
165
|
if (downloaded.size >= 64) fail('Google font call exceeds 64 files.');
|
|
164
|
-
const bytes = await
|
|
166
|
+
const bytes = await resourceBytes(url.href, MAX_FONT, 'font/woff2,font/woff,*/*');
|
|
165
167
|
total += bytes.length;
|
|
166
168
|
if (total > 64 * 1024 * 1024) fail('Google font call exceeds 64 MiB.');
|
|
167
169
|
const ext = path.extname(url.pathname).slice(1).toLowerCase();
|
package/build/fonts.mjs
CHANGED
|
@@ -37,7 +37,7 @@ export function createFonts({ projectRoot, stage, assetBase }) {
|
|
|
37
37
|
async function prepare(kind, name, options, file, position) {
|
|
38
38
|
const id = hash(JSON.stringify([path.relative(projectRoot, file), position, kind, name, options]));
|
|
39
39
|
if (!compilations.has(id)) compilations.set(id, (async () => {
|
|
40
|
-
const compiled = await compileFont({ kind, name, options, file, id, stage, assetBase });
|
|
40
|
+
const compiled = await compileFont({ kind, name, options, file, id, stage, assetBase, projectRoot });
|
|
41
41
|
const moduleFile = path.join(stage, '.entries', `font-${id}.mjs`);
|
|
42
42
|
const cssFile = path.join(stage, '.entries', `font-${id}.css`);
|
|
43
43
|
await mkdir(path.dirname(moduleFile), { recursive: true });
|
package/build/index.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { rename } from '../runtime/fs.mjs';
|
|
2
2
|
import { writeRuntimeContract } from './runtime-contract.mjs';
|
|
3
|
+
import { bundleRuntime } from './bundle-runtime.mjs';
|
|
3
4
|
import {frameworkImportName, frameworkRuntimeImportPattern} from './framework-imports.mjs';
|
|
4
5
|
import { expandLocales, localizedStaticPaths } from './i18n.mjs';
|
|
5
6
|
import { build as bundle, withCompilation, configureCompiler, compilerSource } from './compiler.mjs';
|
|
@@ -294,11 +295,15 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
|
|
|
294
295
|
}
|
|
295
296
|
}
|
|
296
297
|
await compileMiddleware({ project, stage, manifest, dev, moduleResolution: config.turbopack, defineEnvironment: environment });
|
|
298
|
+
if (manifest.middleware) manifest.middleware.bodySizeLimit = config.experimental.proxyClientMaxBodySize;
|
|
297
299
|
await prepareMetadataRoutes(project, stage);
|
|
298
300
|
await compileApp({ project, stage, manifest, moduleResolution: config.turbopack, cssModules, fonts, images, dev, defineEnvironment: environment, productionBrowserSourceMaps: config.productionBrowserSourceMaps, serverExternalPackages: config.serverExternalPackages });
|
|
299
301
|
await checkTypeScript(project.root, config, { dev, tsconfig });
|
|
300
302
|
fonts.attach(manifest);
|
|
301
303
|
expandLocales(manifest);
|
|
304
|
+
// All runtime preparation must finish before its shared modules are built;
|
|
305
|
+
// no staged renderer has been imported before this production-only point.
|
|
306
|
+
if (!dev) await bundleRuntime(stage);
|
|
302
307
|
await prerender(stage, manifest, dev);
|
|
303
308
|
await prerenderApp(stage, manifest, dev);
|
|
304
309
|
const pages = manifest.routes.filter(route => route.kind === 'page' && route.router !== 'app' && !route.internal);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export async function validatePublicMetadata(root, pattern, file, dynamic) {
|
|
5
|
+
const publicFile = path.join(root, 'public', pattern.slice(1));
|
|
6
|
+
try {
|
|
7
|
+
const info = await stat(publicFile);
|
|
8
|
+
// Next production serves public files before App metadata handlers. An
|
|
9
|
+
// identical static duplicate is therefore unambiguous and safe to retain.
|
|
10
|
+
if (!dynamic && info.isFile() && info.size <= 16 * 1024 * 1024 && info.size === (await stat(file)).size && (await readFile(publicFile)).equals(await readFile(file))) return;
|
|
11
|
+
throw new Error(`Metadata route ${pattern} conflicts with a different file in public/. Keep a single source for this URL.`);
|
|
12
|
+
} catch (error) { if (error.code !== 'ENOENT' && error.code !== 'ENOTDIR') throw error; }
|
|
13
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFile
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
4
|
import { execFile } from 'node:child_process';
|
|
@@ -7,6 +7,7 @@ import { resolveNativeBinary } from '../native/resolve.mjs';
|
|
|
7
7
|
import { validateAppConfig, mergeAppConfig } from './app-config.mjs';
|
|
8
8
|
import { metadataExportNames } from './metadata-routes.mjs';
|
|
9
9
|
import { pageExtensionPattern, validatePageSource } from './page-extensions.mjs';
|
|
10
|
+
import { validatePublicMetadata } from './metadata-conflicts.mjs';
|
|
10
11
|
const execute = promisify(execFile);
|
|
11
12
|
const contentTypes = { ico: 'image/x-icon', png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', svg: 'image/svg+xml', gif: 'image/gif' };
|
|
12
13
|
const imageTypes = { favicon: ['ico'], icon: ['ico', 'jpg', 'jpeg', 'png', 'svg'], 'apple-icon': ['jpg', 'jpeg', 'png'], 'opengraph-image': ['jpg', 'jpeg', 'png', 'gif'], 'twitter-image': ['jpg', 'jpeg', 'png', 'gif'] };
|
|
@@ -57,8 +58,7 @@ export async function scanMetadataImages({ root, appRoot, files, routes, pattern
|
|
|
57
58
|
const pattern = validatePattern([...urlParts, filename, ...(multiple ? ['[__metadata_id__]'] : [])], relative);
|
|
58
59
|
const signature = pattern.replace(/\[[^\]]+\]/g, '[]');
|
|
59
60
|
if (patterns.has(signature)) throw new Error(`Conflicting metadata route ${relative} and ${patterns.get(signature)} at ${pattern}.`);
|
|
60
|
-
|
|
61
|
-
catch (error) { if (error.code !== 'ENOENT' && error.code !== 'ENOTDIR') throw error; }
|
|
61
|
+
await validatePublicMetadata(root, pattern, file, dynamic);
|
|
62
62
|
patterns.set(signature, relative);
|
|
63
63
|
const handlerConfig = dynamic ? await validateAppConfig(file) : {};
|
|
64
64
|
handlerConfig.revalidate ??= false;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { readFile, writeFile
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
4
|
import { parse } from '@babel/parser';
|
|
5
5
|
import { validateAppConfig, mergeAppConfig } from './app-config.mjs';
|
|
6
6
|
import { scanMetadataImages } from './metadata-images.mjs';
|
|
7
7
|
import { pageExtensionPattern, validatePageSource } from './page-extensions.mjs';
|
|
8
|
+
import { validatePublicMetadata } from './metadata-conflicts.mjs';
|
|
8
9
|
|
|
9
10
|
const contentTypes = { robots: 'text/plain', sitemap: 'application/xml', manifest: 'application/manifest+json' };
|
|
10
11
|
|
|
@@ -31,8 +32,7 @@ export async function scanMetadataRoutes({ root, appRoot, files, routes, pattern
|
|
|
31
32
|
const signature = pattern.replace(/\[[^\]]+\]/g, '[]');
|
|
32
33
|
if (patterns.has(signature)) throw new Error(`Conflicting metadata route ${relative} and ${patterns.get(signature)} at ${pattern}.`);
|
|
33
34
|
if (kind === 'manifest' && manifest) throw new Error('Multiple manifest metadata files found in the App Router root.');
|
|
34
|
-
|
|
35
|
-
catch (error) { if (error.code !== 'ENOENT' && error.code !== 'ENOTDIR') throw error; }
|
|
35
|
+
await validatePublicMetadata(root, pattern, file, dynamic);
|
|
36
36
|
patterns.set(signature, relative);
|
|
37
37
|
const handlerConfig = dynamic ? await validateAppConfig(file) : {};
|
|
38
38
|
if (dynamic) {
|
|
@@ -89,7 +89,11 @@ export function createNativePackages(projectRoot, { isClientSource }) {
|
|
|
89
89
|
}
|
|
90
90
|
if (args.path.startsWith('.') || args.path.startsWith('#')) return;
|
|
91
91
|
const resolved = await esbuild.resolve(args.path, { importer: args.importer, resolveDir: args.resolveDir, kind: args.kind, pluginData: { prnextNativeResolving: true } });
|
|
92
|
-
|
|
92
|
+
// esbuild knows whether the original require() is inside try/catch.
|
|
93
|
+
// A plugin error would erase that optional-dependency semantics.
|
|
94
|
+
// Let normal resolution report genuine missing imports itself.
|
|
95
|
+
if (resolved.errors.length) return;
|
|
96
|
+
if (resolved.external || resolved.namespace !== 'file') return resolved;
|
|
93
97
|
references.set(resolved.path, { specifier: args.path, importer: args.importer });
|
|
94
98
|
const owner = ownerFor(resolved.path) || await discoverOwner(resolved.path, mode);
|
|
95
99
|
if (owner) {
|
package/build/npm-packages.mjs
CHANGED
|
@@ -91,7 +91,8 @@ export function npmPackagesPlugin({ pages = false, projectRoot, transpilePackage
|
|
|
91
91
|
// same validation as Pages to avoid bypassing compatibility transforms.
|
|
92
92
|
if (!pages && !external.has(name)) return;
|
|
93
93
|
const resolved = await resolve(args.path, args.importer || path.join(projectRoot, 'package.json'), args.kind);
|
|
94
|
-
if (resolved.errors.length
|
|
94
|
+
if (resolved.errors.length) return;
|
|
95
|
+
if (resolved.external || resolved.namespace !== 'file') return resolved;
|
|
95
96
|
// A tsconfig paths/baseUrl import can look like an npm specifier while
|
|
96
97
|
// resolving to application source. Node cannot resolve that alias from
|
|
97
98
|
// the emitted server bundle; keep the resolved source in the graph.
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { parse } from '@babel/parser';
|
|
4
|
+
import generateModule from '@babel/generator';
|
|
5
|
+
import * as t from '@babel/types';
|
|
6
|
+
import { generateMapped } from './source-maps.mjs';
|
|
7
|
+
|
|
8
|
+
const generate = generateModule.default || generateModule;
|
|
9
|
+
const name = node => node?.name ?? node?.value;
|
|
10
|
+
|
|
11
|
+
export function validateOptimizePackageImports(value = []) {
|
|
12
|
+
if (!Array.isArray(value) || value.length > 256 || value.some(item => typeof item !== 'string' || !/^(?:@[a-zA-Z0-9_.-]+\/)?[a-zA-Z0-9_-][a-zA-Z0-9_.-]*$/.test(item))) {
|
|
13
|
+
throw new TypeError('experimental.optimizePackageImports must be an array of at most 256 npm package names');
|
|
14
|
+
}
|
|
15
|
+
return [...new Set(value)];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Only bypass pure ESM barrels in packages declaring sideEffects:false.
|
|
19
|
+
* Uncertain graphs retain their original import and normal bundler semantics.
|
|
20
|
+
* Never execute package code or infer private export paths from symbol names.
|
|
21
|
+
*/
|
|
22
|
+
export function createImportOptimizer(packages) {
|
|
23
|
+
const selected = new Set(packages), barrels = new Map(), dependencies = new Set();
|
|
24
|
+
async function inspect(file) {
|
|
25
|
+
if (!barrels.has(file)) barrels.set(file, (async () => {
|
|
26
|
+
if (!/\.[cm]?jsx?$/.test(file) || (await stat(file)).size > 2 * 1024 * 1024) return;
|
|
27
|
+
let directory = path.dirname(file), owner;
|
|
28
|
+
for (;;) {
|
|
29
|
+
try { owner = JSON.parse(await readFile(path.join(directory, 'package.json'), 'utf8')); break; }
|
|
30
|
+
catch (error) { if (error.code !== 'ENOENT') return; }
|
|
31
|
+
const parent = path.dirname(directory);
|
|
32
|
+
if (parent === directory) return;
|
|
33
|
+
directory = parent;
|
|
34
|
+
}
|
|
35
|
+
if (owner.sideEffects !== false || !selected.has(owner.name)) return;
|
|
36
|
+
dependencies.add(file);
|
|
37
|
+
dependencies.add(path.join(directory, 'package.json'));
|
|
38
|
+
const ast = parse(await readFile(file, 'utf8'), { sourceType: 'module', plugins: ['jsx'] });
|
|
39
|
+
// Crossing either directive here would change the RSC boundary identity.
|
|
40
|
+
if (ast.program.directives.some(d => d.value.value !== 'use strict')) return;
|
|
41
|
+
const imports = new Map(), exports = new Map();
|
|
42
|
+
for (const node of ast.program.body) {
|
|
43
|
+
if (t.isImportDeclaration(node)) {
|
|
44
|
+
if (!node.specifiers.length || node.attributes?.length) return;
|
|
45
|
+
for (const item of node.specifiers) {
|
|
46
|
+
if (t.isImportNamespaceSpecifier(item)) return;
|
|
47
|
+
imports.set(item.local.name, { source: node.source.value, imported: t.isImportDefaultSpecifier(item) ? 'default' : name(item.imported) });
|
|
48
|
+
}
|
|
49
|
+
} else if (!t.isExportNamedDeclaration(node) || node.declaration || node.attributes?.length) return;
|
|
50
|
+
}
|
|
51
|
+
for (const node of ast.program.body) if (t.isExportNamedDeclaration(node)) {
|
|
52
|
+
for (const item of node.specifiers) {
|
|
53
|
+
if (!t.isExportSpecifier(item)) return;
|
|
54
|
+
const target = node.source ? { source: node.source.value, imported: name(item.local) } : imports.get(name(item.local));
|
|
55
|
+
if (!target || !target.source.startsWith('./') && !target.source.startsWith('../')) return;
|
|
56
|
+
exports.set(name(item.exported), target);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return { exports, root: directory };
|
|
60
|
+
})());
|
|
61
|
+
return barrels.get(file);
|
|
62
|
+
}
|
|
63
|
+
async function optimize(source, file, resolve) {
|
|
64
|
+
if (![...selected].some(value => source.includes(value))) return source;
|
|
65
|
+
const ast = parse(source, { sourceType: 'unambiguous', sourceFilename: file, plugins: ['jsx', ...(/\.[cm]?tsx?$/.test(file) ? ['typescript'] : [])] });
|
|
66
|
+
let changed = false;
|
|
67
|
+
for (const node of ast.program.body) {
|
|
68
|
+
if (!t.isImportDeclaration(node) || !selected.has(node.source.value) || node.importKind === 'type' || !node.specifiers.length || node.attributes?.length) continue;
|
|
69
|
+
const resolved = await resolve(node.source.value, file);
|
|
70
|
+
if (!resolved) continue;
|
|
71
|
+
const barrel = await inspect(resolved);
|
|
72
|
+
if (!barrel) continue;
|
|
73
|
+
const replacements = [];
|
|
74
|
+
for (const item of node.specifiers) {
|
|
75
|
+
if (!t.isImportSpecifier(item) || item.importKind === 'type') continue;
|
|
76
|
+
const target = barrel.exports.get(name(item.imported));
|
|
77
|
+
if (!target) continue;
|
|
78
|
+
const leaf = await resolve(target.source, resolved);
|
|
79
|
+
if (!leaf) continue;
|
|
80
|
+
const relative = path.relative(barrel.root, leaf);
|
|
81
|
+
if (relative.startsWith('..') || path.isAbsolute(relative) || relative.split(path.sep).includes('node_modules')) continue;
|
|
82
|
+
replacements.push({ item, declaration: t.importDeclaration([target.imported === 'default' ? t.importDefaultSpecifier(item.local) : t.importSpecifier(item.local, t.isValidIdentifier(target.imported) ? t.identifier(target.imported) : t.stringLiteral(target.imported))], t.stringLiteral(leaf)) });
|
|
83
|
+
}
|
|
84
|
+
if (!replacements.length) continue;
|
|
85
|
+
const at = ast.program.body.indexOf(node);
|
|
86
|
+
node.specifiers = node.specifiers.filter(item => !replacements.some(value => value.item === item));
|
|
87
|
+
ast.program.body.splice(at, node.specifiers.length ? 0 : 1, ...replacements.map(value => value.declaration));
|
|
88
|
+
changed = true;
|
|
89
|
+
}
|
|
90
|
+
return changed ? generateMapped(generate, ast, { retainLines: true }, source, file) : source;
|
|
91
|
+
}
|
|
92
|
+
optimize.dependencies = dependencies;
|
|
93
|
+
return optimize;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function optimizeImportsPlugins(plugins, packages) {
|
|
97
|
+
if (!packages?.length) return plugins;
|
|
98
|
+
const optimize = createImportOptimizer(packages);
|
|
99
|
+
return plugins.map(plugin => ({ ...plugin, setup(builder) {
|
|
100
|
+
return plugin.setup({ ...builder, onLoad(settings, callback) {
|
|
101
|
+
builder.onLoad(settings, async args => {
|
|
102
|
+
const result = await callback(args);
|
|
103
|
+
if (!result || typeof result.contents !== 'string' || args.namespace !== 'file' || !/\.[cm]?[jt]sx?$/.test(args.path) || result.errors?.length) return result;
|
|
104
|
+
const contents = await optimize(result.contents, args.path, async (specifier, importer) => {
|
|
105
|
+
const resolved = await builder.resolve(specifier, { importer, resolveDir: path.dirname(importer), kind: 'import-statement', pluginData: { prnextNpmResolving: true } });
|
|
106
|
+
return !resolved.errors.length && !resolved.external && resolved.namespace === 'file' && !resolved.suffix ? resolved.path : undefined;
|
|
107
|
+
});
|
|
108
|
+
return { ...result, contents, watchFiles: [...(result.watchFiles || []), ...optimize.dependencies] };
|
|
109
|
+
});
|
|
110
|
+
} });
|
|
111
|
+
} }));
|
|
112
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function validateProxyBodySize(value = 10 * 1024 * 1024) {
|
|
2
|
+
let bytes = value;
|
|
3
|
+
if (typeof value === 'string') {
|
|
4
|
+
const match = /^(\d+(?:\.\d+)?)\s*(b|kb|mb|gb)?$/i.exec(value.trim());
|
|
5
|
+
if (!match) throw new TypeError('experimental.proxyClientMaxBodySize expects bytes or a size such as "25mb"');
|
|
6
|
+
bytes = Math.floor(Number(match[1]) * 1024 ** Math.max(0, ['b', 'kb', 'mb', 'gb'].indexOf((match[2] || 'b').toLowerCase())));
|
|
7
|
+
}
|
|
8
|
+
if (!Number.isSafeInteger(bytes) || bytes < 1 || bytes > 32 * 1024 * 1024) throw new TypeError('experimental.proxyClientMaxBodySize must be between 1 byte and the PRNext 32 MiB buffered transport limit');
|
|
9
|
+
return bytes;
|
|
10
|
+
}
|
package/build/scan.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { validateAppConfig, mergeAppConfig } from './app-config.mjs';
|
|
|
6
6
|
import { scanMiddleware } from './middleware.mjs';
|
|
7
7
|
import { scanMetadataRoutes } from './metadata-routes.mjs';
|
|
8
8
|
import { scanAdvancedAppRoutes } from './app-routing.mjs';
|
|
9
|
-
import { pageExtensionPattern, validatePageSource } from './page-extensions.mjs';
|
|
9
|
+
import { pageExtensionPattern, validatePageSource, defaultPageExtensions } from './page-extensions.mjs';
|
|
10
10
|
|
|
11
11
|
const sourceExtension = /\.(?:[cm]?js|jsx|tsx?)$/;
|
|
12
12
|
|
|
@@ -96,7 +96,7 @@ export async function scanProject(projectRoot, { basePath = '', pageExtensions,
|
|
|
96
96
|
});
|
|
97
97
|
}
|
|
98
98
|
const appRoot = appRoots[0];
|
|
99
|
-
const { appNotFound, appGlobalError } = appRoot ? await scanAppRoutes({ root, appRoot, routes, patterns, sourceExtension }) : {};
|
|
99
|
+
const { appNotFound, appGlobalError } = appRoot ? await scanAppRoutes({ root, appRoot, routes, patterns, sourceExtension, pageExtensions }) : {};
|
|
100
100
|
const metadataManifest = appRoot ? await scanMetadataRoutes({ root, appRoot, files: await walk(appRoot), routes, patterns, validatePattern, basePath, pageExtensions, appNotFound }) : undefined;
|
|
101
101
|
if (metadataManifest) for (const route of routes) if (route.router === 'app' && route.kind === 'page') route.staticMetadata = { manifest: metadataManifest };
|
|
102
102
|
if (!routes.length && !appNotFound) throw new Error('No page or API route found in the Pages Router or App Router directories.');
|
|
@@ -108,11 +108,13 @@ const appConventions = new Map([
|
|
|
108
108
|
['loading', 'loading'], ['error', 'error'], ['not-found', 'notFound'], ['default', 'default'],
|
|
109
109
|
]);
|
|
110
110
|
|
|
111
|
-
async function scanAppRoutes({ root, appRoot, routes, patterns, sourceExtension }) {
|
|
111
|
+
async function scanAppRoutes({ root, appRoot, routes, patterns, sourceExtension, pageExtensions = defaultPageExtensions }) {
|
|
112
112
|
const directories = new Map();
|
|
113
113
|
const configs = new Map();
|
|
114
114
|
let appGlobalError;
|
|
115
|
-
|
|
115
|
+
const rank = file => pageExtensions.findIndex(extension => file.endsWith('.' + extension));
|
|
116
|
+
const files = (await walk(appRoot)).sort((a, b) => rank(a) - rank(b) || a.localeCompare(b));
|
|
117
|
+
for (const file of files) {
|
|
116
118
|
if (!sourceExtension.test(file) || file.endsWith('.d.ts')) continue;
|
|
117
119
|
const relative = path.relative(appRoot, file).replaceAll(path.sep, '/');
|
|
118
120
|
const parts = relative.split('/');
|
|
@@ -122,18 +124,18 @@ async function scanAppRoutes({ root, appRoot, routes, patterns, sourceExtension
|
|
|
122
124
|
// This convention replaces the complete root document. A similarly named
|
|
123
125
|
// file inside a route group or segment is not a nested error boundary.
|
|
124
126
|
if (parts.length === 0) {
|
|
125
|
-
if (appGlobalError)
|
|
127
|
+
if (appGlobalError) { console.warn(`Duplicate global-error file ${relative}; using ${path.relative(appRoot, appGlobalError)} according to pageExtensions.`); continue; }
|
|
126
128
|
appGlobalError = file;
|
|
127
129
|
}
|
|
128
130
|
continue;
|
|
129
131
|
}
|
|
130
132
|
if (!appConventions.has(filename)) continue;
|
|
131
133
|
validatePageSource(file);
|
|
132
|
-
if (['page', 'layout', 'route'].includes(filename)) configs.set(file, await validateAppConfig(file));
|
|
133
134
|
const directory = parts.join('/');
|
|
134
135
|
const descriptor = directories.get(directory) || { directory, files: {} };
|
|
135
136
|
const name = appConventions.get(filename);
|
|
136
|
-
if (descriptor.files[name])
|
|
137
|
+
if (descriptor.files[name]) { console.warn(`Duplicate ${filename} file ${relative}; using ${path.relative(appRoot, descriptor.files[name])} according to pageExtensions.`); continue; }
|
|
138
|
+
if (['page', 'layout', 'route'].includes(filename)) configs.set(file, await validateAppConfig(file));
|
|
137
139
|
descriptor.files[name] = file;
|
|
138
140
|
directories.set(directory, descriptor);
|
|
139
141
|
}
|
package/cli.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { readFile, writeFile, rm } from 'node:fs/promises';
|
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import { resolveNativeBinary, packageManifest } from './native/resolve.mjs';
|
|
8
|
+
import { replaceWithNative } from './native/launch.mjs';
|
|
8
9
|
import { readBuildDirectory } from './runtime/build-directory.mjs';
|
|
9
10
|
import { shouldWatchProjectFile } from './runtime/env.mjs';
|
|
10
11
|
|
|
@@ -31,6 +32,11 @@ Use prnext for portable scripts; prn is a shortcut on compatible shells.
|
|
|
31
32
|
prnext host [config] --check Validate hosting configuration and builds
|
|
32
33
|
prnext host [config] --status [--json] Show memory, process and idle status
|
|
33
34
|
prnext migrate [directory] [options] Migrate a Next project's scripts and dependencies
|
|
35
|
+
prn login [--server URL] Connect PRNext Cloud through your browser
|
|
36
|
+
prn deploy [directory] Upload sources, deploy and link the project
|
|
37
|
+
prn update [directory] Deploy changes to the linked Cloud project
|
|
38
|
+
prn whoami / prn logout Show or disconnect your terminal account
|
|
39
|
+
prn deploy --help Cloud options and local development
|
|
34
40
|
|
|
35
41
|
Migration options: --dry-run (preview), --no-install (prepare only), --json
|
|
36
42
|
prnext migrate --check Audit source compatibility without migrating
|
|
@@ -69,6 +75,11 @@ function stop(child) {
|
|
|
69
75
|
async function main() {
|
|
70
76
|
if (['help', '--help', '-h'].includes(command)) { console.log(help); return; }
|
|
71
77
|
if (['--version', '-v'].includes(command)) { console.log(packageManifest.version); return; }
|
|
78
|
+
if (['login', 'logout', 'whoami', 'deploy', 'update'].includes(command)) {
|
|
79
|
+
const { cloudCommand } = await import('./runtime/cloud/client.mjs');
|
|
80
|
+
await cloudCommand(command, argv);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
72
83
|
if (['pstart', 'prestart', 'pstop', 'pstatus', 'plist', 'plogs', 'pdelete', 'pstartup', 'pdown'].includes(command)) {
|
|
73
84
|
const { persistentCommand } = await import('./runtime/persistent/client.mjs');
|
|
74
85
|
await persistentCommand(command, argv);
|
|
@@ -184,6 +195,11 @@ async function main() {
|
|
|
184
195
|
}
|
|
185
196
|
const binary = await resolveNativeBinary();
|
|
186
197
|
let outputDirectory = await readBuildDirectory(root);
|
|
198
|
+
const serverEnvironment = { ...process.env, NODE_ENV: process.env.NODE_ENV || (command === 'dev' ? 'development' : 'production') };
|
|
199
|
+
if (command === 'start' && serverEnvironment.NODE_ENV !== 'development') {
|
|
200
|
+
if (replaceWithNative(binary, ['start', root, '--worker', path.join(root, outputDirectory, 'runtime/worker.mjs'),
|
|
201
|
+
'--node', process.execPath, ...args], serverEnvironment)) return;
|
|
202
|
+
}
|
|
187
203
|
const launch = () => spawn(binary, [command === 'routes' ? 'routes' : 'start', root,
|
|
188
204
|
...(command === 'routes' ? [] : ['--worker', path.join(root, outputDirectory, 'runtime/worker.mjs'), '--node', process.execPath,
|
|
189
205
|
'--shutdown-on-stdin-eof']), ...args], { stdio: ['pipe', 'inherit', 'inherit'], windowsHide: true, env: { ...process.env, NODE_ENV: process.env.NODE_ENV || (command === 'dev' ? 'development' : 'production') } });
|
package/compat/after.cjs
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const { AsyncLocalStorage } = require('node:async_hooks');
|
|
3
|
+
|
|
4
|
+
function after(task) {
|
|
5
|
+
const context = require('./headers.cjs').currentRequest();
|
|
6
|
+
if (require('./data-cache.cjs').inCacheScope()) throw new Error('after() cannot be registered inside a cache scope');
|
|
7
|
+
if (!context.afterScope || !['route', 'action', 'after'].includes(context.phase)) throw new Error('after() currently requires a Node Route Handler or Server Action in PRNext. Server Component and Edge after() lifecycles are not implemented.');
|
|
8
|
+
context.afterScope.add(task, context, AsyncLocalStorage.snapshot());
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function createAfterScope({ signal, timeoutMs = 25000, onTimeout = () => {}, onComplete, report = error => console.error('[prnext] after() failed:', error?.stack || error) } = {}) {
|
|
12
|
+
const queue = []; let count = 0, closed = false, finishing;
|
|
13
|
+
const deadline = Date.now() + timeoutMs;
|
|
14
|
+
const track = promise => signal?.[Symbol.for('prnext.request-work')]?.(promise, { timeoutMs: Math.max(1, deadline - Date.now()) });
|
|
15
|
+
const observe = promise => Promise.resolve(promise).then(value => ({ value }), error => ({ error }));
|
|
16
|
+
const scope = {
|
|
17
|
+
add(task, context, run) {
|
|
18
|
+
if (closed) throw new Error('after() request has already completed');
|
|
19
|
+
if (typeof task !== 'function' && (!task || typeof task.then !== 'function')) throw new TypeError('after() expects a callback or a Promise');
|
|
20
|
+
if (++count > 128) throw new Error('after() exceeds 128 tasks per request');
|
|
21
|
+
const pending = typeof task === 'function' ? null : observe(task);
|
|
22
|
+
if (pending) track(pending);
|
|
23
|
+
queue.push({ task: pending ? null : task, pending, context, run });
|
|
24
|
+
},
|
|
25
|
+
finish() {
|
|
26
|
+
return finishing ||= (async () => {
|
|
27
|
+
try {
|
|
28
|
+
while (queue.length) {
|
|
29
|
+
const item = queue.shift(), remaining = deadline - Date.now();
|
|
30
|
+
if (remaining <= 0) { const error = new Error('after() exceeded the request maxDuration'); report(error); onTimeout(error); break; }
|
|
31
|
+
const abort = new AbortController();
|
|
32
|
+
item.context.cookies._mutable = false;
|
|
33
|
+
item.context.phase = 'after';
|
|
34
|
+
item.context.signal = abort.signal;
|
|
35
|
+
item.context.cacheState.closed = false;
|
|
36
|
+
const work = item.pending || observe(Promise.resolve().then(() => item.run(item.task)));
|
|
37
|
+
track(work);
|
|
38
|
+
let timer;
|
|
39
|
+
try {
|
|
40
|
+
const outcome = await Promise.race([work, new Promise(resolve => { timer = setTimeout(() => { const error = new Error('after() exceeded the request maxDuration'); abort.abort(error); resolve({ error }); }, remaining); })]);
|
|
41
|
+
if ('error' in outcome) report(outcome.error);
|
|
42
|
+
if (abort.signal.aborted) { onTimeout(outcome.error); break; }
|
|
43
|
+
} finally { clearTimeout(timer); item.context.cacheState.closed = true; await onComplete?.(item.context); }
|
|
44
|
+
}
|
|
45
|
+
} finally { closed = true; queue.length = 0; }
|
|
46
|
+
})();
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
return scope;
|
|
50
|
+
}
|
|
51
|
+
module.exports = { after, createAfterScope };
|
package/compat/index.d.ts
CHANGED
|
@@ -118,6 +118,8 @@ export interface NextConfig {
|
|
|
118
118
|
workerRetirement?: { idleSeconds?: number; laneIdleSeconds?: number; keepWarm?: boolean };
|
|
119
119
|
/** Next dev page-retention hints; PRNext builds all routes without that eviction queue. */
|
|
120
120
|
onDemandEntries?: { maxInactiveAge?: number; pagesBufferLength?: number };
|
|
121
|
+
/** Disable the Next development indicator. Error overlays and Fast Refresh remain enabled. */
|
|
122
|
+
devIndicators?: false;
|
|
121
123
|
i18n?: { locales: string[]; defaultLocale: string; localeDetection?: boolean; domains?: Array<{domain: string; defaultLocale: string; locales?: string[]; http?: boolean}> };
|
|
122
124
|
webpack?: (config: import('webpack').Configuration, context: { buildId: string; dev: boolean; isServer: boolean; nextRuntime?: 'nodejs' | 'edge'; webpack: typeof import('webpack'); defaultLoaders: {babel: {loader: string}} }) => import('webpack').Configuration | Promise<import('webpack').Configuration>;
|
|
123
125
|
turbopack?: { root?: string; rules?: Record<string, TurbopackRule | TurbopackRule[]>; resolveAlias?: Record<string, string | { browser: string }>; resolveExtensions?: string[] };
|
|
@@ -141,7 +143,7 @@ export interface NextConfig {
|
|
|
141
143
|
implementation?: 'sass' | 'sass-embedded';
|
|
142
144
|
[option: string]: unknown;
|
|
143
145
|
};
|
|
144
|
-
experimental?: { nextScriptWorkers?: boolean; serverActions?: true | { allowedOrigins?: string[]; bodySizeLimit?: number | string } };
|
|
146
|
+
experimental?: { nextScriptWorkers?: boolean; optimizePackageImports?: string[]; proxyClientMaxBodySize?: number | string; serverActions?: true | { allowedOrigins?: string[]; bodySizeLimit?: number | string } };
|
|
145
147
|
trailingSlash?: boolean;
|
|
146
148
|
skipTrailingSlashRedirect?: boolean;
|
|
147
149
|
skipMiddlewareUrlNormalize?: boolean;
|
package/compat/server.cjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
const { NextRequest, NextResponse, NextURL } = require('./server-primitives.cjs');
|
|
3
|
+
const { after } = require('./after.cjs');
|
|
3
4
|
|
|
4
5
|
async function connection() {
|
|
5
6
|
const context = require('./headers.cjs').currentRequest();
|
|
@@ -10,4 +11,4 @@ async function connection() {
|
|
|
10
11
|
dynamicUsage('connection()', context);
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
module.exports = { NextRequest, NextResponse, NextURL, connection };
|
|
14
|
+
module.exports = { NextRequest, NextResponse, NextURL, connection, after };
|
package/compat/server.d.cts
CHANGED
|
@@ -35,3 +35,5 @@ export interface MiddlewareConfig {
|
|
|
35
35
|
export interface ProxyConfig { matcher?: MiddlewareConfig['matcher'] }
|
|
36
36
|
/** Exclude the following render from prerendering until a request is available. */
|
|
37
37
|
export function connection(): Promise<void>;
|
|
38
|
+
/** Post-response work in Node Route Handlers. Other after() lifecycles fail explicitly. */
|
|
39
|
+
export function after(task: Promise<unknown> | (() => unknown)): void;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// A production CLI has nothing to supervise once the native server starts.
|
|
2
|
+
// On supported Linux Node releases, replace it instead of retaining a Node heap.
|
|
3
|
+
// Keep the portable parent/pipe lifecycle for Windows, older Node, IPC callers
|
|
4
|
+
// and instrumentation which may own descriptors or need JS exit handlers.
|
|
5
|
+
export function replaceWithNative(binary, args, env, runtime = process) {
|
|
6
|
+
const options = [...(runtime.execArgv || []), env.NODE_OPTIONS || ''].join(' ');
|
|
7
|
+
// Only heap sizing is known not to require the Node launcher's lifetime.
|
|
8
|
+
// Be conservative for all other Node flags, including future diagnostics,
|
|
9
|
+
// loaders, preloads, permissions and signal-triggered reports/snapshots.
|
|
10
|
+
const customRuntime = options.replace(/(?:^|\s)--max[-_](?:semi|old)[-_]space[-_]size(?:=|\s+)\d+(?=\s|$)/g, '').trim();
|
|
11
|
+
if (runtime.platform !== 'linux' || typeof runtime.execve !== 'function'
|
|
12
|
+
|| typeof runtime.send === 'function' || env.NODE_V8_COVERAGE || customRuntime) return false;
|
|
13
|
+
// execve keeps stdin/out/err and the PID. No parent pipe is needed: signals
|
|
14
|
+
// reach Rust directly, and its worker pipes still supervise every Node child.
|
|
15
|
+
// The marker also makes an old native binary fail clearly instead of using
|
|
16
|
+
// this lifecycle without the Linux parent-death guard it requires.
|
|
17
|
+
runtime.execve(binary, [binary, ...args, '--cli-exec'], env);
|
|
18
|
+
return true; // Only reached by injected test runtimes; real execve never returns.
|
|
19
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thomas.f/prnext",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13-alpha",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Experimental Rust-powered runtime for Next.js apps, React and npm",
|
|
6
6
|
"homepage": "https://prnext.dev",
|
|
@@ -133,12 +133,12 @@
|
|
|
133
133
|
"react-server-dom-webpack": "19.3.0"
|
|
134
134
|
},
|
|
135
135
|
"optionalDependencies": {
|
|
136
|
-
"prnext-darwin-arm64": "0.1.
|
|
137
|
-
"prnext-darwin-x64": "0.1.
|
|
138
|
-
"prnext-linux-arm64-gnu": "0.1.
|
|
139
|
-
"prnext-linux-x64-gnu": "0.1.
|
|
140
|
-
"@thomas.f/prnext-win32-x64-msvc": "0.1.
|
|
141
|
-
"@thomas.f/prnext-win32-arm64-msvc": "0.1.
|
|
136
|
+
"prnext-darwin-arm64": "0.1.13-alpha",
|
|
137
|
+
"prnext-darwin-x64": "0.1.13-alpha",
|
|
138
|
+
"prnext-linux-arm64-gnu": "0.1.13-alpha",
|
|
139
|
+
"prnext-linux-x64-gnu": "0.1.13-alpha",
|
|
140
|
+
"@thomas.f/prnext-win32-x64-msvc": "0.1.13-alpha",
|
|
141
|
+
"@thomas.f/prnext-win32-arm64-msvc": "0.1.13-alpha"
|
|
142
142
|
},
|
|
143
143
|
"engines": {
|
|
144
144
|
"node": ">=22"
|