@wular/pnext 0.0.2 → 0.0.4
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 +76 -20
- package/package.json +3 -2
- package/reference/data/bench.json +513 -0
- package/reference/performance.md +75 -48
- package/src/api/router/runtime.ts +60 -21
- package/src/cache/context.ts +4 -1
- package/src/cli/build.ts +37 -5
- package/src/cli/dev.ts +6 -0
- package/src/cli/index.ts +17 -3
- package/src/cli/request-pipeline.ts +1340 -0
- package/src/cli/server-entry.ts +180 -0
- package/src/cli/start.ts +39 -1311
- package/src/client/build.ts +59 -20
- package/src/client/chunk-fold.ts +40 -0
- package/src/client/compat-surface.ts +175 -0
- package/src/client/entry.ts +67 -52
- package/src/compat/actions/action-client.ts +8 -1
- package/src/compat/actions/action-dispatch.ts +11 -1
- package/src/compat/actions/discovery.ts +23 -6
- package/src/compat/bundler/optimize-package-imports.ts +5 -1
- package/src/compat/bundler/worker.ts +2 -1
- package/src/compat/client/errors/bare-boundary.ts +32 -0
- package/src/compat/client/errors/error-boundary.ts +1 -15
- package/src/compat/client/errors/primitive-throw.ts +16 -0
- package/src/compat/client/link-status.ts +1 -1
- package/src/compat/css/lightningcss.ts +2 -1
- package/src/compat/css/modules.ts +4 -3
- package/src/compat/lifecycle/instrumentation-client.ts +1 -1
- package/src/compat/lifecycle/instrumentation.ts +5 -2
- package/src/compat/next/config-loader.ts +33 -5
- package/src/compat/next/dynamic.tsx +9 -5
- package/src/compat/next/link-validation-transform.ts +5 -1
- package/src/compat/next/link.tsx +51 -58
- package/src/compat/pages/client-plugin.ts +2 -1
- package/src/compat/react/action-state.ts +159 -0
- package/src/compat/react/client-lite.ts +74 -0
- package/src/compat/react/hooks-extra.ts +92 -0
- package/src/compat/react/parity.ts +128 -0
- package/src/compat/react/preact.ts +33 -420
- package/src/compat/react/server-inserted-html.ts +14 -7
- package/src/compat/react/use.ts +72 -0
- package/src/compat/register/actions.ts +27 -7
- package/src/compat/register/segment.ts +16 -6
- package/src/config.ts +15 -1
- package/src/css/build.ts +13 -2
- package/src/dev/imports.ts +34 -5
- package/src/dev/module-cache.ts +19 -0
- package/src/dev/module-transform.ts +7 -1
- package/src/dev/server.ts +91 -22
- package/src/dynamic/source.ts +36 -27
- package/src/ppr.ts +5 -4
- package/src/proxy.ts +5 -1
- package/src/render/island-context.ts +21 -3
- package/src/render/renderer.ts +102 -26
- package/src/resolve/engine.ts +12 -2
- package/src/resolve/scan-facts.ts +239 -1
- package/src/routing/href.ts +4 -5
- package/src/routing/routes.ts +26 -41
- package/src/runtime/server.ts +8 -4
- package/src/runtime/vendor.ts +1 -1
- package/src/typegen.ts +3 -3
- package/src/utils/esbuild.ts +58 -0
- package/src/utils/fs.ts +14 -2
- package/src/utils/native-require.ts +28 -0
package/src/dynamic/source.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { rewriteFacts } from '../resolve/scan-facts';
|
|
1
|
+
import { dynamicCallFacts, rewriteFacts, type DynamicCallFact } from '../resolve/scan-facts';
|
|
2
2
|
|
|
3
3
|
export interface DynamicCall {
|
|
4
4
|
index: number;
|
|
@@ -86,19 +86,16 @@ export function dynamicCallsFromSource(source: string, dynamicNames: Set<string>
|
|
|
86
86
|
export function rewriteLiteralDynamicCalls(source: string, file?: string) {
|
|
87
87
|
const dynamicNames = pnextDynamicImportNames(source, file);
|
|
88
88
|
if (dynamicNames.size === 0) return source;
|
|
89
|
+
const facts = dynamicCallFacts(source, file);
|
|
89
90
|
|
|
90
91
|
const edits: { start: number; end: number; value: string }[] = [];
|
|
91
92
|
for (const call of dynamicCallsFromSource(source, dynamicNames)) {
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
if (!literal) continue;
|
|
95
|
-
const [match, leading = '', quote = "'", specifier] = literal;
|
|
96
|
-
if (!specifier) continue;
|
|
97
|
-
const start = call.open + 1 + leading.length;
|
|
93
|
+
const fact = factForCall(facts, call);
|
|
94
|
+
if (!fact?.literal) continue;
|
|
98
95
|
edits.push({
|
|
99
|
-
start,
|
|
100
|
-
end:
|
|
101
|
-
value: `() => import(${
|
|
96
|
+
start: fact.specifierStart,
|
|
97
|
+
end: fact.specifierEnd,
|
|
98
|
+
value: `() => import(${source.slice(fact.specifierStart, fact.specifierEnd)})`,
|
|
102
99
|
});
|
|
103
100
|
}
|
|
104
101
|
|
|
@@ -123,18 +120,19 @@ export function rewriteDynamicCallTargets(
|
|
|
123
120
|
) {
|
|
124
121
|
const dynamicNames = pnextDynamicImportNames(source, file);
|
|
125
122
|
if (dynamicNames.size === 0) return source;
|
|
123
|
+
const facts = dynamicCallFacts(source, file);
|
|
126
124
|
|
|
127
125
|
const edits: { at: number; value: string }[] = [];
|
|
128
126
|
for (const call of dynamicCallsFromSource(source, dynamicNames)) {
|
|
129
|
-
const target =
|
|
130
|
-
if (!target
|
|
131
|
-
const file = resolve(target
|
|
127
|
+
const target = loaderImportTargetForCall(call, facts);
|
|
128
|
+
if (!target) continue;
|
|
129
|
+
const file = resolve(target.specifier);
|
|
132
130
|
if (!file) continue;
|
|
133
131
|
|
|
134
132
|
const args = source.slice(call.open + 1, call.close);
|
|
135
133
|
const argCount = topLevelArgCount(args);
|
|
136
134
|
if (argCount < 1 || argCount > 2) continue;
|
|
137
|
-
const literal = JSON.stringify({ file, exportName: target
|
|
135
|
+
const literal = JSON.stringify({ file, exportName: target.exportName });
|
|
138
136
|
const separator = /,\s*$/.test(args) ? '' : ', ';
|
|
139
137
|
edits.push({
|
|
140
138
|
at: call.close,
|
|
@@ -151,8 +149,19 @@ export function rewriteDynamicCallTargets(
|
|
|
151
149
|
return next;
|
|
152
150
|
}
|
|
153
151
|
|
|
154
|
-
|
|
155
|
-
|
|
152
|
+
function factForCall(facts: DynamicCallFact[], call: DynamicCall) {
|
|
153
|
+
return facts.find(fact => fact.start === call.index);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The statically-extractable import behind one dynamic() call — the AST fact
|
|
158
|
+
* alone; a call the scan could not analyze yields no detection. Literal-loader
|
|
159
|
+
* calls (`dynamic('./x')`) answer undefined — they carry no `import()` yet.
|
|
160
|
+
*/
|
|
161
|
+
function loaderImportTargetForCall(call: DynamicCall, facts: DynamicCallFact[]) {
|
|
162
|
+
const fact = factForCall(facts, call);
|
|
163
|
+
return fact && !fact.literal ? fact : undefined;
|
|
164
|
+
}
|
|
156
165
|
|
|
157
166
|
/** Dev split of `dynamic(ssr:false)` targets; `PNEXT_DYNAMIC_SPLIT=0` restores eager builds. */
|
|
158
167
|
export function devDynamicSplitEnabled() {
|
|
@@ -175,17 +184,17 @@ export function rewriteDeferredDynamicImports(
|
|
|
175
184
|
if (dynamicNames.size === 0) return source;
|
|
176
185
|
const deferred = deferredDynamicImportSpecifiers(source, file);
|
|
177
186
|
if (deferred.size === 0) return source;
|
|
187
|
+
const facts = dynamicCallFacts(source, file);
|
|
178
188
|
|
|
179
189
|
const edits: { start: number; end: number; value: string }[] = [];
|
|
180
190
|
for (const call of dynamicCallsFromSource(source, dynamicNames)) {
|
|
181
|
-
const target =
|
|
182
|
-
if (!target
|
|
183
|
-
const resolved = resolve(target
|
|
191
|
+
const target = loaderImportTargetForCall(call, facts);
|
|
192
|
+
if (!target || !deferred.has(target.specifier)) continue;
|
|
193
|
+
const resolved = resolve(target.specifier);
|
|
184
194
|
if (!resolved) continue;
|
|
185
|
-
const url = href({ file: resolved, exportName: target
|
|
195
|
+
const url = href({ file: resolved, exportName: target.exportName });
|
|
186
196
|
if (!url) continue;
|
|
187
|
-
|
|
188
|
-
edits.push({ start: specifierAt, end: specifierAt + target[2].length, value: url });
|
|
197
|
+
edits.push({ start: target.specifierStart + 1, end: target.specifierEnd - 1, value: url });
|
|
189
198
|
}
|
|
190
199
|
if (edits.length === 0) return source;
|
|
191
200
|
let next = source;
|
|
@@ -212,13 +221,13 @@ export function deferredDynamicImportSpecifiers(source: string, file?: string) {
|
|
|
212
221
|
if (constMatch[1] && constMatch[2]) optionConsts.set(constMatch[1], constMatch[2]);
|
|
213
222
|
}
|
|
214
223
|
|
|
224
|
+
const facts = dynamicCallFacts(source, file);
|
|
215
225
|
for (const call of dynamicCallsFromSource(source, dynamicNames)) {
|
|
216
|
-
const target =
|
|
217
|
-
if (!target
|
|
226
|
+
const target = loaderImportTargetForCall(call, facts);
|
|
227
|
+
if (!target) continue;
|
|
218
228
|
// Token scan over the call plus any named option consts it references —
|
|
219
229
|
// tolerant of the compile-appended target argument (rewriteDynamicCallTargets).
|
|
220
|
-
const
|
|
221
|
-
const tail = args.slice(args.indexOf(target[0]) + target[0].length);
|
|
230
|
+
const tail = source.slice(target.loaderEnd, call.close);
|
|
222
231
|
let options = tail;
|
|
223
232
|
for (const identifier of tail.match(/[A-Za-z_$][\w$]*/g) ?? []) {
|
|
224
233
|
const named = optionConsts.get(identifier);
|
|
@@ -227,7 +236,7 @@ export function deferredDynamicImportSpecifiers(source: string, file?: string) {
|
|
|
227
236
|
const ssrFalse = /\bssr\s*:\s*false\b/.test(options);
|
|
228
237
|
const visible =
|
|
229
238
|
/\bload\s*:\s*['"]visible['"]/.test(options) && !/\bssr\s*:\s*true\b/.test(options);
|
|
230
|
-
if (ssrFalse || visible) deferred.add(target
|
|
239
|
+
if (ssrFalse || visible) deferred.add(target.specifier);
|
|
231
240
|
}
|
|
232
241
|
return deferred;
|
|
233
242
|
}
|
package/src/ppr.ts
CHANGED
|
@@ -539,14 +539,15 @@ export function hangingPromise<T = never>(api: string): Promise<T> {
|
|
|
539
539
|
// candidate and whether request APIs hang.
|
|
540
540
|
// ---------------------------------------------------------------------------
|
|
541
541
|
|
|
542
|
-
|
|
542
|
+
// globalThis-anchored: the prebundled server entry inlines its own copy of this module.
|
|
543
|
+
const CACHE_COMPONENTS = Symbol.for('pnext.cacheComponents');
|
|
543
544
|
|
|
544
545
|
export function setCacheComponents(enabled: boolean): void {
|
|
545
|
-
|
|
546
|
+
(globalThis as Record<PropertyKey, unknown>)[CACHE_COMPONENTS] = enabled;
|
|
546
547
|
}
|
|
547
548
|
|
|
548
549
|
export function cacheComponents(): boolean {
|
|
549
|
-
return
|
|
550
|
+
return (globalThis as Record<PropertyKey, unknown>)[CACHE_COMPONENTS] === true;
|
|
550
551
|
}
|
|
551
552
|
|
|
552
553
|
// ---------------------------------------------------------------------------
|
|
@@ -583,7 +584,7 @@ function insideUseCacheScope(): boolean {
|
|
|
583
584
|
}
|
|
584
585
|
|
|
585
586
|
export async function runWithPrerenderDeterminism<T>(fn: () => Promise<T>): Promise<T> {
|
|
586
|
-
if (!
|
|
587
|
+
if (!cacheComponents()) return fn();
|
|
587
588
|
const deterministicTime = 1;
|
|
588
589
|
const realRandom = Math.random;
|
|
589
590
|
const RealDate = Date;
|
package/src/proxy.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
|
|
3
|
+
// Lazy: the oxc-parser native binding costs ~12.6 MB RSS; load it only when a parse happens.
|
|
4
|
+
const parseSync: typeof import('oxc-parser').parseSync = (...args) =>
|
|
5
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
6
|
+
loadNative(() => require('oxc-parser') as typeof import('oxc-parser')).parseSync(...args);
|
|
7
|
+
import { loadNative } from './utils/native-require';
|
|
4
8
|
import { registerServerRuntime } from './runtime/server';
|
|
5
9
|
import { pathToFileHref, type ResolvedConfig } from './config';
|
|
6
10
|
import { devServerModuleHref } from './dev/imports';
|
|
@@ -1,4 +1,16 @@
|
|
|
1
|
-
import { createContext } from 'preact';
|
|
1
|
+
import { createContext, type Context } from 'preact';
|
|
2
|
+
|
|
3
|
+
// Island SSR runs prebundled client chunks — a separate module graph with its own copy of this file, so
|
|
4
|
+
// a plain createContext() would mint two identity-mismatched contexts. Resolve one shared instance.
|
|
5
|
+
function sharedContext<T>(key: string, defaultValue: T): Context<T> {
|
|
6
|
+
const registry = globalThis as typeof globalThis & Record<symbol, Context<unknown>>;
|
|
7
|
+
const symbol = Symbol.for(key);
|
|
8
|
+
const existing = registry[symbol];
|
|
9
|
+
if (existing) return existing as Context<T>;
|
|
10
|
+
const context = createContext<T>(defaultValue);
|
|
11
|
+
registry[symbol] = context as Context<unknown>;
|
|
12
|
+
return context;
|
|
13
|
+
}
|
|
2
14
|
|
|
3
15
|
// ---------------------------------------------------------------------------
|
|
4
16
|
// Per-island layout-segment context (CORE seam for compat useSelectedLayout-
|
|
@@ -28,7 +40,10 @@ export interface LayoutSegmentSnapshot {
|
|
|
28
40
|
slots: Record<string, string[]>;
|
|
29
41
|
}
|
|
30
42
|
|
|
31
|
-
export const LayoutSegmentContext =
|
|
43
|
+
export const LayoutSegmentContext = sharedContext<LayoutSegmentSnapshot | null>(
|
|
44
|
+
'pnext.layoutSegmentContext',
|
|
45
|
+
null,
|
|
46
|
+
);
|
|
32
47
|
|
|
33
48
|
// Per-island params snapshot (CORE seam for compat useParams).
|
|
34
49
|
//
|
|
@@ -44,4 +59,7 @@ export const LayoutSegmentContext = createContext<LayoutSegmentSnapshot | null>(
|
|
|
44
59
|
|
|
45
60
|
export type RouteParamsSnapshot = Record<string, string | string[]>;
|
|
46
61
|
|
|
47
|
-
export const RouteParamsContext =
|
|
62
|
+
export const RouteParamsContext = sharedContext<RouteParamsSnapshot | null>(
|
|
63
|
+
'pnext.routeParamsContext',
|
|
64
|
+
null,
|
|
65
|
+
);
|
package/src/render/renderer.ts
CHANGED
|
@@ -103,7 +103,7 @@ import {
|
|
|
103
103
|
ISLAND_BOUNDARY_ERROR_ELEMENT,
|
|
104
104
|
ISLAND_BOUNDARY_ERROR_MESSAGE_ATTRIBUTE,
|
|
105
105
|
} from '../islands/boundary-error';
|
|
106
|
-
import { runWithCacheScope } from '../cache/context';
|
|
106
|
+
import { captureCacheScope, runWithCacheScope } from '../cache/context';
|
|
107
107
|
import { registerServerRuntime } from '../runtime/server';
|
|
108
108
|
import { readText } from '../utils/fs';
|
|
109
109
|
import {
|
|
@@ -2424,8 +2424,14 @@ export function pprSubShellPath(outPath: string, routeId: string, key: string) {
|
|
|
2424
2424
|
return `${outPath}/ppr/${routeId}.${key}.html`;
|
|
2425
2425
|
}
|
|
2426
2426
|
|
|
2427
|
+
/**
|
|
2428
|
+
* `skipNoindex`: the 404 came out of a server action, where robots the page
|
|
2429
|
+
* declared must stay authoritative — the metadata base stops forcing noindex.
|
|
2430
|
+
* A default noindex is still injected when nothing declares robots at all.
|
|
2431
|
+
*/
|
|
2427
2432
|
export async function renderGlobalNotFoundResponse(
|
|
2428
2433
|
options: Omit<RenderOptions, 'route' | 'params'>,
|
|
2434
|
+
{ skipNoindex = false }: { skipNoindex?: boolean } = {},
|
|
2429
2435
|
) {
|
|
2430
2436
|
const globalFile = ['tsx', 'ts', 'jsx', 'js']
|
|
2431
2437
|
.map(ext => `${options.config.appPath}/global-not-found.${ext}`)
|
|
@@ -2526,7 +2532,9 @@ export async function renderGlobalNotFoundResponse(
|
|
|
2526
2532
|
renderScopeRequest(notFoundOptions),
|
|
2527
2533
|
() =>
|
|
2528
2534
|
runWithFontScope(() =>
|
|
2529
|
-
runWithCacheScope(() =>
|
|
2535
|
+
runWithCacheScope(() =>
|
|
2536
|
+
renderNotFoundPage(notFoundOptions, false, { forceDefault, skipNoindex }),
|
|
2537
|
+
),
|
|
2530
2538
|
),
|
|
2531
2539
|
{},
|
|
2532
2540
|
);
|
|
@@ -2548,6 +2556,44 @@ export async function renderGlobalNotFoundResponse(
|
|
|
2548
2556
|
return response;
|
|
2549
2557
|
}
|
|
2550
2558
|
|
|
2559
|
+
/**
|
|
2560
|
+
* notFound() thrown from a Server Action: the URL DID match a route, so render the not-found
|
|
2561
|
+
* boundary within that route (keeping its layout chain and generateMetadata) instead of anchoring
|
|
2562
|
+
* at the app root like renderGlobalNotFoundResponse.
|
|
2563
|
+
*/
|
|
2564
|
+
export async function renderNotFoundForRoute(
|
|
2565
|
+
options: RenderOptions,
|
|
2566
|
+
{ skipNoindex = false }: { skipNoindex?: boolean } = {},
|
|
2567
|
+
): Promise<Response> {
|
|
2568
|
+
const notFoundRequest = options.request
|
|
2569
|
+
? new Request(options.request.url, { headers: options.request.headers })
|
|
2570
|
+
: undefined;
|
|
2571
|
+
const notFoundOptions: RenderOptions = {
|
|
2572
|
+
...options,
|
|
2573
|
+
...(notFoundRequest ? { request: notFoundRequest } : {}),
|
|
2574
|
+
status: 404,
|
|
2575
|
+
};
|
|
2576
|
+
const page = await runWithRequest(
|
|
2577
|
+
renderScopeRequest(notFoundOptions),
|
|
2578
|
+
() =>
|
|
2579
|
+
runWithFontScope(() =>
|
|
2580
|
+
runWithCacheScope(() => renderNotFoundPage(notFoundOptions, false, { skipNoindex })),
|
|
2581
|
+
),
|
|
2582
|
+
{},
|
|
2583
|
+
);
|
|
2584
|
+
const response = new Response(pageStream(page), {
|
|
2585
|
+
status: 404,
|
|
2586
|
+
headers: { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' },
|
|
2587
|
+
});
|
|
2588
|
+
const html = await response.text();
|
|
2589
|
+
const withRobots = html.includes('name="robots"')
|
|
2590
|
+
? html
|
|
2591
|
+
: html.replace('</head>', '<meta name="robots" content="noindex"/></head>');
|
|
2592
|
+
const headers = new Headers(response.headers);
|
|
2593
|
+
headers.delete('content-length');
|
|
2594
|
+
return new Response(withRobots, { status: response.status, headers });
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2551
2597
|
// Everything after this marker is late metadata, not document bytes: the
|
|
2552
2598
|
// router's streamed read commits the document at the marker and folds the
|
|
2553
2599
|
// remainder into the live head.
|
|
@@ -3351,9 +3397,14 @@ async function renderTree(
|
|
|
3351
3397
|
// is body-streamed anyway, resolve it as a deferred stream chunk instead.
|
|
3352
3398
|
// Only on an RSC navigation render: a hard document load keeps blocking, so a
|
|
3353
3399
|
// metadata notFound()/redirect() still owns the whole document.
|
|
3400
|
+
// Captured explicitly: the page body already populated the render's cache() scope, and a deferred
|
|
3401
|
+
// generateMetadata must reuse it or its cache()-wrapped values drift from what the body rendered.
|
|
3402
|
+
const runInCacheScope = captureCacheScope();
|
|
3354
3403
|
const pendingPageMetadata =
|
|
3355
3404
|
typeof pageMetadata === 'function' && deferrableNavigationMetadata(options, runtimeMetadataInBody, stream)
|
|
3356
|
-
? Promise.resolve().then(() =>
|
|
3405
|
+
? Promise.resolve().then(() =>
|
|
3406
|
+
runInCacheScope(() => pageMetadata(mergeMetadataEntries(layoutRender.metadata))),
|
|
3407
|
+
)
|
|
3357
3408
|
: undefined;
|
|
3358
3409
|
try {
|
|
3359
3410
|
resolvedPageMetadata = pendingPageMetadata
|
|
@@ -4009,9 +4060,16 @@ export function defaultNotFoundDocument(): string {
|
|
|
4009
4060
|
async function renderNotFoundPage(
|
|
4010
4061
|
options: RenderOptions,
|
|
4011
4062
|
stream: boolean,
|
|
4012
|
-
extra: {
|
|
4063
|
+
extra: {
|
|
4064
|
+
status?: number;
|
|
4065
|
+
nextFlightText?: boolean;
|
|
4066
|
+
forceDefault?: boolean;
|
|
4067
|
+
skipNoindex?: boolean;
|
|
4068
|
+
} = {},
|
|
4013
4069
|
) {
|
|
4014
4070
|
const status = extra.status ?? 404;
|
|
4071
|
+
// Server-action 404s carry no fallback robots (Next's NonIndex skip).
|
|
4072
|
+
const fallbackRobots = extra.skipNoindex ? {} : { robots: 'noindex' };
|
|
4015
4073
|
// forceDefault: globalNotFound is enabled but no global-not-found.* exists —
|
|
4016
4074
|
// skip the app's not-found.* boundary and render the built-in default 404.
|
|
4017
4075
|
const file = extra.forceDefault ? undefined : nearestConventionFile(options, 'not-found.tsx');
|
|
@@ -4021,7 +4079,7 @@ async function renderNotFoundPage(
|
|
|
4021
4079
|
notFoundOptions,
|
|
4022
4080
|
defaultNotFoundTree(),
|
|
4023
4081
|
stream,
|
|
4024
|
-
{ title: '404: This page could not be found.',
|
|
4082
|
+
{ title: '404: This page could not be found.', ...fallbackRobots },
|
|
4025
4083
|
{ status },
|
|
4026
4084
|
);
|
|
4027
4085
|
return extra.nextFlightText ? withNextFlightText(page) : page;
|
|
@@ -4049,7 +4107,7 @@ async function renderNotFoundPage(
|
|
|
4049
4107
|
notFoundOptions,
|
|
4050
4108
|
defaultNotFoundTree(),
|
|
4051
4109
|
stream,
|
|
4052
|
-
{ title: '404: This page could not be found.',
|
|
4110
|
+
{ title: '404: This page could not be found.', ...fallbackRobots },
|
|
4053
4111
|
{ status },
|
|
4054
4112
|
);
|
|
4055
4113
|
return extra.nextFlightText ? withNextFlightText(page) : page;
|
|
@@ -4087,7 +4145,7 @@ async function renderNotFoundPage(
|
|
|
4087
4145
|
stream,
|
|
4088
4146
|
// On the skip path a baked `<title>Not found</title>` would precede the
|
|
4089
4147
|
// streamed metadata chunk in document order and win `document.title`.
|
|
4090
|
-
{
|
|
4148
|
+
{ ...fallbackRobots, ...resolvedMetadata },
|
|
4091
4149
|
{
|
|
4092
4150
|
status,
|
|
4093
4151
|
...(resolvedViewport ? { viewport: resolvedViewport } : {}),
|
|
@@ -7367,25 +7425,43 @@ function devReloadScript() {
|
|
|
7367
7425
|
// unstyled and never loses client state. Anything the server could not prove
|
|
7368
7426
|
// was CSS-only still arrives as `reload`.
|
|
7369
7427
|
return `<script data-pnext-dev>
|
|
7370
|
-
|
|
7371
|
-
|
|
7372
|
-
|
|
7373
|
-
|
|
7374
|
-
|
|
7428
|
+
let events;
|
|
7429
|
+
let generation;
|
|
7430
|
+
// The stream is a long-lived connection counted against the browser's per-origin
|
|
7431
|
+
// socket cap (6 on HTTP/1.1). A page the browser keeps alive after navigating away
|
|
7432
|
+
// (bfcache) holds its own open, so every page load would leak one until the pool is
|
|
7433
|
+
// exhausted and further requests stall for minutes. Close on the way out, reopen on
|
|
7434
|
+
// restore.
|
|
7435
|
+
function connectDevEvents() {
|
|
7436
|
+
events = new EventSource('/__pnext/events');
|
|
7437
|
+
// The server stamps its build generation on connect: a page restored onto a
|
|
7438
|
+
// generation it never saw missed a rebuild while it was disconnected.
|
|
7439
|
+
events.addEventListener('ready', event => {
|
|
7440
|
+
if (generation !== undefined && event.data !== generation) return location.reload();
|
|
7441
|
+
generation = event.data;
|
|
7442
|
+
});
|
|
7443
|
+
events.addEventListener('reload', () => location.reload());
|
|
7444
|
+
events.addEventListener('css-update', event => {
|
|
7445
|
+
const links = [...document.querySelectorAll('link[rel=stylesheet]')].filter(link => {
|
|
7446
|
+
try { return new URL(link.href, location.href).origin === location.origin; } catch { return false; }
|
|
7447
|
+
});
|
|
7448
|
+
if (links.length === 0) { location.reload(); return; }
|
|
7449
|
+
for (const link of links) {
|
|
7450
|
+
const url = new URL(link.href, location.href);
|
|
7451
|
+
url.searchParams.set('__pnext_css', event.data);
|
|
7452
|
+
const next = link.cloneNode();
|
|
7453
|
+
// React tracks precedence-carrying links as its own; the swap is ours.
|
|
7454
|
+
next.removeAttribute('data-precedence');
|
|
7455
|
+
next.href = url.href;
|
|
7456
|
+
const drop = () => link.remove();
|
|
7457
|
+
next.addEventListener('load', drop, { once: true });
|
|
7458
|
+
next.addEventListener('error', drop, { once: true });
|
|
7459
|
+
link.after(next);
|
|
7460
|
+
}
|
|
7375
7461
|
});
|
|
7376
|
-
|
|
7377
|
-
|
|
7378
|
-
|
|
7379
|
-
|
|
7380
|
-
const next = link.cloneNode();
|
|
7381
|
-
// React tracks precedence-carrying links as its own; the swap is ours.
|
|
7382
|
-
next.removeAttribute('data-precedence');
|
|
7383
|
-
next.href = url.href;
|
|
7384
|
-
const drop = () => link.remove();
|
|
7385
|
-
next.addEventListener('load', drop, { once: true });
|
|
7386
|
-
next.addEventListener('error', drop, { once: true });
|
|
7387
|
-
link.after(next);
|
|
7388
|
-
}
|
|
7389
|
-
});
|
|
7462
|
+
}
|
|
7463
|
+
connectDevEvents();
|
|
7464
|
+
addEventListener('pagehide', () => events.close());
|
|
7465
|
+
addEventListener('pageshow', event => { if (event.persisted) connectDevEvents(); });
|
|
7390
7466
|
</script>`;
|
|
7391
7467
|
}
|
package/src/resolve/engine.ts
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { ResolverFactory,
|
|
3
|
+
import type { ResolverFactory, NapiResolveOptions } from 'oxc-resolver';
|
|
4
|
+
import { loadNative } from '../utils/native-require';
|
|
5
|
+
|
|
6
|
+
// Lazy: the native binding costs ~1.3 MB RSS and a prod server may never resolve.
|
|
7
|
+
function createResolverFactory(options: NapiResolveOptions): ResolverFactory {
|
|
8
|
+
const { ResolverFactory: Factory } = loadNative(
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
10
|
+
() => require('oxc-resolver') as typeof import('oxc-resolver'),
|
|
11
|
+
);
|
|
12
|
+
return new Factory(options);
|
|
13
|
+
}
|
|
4
14
|
import { clearNodeModulesFsCache } from '../utils/fs-cache';
|
|
5
15
|
|
|
6
16
|
const sourceExtensions = ['.tsx', '.ts', '.jsx', '.js', '.mjs', '.mts', '.cts', '.module.css', '.css'];
|
|
@@ -200,7 +210,7 @@ function baseOptions(): NapiResolveOptions {
|
|
|
200
210
|
}
|
|
201
211
|
|
|
202
212
|
function baseResolver() {
|
|
203
|
-
baseFactory ??=
|
|
213
|
+
baseFactory ??= createResolverFactory(baseOptions());
|
|
204
214
|
return baseFactory;
|
|
205
215
|
}
|
|
206
216
|
|