@wular/pnext 0.1.3 → 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 +17 -9
- package/src/client/router/page-slot.ts +58 -0
- package/src/client/router/runtime.ts +387 -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
package/src/css/build.ts
CHANGED
|
@@ -4,7 +4,7 @@ import path from 'node:path'
|
|
|
4
4
|
import type { Plugin } from 'esbuild'
|
|
5
5
|
import { hashedAssetName } from '../utils/asset-hash'
|
|
6
6
|
import { build } from '../utils/esbuild'
|
|
7
|
-
import { ensureDir, readText } from '../utils/fs'
|
|
7
|
+
import { cssModuleScopePath, ensureDir, readText } from '../utils/fs'
|
|
8
8
|
import { postcssConfigFile, runPostcss } from './postcss'
|
|
9
9
|
import { extraPageExtensions, getCssExtensions } from '../extensions'
|
|
10
10
|
import { nextCompatEnabled } from '../render/hooks'
|
|
@@ -132,8 +132,10 @@ export function globalCssSource(config: ResolvedConfig) {
|
|
|
132
132
|
return globalCssSources(config)[0]
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
export function globalCssSources(
|
|
136
|
-
|
|
135
|
+
export function globalCssSources(
|
|
136
|
+
config: Pick<ResolvedConfig, 'root' | 'appPath'> & Partial<Pick<ResolvedConfig, 'workspaceRoot'>>,
|
|
137
|
+
) {
|
|
138
|
+
return globalCssSourcesForPaths(config.root, config.appPath, config.workspaceRoot)
|
|
137
139
|
}
|
|
138
140
|
|
|
139
141
|
// The walk below reads and resolves the ROOT LAYOUT'S WHOLE IMPORT GRAPH, and every render asks it
|
|
@@ -157,8 +159,8 @@ export function setGlobalCssSourceStore(store: GlobalCssSourceStore | undefined)
|
|
|
157
159
|
globalCssStore = store
|
|
158
160
|
}
|
|
159
161
|
|
|
160
|
-
export function globalCssSourcesForPaths(root: string, appPath: string) {
|
|
161
|
-
const key = `${root}\0${appPath}`
|
|
162
|
+
export function globalCssSourcesForPaths(root: string, appPath: string, workspaceRoot?: string) {
|
|
163
|
+
const key = `${root}\0${appPath}\0${workspaceRoot ?? ''}`
|
|
162
164
|
const cached = globalCssSourceCache.get(key)
|
|
163
165
|
if (cached) return cached
|
|
164
166
|
|
|
@@ -171,7 +173,14 @@ export function globalCssSourcesForPaths(root: string, appPath: string) {
|
|
|
171
173
|
const sources = new Set<string>()
|
|
172
174
|
const visited = new Set<string>()
|
|
173
175
|
const missing = new Set<string>()
|
|
174
|
-
collectGlobalCssSources(
|
|
176
|
+
collectGlobalCssSources(
|
|
177
|
+
root,
|
|
178
|
+
rootLayoutFile(appPath, missing),
|
|
179
|
+
sources,
|
|
180
|
+
visited,
|
|
181
|
+
missing,
|
|
182
|
+
workspaceRoot,
|
|
183
|
+
)
|
|
175
184
|
|
|
176
185
|
const resolved = [...sources].map(file => path.resolve(file))
|
|
177
186
|
globalCssSourceCache.set(key, resolved)
|
|
@@ -194,6 +203,7 @@ function collectGlobalCssSources(
|
|
|
194
203
|
sources: Set<string>,
|
|
195
204
|
visited: Set<string>,
|
|
196
205
|
missing?: Set<string>,
|
|
206
|
+
workspaceRoot?: string,
|
|
197
207
|
) {
|
|
198
208
|
const resolvedFile = path.resolve(file)
|
|
199
209
|
if (visited.has(resolvedFile)) return
|
|
@@ -204,12 +214,12 @@ function collectGlobalCssSources(
|
|
|
204
214
|
visited.add(resolvedFile)
|
|
205
215
|
|
|
206
216
|
for (const specifier of moduleSpecifiers(readFileSync(resolvedFile, 'utf8'))) {
|
|
207
|
-
const resolved = resolveImport(root, resolvedFile, specifier)
|
|
217
|
+
const resolved = resolveImport(root, resolvedFile, specifier, workspaceRoot)
|
|
208
218
|
if (!resolved) continue
|
|
209
219
|
if (isCssFile(resolved)) {
|
|
210
220
|
sources.add(resolved)
|
|
211
221
|
} else {
|
|
212
|
-
collectGlobalCssSources(root, resolved, sources, visited, missing)
|
|
222
|
+
collectGlobalCssSources(root, resolved, sources, visited, missing, workspaceRoot)
|
|
213
223
|
}
|
|
214
224
|
}
|
|
215
225
|
}
|
|
@@ -898,13 +908,6 @@ function cssModuleClassName(file: string, className: string) {
|
|
|
898
908
|
return `${base}_${className}_${hash}`
|
|
899
909
|
}
|
|
900
910
|
|
|
901
|
-
function cssModuleScopePath(file: string) {
|
|
902
|
-
const normalized = file.split(path.sep).join('/')
|
|
903
|
-
const appIndex = normalized.lastIndexOf('/app/')
|
|
904
|
-
if (appIndex !== -1) return normalized.slice(appIndex + 1)
|
|
905
|
-
return normalized.split('/').slice(-3).join('/')
|
|
906
|
-
}
|
|
907
|
-
|
|
908
911
|
function rewriteClassSelector(css: string, scopedClassName: string, originalClassName: string) {
|
|
909
912
|
const esbuildNamePattern = new RegExp(
|
|
910
913
|
`\\.${escapeRegex(originalClassName)}(?![-_a-zA-Z0-9])`,
|
package/src/dev/server.ts
CHANGED
|
@@ -285,7 +285,7 @@ export async function startDevServer(options: DevServerOptions) {
|
|
|
285
285
|
// stylesheet, and both the warm tier and the render force it at Ready+0.
|
|
286
286
|
installGlobalCssCache(config)
|
|
287
287
|
markBoot('boot:registry')
|
|
288
|
-
let routes = await scanRoutes(config.appPath)
|
|
288
|
+
let routes = await scanRoutes(config.appPath, config.workspaceRoot)
|
|
289
289
|
setDevClientRoutes(config, routes)
|
|
290
290
|
markBoot('boot:scanRoutes')
|
|
291
291
|
await validateProxyFiles(config)
|
|
@@ -359,7 +359,7 @@ export async function startDevServer(options: DevServerOptions) {
|
|
|
359
359
|
// The route table is built from file paths only, so only an added, removed
|
|
360
360
|
// or renamed file can change it — a plain save never does.
|
|
361
361
|
if (structural) {
|
|
362
|
-
routes = await scanRoutes(config.appPath)
|
|
362
|
+
routes = await scanRoutes(config.appPath, config.workspaceRoot)
|
|
363
363
|
setDevClientRoutes(config, routes)
|
|
364
364
|
}
|
|
365
365
|
await validateProxyFiles(config)
|
package/src/index.ts
CHANGED
package/src/render/renderer.ts
CHANGED
|
@@ -286,6 +286,10 @@ const renderBufferScope = ((globalThis as Record<PropertyKey, unknown>)[RENDER_B
|
|
|
286
286
|
new AsyncLocalStorage<RenderBufferFrame>()) as AsyncLocalStorage<RenderBufferFrame>
|
|
287
287
|
/** True while a render that SUSPENDS in place is running; see installRawTextRendering. */
|
|
288
288
|
const suspendingStreamScope = new AsyncLocalStorage<boolean>()
|
|
289
|
+
/** The shell body's hoisted resource links; see applyHoistable. */
|
|
290
|
+
const hoistScope = new AsyncLocalStorage<{ nodes?: VNode<Record<string, unknown>>[] }>()
|
|
291
|
+
/** The segment that renders the islands resolved inside it; see islandIdSeed. */
|
|
292
|
+
const islandOwnerScope = new AsyncLocalStorage<string>()
|
|
289
293
|
|
|
290
294
|
type PNextPreactOptions = typeof preactOptions & {
|
|
291
295
|
[rawTextRenderingInstalled]?: true
|
|
@@ -315,8 +319,100 @@ function installRawTextRendering() {
|
|
|
315
319
|
applyServerActionFormVNode(vnode)
|
|
316
320
|
applyLayoutSegmentSnapshot(vnode)
|
|
317
321
|
}
|
|
322
|
+
// `__b` is preact's mangled diff hook: render-to-string calls it with parents linked.
|
|
323
|
+
const diffOptions = preactOptions as { __b?: (vnode: VNode) => void }
|
|
324
|
+
const previousDiff = diffOptions.__b
|
|
325
|
+
diffOptions.__b = vnode => {
|
|
326
|
+
applyHoistable(vnode)
|
|
327
|
+
previousDiff?.(vnode)
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// React 19 hoists resource links rendered in the body into `<head>`, once per resource. Hydrating
|
|
332
|
+
// trees keep theirs in place, as do head, svg and noscript content and Suspense fallbacks.
|
|
333
|
+
const HOIST_BARRIERS = new Set([
|
|
334
|
+
'head',
|
|
335
|
+
'svg',
|
|
336
|
+
'noscript',
|
|
337
|
+
'template',
|
|
338
|
+
'pnext-client',
|
|
339
|
+
'pnext-suspense',
|
|
340
|
+
])
|
|
341
|
+
const RESOURCE_LINKS = new Set([
|
|
342
|
+
'stylesheet',
|
|
343
|
+
'preload',
|
|
344
|
+
'modulepreload',
|
|
345
|
+
'preconnect',
|
|
346
|
+
'dns-prefetch',
|
|
347
|
+
])
|
|
348
|
+
|
|
349
|
+
function applyHoistable(vnode: VNode) {
|
|
350
|
+
if (vnode.type !== 'link') return
|
|
351
|
+
const nodes = hoistScope.getStore()?.nodes
|
|
352
|
+
if (!nodes) return
|
|
353
|
+
const { precedence, ...props } = vnode.props as Record<string, unknown>
|
|
354
|
+
if (
|
|
355
|
+
!RESOURCE_LINKS.has(props.rel as string) ||
|
|
356
|
+
typeof props.href !== 'string' ||
|
|
357
|
+
!props.href ||
|
|
358
|
+
props.itemProp != null ||
|
|
359
|
+
props.disabled != null ||
|
|
360
|
+
props.onLoad ||
|
|
361
|
+
props.onError ||
|
|
362
|
+
(props.rel === 'stylesheet' && typeof precedence !== 'string')
|
|
363
|
+
)
|
|
364
|
+
return
|
|
365
|
+
for (let parent = (vnode as HoistVNode).__; parent; parent = parent.__) {
|
|
366
|
+
const host = parent.type
|
|
367
|
+
if (
|
|
368
|
+
typeof host === 'string'
|
|
369
|
+
? HOIST_BARRIERS.has(host)
|
|
370
|
+
: host === SuspenseFallback || isClientPageComponent(host)
|
|
371
|
+
)
|
|
372
|
+
return
|
|
373
|
+
}
|
|
374
|
+
const key = resourceKey(props)
|
|
375
|
+
if (!nodes.some(node => resourceKey(node.props) === key)) {
|
|
376
|
+
if (precedence != null) props['data-precedence'] = precedence
|
|
377
|
+
nodes.push(h('link', props))
|
|
378
|
+
}
|
|
379
|
+
vnode.props = { vnode, type: vnode.type, props: vnode.props } as never
|
|
380
|
+
vnode.type = RestoreHoisted as VNode['type']
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Renders nothing in the hoisted link's place and restores the vnode, which may be module-shared.
|
|
384
|
+
function RestoreHoisted({
|
|
385
|
+
vnode,
|
|
386
|
+
type,
|
|
387
|
+
props,
|
|
388
|
+
}: {
|
|
389
|
+
vnode: VNode
|
|
390
|
+
type: VNode['type']
|
|
391
|
+
props: VNode['props']
|
|
392
|
+
}) {
|
|
393
|
+
vnode.type = type
|
|
394
|
+
vnode.props = props
|
|
395
|
+
return null
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// React keys a stylesheet by href, a hint also by what picks its fetched variant.
|
|
399
|
+
function resourceKey(props: Record<string, unknown>) {
|
|
400
|
+
const key = [props.rel, props.href]
|
|
401
|
+
const cors = props.crossOrigin ?? props.crossorigin
|
|
402
|
+
if (props.rel !== 'stylesheet')
|
|
403
|
+
key.push(
|
|
404
|
+
props.as,
|
|
405
|
+
props.imageSrcSet ?? props.imagesrcset,
|
|
406
|
+
props.imageSizes ?? props.imagesizes,
|
|
407
|
+
props.media,
|
|
408
|
+
// React's CORS buckets: absent, use-credentials, anonymous for any other value.
|
|
409
|
+
cors == null || cors === 'use-credentials' ? cors : 'anonymous',
|
|
410
|
+
)
|
|
411
|
+
return key.join('\n')
|
|
318
412
|
}
|
|
319
413
|
|
|
414
|
+
type HoistVNode = VNode & { __?: HoistVNode }
|
|
415
|
+
|
|
320
416
|
const LAYOUT_SCOPE_PROP = '__pnextLayoutScope'
|
|
321
417
|
// Rides a client template's stub vnode into clientIslandVNode, which strips it
|
|
322
418
|
// and stamps the island host with `data-pnext-template` (see ClientIsland).
|
|
@@ -551,6 +647,7 @@ interface PageRender {
|
|
|
551
647
|
hasClientMounts?: boolean
|
|
552
648
|
devScript: string
|
|
553
649
|
streamChunks: Promise<string>[]
|
|
650
|
+
shellTime?: boolean
|
|
554
651
|
/** Chunks written AFTER `</html>` (late metadata); no document consumer waits on them. */
|
|
555
652
|
lateChunks?: Promise<string>[]
|
|
556
653
|
status?: number
|
|
@@ -802,6 +899,8 @@ interface DocumentPage extends MetadataRenderPage {
|
|
|
802
899
|
documentHeadTags?: string
|
|
803
900
|
deferredScripts?: string
|
|
804
901
|
nonce?: string
|
|
902
|
+
/** A streamed document load: stamp the shell's first frame for PROMO_RUNTIME. */
|
|
903
|
+
shellTime?: boolean
|
|
805
904
|
}
|
|
806
905
|
|
|
807
906
|
interface RenderTreeExtra {
|
|
@@ -894,7 +993,7 @@ function renderDocumentStart(page: DocumentPage) {
|
|
|
894
993
|
${page.routeScript}
|
|
895
994
|
${page.clientScript}
|
|
896
995
|
${page.deferredScripts ?? ''}
|
|
897
|
-
${page.devScript}`
|
|
996
|
+
${page.devScript}${page.shellTime ? `<script>${SHELL_TIME_SCRIPT}</script>` : ''}`
|
|
898
997
|
return dropLoadingDepthMarkers(stampDocumentNonce(html, page.nonce))
|
|
899
998
|
}
|
|
900
999
|
|
|
@@ -2087,6 +2186,11 @@ export function renderPprResponse(options: RenderOptions, prebuilt: PrebuiltShel
|
|
|
2087
2186
|
if (process.env.__NEXT_TEST_MODE) {
|
|
2088
2187
|
controller.enqueue(encoder.encode('<!-- PPR_BOUNDARY_SENTINEL -->'))
|
|
2089
2188
|
}
|
|
2189
|
+
if (!routerPayload(options)) {
|
|
2190
|
+
const nonce = documentNonce(options.request)
|
|
2191
|
+
const stamp = `<script${nonce ? ` nonce="${escapeHtml(nonce)}"` : ''}>${SHELL_TIME_SCRIPT}</script>`
|
|
2192
|
+
controller.enqueue(encoder.encode(stamp))
|
|
2193
|
+
}
|
|
2090
2194
|
schedulePprShellUpgrade(options)
|
|
2091
2195
|
void (async () => {
|
|
2092
2196
|
try {
|
|
@@ -3327,6 +3431,11 @@ function normalizeRenderOptions(options: RenderOptions): RenderOptions {
|
|
|
3327
3431
|
// Streaming isolates each Suspense boundary, dropping client-provider context
|
|
3328
3432
|
// from ancestor layouts. Compat can narrow this through the streamRoute hook;
|
|
3329
3433
|
// core routes stream by default.
|
|
3434
|
+
/** A response the client router fetched, not a document load. */
|
|
3435
|
+
function routerPayload(options: RenderOptions) {
|
|
3436
|
+
return Boolean(options.nav?.soft) || options.request?.headers.get('rsc') === '1'
|
|
3437
|
+
}
|
|
3438
|
+
|
|
3330
3439
|
function canStreamRoute(options: RenderOptions) {
|
|
3331
3440
|
return getStreamRouteExtensions().canStreamRoute({
|
|
3332
3441
|
config: options.config,
|
|
@@ -3551,10 +3660,13 @@ async function renderTreeInFrame(
|
|
|
3551
3660
|
: await resolveServerTree(layoutRender.tree, resolveState)
|
|
3552
3661
|
: layoutRender.tree,
|
|
3553
3662
|
)
|
|
3554
|
-
const
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3663
|
+
const hoist: { nodes?: VNode<Record<string, unknown>>[] } = { nodes: [] }
|
|
3664
|
+
const renderBody = (vnode: VNode, state?: ActionSerializeState) =>
|
|
3665
|
+
hoistScope.run(hoist, () =>
|
|
3666
|
+
stream && (options.route.client || streamState.inlineSuspense)
|
|
3667
|
+
? renderClientPageStreamShell(vnode, streamState)
|
|
3668
|
+
: renderVNodeToString(vnode, state),
|
|
3669
|
+
)
|
|
3558
3670
|
const document = await profileRenderStep(profile, 'render document body', async () =>
|
|
3559
3671
|
layoutRender.documentLayoutFile
|
|
3560
3672
|
? await readDocumentLayout(
|
|
@@ -3571,6 +3683,10 @@ async function renderTreeInFrame(
|
|
|
3571
3683
|
body: await renderBody(h(Fragment, null, resolved), resolveState),
|
|
3572
3684
|
},
|
|
3573
3685
|
)
|
|
3686
|
+
// Later streamed renders keep theirs in place: the head has gone out by then.
|
|
3687
|
+
const hoisted = hoist.nodes!
|
|
3688
|
+
hoist.nodes = undefined
|
|
3689
|
+
if (hoisted.length) document.head += renderToString(h(Fragment, null, hoisted))
|
|
3574
3690
|
// Stable per-root-layout id so the client router can detect root-layout
|
|
3575
3691
|
// switches and fall back to an MPA navigation (rootLayoutChanged).
|
|
3576
3692
|
if (layoutRender.documentLayoutFile) {
|
|
@@ -3613,8 +3729,7 @@ async function renderTreeInFrame(
|
|
|
3613
3729
|
// render and only metadata that misses the flush stays below it (conformance "metadata body
|
|
3614
3730
|
// placement matches Next" vs metadata-streaming "should delay the metadata render to body").
|
|
3615
3731
|
// A router payload has no head to hoist into, so it always rides the body.
|
|
3616
|
-
const navMetadataRender =
|
|
3617
|
-
Boolean(options.nav?.soft) || options.request?.headers.get('rsc') === '1'
|
|
3732
|
+
const navMetadataRender = routerPayload(options)
|
|
3618
3733
|
const startedPageMetadata =
|
|
3619
3734
|
metadataMayStream && !navMetadataRender && typeof pageMetadata === 'function'
|
|
3620
3735
|
? runInCacheScope(() => pageMetadata(mergeMetadataEntries(layoutRender.metadata)))
|
|
@@ -3978,6 +4093,7 @@ async function renderTreeInFrame(
|
|
|
3978
4093
|
hasClientMounts: pageSlotIsMountTarget(options),
|
|
3979
4094
|
devScript,
|
|
3980
4095
|
streamChunks: streamState.deferred,
|
|
4096
|
+
shellTime: streamState.deferred.length > 0 && !routerPayload(options),
|
|
3981
4097
|
lateChunks,
|
|
3982
4098
|
status: extra.status,
|
|
3983
4099
|
viewport,
|
|
@@ -4867,6 +4983,7 @@ function ClientIsland(
|
|
|
4867
4983
|
__pnextComponent?: ComponentType<Record<string, unknown>>
|
|
4868
4984
|
__pnextSerializedProps?: string
|
|
4869
4985
|
__pnextLayoutScope?: LayoutSegmentSnapshot
|
|
4986
|
+
__pnextOwner?: string
|
|
4870
4987
|
__pnextParamsScope?: RouteParamsSnapshot
|
|
4871
4988
|
__pnextTemplate?: boolean
|
|
4872
4989
|
__pnextSkipSsr?: boolean
|
|
@@ -4878,6 +4995,7 @@ function ClientIsland(
|
|
|
4878
4995
|
__pnextComponent,
|
|
4879
4996
|
__pnextSerializedProps,
|
|
4880
4997
|
__pnextLayoutScope,
|
|
4998
|
+
__pnextOwner,
|
|
4881
4999
|
__pnextParamsScope,
|
|
4882
5000
|
__pnextTemplate,
|
|
4883
5001
|
__pnextSkipSsr,
|
|
@@ -4928,6 +5046,12 @@ function ClientIsland(
|
|
|
4928
5046
|
islandProps['data-pnext-params'] = serializeProps(__pnextParamsScope)
|
|
4929
5047
|
}
|
|
4930
5048
|
const staticChildren = islandStaticChildren(__pnextClient.id, islandChildren)
|
|
5049
|
+
const seed = islandIdSeed(
|
|
5050
|
+
__pnextOwner ?? '',
|
|
5051
|
+
__pnextClient.id,
|
|
5052
|
+
islandProps['data-pnext-props'] as string,
|
|
5053
|
+
)
|
|
5054
|
+
islandProps['data-pnext-id'] = seed
|
|
4931
5055
|
|
|
4932
5056
|
if (__pnextSkipSsr) {
|
|
4933
5057
|
return staticChildren
|
|
@@ -4964,6 +5088,8 @@ function ClientIsland(
|
|
|
4964
5088
|
revivePromiseProps(ssrProps),
|
|
4965
5089
|
plainChildren !== undefined ? islandChildren : probedChildren,
|
|
4966
5090
|
)
|
|
5091
|
+
// useId's mask (preact's mangled `_mask`): the client seeds the island root from data-pnext-id.
|
|
5092
|
+
;(island as { __m?: unknown }).__m = [seed, 0]
|
|
4967
5093
|
if (__pnextLayoutScope) {
|
|
4968
5094
|
island = h(
|
|
4969
5095
|
LayoutSegmentContext.Provider,
|
|
@@ -5007,6 +5133,36 @@ function ClientIsland(
|
|
|
5007
5133
|
})
|
|
5008
5134
|
}
|
|
5009
5135
|
|
|
5136
|
+
// React numbers useId across its one root; each island root here seeds its own id space from its
|
|
5137
|
+
// owning segment and identity, so ids stay unique across islands and across documents that keep a
|
|
5138
|
+
// layout's islands. Identical islands of one owner in one render take the next copy suffix.
|
|
5139
|
+
function islandIdSeed(owner: string, id: string, props: string) {
|
|
5140
|
+
let hash = 0
|
|
5141
|
+
for (const text of [owner, id, props])
|
|
5142
|
+
for (let index = 0; index < text.length; index++)
|
|
5143
|
+
hash = (Math.imul(31, hash) + text.charCodeAt(index)) | 0
|
|
5144
|
+
const seed = (hash >>> 0).toString(36)
|
|
5145
|
+
const frame = renderBufferScope.getStore()
|
|
5146
|
+
const seeds = frame
|
|
5147
|
+
? (frame.islandSeeds ??= new Map<string, number>())
|
|
5148
|
+
: new Map<string, number>()
|
|
5149
|
+
const copies = seeds.get(seed) ?? 0
|
|
5150
|
+
seeds.set(seed, copies + 1)
|
|
5151
|
+
return copies ? `${seed}_${copies}` : seed
|
|
5152
|
+
}
|
|
5153
|
+
|
|
5154
|
+
// Layout and template markers own the islands they render, the page slot owns the route's.
|
|
5155
|
+
function inIslandOwner<T>(node: VNode, options: RenderOptions, resolve: () => T): T {
|
|
5156
|
+
const props = node.props as Record<string, unknown>
|
|
5157
|
+
const owner =
|
|
5158
|
+
node.type === 'pnext-layout'
|
|
5159
|
+
? (props['data-pnext-segment'] as string)
|
|
5160
|
+
: props.id === 'pnext-page'
|
|
5161
|
+
? `page${options.route.route}`
|
|
5162
|
+
: undefined
|
|
5163
|
+
return owner === undefined ? resolve() : islandOwnerScope.run(owner, resolve)
|
|
5164
|
+
}
|
|
5165
|
+
|
|
5010
5166
|
// Marks the island's static-children wrapper as rendered when the component
|
|
5011
5167
|
// actually renders its children (depth-first stringification reaches this
|
|
5012
5168
|
// probe before the fallback sibling below).
|
|
@@ -5257,7 +5413,10 @@ async function resolveServerTree(node: unknown, state: ResolveState): Promise<Co
|
|
|
5257
5413
|
if (state.partial?.shellPending && node.type === 'html') {
|
|
5258
5414
|
state.partial.shellPending = false
|
|
5259
5415
|
}
|
|
5260
|
-
|
|
5416
|
+
const children = await inIslandOwner(node, state.options, () =>
|
|
5417
|
+
resolveServerTree(node.props.children, state),
|
|
5418
|
+
)
|
|
5419
|
+
return elementVNode(node, children, state)
|
|
5261
5420
|
}
|
|
5262
5421
|
|
|
5263
5422
|
const component = node.type
|
|
@@ -5708,14 +5867,18 @@ async function inlineSuspenseBoundary(
|
|
|
5708
5867
|
const fallback = await resolveServerTreeForStream(node.props.fallback, state)
|
|
5709
5868
|
// The fallback must be a single vnode: the stream renderer renders it with no
|
|
5710
5869
|
// parent vnode, and its array branch would crash on one (swallowing the
|
|
5711
|
-
// fallback entirely).
|
|
5870
|
+
// fallback entirely). The wrapper is byte-neutral and marks it for applyHoistable.
|
|
5712
5871
|
return h(
|
|
5713
5872
|
boundaryType,
|
|
5714
|
-
{ ...node.props, fallback: h(
|
|
5873
|
+
{ ...node.props, fallback: h(SuspenseFallback, null, fallback) },
|
|
5715
5874
|
h(Suspended, null),
|
|
5716
5875
|
)
|
|
5717
5876
|
}
|
|
5718
5877
|
|
|
5878
|
+
function SuspenseFallback(props: { children?: ComponentChildren }) {
|
|
5879
|
+
return props.children
|
|
5880
|
+
}
|
|
5881
|
+
|
|
5719
5882
|
// Wraps a pending subtree as a suspending component. A throw AFTER the boundary suspended can only
|
|
5720
5883
|
// ride the boundary's own replacement (the shell and its status already flushed), so it maps to the
|
|
5721
5884
|
// same markup deferSuspenseBoundary emits. A throw that lands BEFORE the boundary ever suspended -
|
|
@@ -5934,7 +6097,9 @@ function resolveServerTreeForStream(
|
|
|
5934
6097
|
// component) wrongly takes the doc-shell blocking branch and awaits its slow
|
|
5935
6098
|
// content before the shell can flush. Mirrors resolveServerTree's html flip.
|
|
5936
6099
|
if (isDocument) state.shellPending = false
|
|
5937
|
-
const children =
|
|
6100
|
+
const children = inIslandOwner(node, state.options, () =>
|
|
6101
|
+
resolveServerTreeForStream(node.props.children, state),
|
|
6102
|
+
)
|
|
5938
6103
|
if (isPromise(children)) {
|
|
5939
6104
|
return children.then(resolved => elementVNode(node, resolved, state))
|
|
5940
6105
|
}
|
|
@@ -6375,7 +6540,15 @@ function dynamicTargetReference(
|
|
|
6375
6540
|
) {
|
|
6376
6541
|
const target = dynamicReference.target
|
|
6377
6542
|
if (!target) return undefined
|
|
6378
|
-
|
|
6543
|
+
const stored = state.clientReferences.get(
|
|
6544
|
+
`c-${clientReferenceId(target.file, target.exportName)}`,
|
|
6545
|
+
)
|
|
6546
|
+
if (stored) return stored
|
|
6547
|
+
for (const reference of state.clientReferences.values()) {
|
|
6548
|
+
if (reference.file === target.file && reference.exportName === target.exportName)
|
|
6549
|
+
return reference
|
|
6550
|
+
}
|
|
6551
|
+
return undefined
|
|
6379
6552
|
}
|
|
6380
6553
|
|
|
6381
6554
|
function clientIslandVNode(
|
|
@@ -6424,6 +6597,7 @@ function clientIslandVNode(
|
|
|
6424
6597
|
__pnextComponent: component,
|
|
6425
6598
|
__pnextTemplate: templateMarker === true,
|
|
6426
6599
|
__pnextLayoutScope: layoutScope as LayoutSegmentSnapshot | undefined,
|
|
6600
|
+
__pnextOwner: islandOwnerScope.getStore(),
|
|
6427
6601
|
// Params visible at this island's position in the route (route params plus
|
|
6428
6602
|
// any enclosing parallel-route slot's own dynamic/catch-all captures). Read
|
|
6429
6603
|
// from the active params scope (render/slots.ts) so a slot island's
|
|
@@ -7279,10 +7453,11 @@ const PROMO_RUNTIME =
|
|
|
7279
7453
|
'if(d.nodeType===8){var mm=/^\\$ps:(\\d+)$/.exec(d.data);if(mm){cur=m[mm[1]]=[];continue}' +
|
|
7280
7454
|
'if(/^\\/\\$ps:\\d+$/.test(d.data)){cur=null;continue}}' +
|
|
7281
7455
|
'if(cur)cur.push(d);}return m}' +
|
|
7456
|
+
"function B(i){return document.querySelector('pnext-suspense[data-pnext-suspense=\"'+i+'\"]')}" +
|
|
7282
7457
|
'function tryPromote(i){' +
|
|
7283
7458
|
"var t=document.querySelector('[data-pnext-stream=\"'+i+'\"]');" +
|
|
7284
7459
|
'if(!t){delete P[i];return true;}' +
|
|
7285
|
-
|
|
7460
|
+
'var e=B(i);' +
|
|
7286
7461
|
'if(!e){P[i]=1;return false;}' +
|
|
7287
7462
|
// A boundary inside a client island: this promotion rewrites DOM the island's mounted
|
|
7288
7463
|
// preact tree adopted at hydration, which still names the fallback. Mark the root so the
|
|
@@ -7304,17 +7479,23 @@ const PROMO_RUNTIME =
|
|
|
7304
7479
|
'}else{while(t.firstChild)f.appendChild(t.firstChild);}' +
|
|
7305
7480
|
'e.replaceWith(f);t.remove();delete P[i];return true;' +
|
|
7306
7481
|
'}' +
|
|
7307
|
-
'function drain(){var changed=true;while(changed){changed=false;var k=Object.keys(P);for(var j=0;j<k.length;j++){if(P[k[j]]&&tryPromote(k[j]))changed=true;}}}' +
|
|
7308
|
-
'
|
|
7482
|
+
'function drain(){var any,changed=true;while(changed){changed=false;var k=Object.keys(P);for(var j=0;j<k.length;j++){if(P[k[j]]&&tryPromote(k[j]))changed=any=true;}}return any}' +
|
|
7483
|
+
// Islands in a promoted chunk hydrate only now: the entry's scan skips unrevealed chunks.
|
|
7484
|
+
'function M(){window.__PNEXT_MATERIALIZE_CLIENT_ISLANDS__&&window.__PNEXT_MATERIALIZE_CLIENT_ISLANDS__(document);' +
|
|
7485
|
+
'window.__PNEXT_MOUNT_ISLANDS__&&window.__PNEXT_MOUNT_ISLANDS__();}' +
|
|
7486
|
+
'if(document.addEventListener)document.addEventListener("DOMContentLoaded",function(){drain()&&M()});' +
|
|
7309
7487
|
'return function(i){' +
|
|
7310
|
-
'
|
|
7311
|
-
'
|
|
7312
|
-
'
|
|
7313
|
-
'if(
|
|
7314
|
-
'};' +
|
|
7488
|
+
// React's $RC: reveal in the shell's first frame, else no sooner than 300 ms after the last reveal.
|
|
7489
|
+
// Inside an island at once: a held reveal lands after hydration, under the island's adopted tree.
|
|
7490
|
+
'(B(i)?.closest("pnext-client")?f=>f():self.$RT?setTimeout:requestAnimationFrame)(function(){$RT=performance.now();' +
|
|
7491
|
+
'if(tryPromote(i))drain();M();' +
|
|
7492
|
+
'},self.$RT+300-performance.now())};' +
|
|
7315
7493
|
'}'
|
|
7316
7494
|
|
|
7317
|
-
|
|
7495
|
+
// React Fizz's shell-time stamp: PROMO_RUNTIME reveals no sooner than 300 ms after this frame.
|
|
7496
|
+
const SHELL_TIME_SCRIPT = 'requestAnimationFrame(()=>$RT=performance.now())'
|
|
7497
|
+
|
|
7498
|
+
export function suspenseReplaceScript(id: string) {
|
|
7318
7499
|
return `(function(){(window.__PNEXT_PROMO__=window.__PNEXT_PROMO__||(${PROMO_RUNTIME})())(${JSON.stringify(id)});var s=document.currentScript;if(s)s.remove();}())`
|
|
7319
7500
|
}
|
|
7320
7501
|
|
|
@@ -7595,7 +7776,7 @@ async function applyLayouts(
|
|
|
7595
7776
|
tree = h(
|
|
7596
7777
|
'pnext-layout',
|
|
7597
7778
|
{
|
|
7598
|
-
'data-pnext-segment': segmentIdentityToken(file),
|
|
7779
|
+
'data-pnext-segment': segmentIdentityToken(relative(options.config.root, file)),
|
|
7599
7780
|
'data-pnext-skip': '',
|
|
7600
7781
|
...(scope !== undefined ? { 'data-pnext-scope': scope } : {}),
|
|
7601
7782
|
style: { display: 'contents' },
|
|
@@ -7728,7 +7909,11 @@ async function wrapSegment(
|
|
|
7728
7909
|
})
|
|
7729
7910
|
return (component as ClientComponent)[clientReferenceSymbol]
|
|
7730
7911
|
? segment
|
|
7731
|
-
: serverSegment(
|
|
7912
|
+
: serverSegment(
|
|
7913
|
+
relative(options.config.root, layoutFile),
|
|
7914
|
+
segment,
|
|
7915
|
+
segmentScopeFor(options, dir),
|
|
7916
|
+
)
|
|
7732
7917
|
}
|
|
7733
7918
|
|
|
7734
7919
|
// An explicit `@children` parallel-route slot directory is Next's way to supply
|
|
@@ -7769,7 +7954,7 @@ async function wrapInTemplate(
|
|
|
7769
7954
|
const Template = markServerReference(module.default)
|
|
7770
7955
|
if (!Template) return tree
|
|
7771
7956
|
return serverSegment(
|
|
7772
|
-
templateFile,
|
|
7957
|
+
relative(options.config.root, templateFile),
|
|
7773
7958
|
h(Template as ComponentType<ServerVNodeProps>, { children: tree }),
|
|
7774
7959
|
segmentScopeFor(options, dirname(templateFile)),
|
|
7775
7960
|
)
|
|
@@ -21,7 +21,10 @@ export const RENDER_BUFFER_SCOPE = Symbol.for('pnext.renderBufferScope')
|
|
|
21
21
|
const RENDER_BUFFER_STRAYS = Symbol.for('pnext.renderBufferStrays')
|
|
22
22
|
|
|
23
23
|
export type RenderBufferKind = 'hints' | 'head' | 'deferred'
|
|
24
|
-
export type RenderBufferFrame = Record<RenderBufferKind, unknown[]>
|
|
24
|
+
export type RenderBufferFrame = Record<RenderBufferKind, unknown[]> & {
|
|
25
|
+
/** Islands rendered per useId seed, so identical islands get distinct seeds. */
|
|
26
|
+
islandSeeds?: Map<string, number>
|
|
27
|
+
}
|
|
25
28
|
|
|
26
29
|
export const newRenderBufferFrame = (): RenderBufferFrame => ({ hints: [], head: [], deferred: [] })
|
|
27
30
|
|
package/src/resolve/imports.ts
CHANGED
|
@@ -189,7 +189,12 @@ export function resolveModuleAlias(specifier: string): string | undefined {
|
|
|
189
189
|
* Resolve a specifier to a LOCAL source file, or undefined when it belongs to
|
|
190
190
|
* an external package. Callers read undefined as "leave this to node/esbuild".
|
|
191
191
|
*/
|
|
192
|
-
export function resolveImport(
|
|
192
|
+
export function resolveImport(
|
|
193
|
+
root: string,
|
|
194
|
+
fromFile: string,
|
|
195
|
+
specifier: string,
|
|
196
|
+
workspaceRoot?: string,
|
|
197
|
+
) {
|
|
193
198
|
// A `?query`/`#fragment` resource belongs to its loader chain (turbopack
|
|
194
199
|
// rules, `.wasm?module`): those plugins strip the suffix and resolve the base
|
|
195
200
|
// themselves, and the query is part of the module's identity — so it is never
|
|
@@ -215,7 +220,7 @@ export function resolveImport(root: string, fromFile: string, specifier: string)
|
|
|
215
220
|
return resolved ? realImportPath(resolved) : undefined
|
|
216
221
|
}
|
|
217
222
|
|
|
218
|
-
const workspaceResolved = resolveWorkspacePackageImport(root, specifier)
|
|
223
|
+
const workspaceResolved = resolveWorkspacePackageImport(root, specifier, workspaceRoot)
|
|
219
224
|
if (workspaceResolved) return workspaceResolved
|
|
220
225
|
|
|
221
226
|
return resolveAppTreePackage(resolutionRoot, root, fromFile, specifier)
|
|
@@ -344,8 +349,8 @@ type PackageExport = unknown
|
|
|
344
349
|
* on-disk subpath fallback no standard resolver takes, because the published `exports` point at build output
|
|
345
350
|
* that does not exist in a checkout.
|
|
346
351
|
*/
|
|
347
|
-
function resolveWorkspacePackageImport(root: string, specifier: string) {
|
|
348
|
-
const workspaceRoot = findWorkspaceRoot(root)
|
|
352
|
+
function resolveWorkspacePackageImport(root: string, specifier: string, boundary?: string) {
|
|
353
|
+
const workspaceRoot = boundary ? path.resolve(boundary) : findWorkspaceRoot(root)
|
|
349
354
|
if (!workspaceRoot) return undefined
|
|
350
355
|
|
|
351
356
|
const packageName = packageNameFromSpecifier(specifier)
|
|
@@ -999,14 +1004,14 @@ const workspaceMembershipCache = new Map<string, boolean>()
|
|
|
999
1004
|
* dependency. An app outside the workspace (a checkout linked into it) sees the package only through
|
|
1000
1005
|
* node_modules, so the package's own real dir is asked about its manifest too.
|
|
1001
1006
|
*/
|
|
1002
|
-
export function isWorkspacePackage(root: string, name: string): boolean {
|
|
1003
|
-
const key = `${path.resolve(root)}\0${name}`
|
|
1007
|
+
export function isWorkspacePackage(root: string, name: string, boundary?: string): boolean {
|
|
1008
|
+
const key = `${path.resolve(root)}\0${name}\0${boundary ?? ''}`
|
|
1004
1009
|
const cached = workspaceMembershipCache.get(key)
|
|
1005
1010
|
if (cached !== undefined) return cached
|
|
1006
1011
|
|
|
1007
|
-
const workspaceRoot = findWorkspaceRoot(root)
|
|
1012
|
+
const workspaceRoot = boundary ? path.resolve(boundary) : findWorkspaceRoot(root)
|
|
1008
1013
|
let member = Boolean(workspaceRoot && workspacePackages(workspaceRoot).has(name))
|
|
1009
|
-
if (!member) {
|
|
1014
|
+
if (!member && !boundary) {
|
|
1010
1015
|
const packageRoot = findNodePackageRoot(path.resolve(root), name)
|
|
1011
1016
|
const real = packageRoot ? realImportPath(packageRoot) : undefined
|
|
1012
1017
|
const linkedRoot = real ? findWorkspaceRoot(real) : undefined
|