@wular/pnext 0.1.4 → 0.1.5
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/config/ts/react.json +2 -1
- package/package.json +1 -1
- package/reference/navigation.md +3 -1
- package/src/api/client-navigation.ts +53 -4
- package/src/api/link.tsx +6 -7
- package/src/cli/adapters/vercel-warm.ts +6 -1
- package/src/cli/adapters/vercel.ts +1 -1
- package/src/cli/build/cache.ts +8 -3
- package/src/cli/build.ts +3 -2
- package/src/cli/serve/pipeline.ts +11 -3
- package/src/cli/typegen.ts +65 -3
- package/src/client/build.ts +10 -7
- package/src/client/entry.ts +50 -29
- package/src/client/reference-stub.ts +1 -1
- package/src/client/router/hub.ts +13 -11
- package/src/client/router/page-slot.ts +58 -0
- package/src/client/router/runtime.ts +377 -388
- package/src/compat/bundler/symlink-imports.ts +1 -1
- package/src/compat/client/errors/bare-boundary.ts +1 -1
- package/src/compat/client/errors/error-boundary.ts +1 -1
- package/src/compat/client/errors/global-error.ts +40 -79
- package/src/compat/client/errors/install.ts +1 -1
- package/src/compat/client/errors/lazy.ts +7 -0
- package/src/compat/client/errors/primitive-throw.ts +36 -53
- package/src/compat/client/segment-cache-policy.ts +5 -22
- package/src/compat/client/segment-cache.ts +7 -26
- package/src/compat/css/modules.ts +1 -7
- package/src/compat/css/sass.ts +1 -7
- package/src/compat/react/parity.ts +1 -1
- package/src/compat/register/bundler.ts +9 -1
- package/src/compat/tsconfig-defaults.ts +4 -7
- package/src/compat-bootstrap.ts +15 -0
- package/src/css/build.ts +18 -15
- package/src/dev/server.ts +2 -2
- package/src/index.ts +2 -0
- package/src/render/renderer.ts +209 -24
- package/src/render/resource-hints.ts +4 -1
- package/src/resolve/imports.ts +13 -8
- package/src/routing/routes.ts +32 -18
- package/src/runtime/loader.ts +29 -10
- package/src/runtime/modules.ts +28 -7
- package/src/runtime/vendor-build.ts +2 -2
- package/src/types.ts +2 -0
- package/src/utils/fs.ts +122 -0
|
@@ -104,7 +104,7 @@ export function serverSymlinkImportPlugin(config: ResolvedConfig): Plugin {
|
|
|
104
104
|
return undefined
|
|
105
105
|
}
|
|
106
106
|
if (await isRealPath(args.importer)) return undefined
|
|
107
|
-
const target = resolveImport(config.root, args.importer, args.path)
|
|
107
|
+
const target = resolveImport(config.root, args.importer, args.path, config.workspaceRoot)
|
|
108
108
|
if (!target) return undefined
|
|
109
109
|
return build.resolve(target, {
|
|
110
110
|
kind: args.kind,
|
|
@@ -24,7 +24,7 @@ export class BareErrorBoundary extends Component<{ children?: unknown }, BareBou
|
|
|
24
24
|
// Everything else re-throws: unmarked, so the window last resort still sees
|
|
25
25
|
// a genuinely uncaught error and drives it.
|
|
26
26
|
if (error === null || typeof error !== 'object') {
|
|
27
|
-
void import('./
|
|
27
|
+
void import('./install').then(module => module.escalateToGlobalError(error))
|
|
28
28
|
return null
|
|
29
29
|
}
|
|
30
30
|
throw error as Error
|
|
@@ -198,7 +198,7 @@ export class ClientErrorBoundary extends Component<BoundaryProps, BoundaryState>
|
|
|
198
198
|
if (error === null || typeof error !== 'object') {
|
|
199
199
|
// global-error rides the deferred tier (see ./lazy) — a primitive throw
|
|
200
200
|
// with no error.js is the one path that reaches it from a render.
|
|
201
|
-
void import('./
|
|
201
|
+
void import('./install').then(m => m.escalateToGlobalError(error))
|
|
202
202
|
return null
|
|
203
203
|
}
|
|
204
204
|
throw error as Error
|
|
@@ -121,22 +121,10 @@ function renderBuiltinFallback(root: HTMLElement, error: unknown): void {
|
|
|
121
121
|
: 'Reload to try again, or go back.'
|
|
122
122
|
|
|
123
123
|
ensureThemeStyle()
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
display: 'flex',
|
|
129
|
-
alignItems: 'center',
|
|
130
|
-
justifyContent: 'center',
|
|
131
|
-
} as Partial<CSSStyleDeclaration>)
|
|
132
|
-
|
|
133
|
-
const card = document.createElement('div')
|
|
134
|
-
Object.assign(card.style, {
|
|
135
|
-
marginTop: '-32px',
|
|
136
|
-
maxWidth: '325px',
|
|
137
|
-
padding: '32px 28px',
|
|
138
|
-
textAlign: 'left',
|
|
139
|
-
} as Partial<CSSStyleDeclaration>)
|
|
124
|
+
root.style.cssText =
|
|
125
|
+
'font-family:system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";height:100vh;display:flex;align-items:center;justify-content:center'
|
|
126
|
+
|
|
127
|
+
const card = styled('div', 'margin-top:-32px;max-width:325px;padding:32px 28px;text-align:left')
|
|
140
128
|
|
|
141
129
|
const icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
|
142
130
|
icon.setAttribute('width', '32')
|
|
@@ -152,46 +140,30 @@ function renderBuiltinFallback(root: HTMLElement, error: unknown): void {
|
|
|
152
140
|
path.setAttribute('fill', 'var(--next-error-title)')
|
|
153
141
|
icon.append(path)
|
|
154
142
|
|
|
155
|
-
const title =
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
margin:
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
Object.assign(body.style, {
|
|
169
|
-
fontSize: '14px',
|
|
170
|
-
fontWeight: '400',
|
|
171
|
-
lineHeight: '21px',
|
|
172
|
-
margin: '0 0 20px 0',
|
|
173
|
-
color: 'var(--next-error-message)',
|
|
174
|
-
} as Partial<CSSStyleDeclaration>)
|
|
175
|
-
|
|
176
|
-
const buttonGroup = document.createElement('div')
|
|
177
|
-
Object.assign(buttonGroup.style, { display: 'flex', gap: '8px', alignItems: 'center' })
|
|
178
|
-
const reloadForm = document.createElement('form')
|
|
179
|
-
reloadForm.style.margin = '0'
|
|
143
|
+
const title = styled(
|
|
144
|
+
'h1',
|
|
145
|
+
'font-size:24px;font-weight:500;letter-spacing:-0.02em;line-height:32px;margin:0 0 12px 0;color:var(--next-error-title)',
|
|
146
|
+
'This page couldn’t load',
|
|
147
|
+
)
|
|
148
|
+
const body = styled(
|
|
149
|
+
'p',
|
|
150
|
+
'font-size:14px;font-weight:400;line-height:21px;margin:0 0 20px 0;color:var(--next-error-message)',
|
|
151
|
+
message,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
const buttonGroup = styled('div', 'display:flex;gap:8px;align-items:center')
|
|
155
|
+
const reloadForm = styled('form', 'margin:0')
|
|
180
156
|
reloadForm.addEventListener('submit', event => {
|
|
181
157
|
event.preventDefault()
|
|
182
158
|
location.reload()
|
|
183
159
|
})
|
|
184
|
-
const reload =
|
|
160
|
+
const reload = styled('button', buttonStyle(''), 'Reload')
|
|
185
161
|
reload.type = 'submit'
|
|
186
|
-
reload.textContent = 'Reload'
|
|
187
|
-
Object.assign(reload.style, buttonStyle(false))
|
|
188
162
|
reloadForm.append(reload)
|
|
189
163
|
buttonGroup.append(reloadForm)
|
|
190
164
|
if (!isServerError) {
|
|
191
|
-
const back =
|
|
165
|
+
const back = styled('button', buttonStyle('secondary-'), 'Back')
|
|
192
166
|
back.type = 'button'
|
|
193
|
-
back.textContent = 'Back'
|
|
194
|
-
Object.assign(back.style, buttonStyle(true))
|
|
195
167
|
back.addEventListener('click', () => {
|
|
196
168
|
if (window.history.length > 1) window.history.back()
|
|
197
169
|
else window.location.href = '/'
|
|
@@ -203,42 +175,31 @@ function renderBuiltinFallback(root: HTMLElement, error: unknown): void {
|
|
|
203
175
|
if (isServerError) {
|
|
204
176
|
// Next's DefaultGlobalError digest footer (`ERROR <digest>`), fixed to the
|
|
205
177
|
// bottom so operators can correlate the redacted UI with server logs.
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
textAlign: 'center',
|
|
214
|
-
fontFamily: 'ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace',
|
|
215
|
-
fontSize: '12px',
|
|
216
|
-
lineHeight: '18px',
|
|
217
|
-
fontWeight: '400',
|
|
218
|
-
margin: '0',
|
|
219
|
-
color: 'var(--next-error-digest)',
|
|
220
|
-
} as Partial<CSSStyleDeclaration>)
|
|
221
|
-
root.append(footer)
|
|
178
|
+
root.append(
|
|
179
|
+
styled(
|
|
180
|
+
'p',
|
|
181
|
+
'position:fixed;bottom:32px;left:0;right:0;text-align:center;font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;font-size:12px;line-height:18px;font-weight:400;margin:0;color:var(--next-error-digest)',
|
|
182
|
+
`ERROR ${digest}`,
|
|
183
|
+
),
|
|
184
|
+
)
|
|
222
185
|
}
|
|
223
186
|
if (typeof document.title !== 'undefined') document.title = '500: This page couldn’t load'
|
|
224
187
|
}
|
|
225
188
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
border: secondary ? 'var(--next-error-btn-secondary-border)' : 'var(--next-error-btn-border)',
|
|
241
|
-
}
|
|
189
|
+
// Styles go through CSSOM (style.cssText), never style attributes, so a strict style-src CSP allows them.
|
|
190
|
+
function styled<K extends keyof HTMLElementTagNameMap>(
|
|
191
|
+
tag: K,
|
|
192
|
+
css: string,
|
|
193
|
+
text?: string,
|
|
194
|
+
): HTMLElementTagNameMap[K] {
|
|
195
|
+
const element = document.createElement(tag)
|
|
196
|
+
element.style.cssText = css
|
|
197
|
+
if (text !== undefined) element.textContent = text
|
|
198
|
+
return element
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function buttonStyle(variant: string): string {
|
|
202
|
+
return `display:inline-flex;align-items:center;justify-content:center;height:32px;padding:0 12px;font-size:14px;font-weight:500;line-height:20px;border-radius:6px;cursor:pointer;color:var(--next-error-btn-${variant}text);background:var(--next-error-btn-${variant}bg);border:var(--next-error-btn-${variant}border)`
|
|
242
203
|
}
|
|
243
204
|
|
|
244
205
|
const THEME_STYLE_ID = '__pnext_global_error_theme__'
|
|
@@ -42,7 +42,7 @@ export { ClientErrorBoundary, toClientError } from './error-boundary'
|
|
|
42
42
|
export { markErrorHandled, wasErrorHandled } from './error-boundary'
|
|
43
43
|
export type { RouteErrorComponent } from './error-boundary'
|
|
44
44
|
export { isControlFlowError, handleControlFlowError } from './control-flow'
|
|
45
|
-
export { mountGlobalError } from './global-error'
|
|
45
|
+
export { escalateToGlobalError, mountGlobalError } from './global-error'
|
|
46
46
|
export { softRefreshRoute } from './soft-refresh'
|
|
47
47
|
|
|
48
48
|
export interface InstallOptions {
|
|
@@ -79,5 +79,12 @@ export function installClientErrors(installOptions: InstallOptions = {}): void {
|
|
|
79
79
|
window.addEventListener('error', onError)
|
|
80
80
|
window.addEventListener('unhandledrejection', onRejection)
|
|
81
81
|
|
|
82
|
+
// A streamed server error has already thrown: Next shows its boundary while hydrating, not on idle.
|
|
83
|
+
const pullForStreamError = () => {
|
|
84
|
+
if (document.querySelector('pnext-error')) void load()
|
|
85
|
+
}
|
|
86
|
+
pullForStreamError()
|
|
87
|
+
document.addEventListener('DOMContentLoaded', pullForStreamError)
|
|
88
|
+
|
|
82
89
|
registerDeferredInstall(load)
|
|
83
90
|
}
|
|
@@ -19,8 +19,7 @@ export interface TaggedPrimitiveThrow extends Error {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export function tagPrimitiveThrow(value: unknown): TaggedPrimitiveThrow {
|
|
22
|
-
const
|
|
23
|
-
const error = new Error(message) as TaggedPrimitiveThrow
|
|
22
|
+
const error = new Error(String(value)) as TaggedPrimitiveThrow
|
|
24
23
|
error[RAW_VALUE] = value
|
|
25
24
|
return error
|
|
26
25
|
}
|
|
@@ -58,6 +57,10 @@ function crashesPreactThenCheck(value: unknown): boolean {
|
|
|
58
57
|
return value === null || value === undefined
|
|
59
58
|
}
|
|
60
59
|
|
|
60
|
+
function retagThrow(value: unknown): unknown {
|
|
61
|
+
return crashesPreactThenCheck(value) ? tagPrimitiveThrow(value) : value
|
|
62
|
+
}
|
|
63
|
+
|
|
61
64
|
/**
|
|
62
65
|
* Wrap a function/class component so a thrown value that would crash preact's own `e.then` suspense
|
|
63
66
|
* check is re-thrown as a tagged Error instead. Safe to call repeatedly on the same component type -
|
|
@@ -76,69 +79,49 @@ const wrapped = new WeakMap<object, unknown>()
|
|
|
76
79
|
// unmount and remount of that subtree instead of an update.
|
|
77
80
|
const ALREADY_WRAPPED = Symbol('pnext.primitiveThrowWrapped')
|
|
78
81
|
|
|
79
|
-
function copyMetadata(from: object, to: object): void {
|
|
80
|
-
for (const key of Object.getOwnPropertyNames(from)) {
|
|
81
|
-
if (key === 'length' || key === 'name' || key === 'prototype') continue
|
|
82
|
-
const descriptor = Object.getOwnPropertyDescriptor(from, key)
|
|
83
|
-
if (descriptor) Object.defineProperty(to, key, descriptor)
|
|
84
|
-
}
|
|
85
|
-
for (const symbol of Object.getOwnPropertySymbols(from)) {
|
|
86
|
-
const descriptor = Object.getOwnPropertyDescriptor(from, symbol)
|
|
87
|
-
if (descriptor) Object.defineProperty(to, symbol, descriptor)
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
82
|
export function wrapComponentForPrimitiveThrows<T extends object>(type: T): T {
|
|
92
83
|
if ((type as { [ALREADY_WRAPPED]?: true })[ALREADY_WRAPPED]) return type
|
|
93
84
|
const cached = wrapped.get(type)
|
|
94
85
|
if (cached) return cached as T
|
|
95
86
|
if (typeof type !== 'function') return type
|
|
96
87
|
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
88
|
+
const Base = type as unknown as new (...args: unknown[]) => {
|
|
89
|
+
render(...args: unknown[]): unknown
|
|
90
|
+
}
|
|
91
|
+
// Class components: preact does `new Type(props, context)` then calls
|
|
92
|
+
// `.render()`. Subclass so `render()` is wrapped; static lifecycle methods
|
|
93
|
+
// (getDerivedStateFromError, etc.) and everything else pass through
|
|
94
|
+
// untouched via normal prototype-chain inheritance.
|
|
95
|
+
const result: object = (type as { prototype?: { render?: unknown } }).prototype?.render
|
|
96
|
+
? class PrimitiveThrowSafe extends Base {
|
|
97
|
+
render(...args: unknown[]) {
|
|
98
|
+
try {
|
|
99
|
+
return super.render(...args)
|
|
100
|
+
} catch (e) {
|
|
101
|
+
throw retagThrow(e)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
: function pnextPrimitiveThrowSafe(this: unknown, ...args: unknown[]) {
|
|
110
106
|
try {
|
|
111
|
-
return
|
|
107
|
+
return (type as unknown as (...args: unknown[]) => unknown).apply(this, args)
|
|
112
108
|
} catch (e) {
|
|
113
|
-
|
|
114
|
-
throw e
|
|
109
|
+
throw retagThrow(e)
|
|
115
110
|
}
|
|
116
111
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const fn = type as unknown as (...args: unknown[]) => unknown
|
|
128
|
-
const wrappedFn = function pnextPrimitiveThrowSafe(this: unknown, ...args: unknown[]) {
|
|
129
|
-
try {
|
|
130
|
-
return fn.apply(this, args)
|
|
131
|
-
} catch (e) {
|
|
132
|
-
if (crashesPreactThenCheck(e)) throw tagPrimitiveThrow(e)
|
|
133
|
-
throw e
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
Object.defineProperty(wrappedFn, 'name', { value: fn.name })
|
|
137
|
-
copyMetadata(fn, wrappedFn)
|
|
138
|
-
result = wrappedFn
|
|
112
|
+
Object.defineProperty(result, 'name', { value: Base.name })
|
|
113
|
+
// Copy static properties AND symbol-keyed metadata (displayName, getDerivedStateFromError,
|
|
114
|
+
// contextType, defaultProps, plus the framework's own client-reference/dynamic-reference/params-
|
|
115
|
+
// scope symbol markers stashed directly on component function objects). resolveServerTree branches
|
|
116
|
+
// on those, so losing them mis-routes a client-reference class through the server-component call
|
|
117
|
+
// path instead of the island path. ownKeys = string names, then symbols, in the same order as before.
|
|
118
|
+
for (const key of Reflect.ownKeys(Base)) {
|
|
119
|
+
if (key === 'length' || key === 'name' || key === 'prototype') continue
|
|
120
|
+
const descriptor = Object.getOwnPropertyDescriptor(Base, key)
|
|
121
|
+
if (descriptor) Object.defineProperty(result, key, descriptor)
|
|
139
122
|
}
|
|
140
123
|
|
|
141
|
-
Object.defineProperty(result
|
|
124
|
+
Object.defineProperty(result, ALREADY_WRAPPED, { value: true })
|
|
142
125
|
wrapped.set(type, result)
|
|
143
126
|
return result as T
|
|
144
127
|
}
|
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
|
|
6
6
|
import type { SegmentCacheHit, SegmentCachePolicy, SegmentCacheRecord } from '../../client/router'
|
|
7
7
|
import {
|
|
8
|
-
BODY_SEGMENT_PATHS,
|
|
9
8
|
clearSegmentCache,
|
|
10
9
|
composeFetchedLayoutFrame,
|
|
11
10
|
composeCachedLayout,
|
|
@@ -20,7 +19,6 @@ import {
|
|
|
20
19
|
recordSegment,
|
|
21
20
|
resolvedRouteFor,
|
|
22
21
|
segmentPrefetchCovered,
|
|
23
|
-
takeComposedSegment,
|
|
24
22
|
takeSegment,
|
|
25
23
|
type SegmentPayloadMeta,
|
|
26
24
|
} from './segment-cache'
|
|
@@ -119,27 +117,12 @@ function take(input: {
|
|
|
119
117
|
segmentPath: string
|
|
120
118
|
}): SegmentCacheHit | null {
|
|
121
119
|
const hit = takeSegment(input)
|
|
122
|
-
|
|
123
|
-
return {
|
|
124
|
-
html: hit.html,
|
|
125
|
-
networkFree: hit.networkFree,
|
|
126
|
-
prefetchSatisfied: hit.prefetchSatisfied,
|
|
127
|
-
postponedShell: hit.postponedShell,
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
// w9-segment-split: the whole-document entry missed because ONE of the two
|
|
131
|
-
// frames varied. Compose the cached layout + page frames instead of going to
|
|
132
|
-
// the network for both. No-op until segment-split commit is enabled.
|
|
133
|
-
if (!BODY_SEGMENT_PATHS.includes(input.segmentPath as (typeof BODY_SEGMENT_PATHS)[number])) {
|
|
134
|
-
return null
|
|
135
|
-
}
|
|
136
|
-
const composed = takeComposedSegment({ pathname: input.pathname, search: input.search })
|
|
137
|
-
return composed
|
|
120
|
+
return hit
|
|
138
121
|
? {
|
|
139
|
-
html:
|
|
140
|
-
networkFree:
|
|
141
|
-
prefetchSatisfied:
|
|
142
|
-
postponedShell:
|
|
122
|
+
html: hit.html,
|
|
123
|
+
networkFree: hit.networkFree,
|
|
124
|
+
prefetchSatisfied: hit.prefetchSatisfied,
|
|
125
|
+
postponedShell: hit.postponedShell,
|
|
143
126
|
}
|
|
144
127
|
: null
|
|
145
128
|
}
|
|
@@ -300,11 +300,7 @@ function recordOneSegment(rawInput: SegmentRecordInput): void {
|
|
|
300
300
|
// really did answer this pathname with that route, and the sibling guard
|
|
301
301
|
// in `candidateKeys` reads it to keep other routes off this URL.
|
|
302
302
|
resolvedRoutes.set(rawInput.pathname, rawInput.route)
|
|
303
|
-
|
|
304
|
-
const oldest = resolvedRoutes.keys().next().value
|
|
305
|
-
if (oldest === undefined) break
|
|
306
|
-
resolvedRoutes.delete(oldest)
|
|
307
|
-
}
|
|
303
|
+
trimOldest(resolvedRoutes)
|
|
308
304
|
}
|
|
309
305
|
const key = entryKey(input.segmentPath, input.route, input.params, input.search, input.vary, {
|
|
310
306
|
pathname: input.pathname,
|
|
@@ -332,7 +328,7 @@ function recordOneSegment(rawInput: SegmentRecordInput): void {
|
|
|
332
328
|
// LRU-bound like `entries`/`resolvedRoutes`: a route publishing a new vary
|
|
333
329
|
// set per prefetch (dynamic params in the vary set) would otherwise grow this
|
|
334
330
|
// map without limit (memory-pressure leak-slope test).
|
|
335
|
-
|
|
331
|
+
trimOldest(byRoute)
|
|
336
332
|
}
|
|
337
333
|
const sharedAppShell = input.sharedAppShell === true || isRouteWideAppShell(input)
|
|
338
334
|
const existing = entries.get(key)
|
|
@@ -363,7 +359,7 @@ function recordOneSegment(rawInput: SegmentRecordInput): void {
|
|
|
363
359
|
sharedAppShell,
|
|
364
360
|
})
|
|
365
361
|
touch(key)
|
|
366
|
-
|
|
362
|
+
trimOldest(entries)
|
|
367
363
|
}
|
|
368
364
|
|
|
369
365
|
/**
|
|
@@ -620,11 +616,7 @@ export function recordDocumentLayoutFrame(input: {
|
|
|
620
616
|
time: now(),
|
|
621
617
|
staleTimeMs: input.staleTimeMs,
|
|
622
618
|
})
|
|
623
|
-
|
|
624
|
-
const oldest = documentLayoutFrames.keys().next().value
|
|
625
|
-
if (oldest === undefined) break
|
|
626
|
-
documentLayoutFrames.delete(oldest)
|
|
627
|
-
}
|
|
619
|
+
trimOldest(documentLayoutFrames, DOCUMENT_LAYOUT_LIMIT)
|
|
628
620
|
}
|
|
629
621
|
|
|
630
622
|
/**
|
|
@@ -968,12 +960,9 @@ function touch(key: string): void {
|
|
|
968
960
|
entries.set(key, entry)
|
|
969
961
|
}
|
|
970
962
|
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
if (oldest === undefined) return
|
|
975
|
-
entries.delete(oldest)
|
|
976
|
-
}
|
|
963
|
+
// Drop insertion-order-oldest entries beyond `limit` (keys are never undefined).
|
|
964
|
+
function trimOldest(map: Map<string, unknown>, limit = SEGMENT_ENTRY_LIMIT): void {
|
|
965
|
+
while (map.size > limit) map.delete(map.keys().next().value!)
|
|
977
966
|
}
|
|
978
967
|
|
|
979
968
|
function touchLearnedRoute(segmentPath: string, learnedKey: string): void {
|
|
@@ -984,14 +973,6 @@ function touchLearnedRoute(segmentPath: string, learnedKey: string): void {
|
|
|
984
973
|
byRoute.set(learnedKey, learned)
|
|
985
974
|
}
|
|
986
975
|
|
|
987
|
-
function trimLearnedRoutes(byRoute: Map<string, LearnedRoute>): void {
|
|
988
|
-
while (byRoute.size > SEGMENT_ENTRY_LIMIT) {
|
|
989
|
-
const oldest = byRoute.keys().next().value
|
|
990
|
-
if (oldest === undefined) return
|
|
991
|
-
byRoute.delete(oldest)
|
|
992
|
-
}
|
|
993
|
-
}
|
|
994
|
-
|
|
995
976
|
/**
|
|
996
977
|
* The route pattern the SERVER resolved `pathname` to (learned from a segment
|
|
997
978
|
* or document payload), or null when this exact pathname was never answered.
|
|
@@ -7,6 +7,7 @@ import { resolveImport } from '../../resolve/imports'
|
|
|
7
7
|
import { hoistCssImports } from '../../css/build'
|
|
8
8
|
import { maybeLightningcssTransform } from './lightningcss'
|
|
9
9
|
import { escapeRegex } from '../../utils/code'
|
|
10
|
+
import { cssModuleScopePath } from '../../utils/fs'
|
|
10
11
|
|
|
11
12
|
interface ComposeRef {
|
|
12
13
|
names: string[]
|
|
@@ -345,13 +346,6 @@ function cssModuleClassName(file: string, className: string) {
|
|
|
345
346
|
return `${base}_${className}_${hash}`
|
|
346
347
|
}
|
|
347
348
|
|
|
348
|
-
function cssModuleScopePath(file: string) {
|
|
349
|
-
const normalized = file.split(path.sep).join('/')
|
|
350
|
-
const appIndex = normalized.lastIndexOf('/app/')
|
|
351
|
-
if (appIndex !== -1) return normalized.slice(appIndex + 1)
|
|
352
|
-
return normalized.split('/').slice(-3).join('/')
|
|
353
|
-
}
|
|
354
|
-
|
|
355
349
|
function rewriteClassSelector(css: string, scopedClassName: string, originalClassName: string) {
|
|
356
350
|
const pattern = new RegExp(`\\.${escapeRegex(originalClassName)}(?![-_a-zA-Z0-9])`, 'g')
|
|
357
351
|
return css.replace(pattern, `.${scopedClassName}`)
|
package/src/compat/css/sass.ts
CHANGED
|
@@ -22,6 +22,7 @@ import path from 'node:path'
|
|
|
22
22
|
import { resolveImport } from '../../resolve/imports'
|
|
23
23
|
import { nextCssSource } from './modules'
|
|
24
24
|
import { escapeRegex } from '../../utils/code'
|
|
25
|
+
import { cssModuleScopePath } from '../../utils/fs'
|
|
25
26
|
|
|
26
27
|
// Structural typing of the slice of the Dart Sass JS API we use. Avoids a hard
|
|
27
28
|
// type dependency on the optional `sass` package (mirrors how css.ts/runtime.ts
|
|
@@ -364,13 +365,6 @@ function keyframeNames(css: string): Set<string> {
|
|
|
364
365
|
return names
|
|
365
366
|
}
|
|
366
367
|
|
|
367
|
-
function cssModuleScopePath(file: string): string {
|
|
368
|
-
const normalized = file.split(path.sep).join('/')
|
|
369
|
-
const appIndex = normalized.lastIndexOf('/app/')
|
|
370
|
-
if (appIndex !== -1) return normalized.slice(appIndex + 1)
|
|
371
|
-
return normalized.split('/').slice(-3).join('/')
|
|
372
|
-
}
|
|
373
|
-
|
|
374
368
|
function rewriteClassSelector(
|
|
375
369
|
css: string,
|
|
376
370
|
scopedClassName: string,
|
|
@@ -3,7 +3,7 @@ import { wrapComponentForPrimitiveThrows } from '../client/errors/primitive-thro
|
|
|
3
3
|
|
|
4
4
|
const reactForwardRefSymbol = Symbol.for('react.forward_ref')
|
|
5
5
|
const react19RefCompatInstalled = Symbol.for('pnext.react19-ref-compat-installed')
|
|
6
|
-
const reactTextSeparatorSymbol = Symbol.for('pnext.react-text-separator')
|
|
6
|
+
const reactTextSeparatorSymbol = /* @__PURE__ */ Symbol.for('pnext.react-text-separator')
|
|
7
7
|
|
|
8
8
|
type PNextPreactOptions = typeof options & {
|
|
9
9
|
[react19RefCompatInstalled]?: true
|
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
} from '../../extensions'
|
|
36
36
|
import {
|
|
37
37
|
isEsmModuleFile,
|
|
38
|
+
findWorkspaceRoot,
|
|
38
39
|
isWorkspacePackage,
|
|
39
40
|
packageNameForFile,
|
|
40
41
|
packageNameOfSpecifier,
|
|
@@ -593,5 +594,12 @@ function clientSourceRootPrefixes(config: ResolvedConfig): string[] {
|
|
|
593
594
|
// First-party workspace source is implicitly transpiled. Hoisting puts the
|
|
594
595
|
// package only in the workspace root's node_modules, so both roots are asked.
|
|
595
596
|
function isLinkedWorkspacePackage(config: ResolvedConfig, name: string) {
|
|
596
|
-
|
|
597
|
+
const boundary =
|
|
598
|
+
config.workspaceRoot === (findWorkspaceRoot(config.root) ?? config.root)
|
|
599
|
+
? undefined
|
|
600
|
+
: config.workspaceRoot
|
|
601
|
+
return (
|
|
602
|
+
isWorkspacePackage(config.root, name, boundary) ||
|
|
603
|
+
isWorkspacePackage(config.workspaceRoot, name, boundary)
|
|
604
|
+
)
|
|
597
605
|
}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import { existsSync, readdirSync } from 'node:fs'
|
|
16
16
|
import { readFile } from 'node:fs/promises'
|
|
17
|
-
import { writeFileAtomic } from '../utils/fs'
|
|
17
|
+
import { includeTypes, writeFileAtomic } from '../utils/fs'
|
|
18
18
|
import { createRequire } from 'node:module'
|
|
19
19
|
import path from 'node:path'
|
|
20
20
|
import type { ResolvedConfig } from '../config'
|
|
@@ -236,16 +236,13 @@ export async function writeTsconfigDefaults(config: ResolvedConfig): Promise<voi
|
|
|
236
236
|
hasAppDir && !strictRouteTypes
|
|
237
237
|
? ['next-env.d.ts', ...nextTypes, '**/*.mts', '**/*.ts', '**/*.tsx']
|
|
238
238
|
: ['next-env.d.ts', '**/*.mts', '**/*.ts', '**/*.tsx']
|
|
239
|
-
userTsConfig
|
|
239
|
+
includeTypes(userTsConfig, [], defaultInclude)
|
|
240
240
|
suggestedActions.push(
|
|
241
241
|
`include was set to [${defaultInclude.map(type => `'${type}'`).join(', ')}]`,
|
|
242
242
|
)
|
|
243
243
|
} else if (hasAppDir && !strictRouteTypes && Array.isArray(userTsConfig.include)) {
|
|
244
|
-
for (const type of nextTypes) {
|
|
245
|
-
|
|
246
|
-
userTsConfig.include.push(type)
|
|
247
|
-
suggestedActions.push(`include was updated to add '${type}'`)
|
|
248
|
-
}
|
|
244
|
+
for (const type of includeTypes(userTsConfig, nextTypes, [])) {
|
|
245
|
+
suggestedActions.push(`include was updated to add '${type}'`)
|
|
249
246
|
}
|
|
250
247
|
}
|
|
251
248
|
|
package/src/compat-bootstrap.ts
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
// compat/aliases.ts reactCompatEnabled/nextCompatEnabled.
|
|
20
20
|
|
|
21
21
|
import type { ResolvedConfig } from './config'
|
|
22
|
+
import type { RouteManifestEntry } from './types'
|
|
22
23
|
import { onExtensionHostReset } from './extensions'
|
|
23
24
|
import { installFetchHostNormalization } from './runtime/fetch-host'
|
|
24
25
|
|
|
@@ -69,3 +70,17 @@ export function bootstrapCompat(
|
|
|
69
70
|
.then(module => module.registerCompatExtensions(config, options))
|
|
70
71
|
return registration
|
|
71
72
|
}
|
|
73
|
+
|
|
74
|
+
/** Generate compat types without running build hooks or a typecheck. */
|
|
75
|
+
export async function generateCompatRouteTypes(
|
|
76
|
+
config: ResolvedConfig,
|
|
77
|
+
routes: RouteManifestEntry[],
|
|
78
|
+
): Promise<void> {
|
|
79
|
+
if (!config.compat?.next) return
|
|
80
|
+
const [{ writeTsconfigDefaults }, { generateTypedRoutes }] = await Promise.all([
|
|
81
|
+
import('./compat/tsconfig-defaults'),
|
|
82
|
+
import('./compat/typed-routes/generate'),
|
|
83
|
+
])
|
|
84
|
+
await writeTsconfigDefaults(config)
|
|
85
|
+
await generateTypedRoutes(config, routes)
|
|
86
|
+
}
|