@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
package/build/app.mjs
CHANGED
|
@@ -118,7 +118,7 @@ function boundaryEntry(boundary) {
|
|
|
118
118
|
return `import * as client from ${JSON.stringify(boundary.file)};\n` + boundary.exports.map((name, index) => `const ref${index}=client[${JSON.stringify(name)}];export {ref${index} as ${JSON.stringify(name)}};`).join('\n');
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
function routeEntry(route, globalError) {
|
|
121
|
+
function routeEntry(route, globalError, dev) {
|
|
122
122
|
if (route.kind === 'api') return `export * from ${JSON.stringify(route.file)};`;
|
|
123
123
|
const imports = [`import * as page from ${JSON.stringify(route.file)};`, 'export {page};', `export const pageConfig=${JSON.stringify(route.pageConfig || {})};`];
|
|
124
124
|
const rootLayout = route.segments.find(segment => segment.layout)?.layout;
|
|
@@ -144,6 +144,9 @@ function routeEntry(route, globalError) {
|
|
|
144
144
|
if (route.routing || route.segments.some(segment => segment.error)) imports.push(`export {default as ErrorBoundary} from ${JSON.stringify(path.join(packageRoot, 'compat/app-error-boundary.cjs'))};`);
|
|
145
145
|
if (route.routing) {
|
|
146
146
|
let index = 0;
|
|
147
|
+
// Only scaffold objects owned by this compiler are immutable. Imported
|
|
148
|
+
// modules and config values retain their original live bindings/behavior.
|
|
149
|
+
const structural = source => dev ? source : `Object.freeze(${source})`;
|
|
147
150
|
function serialize(value) {
|
|
148
151
|
const files = [];
|
|
149
152
|
for (const [name, file] of Object.entries(value.files)) {
|
|
@@ -151,7 +154,7 @@ function routeEntry(route, globalError) {
|
|
|
151
154
|
imports.push(`import * as ${binding} from ${JSON.stringify(file)};`);
|
|
152
155
|
files.push(`${JSON.stringify(name)}:${binding}`);
|
|
153
156
|
}
|
|
154
|
-
return `{path:${JSON.stringify(value.path)},segment:${JSON.stringify(value.segment)},pattern:${JSON.stringify(value.pattern)},layoutId:${JSON.stringify(value.layoutId)},config:${JSON.stringify(value.config || {})},interception:${JSON.stringify(value.interception || null)},files
|
|
157
|
+
return structural(`{path:${JSON.stringify(value.path)},segment:${JSON.stringify(value.segment)},pattern:${JSON.stringify(value.pattern)},layoutId:${JSON.stringify(value.layoutId)},config:${JSON.stringify(value.config || {})},interception:${JSON.stringify(value.interception || null)},files:${structural(`{${files.join(',')}}`)},children:${structural(`[${value.children.map(serialize).join(',')}]`)},slots:${structural(`{${Object.entries(value.slots).map(([name, child]) => `${JSON.stringify(name)}:${serialize(child)}`).join(',')}}`)}}`);
|
|
155
158
|
}
|
|
156
159
|
const routing = serialize(route.routing);
|
|
157
160
|
imports.push(`export const routing=${routing};export const interceptionOnly=${!!route.interceptionOnly};`);
|
|
@@ -263,8 +266,17 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
|
|
|
263
266
|
return { path: args.path, namespace: 'prnext-app-empty' };
|
|
264
267
|
});
|
|
265
268
|
esbuild.onLoad({ filter: /.*/, namespace: 'prnext-app-empty' }, () => ({ contents: '', loader: 'js' }));
|
|
269
|
+
if (rsc) esbuild.onLoad({ filter: /^react$/, namespace: 'prnext-rsc-react' }, () => ({
|
|
270
|
+
// Keep the real singleton while preserving bundler-style CJS access.
|
|
271
|
+
// Mixed npm modules may export both server-safe components and unused
|
|
272
|
+
// hook-based components. Native ESM named imports would reject the
|
|
273
|
+
// entire module because React's server build has no client hooks.
|
|
274
|
+
contents: `import React from 'react';export default React;export const {${Object.keys(resolveFromProject('react')).filter(name => t.isValidIdentifier(name) && name !== 'default').join(',')}}=React;`,
|
|
275
|
+
loader: 'js',
|
|
276
|
+
}));
|
|
266
277
|
esbuild.onResolve({ filter: /^(?:react|react-dom|react-server-dom-webpack)(?:\/|$)/ }, args => {
|
|
267
278
|
if (args.pluginData?.prnextExportScan) return;
|
|
279
|
+
if (rsc && args.path === 'react' && args.namespace !== 'prnext-rsc-react') return { path: 'react', namespace: 'prnext-rsc-react' };
|
|
268
280
|
return browser ? { path: resolveFromProject.resolve(args.path) } : { path: args.path, external: true };
|
|
269
281
|
});
|
|
270
282
|
if (!browser) {
|
|
@@ -446,14 +458,14 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
|
|
|
446
458
|
}
|
|
447
459
|
|
|
448
460
|
await mkdir(path.join(stage, 'assets'), { recursive: true });
|
|
449
|
-
const entries = {};
|
|
461
|
+
const entries = {}, handlerEntries = {};
|
|
450
462
|
for (const route of routes) {
|
|
451
463
|
const entry = path.join(stage, '.entries', route.id + '.server.mjs');
|
|
452
464
|
if ((route.handlerConfig?.runtime === 'edge' || route.cacheConfig?.runtime === 'edge') && manifest.config?.cacheComponents) throw new Error(`Cache Components cannot run in the Edge Runtime (${route.file}).`);
|
|
453
|
-
await writeFile(entry, routeEntry(route, globalError));
|
|
465
|
+
await writeFile(entry, routeEntry(route, globalError, dev));
|
|
454
466
|
if (route.cacheConfig?.runtime === 'edge' && route.kind === 'page') await edgeAppBuild(entry, path.join(stage, 'server', route.id + '.mjs'), 'rsc');
|
|
455
467
|
else if (route.handlerConfig?.runtime === 'edge') await compileEdge({ file: route.file, outfile: path.join(stage, 'server', route.id + '.mjs'), projectRoot: project.root, dev, defineEnvironment });
|
|
456
|
-
else entries[route.id] = entry;
|
|
468
|
+
else (route.kind === 'api' ? handlerEntries : entries)[route.id] = entry;
|
|
457
469
|
const instantConfigs = [route.pageConfig, ...(route.segments || []).map(segment=>segment.staticConfig)];
|
|
458
470
|
const collectInstant = node => {if(!node)return;instantConfigs.push(...Object.values(node.config || {}));for(const child of [...(node.children || []),...Object.values(node.slots || {})])collectInstant(child);};
|
|
459
471
|
collectInstant(route.routing);
|
|
@@ -466,6 +478,9 @@ export async function compileApp({ project, stage, manifest, cssModules, fonts,
|
|
|
466
478
|
}
|
|
467
479
|
manifest.app = { clientModules: {}, actions: {}, actionKey: actions.actionKey };
|
|
468
480
|
const actionRevision = () => [...actions.actions.values()].map(action=>action.id+':'+[...(action.runtimes || [])].sort().join(',')).join(';');
|
|
481
|
+
// Route Handlers have a Node React environment and must not share RSC chunks
|
|
482
|
+
// that import registerServerReference or client-reference registration.
|
|
483
|
+
await serverBuild(handlerEntries, 'server', 'route');
|
|
469
484
|
for (;;) {
|
|
470
485
|
const revision = `${actionRevision()}:${boundaries.size}:${cssFiles.size}:${globalErrorCssFiles.size}`;
|
|
471
486
|
for (const action of actions.actions.values()) {
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { build } from 'esbuild';
|
|
2
|
+
import { lstat, realpath, readdir, writeFile, unlink } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
// Paths used by the native worker, generated application modules, prerender,
|
|
6
|
+
// standalone tracing, compatibility modules and the runtime lifecycle contract.
|
|
7
|
+
// Other relative imports are resolved into shared chunks by this single build.
|
|
8
|
+
export const runtimeEntryNames = Object.freeze([
|
|
9
|
+
'worker', 'rsc-worker', 'app-render', 'app-stream-html', 'transport', 'request-work',
|
|
10
|
+
'http', 'app-static', 'route-static', 'render', 'api', 'middleware', 'incremental-cache', 'app-instant',
|
|
11
|
+
'action-crypto', 'action-client', 'action-ssr', 'edge', 'metadata-route', 'og-loader',
|
|
12
|
+
].sort());
|
|
13
|
+
|
|
14
|
+
/** Run once, after compiling application/Edge graphs and before any prerender. */
|
|
15
|
+
export async function bundleRuntime(stage) {
|
|
16
|
+
const resolvedStage = path.resolve(stage);
|
|
17
|
+
if (!path.basename(resolvedStage).startsWith('.prnext-build-')) {
|
|
18
|
+
throw new Error('Runtime bundling requires an unpublished PRNext build stage.');
|
|
19
|
+
}
|
|
20
|
+
const runtime = path.join(resolvedStage, 'runtime');
|
|
21
|
+
const [stageInfo, runtimeInfo] = await Promise.all([lstat(resolvedStage), lstat(runtime)]);
|
|
22
|
+
if (!stageInfo.isDirectory() || stageInfo.isSymbolicLink() || !runtimeInfo.isDirectory() || runtimeInfo.isSymbolicLink()) {
|
|
23
|
+
throw new Error('Runtime bundling requires ordinary stage and runtime directories.');
|
|
24
|
+
}
|
|
25
|
+
const [canonicalStage, canonicalRuntime] = await Promise.all([realpath(resolvedStage), realpath(runtime)]);
|
|
26
|
+
if (path.relative(canonicalStage, canonicalRuntime) !== 'runtime') {
|
|
27
|
+
throw new Error('Runtime bundling escaped the build stage.');
|
|
28
|
+
}
|
|
29
|
+
const entries = await readdir(runtime, { withFileTypes: true });
|
|
30
|
+
const originals = entries.filter(entry => entry.name.endsWith('.mjs'));
|
|
31
|
+
if (originals.some(entry => !entry.isFile() || entry.isSymbolicLink())) {
|
|
32
|
+
throw new Error('Runtime source modules must be ordinary files.');
|
|
33
|
+
}
|
|
34
|
+
for (const name of runtimeEntryNames) {
|
|
35
|
+
if (!originals.some(entry => entry.name === `${name}.mjs`)) throw new Error(`Missing runtime entry ${name}.mjs`);
|
|
36
|
+
}
|
|
37
|
+
// write:false makes compilation atomic with respect to the source runtime:
|
|
38
|
+
// no files change if parsing or resolution fails. The virtual output is a
|
|
39
|
+
// sibling so all relative paths have the same depth as the final runtime.
|
|
40
|
+
const virtualOutput = path.join(resolvedStage, '.runtime-bundle');
|
|
41
|
+
const result = await build({
|
|
42
|
+
absWorkingDir: runtime,
|
|
43
|
+
entryPoints: runtimeEntryNames.map(name => path.join(runtime, `${name}.mjs`)),
|
|
44
|
+
outdir: virtualOutput,
|
|
45
|
+
outbase: runtime,
|
|
46
|
+
entryNames: '[name]',
|
|
47
|
+
chunkNames: '_shared-[hash]',
|
|
48
|
+
outExtension: { '.js': '.mjs' },
|
|
49
|
+
bundle: true,
|
|
50
|
+
splitting: true,
|
|
51
|
+
format: 'esm',
|
|
52
|
+
platform: 'node',
|
|
53
|
+
target: 'node22',
|
|
54
|
+
packages: 'external',
|
|
55
|
+
// Edge's prepared VM is already a self-contained CJS artifact. Keeping it
|
|
56
|
+
// external preserves its delayed import and original execution boundary.
|
|
57
|
+
external: ['../compat/*', './profiles.json', './*.cjs'],
|
|
58
|
+
minify: false,
|
|
59
|
+
sourcemap: false,
|
|
60
|
+
metafile: true,
|
|
61
|
+
write: false,
|
|
62
|
+
logLevel: 'silent',
|
|
63
|
+
});
|
|
64
|
+
const emitted = new Map();
|
|
65
|
+
for (const file of result.outputFiles) {
|
|
66
|
+
if (path.dirname(file.path) !== virtualOutput || !file.path.endsWith('.mjs')) {
|
|
67
|
+
throw new Error('Runtime bundler emitted a file outside its flat output directory.');
|
|
68
|
+
}
|
|
69
|
+
emitted.set(path.basename(file.path), file.contents);
|
|
70
|
+
}
|
|
71
|
+
if (runtimeEntryNames.some(name => !emitted.has(`${name}.mjs`))) throw new Error('Runtime bundle omitted a required entry.');
|
|
72
|
+
// This is an unpublished, disposable stage. On any write failure buildProject
|
|
73
|
+
// discards the stage and preserves the previous published build unchanged.
|
|
74
|
+
for (const [name, contents] of emitted) await writeFile(path.join(runtime, name), contents);
|
|
75
|
+
for (const entry of originals) {
|
|
76
|
+
if (!emitted.has(entry.name)) await unlink(path.join(runtime, entry.name));
|
|
77
|
+
}
|
|
78
|
+
// JSON, CJS, license files and the OG assets directory are never traversed or
|
|
79
|
+
// removed. The caller writes the normal runtime contract after prerender.
|
|
80
|
+
return { entryCount: runtimeEntryNames.length, moduleCount: emitted.size,
|
|
81
|
+
bytes: [...emitted.values()].reduce((sum, contents) => sum + contents.length, 0) };
|
|
82
|
+
}
|
package/build/cache-handlers.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { build } from 'esbuild';
|
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import {frameworkImportName} from './framework-imports.mjs';
|
|
5
|
+
import { projectTsconfig } from './typescript.mjs';
|
|
5
6
|
|
|
6
7
|
export function validateCacheHandler(input) {
|
|
7
8
|
if (input === undefined) return undefined;
|
|
@@ -11,7 +12,7 @@ export function validateCacheHandler(input) {
|
|
|
11
12
|
|
|
12
13
|
export async function prepareCacheHandler(options) {
|
|
13
14
|
if (!options.config.cacheHandler) return undefined;
|
|
14
|
-
const handlers = await prepareCacheHandlers({ ...options, config: { cacheHandlers: { __legacy: options.config.cacheHandler } } });
|
|
15
|
+
const handlers = await prepareCacheHandlers({ ...options, config: { ...options.config, cacheHandlers: { __legacy: options.config.cacheHandler } } });
|
|
15
16
|
return handlers.__legacy;
|
|
16
17
|
}
|
|
17
18
|
|
|
@@ -33,6 +34,7 @@ export function validateCacheHandlers(input) {
|
|
|
33
34
|
export async function prepareCacheHandlers({ config, projectRoot, stage, environment = {} }) {
|
|
34
35
|
const result = {};
|
|
35
36
|
const compiled = new Map();
|
|
37
|
+
const tsconfig = config.typescript ? await projectTsconfig(projectRoot, config) : undefined;
|
|
36
38
|
const require = createRequire(path.join(projectRoot, 'package.json'));
|
|
37
39
|
for (const [name, source] of Object.entries(config.cacheHandlers || {})) {
|
|
38
40
|
let entry;
|
|
@@ -41,6 +43,7 @@ export async function prepareCacheHandlers({ config, projectRoot, stage, environ
|
|
|
41
43
|
if (compiled.has(entry)) { result[name] = compiled.get(entry); continue; }
|
|
42
44
|
const output = `server/cache-handler-${name}.mjs`;
|
|
43
45
|
await build({ absWorkingDir: projectRoot, entryPoints: [entry], outfile: path.join(stage, output), bundle: true,
|
|
46
|
+
...(tsconfig ? { tsconfig } : {}),
|
|
44
47
|
platform: 'node', target: 'node22', format: 'esm', packages: 'external', logLevel: 'silent', define: environment,
|
|
45
48
|
banner: { js: "import {createRequire as __prnextCreateRequire} from 'node:module';const require=__prnextCreateRequire(import.meta.url);" },
|
|
46
49
|
plugins: [{ name: 'cache-handler-compatibility', setup(builder) {
|
package/build/compiler.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import { incrementalBuild } from './incremental.mjs';
|
|
|
12
12
|
import { webpackGraph } from './webpack-graph.mjs';
|
|
13
13
|
import { turbopackRules, sourceReader } from './turbopack-conditions.mjs';
|
|
14
14
|
import {withSourceMaps,configureSourceMaps,inlineSourceMap} from './source-maps.mjs';
|
|
15
|
+
import { optimizeImportsPlugins } from './optimize-imports.mjs';
|
|
15
16
|
|
|
16
17
|
let validateOptions;
|
|
17
18
|
const compilation = new AsyncLocalStorage();
|
|
@@ -20,7 +21,7 @@ export function configureCompiler(options) { Object.assign(compilation.getStore(
|
|
|
20
21
|
export async function compilerSource(file, fallback) {
|
|
21
22
|
return compilation.getStore()?.loaded.get(file)?.source ?? fallback ?? await readFile(file, 'utf8');
|
|
22
23
|
}
|
|
23
|
-
const framework = /^(?:prnext-pages-compatibility|prnext-app-(?:rsc|ssr|browser)|prnext-edge-app|prnext-edge|prnext-middleware-compatibility)$/;
|
|
24
|
+
const framework = /^(?:prnext-pages-compatibility|prnext-app-(?:rsc|ssr|browser|route)|prnext-edge-app|prnext-edge|prnext-middleware-compatibility)$/;
|
|
24
25
|
function condition(value, input) {
|
|
25
26
|
if (value === undefined) return true;
|
|
26
27
|
if (value instanceof RegExp) { value.lastIndex = 0; return value.test(input); }
|
|
@@ -120,6 +121,7 @@ export async function validateCompilerConfig(config, root, {dev=false,edge=false
|
|
|
120
121
|
}
|
|
121
122
|
export async function build(options) {
|
|
122
123
|
const state = compilation.getStore();
|
|
124
|
+
if ((options.plugins || []).some(plugin => framework.test(plugin.name))) options = { ...options, plugins: optimizeImportsPlugins(options.plugins, state?.config?.experimental?.optimizePackageImports) };
|
|
123
125
|
// esbuild's shared service dispatches callbacks from its own async context.
|
|
124
126
|
// Re-enter this build so transforms never inherit another project's map mode,
|
|
125
127
|
// action registry or configuration from the first service invocation.
|
|
@@ -134,6 +136,7 @@ export async function build(options) {
|
|
|
134
136
|
}}))};
|
|
135
137
|
// Runtime vendor bundles are framework-owned, not project webpack targets.
|
|
136
138
|
const projectGraph = (options.plugins || []).some(plugin => framework.test(plugin.name));
|
|
139
|
+
if (projectGraph && state?.tsconfig) options = { ...options, tsconfig: state.tsconfig };
|
|
137
140
|
const compile = input => state?.incremental && state.dev && (projectGraph || !input.plugins?.length) ? incrementalBuild(input,{root:state.root,stage:state.stage,group:projectGraph?'project':'auxiliary',run:callback=>compilation.run(state,callback)}) : esbuild(input);
|
|
138
141
|
if (!(state?.config?.webpack || Object.keys(state?.config?.turbopack?.rules || {}).length) || !projectGraph) return compile(options);
|
|
139
142
|
const target = options.define?.['process.env.NEXT_RUNTIME'] === '"edge"' ? 'edge' : options.platform === 'browser' ? 'browser' : 'node';
|
package/build/config.mjs
CHANGED
|
@@ -15,17 +15,24 @@ import { validateStandaloneConfig } from './standalone-config.mjs';
|
|
|
15
15
|
import { validateTurbopack } from './module-resolution.mjs';
|
|
16
16
|
import { validateI18n } from './i18n.mjs';
|
|
17
17
|
import { frameworkPackages } from './framework-imports.mjs';
|
|
18
|
+
import { validateTypeScript } from './typescript.mjs';
|
|
19
|
+
import { validateWorkerRetirement } from './worker-retirement.mjs';
|
|
20
|
+
import { validateOptimizePackageImports } from './optimize-imports.mjs';
|
|
21
|
+
import { validateProxyBodySize } from './proxy-body-config.mjs';
|
|
18
22
|
|
|
19
23
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
20
24
|
const configName = /^(?:next|prnext)\.config\.(?:js|mjs|cjs|ts)$/;
|
|
21
25
|
const supported = new Set(['env', 'redirects', 'headers', 'rewrites', 'generateBuildId', 'compress', 'poweredByHeader', 'productionBrowserSourceMaps', 'basePath', 'assetPrefix', 'experimental', 'sassOptions', 'transpilePackages', 'serverExternalPackages', 'cacheComponents', 'cacheLife', 'images', 'reactStrictMode', 'pageExtensions', 'trailingSlash', 'skipTrailingSlashRedirect', 'skipMiddlewareUrlNormalize', 'skipProxyUrlNormalize', 'cacheHandlers', 'distDir']);
|
|
22
26
|
const defaults = { distDir: '.prnext', env: {}, compress: true, poweredByHeader: true, productionBrowserSourceMaps: false, basePath: '', assetPrefix: '', trailingSlash: false, skipTrailingSlashRedirect: false, skipMiddlewareUrlNormalize: false, experimental: { nextScriptWorkers: false } };
|
|
23
27
|
supported.add('cacheHandler');
|
|
28
|
+
supported.add('workerRetirement');
|
|
24
29
|
supported.add('cacheMaxMemorySize');
|
|
25
30
|
supported.add('turbopack');
|
|
26
31
|
supported.add('i18n');
|
|
27
32
|
supported.add('webpack');
|
|
28
33
|
supported.add('onDemandEntries');
|
|
34
|
+
supported.add('devIndicators');
|
|
35
|
+
for (const key of ['reactCompiler', 'typedRoutes', 'typescript']) supported.add(key);
|
|
29
36
|
for (const key of ['output', 'outputFileTracingRoot', 'outputFileTracingIncludes', 'outputFileTracingExcludes']) supported.add(key);
|
|
30
37
|
|
|
31
38
|
function validPrefixPath(value, where, { root = false } = {}) {
|
|
@@ -78,6 +85,14 @@ export function validateProjectConfig(input, filename = 'project config') {
|
|
|
78
85
|
// Optional properties explicitly set to undefined still use their defaults,
|
|
79
86
|
// as in common process.env-based Next configuration wrappers.
|
|
80
87
|
const config = { ...defaults, ...Object.fromEntries(Object.entries(input).filter(([,value]) => value !== undefined)) };
|
|
88
|
+
for (const key of ['reactCompiler', 'typedRoutes']) {
|
|
89
|
+
if (config[key] === undefined || config[key] === false) continue;
|
|
90
|
+
if (key === 'reactCompiler') throw new Error('reactCompiler currently supports only false. PRNext does not run the React Compiler transform; keep Next.js for builds that require it.');
|
|
91
|
+
throw new Error('typedRoutes currently supports only false. PRNext does not generate or validate typed route declarations; keep Next.js for builds that require them.');
|
|
92
|
+
}
|
|
93
|
+
config.typescript = validateTypeScript(config.typescript);
|
|
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.');
|
|
81
96
|
if (config.onDemandEntries !== undefined) {
|
|
82
97
|
const entries = config.onDemandEntries;
|
|
83
98
|
if (!entries || typeof entries !== 'object' || Array.isArray(entries)) throw new TypeError('onDemandEntries must be an object');
|
|
@@ -106,14 +121,17 @@ export function validateProjectConfig(input, filename = 'project config') {
|
|
|
106
121
|
config[key] = [...new Set(names)];
|
|
107
122
|
}
|
|
108
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');
|
|
109
127
|
if (config.sassOptions !== undefined && (!config.sassOptions || typeof config.sassOptions !== 'object' || Array.isArray(config.sassOptions))) throw new Error(`${filename}: sassOptions must be an object.`);
|
|
110
128
|
if (!config.experimental || typeof config.experimental !== 'object' || Array.isArray(config.experimental)) throw new Error(`${filename}: experimental must be an object.`);
|
|
111
129
|
for (const key of Object.keys(config.experimental)) {
|
|
112
|
-
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.`);
|
|
113
131
|
}
|
|
114
132
|
const nextScriptWorkers = config.experimental.nextScriptWorkers === undefined ? false : config.experimental.nextScriptWorkers;
|
|
115
133
|
if (typeof nextScriptWorkers !== 'boolean') throw new Error(`${filename}: experimental.nextScriptWorkers must be a boolean.`);
|
|
116
|
-
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) };
|
|
117
135
|
for (const key of ['basePath', 'assetPrefix']) {
|
|
118
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.`);
|
|
119
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,4 +1,6 @@
|
|
|
1
1
|
import { rename } from '../runtime/fs.mjs';
|
|
2
|
+
import { writeRuntimeContract } from './runtime-contract.mjs';
|
|
3
|
+
import { bundleRuntime } from './bundle-runtime.mjs';
|
|
2
4
|
import {frameworkImportName, frameworkRuntimeImportPattern} from './framework-imports.mjs';
|
|
3
5
|
import { expandLocales, localizedStaticPaths } from './i18n.mjs';
|
|
4
6
|
import { build as bundle, withCompilation, configureCompiler, compilerSource } from './compiler.mjs';
|
|
@@ -29,6 +31,7 @@ import { npmPackagesPlugin } from './npm-packages.mjs';
|
|
|
29
31
|
import { createRefreshTransform, devSingletonsPlugin, devClientFile } from './dev.mjs';
|
|
30
32
|
import { snapshotEnvConfig } from '../runtime/env.mjs';
|
|
31
33
|
import { validateStaticPaths } from '../runtime/pages-paths.mjs';
|
|
34
|
+
import { projectTsconfig, checkTypeScript } from './typescript.mjs';
|
|
32
35
|
export { staticPath } from '../runtime/pages-paths.mjs';
|
|
33
36
|
|
|
34
37
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
@@ -197,6 +200,7 @@ export function build(projectRoot, options = {}) {
|
|
|
197
200
|
async function buildProject(projectRoot, { dev = false, envMode, incremental = false, devInputs } = {}) {
|
|
198
201
|
const started = performance.now();
|
|
199
202
|
const config = await loadProjectConfig(projectRoot, { dev, envMode });
|
|
203
|
+
const tsconfig = config.typescript ? await projectTsconfig(projectRoot, config) : undefined;
|
|
200
204
|
const environment = defineEnvironment(config);
|
|
201
205
|
const publicPaths = { trailingSlash: config.trailingSlash, skipTrailingSlashRedirect: config.skipTrailingSlashRedirect, basePath: config.basePath, assetPrefix: config.assetPrefix, assetBase: publicAssetBase(config) };
|
|
202
206
|
const customRoutes = await compileCustomRoutes(config);
|
|
@@ -219,7 +223,7 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
|
|
|
219
223
|
const refresh = dev ? createRefreshTransform(project.root) : undefined;
|
|
220
224
|
const fonts = createFonts({ projectRoot: project.root, stage, assetBase: publicPaths.assetBase });
|
|
221
225
|
const cssModules = createCssModules({ projectRoot: project.root, stage, assetBase: publicPaths.assetBase, sassOptions: config.sassOptions });
|
|
222
|
-
configureCompiler({config,root:project.root,stage,buildId,dev,incremental,devInputs,assetBase:publicPaths.assetBase});
|
|
226
|
+
configureCompiler({config,root:project.root,stage,buildId,dev,incremental,devInputs,tsconfig,assetBase:publicPaths.assetBase});
|
|
223
227
|
try {
|
|
224
228
|
await mkdir(path.join(stage, 'runtime'), { recursive: true });
|
|
225
229
|
await mkdir(path.join(stage, 'compat'), { recursive: true });
|
|
@@ -235,6 +239,7 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
|
|
|
235
239
|
}
|
|
236
240
|
await prepareImageResponse(stage);
|
|
237
241
|
manifest.config.serverActions = config.experimental.serverActions;
|
|
242
|
+
if (config.workerRetirement) manifest.config.workerRetirement = config.workerRetirement;
|
|
238
243
|
const cacheHandlers = await prepareCacheHandlers({ config, projectRoot: project.root, stage, environment });
|
|
239
244
|
if (Object.keys(cacheHandlers).length) manifest.config.cacheHandlers = cacheHandlers;
|
|
240
245
|
const cacheHandler = await prepareCacheHandler({ config, projectRoot: project.root, stage, environment });
|
|
@@ -290,10 +295,15 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
|
|
|
290
295
|
}
|
|
291
296
|
}
|
|
292
297
|
await compileMiddleware({ project, stage, manifest, dev, moduleResolution: config.turbopack, defineEnvironment: environment });
|
|
298
|
+
if (manifest.middleware) manifest.middleware.bodySizeLimit = config.experimental.proxyClientMaxBodySize;
|
|
293
299
|
await prepareMetadataRoutes(project, stage);
|
|
294
300
|
await compileApp({ project, stage, manifest, moduleResolution: config.turbopack, cssModules, fonts, images, dev, defineEnvironment: environment, productionBrowserSourceMaps: config.productionBrowserSourceMaps, serverExternalPackages: config.serverExternalPackages });
|
|
301
|
+
await checkTypeScript(project.root, config, { dev, tsconfig });
|
|
295
302
|
fonts.attach(manifest);
|
|
296
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);
|
|
297
307
|
await prerender(stage, manifest, dev);
|
|
298
308
|
await prerenderApp(stage, manifest, dev);
|
|
299
309
|
const pages = manifest.routes.filter(route => route.kind === 'page' && route.router !== 'app' && !route.internal);
|
|
@@ -323,6 +333,7 @@ async function buildProject(projectRoot, { dev = false, envMode, incremental = f
|
|
|
323
333
|
...(appRoute ? { app: { client: appRoute.client, css: appRoute.css } } : {}),
|
|
324
334
|
}));
|
|
325
335
|
}
|
|
336
|
+
await writeRuntimeContract(stage);
|
|
326
337
|
await writeFile(path.join(stage, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n');
|
|
327
338
|
if (config.compress) await precompressBuild(stage, manifest, {cacheDirectory:path.join(project.root,'.prnext-cache/precompress-v1')});
|
|
328
339
|
if (config.output === 'standalone' && !dev) await (await import('./standalone-runner.mjs')).prepareStandalone({ projectRoot: project.root, stage, manifest, config });
|
|
@@ -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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
|
-
import { builtinModules } from 'node:module';
|
|
3
|
+
import { builtinModules, createRequire } from 'node:module';
|
|
4
4
|
import { parse } from '@babel/parser';
|
|
5
5
|
import { VISITOR_KEYS } from '@babel/types';
|
|
6
6
|
import { createNativeDiscovery } from './native-discovery.mjs';
|
|
@@ -15,6 +15,7 @@ const bare = name => !name.startsWith('.') && !name.startsWith('#') && !path.isA
|
|
|
15
15
|
/** Inspect reached imports once per compiler graph; never execute npm modules. */
|
|
16
16
|
export function npmPackagesPlugin({ pages = false, projectRoot, transpilePackages = [], serverExternalPackages = [] }) {
|
|
17
17
|
const transpile = new Set(transpilePackages), external = new Set(serverExternalPackages);
|
|
18
|
+
const nodeResolve = createRequire(path.join(projectRoot, 'package.json')).resolve;
|
|
18
19
|
const sources = new Map(), decisions = new Map();
|
|
19
20
|
let sourceBytes = 0;
|
|
20
21
|
const native = createNativeDiscovery({ projectRoot, isClientSource: source => /['"]use (?:client|server)['"]/.test(source) });
|
|
@@ -90,7 +91,18 @@ export function npmPackagesPlugin({ pages = false, projectRoot, transpilePackage
|
|
|
90
91
|
// same validation as Pages to avoid bypassing compatibility transforms.
|
|
91
92
|
if (!pages && !external.has(name)) return;
|
|
92
93
|
const resolved = await resolve(args.path, args.importer || path.join(projectRoot, 'package.json'), args.kind);
|
|
93
|
-
if (resolved.errors.length
|
|
94
|
+
if (resolved.errors.length) return;
|
|
95
|
+
if (resolved.external || resolved.namespace !== 'file') return resolved;
|
|
96
|
+
// A tsconfig paths/baseUrl import can look like an npm specifier while
|
|
97
|
+
// resolving to application source. Node cannot resolve that alias from
|
|
98
|
+
// the emitted server bundle; keep the resolved source in the graph.
|
|
99
|
+
if (!resolved.path.split(path.sep).includes('node_modules')) {
|
|
100
|
+
let nodePath;
|
|
101
|
+
try { nodePath = nodeResolve(args.path, { paths: [path.dirname(args.importer || path.join(projectRoot, 'package.json'))] }); } catch {}
|
|
102
|
+
// Preserve externalization of real workspace packages that Node can
|
|
103
|
+
// resolve through a node_modules link, even after esbuild realpaths it.
|
|
104
|
+
if (nodePath !== resolved.path) return resolved;
|
|
105
|
+
}
|
|
94
106
|
const decision = await classify(resolved.path);
|
|
95
107
|
const fromRoot = await resolve(args.path, path.join(projectRoot, 'package.json'), args.kind);
|
|
96
108
|
const nestedResolution = fromRoot.errors.length || fromRoot.path !== resolved.path;
|