@wular/pnext 0.0.10 → 0.0.11
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/package.json +1 -1
- package/reference/dev.md +17 -4
- package/src/api/dynamic.tsx +1 -0
- package/src/api/link.tsx +1 -0
- package/src/cli/analyze.ts +1 -1
- package/src/cli/boot/trace.ts +40 -7
- package/src/cli/build.ts +2 -2
- package/src/cli/migrate/package-json.ts +1 -1
- package/src/cli/migrate/report.ts +5 -3
- package/src/cli/migrate/run.ts +30 -4
- package/src/cli/migrate/scan.ts +34 -7
- package/src/cli/typegen.ts +8 -1
- package/src/client/build.ts +4 -5
- package/src/client/profile.ts +4 -3
- package/src/client/router/runtime.ts +8 -0
- package/src/compat/actions/config.ts +2 -0
- package/src/compat/aliases.ts +2 -2
- package/src/compat/bundler/react-profiler.tsx +1 -0
- package/src/compat/cache-control.ts +49 -119
- package/src/compat/client/errors/global-error.ts +10 -5
- package/src/compat/client/errors/install.ts +30 -6
- package/src/compat/client/errors/lazy.ts +32 -2
- package/src/compat/next/client-script.tsx +1 -0
- package/src/compat/next/config-headers.ts +139 -0
- package/src/compat/next/config-loader.ts +32 -2
- package/src/compat/next/dynamic.tsx +1 -0
- package/src/compat/next/error.tsx +1 -0
- package/src/compat/next/form.tsx +1 -0
- package/src/compat/next/head.tsx +1 -0
- package/src/compat/next/headers.ts +12 -6
- package/src/compat/next/image/client.tsx +1 -0
- package/src/compat/next/image.tsx +1 -0
- package/src/compat/next/legacy-image.tsx +1 -0
- package/src/compat/next/legacy-request-apis.ts +62 -0
- package/src/compat/next/link.tsx +1 -0
- package/src/compat/next/rewrites.ts +7 -0
- package/src/compat/next/script.tsx +1 -0
- package/src/compat/register/bundler.ts +6 -1
- package/src/compat/segment/loading-boundary.ts +1 -5
- package/src/compat/validation/validate.ts +10 -2
- package/src/config.ts +12 -1
- package/src/css/build.ts +111 -4
- package/src/dev/restart/global-css.ts +3 -3
- package/src/dev/restart/route-facts.ts +3 -3
- package/src/dev/server.ts +2 -2
- package/src/render/renderer.ts +2 -2
- package/src/render/slots.tsx +1 -0
- package/src/request/context.ts +15 -0
- package/src/runtime/loader.ts +2 -2
- package/src/runtime/module-cache.ts +12 -5
- package/src/runtime/modules.ts +44 -31
- package/src/runtime/vendor-build.ts +5 -7
- package/src/runtime/vendor.ts +29 -13
- package/src/types.ts +10 -1
- package/src/utils/code.ts +5 -0
- package/src/utils/dev-profile.ts +1 -1
- package/src/utils/trace-flags.ts +66 -0
|
@@ -5,162 +5,92 @@ import type { ResolvedConfig } from '../config'
|
|
|
5
5
|
import type { ResponseFinalizer, ResponseFinalizerContext } from '../extensions'
|
|
6
6
|
import { getNextConfig } from './next/config-loader'
|
|
7
7
|
import { escapeRegex } from '../utils/code'
|
|
8
|
+
import { canonicalUrlHref } from './next/canonical-url'
|
|
8
9
|
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
type
|
|
13
|
-
type
|
|
14
|
-
} from './next/
|
|
10
|
+
appPathname,
|
|
11
|
+
loadCompatHeaders,
|
|
12
|
+
resolveCompatHeaders,
|
|
13
|
+
type CompatHeaderPair,
|
|
14
|
+
type CompatHeaderRule,
|
|
15
|
+
} from './next/config-headers'
|
|
16
|
+
import type { RewriteRequestContext } from './next/rewrites'
|
|
15
17
|
|
|
16
18
|
const DEFAULT_EXPIRE_TIME = 31536000
|
|
17
19
|
|
|
18
|
-
interface HeaderRule {
|
|
19
|
-
regex: RegExp
|
|
20
|
-
paramNames: string[]
|
|
21
|
-
headers: [string, string][]
|
|
22
|
-
has: RewriteCondition[]
|
|
23
|
-
missing: RewriteCondition[]
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
interface HeaderEntry {
|
|
27
|
-
source?: string
|
|
28
|
-
headers?: unknown
|
|
29
|
-
has?: unknown
|
|
30
|
-
missing?: unknown
|
|
31
|
-
}
|
|
32
|
-
|
|
33
20
|
interface RevalidateRule {
|
|
34
21
|
regex: RegExp
|
|
35
22
|
revalidateSeconds: number
|
|
36
23
|
}
|
|
37
24
|
|
|
38
|
-
interface ConfigHeader {
|
|
39
|
-
key?: unknown
|
|
40
|
-
value?: unknown
|
|
41
|
-
}
|
|
42
|
-
|
|
43
25
|
export function cacheControlFinalizers(config: ResolvedConfig): ResponseFinalizer[] {
|
|
44
26
|
if (!nextCompatEnabled(config)) return []
|
|
45
27
|
|
|
46
|
-
let rules: Promise<
|
|
28
|
+
let rules: Promise<CompatHeaderRule[]> | undefined
|
|
47
29
|
let pagesRevalidateRules: RevalidateRule[] | undefined
|
|
48
30
|
const finalizer: ResponseFinalizer = async ctx => {
|
|
49
|
-
rules ??=
|
|
50
|
-
applyConfigHeaders(ctx, await rules)
|
|
31
|
+
rules ??= loadCompatHeaders(config.root)
|
|
32
|
+
applyConfigHeaders(ctx, await rules, config.basePath)
|
|
33
|
+
applyPoweredByHeader(ctx)
|
|
51
34
|
pagesRevalidateRules ??= loadPagesRevalidateRules(config.root)
|
|
52
35
|
applyDefaultIsrHeader(ctx, pagesRevalidateRules)
|
|
53
36
|
}
|
|
54
37
|
return [finalizer]
|
|
55
38
|
}
|
|
56
39
|
|
|
57
|
-
async function loadHeaderRules(): Promise<HeaderRule[]> {
|
|
58
|
-
const producer = getNextConfig().headers
|
|
59
|
-
if (typeof producer !== 'function') return []
|
|
60
|
-
|
|
61
|
-
let entries: unknown
|
|
62
|
-
try {
|
|
63
|
-
entries = await (producer as () => unknown)()
|
|
64
|
-
} catch {
|
|
65
|
-
return []
|
|
66
|
-
}
|
|
67
|
-
if (!Array.isArray(entries)) return []
|
|
68
|
-
|
|
69
|
-
const rules: HeaderRule[] = []
|
|
70
|
-
for (const entry of entries as HeaderEntry[]) {
|
|
71
|
-
if (!entry?.source || !Array.isArray(entry.headers)) continue
|
|
72
|
-
const compiled = compileHeaderSource(entry.source)
|
|
73
|
-
if (!compiled) continue
|
|
74
|
-
const has = parseConditions(entry.has)
|
|
75
|
-
const missing = parseConditions(entry.missing)
|
|
76
|
-
if (!has || !missing) continue
|
|
77
|
-
const headers = headerPairs(entry.headers)
|
|
78
|
-
if (headers.length === 0) continue
|
|
79
|
-
rules.push({ ...compiled, headers, has, missing })
|
|
80
|
-
}
|
|
81
|
-
return rules
|
|
82
|
-
}
|
|
83
|
-
|
|
84
40
|
/**
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
* in plain segments (the `.` in `/favicon.ico`, say) instead of rejecting the rule - Next's
|
|
88
|
-
* path-to-regexp treats those as literals. Raw regex bodies still only appear inside `:param(...)`.
|
|
41
|
+
* Next sends `X-Powered-By: Next.js` on HTML payloads (send-payload.ts gates on the HTML content
|
|
42
|
+
* type) and omits it entirely under `poweredByHeader: false`.
|
|
89
43
|
*/
|
|
90
|
-
function
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const custom = /^:([A-Za-z_$][\w$]*)\((.*)\)$/.exec(segment)
|
|
95
|
-
if (custom?.[1] !== undefined && custom[2] !== undefined) {
|
|
96
|
-
paramNames.push(custom[1])
|
|
97
|
-
parts.push(`(${custom[2]})`)
|
|
98
|
-
continue
|
|
99
|
-
}
|
|
100
|
-
const catchAll = /^:([A-Za-z_$][\w$]*)[*+]$/.exec(segment)
|
|
101
|
-
if (catchAll?.[1]) {
|
|
102
|
-
paramNames.push(catchAll[1])
|
|
103
|
-
parts.push('(.*)')
|
|
104
|
-
continue
|
|
105
|
-
}
|
|
106
|
-
const optional = /^:([A-Za-z_$][\w$]*)\?$/.exec(segment)
|
|
107
|
-
if (optional?.[1]) {
|
|
108
|
-
paramNames.push(optional[1])
|
|
109
|
-
parts.push('([^/]*)')
|
|
110
|
-
continue
|
|
111
|
-
}
|
|
112
|
-
const param = /^:([A-Za-z_$][\w$]*)$/.exec(segment)
|
|
113
|
-
if (param?.[1]) {
|
|
114
|
-
paramNames.push(param[1])
|
|
115
|
-
parts.push('([^/]+)')
|
|
116
|
-
continue
|
|
117
|
-
}
|
|
118
|
-
// Plain literal segment: escape regex-special characters so `.` etc. match
|
|
119
|
-
// literally rather than acting as regex metacharacters.
|
|
120
|
-
parts.push(segment.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
|
|
121
|
-
}
|
|
122
|
-
return { regex: new RegExp(`^/${parts.join('/')}/?$`), paramNames }
|
|
44
|
+
function applyPoweredByHeader(ctx: ResponseFinalizerContext): void {
|
|
45
|
+
if (getNextConfig().poweredByHeader === false) return
|
|
46
|
+
if (!ctx.headers.get('content-type')?.startsWith('text/html')) return
|
|
47
|
+
ctx.headers.set('x-powered-by', 'Next.js')
|
|
123
48
|
}
|
|
124
49
|
|
|
125
|
-
function
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
return pairs
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function applyConfigHeaders(ctx: ResponseFinalizerContext, rules: readonly HeaderRule[]): void {
|
|
50
|
+
function applyConfigHeaders(
|
|
51
|
+
ctx: ResponseFinalizerContext,
|
|
52
|
+
rules: readonly CompatHeaderRule[],
|
|
53
|
+
basePath: string,
|
|
54
|
+
): void {
|
|
135
55
|
if (rules.length === 0) return
|
|
136
|
-
|
|
56
|
+
// Header routes precede rewrites (and middleware) in Next's route list, so
|
|
57
|
+
// they match the URL the browser asked for, never a rewrite destination.
|
|
58
|
+
const url = new URL(canonicalUrlHref() ?? ctx.request.url.href)
|
|
137
59
|
const requestCtx: RewriteRequestContext = {
|
|
138
60
|
host: url.host,
|
|
139
61
|
headers: ctx.request.headers,
|
|
140
62
|
cookies: parseCookieHeader(ctx.request.headers.get('cookie')),
|
|
141
63
|
query: url.searchParams,
|
|
142
64
|
}
|
|
65
|
+
// `basePath: false` entries match the RAW path; every other entry's source is
|
|
66
|
+
// basePath-prefixed, i.e. matches the in-app path. The two passes are disjoint.
|
|
67
|
+
const inApp = appPathname(url.pathname, basePath)
|
|
68
|
+
const matched: CompatHeaderPair[] = [
|
|
69
|
+
...(inApp === undefined
|
|
70
|
+
? []
|
|
71
|
+
: resolveCompatHeaders(
|
|
72
|
+
rules.filter(rule => !rule.outsideBasePath),
|
|
73
|
+
inApp,
|
|
74
|
+
requestCtx,
|
|
75
|
+
)),
|
|
76
|
+
...resolveCompatHeaders(
|
|
77
|
+
rules.filter(rule => rule.outsideBasePath),
|
|
78
|
+
url.pathname,
|
|
79
|
+
requestCtx,
|
|
80
|
+
),
|
|
81
|
+
]
|
|
143
82
|
|
|
144
83
|
// Config `Set-Cookie` headers must be appended (each entry is its own header,
|
|
145
84
|
// never overwriting) and must precede the response's own Set-Cookie headers —
|
|
146
85
|
// Next applies config headers at the router layer before the route's own
|
|
147
86
|
// cookies (set-cookies suite asserts [...nextConfigHeaders, ...cookies]).
|
|
148
87
|
const configSetCookies: string[] = []
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
})
|
|
156
|
-
const conditionParams = evaluateConditions(rule, requestCtx)
|
|
157
|
-
if (conditionParams === null) continue
|
|
158
|
-
Object.assign(params, conditionParams)
|
|
159
|
-
for (const [key, value] of rule.headers) {
|
|
160
|
-
const resolved = substitute(value, params).pathname
|
|
161
|
-
if (key.toLowerCase() === 'set-cookie') configSetCookies.push(resolved)
|
|
162
|
-
else ctx.headers.set(key, resolved)
|
|
163
|
-
}
|
|
88
|
+
// A route handler's Response headers are user code running AFTER the router
|
|
89
|
+
// set these, so those keys stay the handler's (Next: res.setHeader last wins).
|
|
90
|
+
const pageOwnsHeaders = ctx.routeKind === 'route-handler'
|
|
91
|
+
for (const { key, value } of matched) {
|
|
92
|
+
if (key.toLowerCase() === 'set-cookie') configSetCookies.push(value)
|
|
93
|
+
else if (!pageOwnsHeaders || !ctx.headers.has(key)) ctx.headers.set(key, value)
|
|
164
94
|
}
|
|
165
95
|
if (configSetCookies.length > 0) {
|
|
166
96
|
const existing =
|
|
@@ -16,7 +16,12 @@ import { softNavigate } from '../../../client/router'
|
|
|
16
16
|
|
|
17
17
|
export type GlobalErrorComponent = ComponentType<{ error: unknown; reset: () => void }>
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
// Mounted PER DOCUMENT, not per process: idempotence is a property of the document being
|
|
20
|
+
// replaced. Identical in a browser (one document per page lifetime); differs in multi-document
|
|
21
|
+
// hosts (jsdom embedders, the test process), where a process boolean wrongly suppresses mounts.
|
|
22
|
+
let globalErrorMountedIn: Document | undefined
|
|
23
|
+
const globalErrorMounted = () =>
|
|
24
|
+
typeof document !== 'undefined' && globalErrorMountedIn === document
|
|
20
25
|
|
|
21
26
|
// The app-root global-error.js component, registered by installClientErrors so the in-tree
|
|
22
27
|
// ClientErrorBoundary can escalate straight to it WITHOUT bouncing a re-throw through the window
|
|
@@ -75,15 +80,15 @@ function reportUncaught(error: Error): void {
|
|
|
75
80
|
* top-level replacement). A soft refresh via reset() clears the flag.
|
|
76
81
|
*/
|
|
77
82
|
export function mountGlobalError(error: unknown, component?: GlobalErrorComponent): void {
|
|
78
|
-
if (typeof document === 'undefined' || globalErrorMounted) return
|
|
79
|
-
|
|
83
|
+
if (typeof document === 'undefined' || globalErrorMounted()) return
|
|
84
|
+
globalErrorMountedIn = document
|
|
80
85
|
|
|
81
86
|
document.documentElement.id = '__next_error__'
|
|
82
87
|
const root = document.createElement('div')
|
|
83
88
|
document.body.replaceChildren(root)
|
|
84
89
|
|
|
85
90
|
const reset = () => {
|
|
86
|
-
|
|
91
|
+
globalErrorMountedIn = undefined
|
|
87
92
|
render(null, root)
|
|
88
93
|
// Re-render the current URL from the server (recovers server + client errors).
|
|
89
94
|
void softNavigate(location.href, { replace: true, scroll: false }).catch(() => {
|
|
@@ -98,7 +103,7 @@ export function mountGlobalError(error: unknown, component?: GlobalErrorComponen
|
|
|
98
103
|
render(h(component, { error, reset }), root)
|
|
99
104
|
return
|
|
100
105
|
} catch {
|
|
101
|
-
|
|
106
|
+
globalErrorMountedIn = document // keep mounted; render the fallback below.
|
|
102
107
|
}
|
|
103
108
|
}
|
|
104
109
|
|
|
@@ -13,8 +13,12 @@
|
|
|
13
13
|
// 1. CONTROL-FLOW errors that escape - driven to their navigation outcome, never shown.
|
|
14
14
|
// 2. ACTION errors - left to the action-error overlay, ignored here.
|
|
15
15
|
// 3. Errors ALREADY HANDLED by a boundary - ignored, so one failure is never rendered twice.
|
|
16
|
-
// 4.
|
|
17
|
-
//
|
|
16
|
+
// 4. UNCAUGHT RENDER escapes (tagged at the preact boundary walk, see ./lazy) - the root render
|
|
17
|
+
// failed, so global-error replaces the document, exactly Next's root boundary.
|
|
18
|
+
// 5. Anything else - event-handler throws, third-party script noise, benign browser error events
|
|
19
|
+
// (ResizeObserver loops report with `error: null`) - stays uncaught and the page SURVIVES.
|
|
20
|
+
// Next mounts global-error only from its root render boundary, never from window events
|
|
21
|
+
// (its only production window listeners drive redirect control flow).
|
|
18
22
|
//
|
|
19
23
|
// Client-escaped errors carry NO digest, matching Next.
|
|
20
24
|
|
|
@@ -60,6 +64,24 @@ export interface InstallOptions {
|
|
|
60
64
|
|
|
61
65
|
let installed = false
|
|
62
66
|
|
|
67
|
+
// Cross-chunk marker (./lazy tags before this chunk exists): a value the preact boundary walk
|
|
68
|
+
// rethrew is a ROOT RENDER failure, the one case the window channel mounts global-error for.
|
|
69
|
+
const RENDER_ESCAPE = Symbol.for('pnext.renderEscape')
|
|
70
|
+
|
|
71
|
+
export function tagRenderEscape(error: unknown): void {
|
|
72
|
+
if (error !== null && typeof error === 'object') {
|
|
73
|
+
try {
|
|
74
|
+
;(error as Record<symbol, unknown>)[RENDER_ESCAPE] = true
|
|
75
|
+
} catch {
|
|
76
|
+
// frozen throw value: the post-install escalation still mounts synchronously
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isRenderEscape(error: unknown): boolean {
|
|
82
|
+
return error !== null && typeof error === 'object' && RENDER_ESCAPE in error
|
|
83
|
+
}
|
|
84
|
+
|
|
63
85
|
// Set by installClientErrors so ./lazy can replay the errors that escaped
|
|
64
86
|
// before this chunk landed (a throw during hydration or in a first-paint
|
|
65
87
|
// effect) through exactly the same triage.
|
|
@@ -92,10 +114,11 @@ export function installClientErrors(options: InstallOptions = {}): void {
|
|
|
92
114
|
// Action errors belong to the action-error overlay.
|
|
93
115
|
if (isActionError?.(error)) return
|
|
94
116
|
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
|
|
117
|
+
// Only a RENDER escape (the preact boundary walk rethrew it — tagged in ./lazy before this
|
|
118
|
+
// chunk exists, escalated synchronously after install) replaces the document: that is Next's
|
|
119
|
+
// root render boundary. Every other uncaught value — event handlers, third-party scripts,
|
|
120
|
+
// browser noise — stays uncaught and the page survives, as it does under Next.
|
|
121
|
+
if (isRenderEscape(error)) mountGlobalError(error, globalErrorComponent)
|
|
99
122
|
}
|
|
100
123
|
escapedErrorHandler = onError
|
|
101
124
|
|
|
@@ -143,6 +166,7 @@ function installRenderEscapeEscalation(
|
|
|
143
166
|
// have an owner — only a genuinely uncaught error escapes to global-error.
|
|
144
167
|
// escalate (not mount): the error must also report as uncaught in THIS
|
|
145
168
|
// task, not a task later via the natural unhandled rejection.
|
|
169
|
+
tagRenderEscape(error)
|
|
146
170
|
if (!wasErrorHandled(error) && !isControlFlowError(error) && !isActionError?.(error)) {
|
|
147
171
|
escalateToGlobalError(error)
|
|
148
172
|
}
|
|
@@ -6,13 +6,43 @@
|
|
|
6
6
|
// a first-paint effect fires long before the idle tier lands, and Next still shows global-error for
|
|
7
7
|
// it. They buffer the value, pull ./install right away, and hand it over - so a healthy page still
|
|
8
8
|
// never fetches the chunk.
|
|
9
|
+
import { options } from 'preact'
|
|
9
10
|
import { registerDeferredInstall } from '../../../client/router'
|
|
10
11
|
import type { InstallOptions } from './install'
|
|
11
12
|
|
|
13
|
+
// Same key ./install reads (Symbol.for crosses the chunk boundary): only values the preact
|
|
14
|
+
// boundary walk rethrew — a ROOT RENDER failure, Next's global-error case — may replace the
|
|
15
|
+
// document. Every other uncaught window value (event handlers, third-party noise, ResizeObserver
|
|
16
|
+
// loop events) is buffered for triage but the page survives, as it does under Next.
|
|
17
|
+
const RENDER_ESCAPE = Symbol.for('pnext.renderEscape')
|
|
18
|
+
|
|
19
|
+
/** Tag boundary-walk escapes before the deferred runtime exists (a hydration throw fires first). */
|
|
20
|
+
function tagRenderEscapes(): void {
|
|
21
|
+
const hooks = options as unknown as Record<string, unknown>
|
|
22
|
+
const key = (['_catchError', '__e'] as const).find(name => typeof hooks[name] === 'function')
|
|
23
|
+
if (!key) return
|
|
24
|
+
const walkBoundaries = hooks[key] as (...args: unknown[]) => unknown
|
|
25
|
+
hooks[key] = function taggedCatchError(this: unknown, ...args: unknown[]) {
|
|
26
|
+
try {
|
|
27
|
+
return walkBoundaries.apply(this, args)
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if (error !== null && typeof error === 'object') {
|
|
30
|
+
try {
|
|
31
|
+
;(error as Record<symbol, unknown>)[RENDER_ESCAPE] = true
|
|
32
|
+
} catch {
|
|
33
|
+
// frozen throw value: the runtime's own escalation still mounts post-install
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
throw error
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
12
41
|
type ErrorRuntime = typeof import('./install')
|
|
13
42
|
|
|
14
|
-
export function installClientErrors(
|
|
43
|
+
export function installClientErrors(installOptions: InstallOptions = {}): void {
|
|
15
44
|
if (!process.browser && typeof window === 'undefined') return
|
|
45
|
+
tagRenderEscapes()
|
|
16
46
|
|
|
17
47
|
let runtime: Promise<ErrorRuntime> | undefined
|
|
18
48
|
const load = () =>
|
|
@@ -21,7 +51,7 @@ export function installClientErrors(options: InstallOptions = {}): void {
|
|
|
21
51
|
// error is triaged once rather than by both paths.
|
|
22
52
|
window.removeEventListener('error', onError)
|
|
23
53
|
window.removeEventListener('unhandledrejection', onRejection)
|
|
24
|
-
module.installClientErrors(
|
|
54
|
+
module.installClientErrors(installOptions)
|
|
25
55
|
return module
|
|
26
56
|
}))
|
|
27
57
|
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* next.config `headers()` support (Next compat): every rule whose `source` matches the as-requested
|
|
3
|
+
* pathname contributes its `headers` to the response. Shares the source/condition/substitution
|
|
4
|
+
* machinery with rewrites and redirects (`:param`, `:param*`/`+`/`?`, `:param(regex)`, bare `(regex)`,
|
|
5
|
+
* has/missing of type host/header/query/cookie, `basePath: false`).
|
|
6
|
+
*
|
|
7
|
+
* Unlike redirects/rewrites, matching does NOT stop at the first hit: Next walks the whole header route
|
|
8
|
+
* list and accumulates, so a later rule overwrites an earlier one per key while `set-cookie` appends.
|
|
9
|
+
* Key AND value take `:param` substitution (Next's compileNonPath).
|
|
10
|
+
*
|
|
11
|
+
* The rules are applied by the cache-control finalizer (compat/cache-control.ts), which owns the
|
|
12
|
+
* response seam headers share with the ISR Cache-Control defaults - config headers land first, exactly
|
|
13
|
+
* as Next's router sets them on `res` before the render runs.
|
|
14
|
+
*/
|
|
15
|
+
import { loadNextConfig, loadResolvedNextConfig } from '../actions/config'
|
|
16
|
+
import {
|
|
17
|
+
compileSource,
|
|
18
|
+
evaluateConditions,
|
|
19
|
+
parseConditions,
|
|
20
|
+
substitute,
|
|
21
|
+
type RewriteCondition,
|
|
22
|
+
type RewriteRequestContext,
|
|
23
|
+
} from './rewrites'
|
|
24
|
+
|
|
25
|
+
export interface CompatHeaderPair {
|
|
26
|
+
key: string
|
|
27
|
+
value: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface CompatHeaderRule {
|
|
31
|
+
regex: RegExp
|
|
32
|
+
paramNames: string[]
|
|
33
|
+
headers: CompatHeaderPair[]
|
|
34
|
+
has: RewriteCondition[]
|
|
35
|
+
missing: RewriteCondition[]
|
|
36
|
+
/** `basePath: false`: the source matches the RAW path, outside the basePath. */
|
|
37
|
+
outsideBasePath: boolean
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface HeaderEntry {
|
|
41
|
+
source?: string
|
|
42
|
+
headers?: unknown
|
|
43
|
+
has?: unknown
|
|
44
|
+
missing?: unknown
|
|
45
|
+
basePath?: unknown
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function loadCompatHeaders(root: string): Promise<CompatHeaderRule[]> {
|
|
49
|
+
let config = await loadResolvedNextConfig(root)
|
|
50
|
+
if (typeof config?.headers !== 'function') config = await loadNextConfig(root)
|
|
51
|
+
const producer = config?.headers
|
|
52
|
+
if (typeof producer !== 'function') return []
|
|
53
|
+
let value: unknown
|
|
54
|
+
try {
|
|
55
|
+
value = await producer()
|
|
56
|
+
} catch {
|
|
57
|
+
return []
|
|
58
|
+
}
|
|
59
|
+
if (!Array.isArray(value)) return []
|
|
60
|
+
|
|
61
|
+
const rules: CompatHeaderRule[] = []
|
|
62
|
+
for (const entry of value as HeaderEntry[]) {
|
|
63
|
+
if (!entry?.source || !Array.isArray(entry.headers)) continue
|
|
64
|
+
const headers = parseHeaderPairs(entry.headers)
|
|
65
|
+
if (headers.length === 0) continue
|
|
66
|
+
const has = parseConditions(entry.has)
|
|
67
|
+
const missing = parseConditions(entry.missing)
|
|
68
|
+
if (has === null || missing === null) continue // unsupported condition shape
|
|
69
|
+
const compiled = compileSource(entry.source)
|
|
70
|
+
if (!compiled) continue
|
|
71
|
+
rules.push({
|
|
72
|
+
...compiled,
|
|
73
|
+
headers,
|
|
74
|
+
has,
|
|
75
|
+
missing,
|
|
76
|
+
outsideBasePath: entry.basePath === false,
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
return rules
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function parseHeaderPairs(value: readonly unknown[]): CompatHeaderPair[] {
|
|
83
|
+
const pairs: CompatHeaderPair[] = []
|
|
84
|
+
for (const raw of value) {
|
|
85
|
+
if (!raw || typeof raw !== 'object') continue
|
|
86
|
+
const { key, value: headerValue } = raw as Record<string, unknown>
|
|
87
|
+
if (typeof key !== 'string' || typeof headerValue !== 'string') continue
|
|
88
|
+
pairs.push({ key, value: headerValue })
|
|
89
|
+
}
|
|
90
|
+
return pairs
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Every matching rule's headers, in rule order (the caller resolves per-key precedence). */
|
|
94
|
+
export function resolveCompatHeaders(
|
|
95
|
+
rules: readonly CompatHeaderRule[],
|
|
96
|
+
pathname: string,
|
|
97
|
+
ctx?: RewriteRequestContext,
|
|
98
|
+
): CompatHeaderPair[] {
|
|
99
|
+
const context: RewriteRequestContext = ctx ?? {
|
|
100
|
+
host: '',
|
|
101
|
+
headers: new Headers(),
|
|
102
|
+
cookies: {},
|
|
103
|
+
query: new URLSearchParams(),
|
|
104
|
+
}
|
|
105
|
+
const resolved: CompatHeaderPair[] = []
|
|
106
|
+
for (const rule of rules) {
|
|
107
|
+
const match = rule.regex.exec(pathname)
|
|
108
|
+
if (!match) continue
|
|
109
|
+
|
|
110
|
+
const params: Record<string, string> = {}
|
|
111
|
+
rule.paramNames.forEach((name, index) => {
|
|
112
|
+
params[name] = match[index + 1] ?? ''
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
const conditionParams = evaluateConditions(rule, context)
|
|
116
|
+
if (conditionParams === null) continue // has/missing gate failed
|
|
117
|
+
Object.assign(params, conditionParams)
|
|
118
|
+
|
|
119
|
+
const hasParams = Object.keys(params).length > 0
|
|
120
|
+
for (const pair of rule.headers) {
|
|
121
|
+
resolved.push(
|
|
122
|
+
hasParams
|
|
123
|
+
? {
|
|
124
|
+
key: substitute(pair.key, params).pathname,
|
|
125
|
+
value: substitute(pair.value, params).pathname,
|
|
126
|
+
}
|
|
127
|
+
: pair,
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return resolved
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** The in-app path for a raw request pathname, or undefined when it lies outside the basePath. */
|
|
135
|
+
export function appPathname(pathname: string, basePath: string): string | undefined {
|
|
136
|
+
if (!basePath) return pathname
|
|
137
|
+
if (pathname === basePath) return '/'
|
|
138
|
+
return pathname.startsWith(`${basePath}/`) ? pathname.slice(basePath.length) : undefined
|
|
139
|
+
}
|
|
@@ -393,27 +393,54 @@ const VALID_DEV_INDICATOR_POSITIONS = new Set([
|
|
|
393
393
|
|
|
394
394
|
const KNOWN_TOP_LEVEL_CONFIG_KEYS = new Set([
|
|
395
395
|
'adapterPath',
|
|
396
|
+
'agentRules',
|
|
397
|
+
'allowedDevOrigins',
|
|
396
398
|
'assetPrefix',
|
|
397
399
|
'basePath',
|
|
400
|
+
'bundlePagesRouterDependencies',
|
|
398
401
|
'cacheComponents',
|
|
399
402
|
'cacheHandler',
|
|
400
403
|
'cacheHandlers',
|
|
401
404
|
'cacheLife',
|
|
405
|
+
'cacheMaxMemorySize',
|
|
406
|
+
'cleanDistDir',
|
|
402
407
|
'compiler',
|
|
408
|
+
'compress',
|
|
409
|
+
'configFile',
|
|
410
|
+
'configFileName',
|
|
411
|
+
'configOrigin',
|
|
412
|
+
'crossOrigin',
|
|
413
|
+
'deploymentId',
|
|
403
414
|
'devIndicators',
|
|
404
415
|
'distDir',
|
|
416
|
+
'enablePrerenderSourceMaps',
|
|
405
417
|
'env',
|
|
418
|
+
'excludeDefaultMomentLocales',
|
|
406
419
|
'expireTime',
|
|
407
420
|
'experimental',
|
|
421
|
+
'exportPathMap',
|
|
422
|
+
'generateBuildId',
|
|
423
|
+
'generateEtags',
|
|
424
|
+
'headers',
|
|
425
|
+
'htmlLimitedBots',
|
|
426
|
+
'httpAgentOptions',
|
|
408
427
|
'images',
|
|
428
|
+
'logging',
|
|
429
|
+
'modularizeImports',
|
|
430
|
+
'onDemandEntries',
|
|
409
431
|
'output',
|
|
432
|
+
'outputFileTracingExcludes',
|
|
433
|
+
'outputFileTracingIncludes',
|
|
410
434
|
'outputFileTracingRoot',
|
|
411
435
|
'pageExtensions',
|
|
412
436
|
'partialPrefetching',
|
|
437
|
+
'poweredByHeader',
|
|
413
438
|
'productionBrowserSourceMaps',
|
|
414
439
|
'publicRuntimeConfig',
|
|
415
440
|
'reactCompiler',
|
|
416
441
|
'reactMaxHeadersLength',
|
|
442
|
+
'reactProductionProfiling',
|
|
443
|
+
'reactStrictMode',
|
|
417
444
|
'resolveExtensions',
|
|
418
445
|
'redirects',
|
|
419
446
|
'rewrites',
|
|
@@ -422,6 +449,7 @@ const KNOWN_TOP_LEVEL_CONFIG_KEYS = new Set([
|
|
|
422
449
|
'serverRuntimeConfig',
|
|
423
450
|
'skipProxyUrlNormalize',
|
|
424
451
|
'skipTrailingSlashRedirect',
|
|
452
|
+
'staticPageGenerationTimeout',
|
|
425
453
|
'i18n',
|
|
426
454
|
'instrumentationClientInject',
|
|
427
455
|
'trailingSlash',
|
|
@@ -430,6 +458,8 @@ const KNOWN_TOP_LEVEL_CONFIG_KEYS = new Set([
|
|
|
430
458
|
'turbopack',
|
|
431
459
|
'typedRoutes',
|
|
432
460
|
'typescript',
|
|
461
|
+
'useFileSystemPublicRoutes',
|
|
462
|
+
'watchOptions',
|
|
433
463
|
'webpack',
|
|
434
464
|
])
|
|
435
465
|
|
|
@@ -523,12 +553,12 @@ function warnWebpackCompat(config: NextConfigObject, configFileName: string): vo
|
|
|
523
553
|
console.warn(
|
|
524
554
|
`\`webpack\` in ${configFileName} is not executed by pnext — esbuild is the only bundler. ` +
|
|
525
555
|
'`transpilePackages`, `experimental.optimizePackageImports`, and `turbopack.resolveAlias` are ' +
|
|
526
|
-
'supported natively; see
|
|
556
|
+
'supported natively; see https://pnext.dev/docs/compat.',
|
|
527
557
|
)
|
|
528
558
|
if (webpackReferencesSvgr(config)) {
|
|
529
559
|
console.info(
|
|
530
560
|
'pnext detected @svgr/webpack in `webpack()` and will compile `.svg` imports to inline Preact ' +
|
|
531
|
-
'components instead (see
|
|
561
|
+
'components instead (see https://pnext.dev/docs/compat).',
|
|
532
562
|
)
|
|
533
563
|
}
|
|
534
564
|
}
|
package/src/compat/next/form.tsx
CHANGED
package/src/compat/next/head.tsx
CHANGED