@thomas.f/prnext 0.1.11-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 +395 -29
- 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/cache-handlers.mjs +4 -1
- package/build/compiler.mjs +4 -1
- package/build/config.mjs +20 -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 +12 -1
- 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 +14 -2
- package/build/optimize-imports.mjs +112 -0
- package/build/proxy-body-config.mjs +10 -0
- package/build/runtime-contract.mjs +16 -0
- package/build/scan.mjs +9 -7
- package/build/typescript-check.mjs +20 -0
- package/build/typescript.mjs +49 -0
- package/build/worker-retirement.mjs +14 -0
- package/cli.mjs +17 -0
- package/compat/after.cjs +51 -0
- package/compat/index.d.ts +13 -1
- package/compat/server.cjs +2 -1
- package/compat/server.d.cts +2 -0
- package/native/launch.mjs +19 -0
- package/package.json +25 -9
- package/release/verify.mjs +9 -0
- package/runtime/api.mjs +15 -5
- package/runtime/app-render.mjs +164 -53
- 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 +3 -53
- 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 +136 -0
- package/runtime/http.mjs +9 -6
- package/runtime/middleware.mjs +3 -3
- package/runtime/profiles.json +13 -8
- package/runtime/request-transport.mjs +1 -1
- package/runtime/request-work.mjs +2 -2
- package/runtime/route-static.mjs +1 -1
- package/runtime/rsc-lifecycle.mjs +119 -0
- package/runtime/rsc-work-watchdog.mjs +75 -0
- package/runtime/rsc-worker.mjs +36 -9
- package/runtime/stream-utils.mjs +22 -3
- package/runtime/transport.mjs +89 -53
- package/runtime/worker-metadata.mjs +23 -0
- package/runtime/worker.mjs +119 -27
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFile, readdir, writeFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
// Protocol compatibility does not imply safe retirement. These modules jointly
|
|
6
|
+
// implement activity tracking, cancellation and post-response draining.
|
|
7
|
+
export const lifecycleModules = ['worker.mjs', 'transport.mjs', 'request-work.mjs', 'app-render.mjs', 'app-stream-html.mjs', 'rsc-worker.mjs'];
|
|
8
|
+
export async function writeRuntimeContract(stage) {
|
|
9
|
+
const files = (await Promise.all(['runtime', 'compat'].map(async directory =>
|
|
10
|
+
(await readdir(path.join(stage,directory))).filter(name => /\.(?:mjs|cjs|json)$/.test(name)).map(name => `${directory}/${name}`)))).flat().sort();
|
|
11
|
+
const modules = Object.fromEntries(await Promise.all(files.map(async name =>
|
|
12
|
+
[name, createHash('sha256').update(await readFile(path.join(stage, name))).digest('hex')])));
|
|
13
|
+
const contract = { schema: 1, protocol: 2, lifecycle: 1,
|
|
14
|
+
capabilities: ['activity', 'tracked-finalization', 'graceful-drain', 'lane-cancellation'], modules };
|
|
15
|
+
await writeFile(path.join(stage, 'runtime-contract.json'), JSON.stringify(contract, null, 2) + '\n');
|
|
16
|
+
}
|
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
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
|
|
3
|
+
const [, , compiler, filename] = process.argv;
|
|
4
|
+
const ts = createRequire(import.meta.url)(compiler);
|
|
5
|
+
const host = {
|
|
6
|
+
...ts.sys,
|
|
7
|
+
onUnRecoverableConfigFileDiagnostic(diagnostic) {
|
|
8
|
+
process.stderr.write(ts.formatDiagnostics([diagnostic], formatHost));
|
|
9
|
+
process.exit(1);
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
const formatHost = { getCanonicalFileName: name => name, getCurrentDirectory: ts.sys.getCurrentDirectory, getNewLine: () => ts.sys.newLine };
|
|
13
|
+
const parsed = ts.getParsedCommandLineOfConfigFile(filename, { noEmit: true }, host);
|
|
14
|
+
const diagnostics = parsed.errors.length ? parsed.errors : ts.getPreEmitDiagnostics(ts.createProgram({
|
|
15
|
+
rootNames: parsed.fileNames, options: parsed.options, projectReferences: parsed.projectReferences,
|
|
16
|
+
}));
|
|
17
|
+
if (diagnostics.length) {
|
|
18
|
+
process.stderr.write(ts.formatDiagnostics(diagnostics, formatHost));
|
|
19
|
+
process.exitCode = 1;
|
|
20
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { stat } from 'node:fs/promises';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { execFile } from 'node:child_process';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
|
|
8
|
+
const execute = promisify(execFile);
|
|
9
|
+
|
|
10
|
+
export function validateTypeScript(input) {
|
|
11
|
+
if (input === undefined) return undefined;
|
|
12
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) throw new TypeError('typescript must be an object');
|
|
13
|
+
for (const key of Object.keys(input)) if (!['ignoreBuildErrors', 'tsconfigPath'].includes(key)) throw new TypeError(`typescript.${key} is not supported`);
|
|
14
|
+
const { ignoreBuildErrors = false, tsconfigPath = 'tsconfig.json' } = input;
|
|
15
|
+
if (typeof ignoreBuildErrors !== 'boolean') throw new TypeError('typescript.ignoreBuildErrors must be a boolean');
|
|
16
|
+
if (typeof tsconfigPath !== 'string' || !tsconfigPath || tsconfigPath.includes('\0') || Buffer.byteLength(tsconfigPath) > 4096) throw new TypeError('typescript.tsconfigPath must be a nonempty filesystem path of at most 4096 bytes');
|
|
17
|
+
return { ignoreBuildErrors, tsconfigPath };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function projectTsconfig(root, config) {
|
|
21
|
+
const name = config.typescript?.tsconfigPath ?? 'tsconfig.json';
|
|
22
|
+
const filename = path.resolve(root, name);
|
|
23
|
+
try {
|
|
24
|
+
if (!(await stat(filename)).isFile()) throw new Error(`TypeScript configuration is not a file: ${filename}`);
|
|
25
|
+
return filename;
|
|
26
|
+
} catch (error) {
|
|
27
|
+
if (error.code !== 'ENOENT') throw error;
|
|
28
|
+
if (name !== 'tsconfig.json') throw new Error(`typescript.tsconfigPath does not exist: ${filename}`, { cause: error });
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Run the checker outside the build/dev process so its graph is released on exit.
|
|
34
|
+
* Never emit files, rewrite the user's config, or install packages implicitly. */
|
|
35
|
+
export async function checkTypeScript(root, config, { dev = false, tsconfig } = {}) {
|
|
36
|
+
if (dev || !config.typescript || config.typescript.ignoreBuildErrors) return;
|
|
37
|
+
tsconfig ??= await projectTsconfig(root, config);
|
|
38
|
+
if (!tsconfig) throw new Error('TypeScript checking was explicitly enabled, but tsconfig.json does not exist. Create a TypeScript configuration, set typescript.tsconfigPath to an existing configuration, or explicitly set typescript.ignoreBuildErrors: true if types are checked separately.');
|
|
39
|
+
let compiler;
|
|
40
|
+
try { compiler = createRequire(path.join(root, 'package.json')).resolve('typescript'); }
|
|
41
|
+
catch (error) { throw new Error('TypeScript checking requires typescript in the project. Install it as a dev dependency, or explicitly set typescript.ignoreBuildErrors: true if a separate process checks types.', { cause: error }); }
|
|
42
|
+
try {
|
|
43
|
+
await execute(process.execPath, [fileURLToPath(new URL('./typescript-check.mjs', import.meta.url)), compiler, tsconfig], {
|
|
44
|
+
cwd: root, windowsHide: true, maxBuffer: 4 * 1024 * 1024,
|
|
45
|
+
});
|
|
46
|
+
} catch (error) {
|
|
47
|
+
throw new Error(`TypeScript check failed (${path.relative(root, tsconfig)}):\n${error.stdout || error.stderr || error.message}`, { cause: error });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function validateWorkerRetirement(value) {
|
|
2
|
+
if (value === undefined) return undefined;
|
|
3
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new TypeError('workerRetirement must be an object');
|
|
4
|
+
for (const [key, setting] of Object.entries(value)) {
|
|
5
|
+
if (!['idleSeconds', 'laneIdleSeconds', 'keepWarm'].includes(key)) throw new TypeError(`workerRetirement.${key} is not supported`);
|
|
6
|
+
if (setting === undefined) continue;
|
|
7
|
+
if (key === 'keepWarm') {
|
|
8
|
+
if (typeof setting !== 'boolean') throw new TypeError('workerRetirement.keepWarm must be a boolean');
|
|
9
|
+
} else if (!Number.isSafeInteger(setting) || setting < 0 || setting > 604800) {
|
|
10
|
+
throw new TypeError(`workerRetirement.${key} must be an integer between 0 and 604800 seconds (0 disables retirement)`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== undefined));
|
|
14
|
+
}
|
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
|
|
@@ -41,6 +47,7 @@ Simple next dev/build/start scripts become prnext commands; custom shell scripts
|
|
|
41
47
|
|
|
42
48
|
Server options: --port 3000 --hostname 127.0.0.1 --workers 1 --inspect
|
|
43
49
|
Production profiles: --profile balanced|speed|memory|classic
|
|
50
|
+
Worker retirement: --worker-idle-seconds 300 --lane-idle-seconds 30 --keep-warm [true|false]
|
|
44
51
|
Memory favors lower RAM under concurrency, accepting more waiting and CPU per response.
|
|
45
52
|
Without an option: PRNEXT_PROFILE, legacy PRNEXT_MEMORY_PROFILE=compact, then balanced.
|
|
46
53
|
Classic preserves the former standard settings; standard remains an alias. Legacy compact remains accepted.
|
|
@@ -68,6 +75,11 @@ function stop(child) {
|
|
|
68
75
|
async function main() {
|
|
69
76
|
if (['help', '--help', '-h'].includes(command)) { console.log(help); return; }
|
|
70
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
|
+
}
|
|
71
83
|
if (['pstart', 'prestart', 'pstop', 'pstatus', 'plist', 'plogs', 'pdelete', 'pstartup', 'pdown'].includes(command)) {
|
|
72
84
|
const { persistentCommand } = await import('./runtime/persistent/client.mjs');
|
|
73
85
|
await persistentCommand(command, argv);
|
|
@@ -183,6 +195,11 @@ async function main() {
|
|
|
183
195
|
}
|
|
184
196
|
const binary = await resolveNativeBinary();
|
|
185
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
|
+
}
|
|
186
203
|
const launch = () => spawn(binary, [command === 'routes' ? 'routes' : 'start', root,
|
|
187
204
|
...(command === 'routes' ? [] : ['--worker', path.join(root, outputDirectory, 'runtime/worker.mjs'), '--node', process.execPath,
|
|
188
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
|
@@ -112,14 +112,26 @@ export interface Rewrite extends CustomRoute { destination: string }
|
|
|
112
112
|
export type ConfigRedirect = CustomRoute & Redirect;
|
|
113
113
|
/** Implemented project options. Unsupported behavior-changing options fail the build. */
|
|
114
114
|
export interface NextConfig {
|
|
115
|
+
/** PRNext only. CLI > environment > build configuration > profile.
|
|
116
|
+
* 0 disables the corresponding retirement; keepWarm retains one lane per used host.
|
|
117
|
+
*/
|
|
118
|
+
workerRetirement?: { idleSeconds?: number; laneIdleSeconds?: number; keepWarm?: boolean };
|
|
115
119
|
/** Next dev page-retention hints; PRNext builds all routes without that eviction queue. */
|
|
116
120
|
onDemandEntries?: { maxInactiveAge?: number; pagesBufferLength?: number };
|
|
121
|
+
/** Disable the Next development indicator. Error overlays and Fast Refresh remain enabled. */
|
|
122
|
+
devIndicators?: false;
|
|
117
123
|
i18n?: { locales: string[]; defaultLocale: string; localeDetection?: boolean; domains?: Array<{domain: string; defaultLocale: string; locales?: string[]; http?: boolean}> };
|
|
118
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>;
|
|
119
125
|
turbopack?: { root?: string; rules?: Record<string, TurbopackRule | TurbopackRule[]>; resolveAlias?: Record<string, string | { browser: string }>; resolveExtensions?: string[] };
|
|
120
126
|
images?: import('./image.cjs').ImageConfig;
|
|
121
127
|
pageExtensions?: string[];
|
|
122
128
|
reactStrictMode?: boolean;
|
|
129
|
+
/** Enabled React Compiler transforms are not implemented. */
|
|
130
|
+
reactCompiler?: false;
|
|
131
|
+
/** Typed route generation and validation are not implemented. */
|
|
132
|
+
typedRoutes?: false;
|
|
133
|
+
/** Production checks use the project's installed TypeScript without emitting files. */
|
|
134
|
+
typescript?: { ignoreBuildErrors?: boolean; tsconfigPath?: string };
|
|
123
135
|
cacheComponents?: boolean;
|
|
124
136
|
cacheLife?: Record<string, import('./cache.cjs').CacheLife>;
|
|
125
137
|
transpilePackages?: string[];
|
|
@@ -131,7 +143,7 @@ export interface NextConfig {
|
|
|
131
143
|
implementation?: 'sass' | 'sass-embedded';
|
|
132
144
|
[option: string]: unknown;
|
|
133
145
|
};
|
|
134
|
-
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 } };
|
|
135
147
|
trailingSlash?: boolean;
|
|
136
148
|
skipTrailingSlashRedirect?: boolean;
|
|
137
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",
|
|
@@ -17,14 +17,30 @@
|
|
|
17
17
|
"name": "Thomas",
|
|
18
18
|
"url": "https://github.com/Lorkydey"
|
|
19
19
|
},
|
|
20
|
-
"keywords": [
|
|
20
|
+
"keywords": [
|
|
21
|
+
"prnext",
|
|
22
|
+
"nextjs",
|
|
23
|
+
"react",
|
|
24
|
+
"rust",
|
|
25
|
+
"framework",
|
|
26
|
+
"ssr",
|
|
27
|
+
"server-components"
|
|
28
|
+
],
|
|
21
29
|
"type": "module",
|
|
22
30
|
"publishConfig": {
|
|
23
31
|
"access": "public",
|
|
24
32
|
"tag": "alpha",
|
|
25
33
|
"registry": "https://registry.npmjs.org/"
|
|
26
34
|
},
|
|
27
|
-
"files": [
|
|
35
|
+
"files": [
|
|
36
|
+
"cli.mjs",
|
|
37
|
+
"index.mjs",
|
|
38
|
+
"build/",
|
|
39
|
+
"runtime/",
|
|
40
|
+
"compat/",
|
|
41
|
+
"native/",
|
|
42
|
+
"release/"
|
|
43
|
+
],
|
|
28
44
|
"scripts": {
|
|
29
45
|
"prepublishOnly": "node release/verify.mjs"
|
|
30
46
|
},
|
|
@@ -117,12 +133,12 @@
|
|
|
117
133
|
"react-server-dom-webpack": "19.3.0"
|
|
118
134
|
},
|
|
119
135
|
"optionalDependencies": {
|
|
120
|
-
"prnext-darwin-arm64": "0.1.
|
|
121
|
-
"prnext-darwin-x64": "0.1.
|
|
122
|
-
"prnext-linux-arm64-gnu": "0.1.
|
|
123
|
-
"prnext-linux-x64-gnu": "0.1.
|
|
124
|
-
"@thomas.f/prnext-win32-
|
|
125
|
-
"@thomas.f/prnext-win32-
|
|
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"
|
|
126
142
|
},
|
|
127
143
|
"engines": {
|
|
128
144
|
"node": ">=22"
|
package/release/verify.mjs
CHANGED
|
@@ -26,6 +26,15 @@ export async function verifyPackage() {
|
|
|
26
26
|
if (source) {
|
|
27
27
|
const cargo = await readFile(path.join(source, 'crates/prnext/Cargo.toml'), 'utf8');
|
|
28
28
|
if (!cargo.includes(`version = "${pkg.version}"`)) throw new Error('Cargo and npm versions differ. Update crates/prnext/Cargo.toml and Cargo.lock before building release binaries.');
|
|
29
|
+
const top = JSON.parse(await readFile(path.join(source, 'package.json'), 'utf8'));
|
|
30
|
+
const lock = JSON.parse(await readFile(path.join(source, 'package-lock.json'), 'utf8'));
|
|
31
|
+
const cargoLock = (await readFile(path.join(source, 'Cargo.lock'), 'utf8')).replaceAll('\r\n', '\n');
|
|
32
|
+
if ([top.version, lock.version, lock.packages[''].version, lock.packages['packages/prnext'].version].some(version => version !== pkg.version) ||
|
|
33
|
+
!cargoLock.includes(`name = "prnext"\nversion = "${pkg.version}"`)) throw new Error('Workspace and lockfile versions differ; run scripts/release-version.mjs before building.');
|
|
34
|
+
for (const target of platforms) {
|
|
35
|
+
const native = lock.packages[`node_modules/${target.package}`];
|
|
36
|
+
if (native?.version !== pkg.version || native.optional !== true || lock.packages['packages/prnext'].optionalDependencies[target.package] !== pkg.version) throw new Error(`Missing or stale native lock entry: ${target.package}`);
|
|
37
|
+
}
|
|
29
38
|
}
|
|
30
39
|
}
|
|
31
40
|
|
package/runtime/api.mjs
CHANGED
|
@@ -178,6 +178,7 @@ function parseBody(bytes, contentType = '') {
|
|
|
178
178
|
}
|
|
179
179
|
|
|
180
180
|
export function runApi(options) {
|
|
181
|
+
if (options.timeoutMs === undefined && options.route?.cacheConfig?.maxDuration) options = { ...options, timeoutMs: options.route.cacheConfig.maxDuration * 1000 };
|
|
181
182
|
return withCacheRequest(options, options.route?.router === 'app' ? 'route' : 'pages', context => runApiInner(options, context));
|
|
182
183
|
}
|
|
183
184
|
|
|
@@ -189,11 +190,19 @@ async function runApiInner({ modulePath, route = {}, manifest = {}, production =
|
|
|
189
190
|
const mod = (pages && loadedPagesModule(modulePath)) || await withSignal(loadModule(modulePath, pages), stream ? signal : undefined);
|
|
190
191
|
const requestOriginal = originalUrl ? new URL(originalUrl, url) : undefined;
|
|
191
192
|
if (requestOriginal) requestOriginal.pathname = removeBasePath(requestOriginal.pathname, manifest.config?.basePath || '');
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
193
|
+
const appHandler = typeof mod.default !== 'function';
|
|
194
|
+
// App Route Handlers receive a NextRequest below. Constructing the Pages
|
|
195
|
+
// IncomingMessage here duplicates URL, header and cookie work and allocates
|
|
196
|
+
// an unused Duplex. Keep the existing Pages request path unchanged.
|
|
197
|
+
const { request, bytes } = appHandler
|
|
198
|
+
? { bytes: Buffer.isBuffer(body) ? body : Buffer.from(body || '', 'base64') }
|
|
199
|
+
: createRequest({ url, originalUrl: requestOriginal?.href, params, method, headers, body });
|
|
200
|
+
if (!appHandler) {
|
|
201
|
+
request.draftMode = cacheContext.draftMode;
|
|
202
|
+
request.previewData = cacheContext.previewData;
|
|
203
|
+
request.preview = cacheContext.previewData !== false;
|
|
204
|
+
}
|
|
205
|
+
if (appHandler) {
|
|
197
206
|
const {NextRequest, proxyRouteRequest} = await appRequestPrimitives();
|
|
198
207
|
if (route.router === 'app' && manifest.dev) {
|
|
199
208
|
const listing = await collectStaticParams({ page: mod, pageConfig: route.handlerConfig, segments: [] }, route.pattern);
|
|
@@ -221,6 +230,7 @@ async function runApiInner({ modulePath, route = {}, manifest = {}, production =
|
|
|
221
230
|
const timeout = lifecycle && setTimeout(() => lifecycle.controller.abort(apiTimeoutError('response headers', timeoutMs)), timeoutMs);
|
|
222
231
|
timeout?.unref();
|
|
223
232
|
const pendingResponse = runRequestContext({ url, method, headers, params, production, cacheComponents: manifest.config?.cacheComponents, cacheLife: manifest.config?.cacheLife, cacheHandlers: manifest.config?.cacheHandlers, cacheHandler: manifest.config?.cacheHandler, cacheMaxMemorySize: manifest.config?.cacheMaxMemorySize, distDir: path.dirname(path.dirname(modulePath)), signal: lifecycle?.controller.signal || signal, previewModeId: manifest.previewModeId, basePath: manifest.config?.basePath || '', routePattern: route.pattern, cacheConfig: route.cacheConfig, phase: 'route', cacheState: cacheContext.cacheState, staticState: cacheContext.staticState, mutableCookies: true }, async () => {
|
|
233
|
+
currentRequest().afterScope = cacheContext.afterScope;
|
|
224
234
|
let result;
|
|
225
235
|
try { result = await handler(proxyRouteRequest(new NextRequest(requestOriginal || url, init), currentRequest()), { params: Promise.resolve(params) }); }
|
|
226
236
|
catch (error) {
|