@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.
Files changed (44) hide show
  1. package/config/ts/react.json +2 -1
  2. package/package.json +1 -1
  3. package/reference/navigation.md +3 -1
  4. package/src/api/client-navigation.ts +53 -4
  5. package/src/api/link.tsx +6 -7
  6. package/src/cli/adapters/vercel-warm.ts +6 -1
  7. package/src/cli/adapters/vercel.ts +1 -1
  8. package/src/cli/build/cache.ts +8 -3
  9. package/src/cli/build.ts +3 -2
  10. package/src/cli/serve/pipeline.ts +11 -3
  11. package/src/cli/typegen.ts +65 -3
  12. package/src/client/build.ts +10 -7
  13. package/src/client/entry.ts +50 -29
  14. package/src/client/reference-stub.ts +1 -1
  15. package/src/client/router/hub.ts +13 -11
  16. package/src/client/router/page-slot.ts +58 -0
  17. package/src/client/router/runtime.ts +377 -388
  18. package/src/compat/bundler/symlink-imports.ts +1 -1
  19. package/src/compat/client/errors/bare-boundary.ts +1 -1
  20. package/src/compat/client/errors/error-boundary.ts +1 -1
  21. package/src/compat/client/errors/global-error.ts +40 -79
  22. package/src/compat/client/errors/install.ts +1 -1
  23. package/src/compat/client/errors/lazy.ts +7 -0
  24. package/src/compat/client/errors/primitive-throw.ts +36 -53
  25. package/src/compat/client/segment-cache-policy.ts +5 -22
  26. package/src/compat/client/segment-cache.ts +7 -26
  27. package/src/compat/css/modules.ts +1 -7
  28. package/src/compat/css/sass.ts +1 -7
  29. package/src/compat/react/parity.ts +1 -1
  30. package/src/compat/register/bundler.ts +9 -1
  31. package/src/compat/tsconfig-defaults.ts +4 -7
  32. package/src/compat-bootstrap.ts +15 -0
  33. package/src/css/build.ts +18 -15
  34. package/src/dev/server.ts +2 -2
  35. package/src/index.ts +2 -0
  36. package/src/render/renderer.ts +209 -24
  37. package/src/render/resource-hints.ts +4 -1
  38. package/src/resolve/imports.ts +13 -8
  39. package/src/routing/routes.ts +32 -18
  40. package/src/runtime/loader.ts +29 -10
  41. package/src/runtime/modules.ts +28 -7
  42. package/src/runtime/vendor-build.ts +2 -2
  43. package/src/types.ts +2 -0
  44. 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('./global-error').then(module => module.escalateToGlobalError(error))
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('./global-error').then(m => m.escalateToGlobalError(error))
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
- Object.assign(root.style, {
125
- fontFamily:
126
- 'system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"',
127
- height: '100vh',
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 = document.createElement('h1')
156
- title.textContent = 'This page couldn’t load'
157
- Object.assign(title.style, {
158
- fontSize: '24px',
159
- fontWeight: '500',
160
- letterSpacing: '-0.02em',
161
- lineHeight: '32px',
162
- margin: '0 0 12px 0',
163
- color: 'var(--next-error-title)',
164
- } as Partial<CSSStyleDeclaration>)
165
-
166
- const body = document.createElement('p')
167
- body.textContent = message
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 = document.createElement('button')
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 = document.createElement('button')
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
- const footer = document.createElement('p')
207
- footer.textContent = `ERROR ${digest}`
208
- Object.assign(footer.style, {
209
- position: 'fixed',
210
- bottom: '32px',
211
- left: '0',
212
- right: '0',
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
- function buttonStyle(secondary: boolean): Partial<CSSStyleDeclaration> {
227
- return {
228
- display: 'inline-flex',
229
- alignItems: 'center',
230
- justifyContent: 'center',
231
- height: '32px',
232
- padding: '0 12px',
233
- fontSize: '14px',
234
- fontWeight: '500',
235
- lineHeight: '20px',
236
- borderRadius: '6px',
237
- cursor: 'pointer',
238
- color: secondary ? 'var(--next-error-btn-secondary-text)' : 'var(--next-error-btn-text)',
239
- background: secondary ? 'var(--next-error-btn-secondary-bg)' : 'var(--next-error-btn-bg)',
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 message = typeof value === 'string' ? value : String(value)
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 isClassComponent = Boolean((type as { prototype?: { render?: unknown } }).prototype?.render)
98
- let result: unknown
99
-
100
- if (isClassComponent) {
101
- // Class components: preact does `new Type(props, context)` then calls
102
- // `.render()`. Subclass so `render()` is wrapped; static lifecycle methods
103
- // (getDerivedStateFromError, etc.) and everything else pass through
104
- // untouched via normal prototype-chain inheritance.
105
- const Base = type as unknown as new (...args: unknown[]) => {
106
- render(...args: unknown[]): unknown
107
- }
108
- class PrimitiveThrowSafe extends Base {
109
- render(...args: unknown[]) {
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 super.render(...args)
107
+ return (type as unknown as (...args: unknown[]) => unknown).apply(this, args)
112
108
  } catch (e) {
113
- if (crashesPreactThenCheck(e)) throw tagPrimitiveThrow(e)
114
- throw e
109
+ throw retagThrow(e)
115
110
  }
116
111
  }
117
- }
118
- Object.defineProperty(PrimitiveThrowSafe, 'name', { value: Base.name })
119
- // Copy static properties AND symbol-keyed metadata (displayName, getDerivedStateFromError,
120
- // contextType, defaultProps, plus the framework's own client-reference/dynamic-reference/params-
121
- // scope symbol markers stashed directly on component function objects). resolveServerTree branches
122
- // on those, so losing them mis-routes a client-reference class through the server-component call
123
- // path instead of the island path.
124
- copyMetadata(Base, PrimitiveThrowSafe)
125
- result = PrimitiveThrowSafe
126
- } else {
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 as object, ALREADY_WRAPPED, { value: true })
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
- if (hit) {
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: composed.html,
140
- networkFree: composed.networkFree,
141
- prefetchSatisfied: composed.prefetchSatisfied,
142
- postponedShell: composed.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
- while (resolvedRoutes.size > SEGMENT_ENTRY_LIMIT) {
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
- trimLearnedRoutes(byRoute)
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
- trim()
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
- while (documentLayoutFrames.size > DOCUMENT_LAYOUT_LIMIT) {
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
- function trim(): void {
972
- while (entries.size > SEGMENT_ENTRY_LIMIT) {
973
- const oldest = entries.keys().next().value
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}`)
@@ -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
- return isWorkspacePackage(config.root, name) || isWorkspacePackage(config.workspaceRoot, name)
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.include = defaultInclude
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
- if (!userTsConfig.include.includes(type)) {
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
 
@@ -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
+ }