@wular/pnext 0.0.19 → 0.0.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/package.json +1 -1
- package/reference/compat.md +2 -2
- package/reference/getting-started.md +2 -2
- package/src/cli/adapters/vercel.ts +27 -0
- package/src/cli/analyze.ts +8 -7
- package/src/cli/boot/trace.ts +9 -3
- package/src/cli/build.ts +23 -15
- package/src/cli/dev.ts +23 -0
- package/src/cli/migrate/report.ts +1 -1
- package/src/cli/migrate/run.ts +2 -2
- package/src/cli/migrate/scan.ts +128 -1
- package/src/cli/serve/pipeline.ts +42 -42
- package/src/cli/start.ts +4 -2
- package/src/client/build.ts +51 -25
- package/src/client/entry.ts +27 -4
- package/src/client/router/page-slot.ts +81 -0
- package/src/client/router/runtime.ts +107 -112
- package/src/compat/bundler/cjs-exports.ts +26 -20
- package/src/compat/client/css-order.ts +1 -1
- package/src/compat/client/navigation-scroll.ts +36 -40
- package/src/compat/css/modules.ts +6 -2
- package/src/compat/next/font/cache.ts +10 -22
- package/src/compat/next/font/runtime.ts +20 -0
- package/src/compat/next/legacy-request-apis.ts +41 -5
- package/src/compat/next/script.tsx +16 -24
- package/src/compat/react/client-lite.ts +1 -0
- package/src/compat/react/client.ts +11 -0
- package/src/compat/react/dom-client.ts +11 -2
- package/src/compat/react/dom-react-server.ts +4 -0
- package/src/compat/react/dom.ts +11 -2
- package/src/compat/react/internals.ts +65 -0
- package/src/compat/react/parity.ts +47 -0
- package/src/compat/react/preact.ts +31 -1
- package/src/compat/react/react-server.ts +7 -0
- package/src/compat/react/server.ts +16 -2
- package/src/compat/react/style-values.ts +68 -0
- package/src/compat/register/render.ts +14 -3
- package/src/compat/register/routing.ts +57 -7
- package/src/compat/tsconfig-defaults.ts +4 -0
- package/src/compat/typecheck/check.ts +26 -71
- package/src/compat/typecheck/worker.ts +3 -2
- package/src/compat-bootstrap.ts +4 -0
- package/src/css/build.ts +153 -14
- package/src/css/postcss.ts +32 -3
- package/src/dev/restart/client-key.ts +1 -1
- package/src/dev/restart/global-css.ts +12 -94
- package/src/dev/restart/record.ts +106 -0
- package/src/dev/restart/route-facts.ts +11 -90
- package/src/dev/server.ts +208 -41
- package/src/extensions.ts +69 -3
- package/src/render/hooks.ts +1 -1
- package/src/render/metadata.ts +89 -17
- package/src/render/renderer.ts +322 -136
- package/src/render/resource-hints.ts +62 -9
- package/src/render/slots.tsx +2 -1
- package/src/resolve/scan-facts.ts +13 -5
- package/src/routing/forwarded.ts +20 -7
- package/src/routing/handler.ts +6 -2
- package/src/routing/href.ts +11 -1
- package/src/routing/metadata-files.ts +16 -3
- package/src/routing/proxy.ts +6 -3
- package/src/routing/routes.ts +253 -34
- package/src/runtime/fetch-host.ts +42 -0
- package/src/runtime/fingerprint.ts +132 -0
- package/src/runtime/loader.ts +7 -5
- package/src/runtime/module-cache.ts +3 -36
- package/src/runtime/modules.ts +333 -77
- package/src/runtime/vendor-build.ts +34 -5
- package/src/runtime/vendor.ts +1 -18
- package/src/types.ts +6 -0
- package/src/utils/asset-hash.ts +22 -0
- package/src/utils/code.ts +41 -0
- package/src/utils/html.ts +4 -1
- package/src/utils/serialize.ts +66 -7
|
@@ -45,6 +45,7 @@ import type {
|
|
|
45
45
|
SoftNavigateOptions,
|
|
46
46
|
} from './types'
|
|
47
47
|
import type { LinkClickTarget } from './hub'
|
|
48
|
+
import { elementInPageSlot, graftPageSlot, loadingShellTarget } from './page-slot'
|
|
48
49
|
// ---------------------------------------------------------------------------
|
|
49
50
|
// DOCUMENTS
|
|
50
51
|
// ---------------------------------------------------------------------------
|
|
@@ -507,10 +508,13 @@ function pruneStylesheets(doc: Document) {
|
|
|
507
508
|
}
|
|
508
509
|
}
|
|
509
510
|
|
|
511
|
+
// Built stylesheets answer to `/assets/` (core) and `/_next/static/` (next-compat, Next's own path).
|
|
512
|
+
const BUILT_SHEET = /^\/(?:assets|_next\/static)\//
|
|
513
|
+
|
|
510
514
|
function pnextStylesheet(href: string) {
|
|
511
515
|
try {
|
|
512
516
|
const url = new URL(href, location.href)
|
|
513
|
-
return url.origin === location.origin && url.pathname
|
|
517
|
+
return url.origin === location.origin && BUILT_SHEET.test(url.pathname)
|
|
514
518
|
} catch {
|
|
515
519
|
return false
|
|
516
520
|
}
|
|
@@ -695,7 +699,7 @@ function preservedSegmentIslands(segments: Element[]): LiveIslandRoot[] {
|
|
|
695
699
|
for (const root of segment.querySelectorAll('pnext-client[data-pnext-client]')) {
|
|
696
700
|
const live = root as LiveIslandRoot
|
|
697
701
|
if (!isTopLevelIslandRoot(root)) continue
|
|
698
|
-
if (root
|
|
702
|
+
if (elementInPageSlot(root)) continue
|
|
699
703
|
if (!live.__pnextLive) continue
|
|
700
704
|
live.__pnextIncoming ??= live.cloneNode(true) as Element
|
|
701
705
|
roots.push(live)
|
|
@@ -704,72 +708,17 @@ function preservedSegmentIslands(segments: Element[]): LiveIslandRoot[] {
|
|
|
704
708
|
return roots
|
|
705
709
|
}
|
|
706
710
|
|
|
707
|
-
/**
|
|
708
|
-
* Resolve a live segment's page slot as a "replace its content" function. A page
|
|
709
|
-
* that mounts nothing never materializes the wrapper div (the renderer's `keep`
|
|
710
|
-
* marker mode, which preserves Next's structural DOM), so the graft splices
|
|
711
|
-
* between the `pnext-page:` comment anchors instead. Null when neither form is
|
|
712
|
-
* present, so there is nothing to graft.
|
|
713
|
-
*/
|
|
714
|
-
function livePageSlot(segment: Element): ((nodes: Node[]) => void) | null {
|
|
715
|
-
const element = segment.querySelector('#pnext-page')
|
|
716
|
-
if (element)
|
|
717
|
-
return nodes => element.replaceChildren(...reusePageNodes([...element.childNodes], nodes))
|
|
718
|
-
const walker = document.createTreeWalker(segment, NodeFilter.SHOW_COMMENT)
|
|
719
|
-
let start: Comment | null = null
|
|
720
|
-
while (walker.nextNode()) {
|
|
721
|
-
const comment = walker.currentNode as Comment
|
|
722
|
-
if (comment.data.startsWith('pnext-page:')) {
|
|
723
|
-
start = comment
|
|
724
|
-
break
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
const parent = start?.parentNode
|
|
728
|
-
if (!start || !parent) return null
|
|
729
|
-
return nodes => {
|
|
730
|
-
const liveNodes: ChildNode[] = []
|
|
731
|
-
let node = start.nextSibling
|
|
732
|
-
while (node && !(node.nodeType === Node.COMMENT_NODE && node.nodeValue === '/pnext-page')) {
|
|
733
|
-
liveNodes.push(node)
|
|
734
|
-
node = node.nextSibling
|
|
735
|
-
}
|
|
736
|
-
const next = reusePageNodes(liveNodes, nodes)
|
|
737
|
-
for (const stale of liveNodes) stale.remove()
|
|
738
|
-
for (const incoming of next) parent.insertBefore(incoming, node)
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
// Next re-renders a page in place, so unchanged DOM keeps its node identity.
|
|
743
|
-
// An incoming child byte-identical to a live one IS that element: hand back the
|
|
744
|
-
// live node instead of the imported copy. Islands keep their own graft path.
|
|
745
|
-
function reusePageNodes(live: readonly ChildNode[], incoming: readonly Node[]): Node[] {
|
|
746
|
-
const candidates = live.filter(
|
|
747
|
-
(node): node is Element =>
|
|
748
|
-
node instanceof Element &&
|
|
749
|
-
!(node instanceof HTMLScriptElement) &&
|
|
750
|
-
!node.matches('pnext-client, #pnext-page') &&
|
|
751
|
-
node.querySelector(`pnext-client, #pnext-page, [${PRESERVE_ATTRIBUTE}]`) == null,
|
|
752
|
-
)
|
|
753
|
-
if (candidates.length === 0) return [...incoming]
|
|
754
|
-
return incoming.map(node => {
|
|
755
|
-
if (!(node instanceof Element)) return node
|
|
756
|
-
const index = candidates.findIndex(candidate => candidate.outerHTML === node.outerHTML)
|
|
757
|
-
return index === -1 ? node : candidates.splice(index, 1)[0]!
|
|
758
|
-
})
|
|
759
|
-
}
|
|
760
|
-
|
|
761
711
|
function graftPreservedServerSegments(fragment: DocumentFragment, preserved: Element[]) {
|
|
762
712
|
for (const placeholder of [...fragment.querySelectorAll(`[${SEGMENT_PRESERVE_ATTRIBUTE}]`)]) {
|
|
763
713
|
const live = preserved[Number(placeholder.getAttribute(SEGMENT_PRESERVE_ATTRIBUTE))]
|
|
764
714
|
placeholder.removeAttribute(SEGMENT_PRESERVE_ATTRIBUTE)
|
|
765
715
|
if (!live) continue
|
|
766
|
-
|
|
767
|
-
const incomingPage = placeholder.querySelector('#pnext-page')
|
|
768
|
-
if (!livePage || !incomingPage) continue
|
|
769
|
-
// MOVE (not clone) the incoming page nodes: they already belong to this
|
|
716
|
+
// MOVE (not clone) the incoming page slot: it already belongs to this
|
|
770
717
|
// document, and cloning would turn an island root the island graft just
|
|
771
|
-
// spliced in back into an inert copy, dropping its live preact tree.
|
|
772
|
-
|
|
718
|
+
// spliced in back into an inert copy, dropping its live preact tree. Moving
|
|
719
|
+
// the slot itself also preserves whether this destination needs a mount
|
|
720
|
+
// element or Next-flat comment anchors.
|
|
721
|
+
if (!graftPageSlot(live, placeholder)) continue
|
|
773
722
|
placeholder.replaceWith(live)
|
|
774
723
|
}
|
|
775
724
|
}
|
|
@@ -879,7 +828,7 @@ function matchPreservedIslands(
|
|
|
879
828
|
if (!isTopLevelIslandRoot(root)) continue
|
|
880
829
|
if (!(root as LiveIslandRoot).__pnextLive) continue
|
|
881
830
|
if (remountTemplates && isTemplateIsland(root)) continue
|
|
882
|
-
if (remountPageIslands && root
|
|
831
|
+
if (remountPageIslands && elementInPageSlot(root)) continue
|
|
883
832
|
const id = root.getAttribute('data-pnext-client') ?? ''
|
|
884
833
|
const queue = liveById.get(id)
|
|
885
834
|
if (queue) queue.push(root)
|
|
@@ -890,13 +839,13 @@ function matchPreservedIslands(
|
|
|
890
839
|
for (const placeholder of doc.body.querySelectorAll('pnext-client[data-pnext-client]')) {
|
|
891
840
|
if (!isTopLevelIslandRoot(placeholder)) continue
|
|
892
841
|
if (remountTemplates && isTemplateIsland(placeholder)) continue
|
|
893
|
-
if (remountPageIslands && placeholder
|
|
842
|
+
if (remountPageIslands && elementInPageSlot(placeholder)) continue
|
|
894
843
|
// Inside a preserved server segment the LAYOUT-level islands travel with the
|
|
895
844
|
// live segment DOM that graftPreservedServerSegments splices in, so the island
|
|
896
845
|
// graft (which runs first) must not move them out of it. Page-slot islands
|
|
897
846
|
// still preserve: their placeholders move into the live page slot.
|
|
898
847
|
const preservedSegment = placeholder.closest(`[${SEGMENT_PRESERVE_ATTRIBUTE}]`)
|
|
899
|
-
if (preservedSegment && !placeholder
|
|
848
|
+
if (preservedSegment && !elementInPageSlot(placeholder)) continue
|
|
900
849
|
const live = liveById.get(placeholder.getAttribute('data-pnext-client') ?? '')?.shift()
|
|
901
850
|
if (!live) continue
|
|
902
851
|
// An island carrying its own params scope (a parallel-route slot stamps its
|
|
@@ -974,9 +923,9 @@ function swapBody(
|
|
|
974
923
|
preserved: LiveIslandRoot[] = [],
|
|
975
924
|
segments: Element[] = [],
|
|
976
925
|
preservedPage: PreservedClientPage | null = null,
|
|
926
|
+
reusable = reusableBodyChildren(),
|
|
977
927
|
) {
|
|
978
928
|
const fragment = document.createDocumentFragment()
|
|
979
|
-
const reusable = reusableBodyChildren()
|
|
980
929
|
// The incoming document's entry src, remembered on <html> below so a snapshot
|
|
981
930
|
// of the swapped (script-less) live DOM can still name its entry.
|
|
982
931
|
let entrySrc: string | null = null
|
|
@@ -1043,7 +992,7 @@ function restoreSwapFocus(focused: Element | null): void {
|
|
|
1043
992
|
// document) and nothing the island/segment/page grafts need to target.
|
|
1044
993
|
function isReusableBodyChild(node: Node): node is Element {
|
|
1045
994
|
if (!(node instanceof Element)) return false
|
|
1046
|
-
if (node
|
|
995
|
+
if (node.tagName === 'SCRIPT') return false
|
|
1047
996
|
if (node.matches('pnext-client, pnext-layout, #pnext-page')) return false
|
|
1048
997
|
return (
|
|
1049
998
|
node.querySelector(
|
|
@@ -1053,22 +1002,12 @@ function isReusableBodyChild(node: Node): node is Element {
|
|
|
1053
1002
|
}
|
|
1054
1003
|
|
|
1055
1004
|
function reusableBodyChildren(): Element[] {
|
|
1056
|
-
const candidates: Element[] = []
|
|
1057
1005
|
// Once the entry has mounted, a client page dissolves `#pnext-page` into comment
|
|
1058
1006
|
// anchors, so its elements sit directly in <body>. They belong to the departing
|
|
1059
1007
|
// render and must not be mistaken for root-layout DOM - skip the anchored range.
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
const data = node.nodeValue ?? ''
|
|
1064
|
-
if (data.startsWith('pnext-page:')) insidePageSlot = true
|
|
1065
|
-
else if (data === '/pnext-page') insidePageSlot = false
|
|
1066
|
-
continue
|
|
1067
|
-
}
|
|
1068
|
-
if (insidePageSlot || !isReusableBodyChild(node)) continue
|
|
1069
|
-
candidates.push(node)
|
|
1070
|
-
}
|
|
1071
|
-
return candidates
|
|
1008
|
+
return [...document.body.children].filter(
|
|
1009
|
+
node => !elementInPageSlot(node) && isReusableBodyChild(node),
|
|
1010
|
+
)
|
|
1072
1011
|
}
|
|
1073
1012
|
|
|
1074
1013
|
// Match an incoming body child to a still-available live one and hand back the
|
|
@@ -2661,7 +2600,10 @@ export function prefetchRoute(
|
|
|
2661
2600
|
const cacheKey = prefetchCacheKey(key, full)
|
|
2662
2601
|
// Prefetching the page we are already on is a no-op (Next's router produces
|
|
2663
2602
|
// an empty delta for the current tree) — and it must not spend a request.
|
|
2664
|
-
if (
|
|
2603
|
+
if (
|
|
2604
|
+
(key === location.pathname + location.search || key === routerState.activeRouteKey) &&
|
|
2605
|
+
!options.currentUrl
|
|
2606
|
+
) {
|
|
2665
2607
|
return Promise.resolve(null)
|
|
2666
2608
|
}
|
|
2667
2609
|
// A default prefetch carries only static segment data; search params are dynamic data
|
|
@@ -3746,8 +3688,7 @@ function showLoadingShell(
|
|
|
3746
3688
|
const suspense = marker ?? inline?.anchor
|
|
3747
3689
|
if (!suspense && !allowWithoutBoundary) return false
|
|
3748
3690
|
if (!loadingBoundaryChanges(marker, target)) return false
|
|
3749
|
-
const container = loadingShellTarget()
|
|
3750
|
-
if (!container) return false
|
|
3691
|
+
const { container, markerRange } = loadingShellTarget()
|
|
3751
3692
|
// A painted loading shell IS a committed navigation (pushOptimisticUrl moves the address
|
|
3752
3693
|
// bar the instant this returns true), so the window route state must reflect the
|
|
3753
3694
|
// DESTINATION before any island reads useParams - otherwise usePathname and useParams
|
|
@@ -3759,6 +3700,7 @@ function showLoadingShell(
|
|
|
3759
3700
|
// loading boundary. Keep that prefix when painting the fallback: replacing the target
|
|
3760
3701
|
// with only the suspense children loses the eagerly prefetched layout.
|
|
3761
3702
|
const incomingTarget =
|
|
3703
|
+
(markerRange ? doc.getElementById('pnext-page') : null) ??
|
|
3762
3704
|
(container.id ? doc.getElementById(container.id) : null) ??
|
|
3763
3705
|
doc.querySelector('[data-pnext-root]') ??
|
|
3764
3706
|
(container === document.body ? doc.body : null)
|
|
@@ -3787,7 +3729,10 @@ function showLoadingShell(
|
|
|
3787
3729
|
const liveOwner = liveIslandOwner(container)
|
|
3788
3730
|
const incomingOwner = liveOwner && incomingIslandFor(doc, liveOwner)
|
|
3789
3731
|
if (liveOwner && incomingOwner) liveOwner.__pnextIncoming = incomingOwner
|
|
3790
|
-
else
|
|
3732
|
+
else if (markerRange) {
|
|
3733
|
+
for (const node of markerRange[2]) node.remove()
|
|
3734
|
+
markerRange[0].after(fragment)
|
|
3735
|
+
} else container.replaceChildren(fragment)
|
|
3791
3736
|
// Any painted shell can carry islands (a loading fallback that calls
|
|
3792
3737
|
// useOffline(), say), and an unmounted island keeps its SSR value forever.
|
|
3793
3738
|
// mountRoute is idempotent, so mounting on every paint is safe.
|
|
@@ -3985,13 +3930,6 @@ function loadingBoundaryChanges(marker: Element | null, target: URL): boolean {
|
|
|
3985
3930
|
return divergence <= depth
|
|
3986
3931
|
}
|
|
3987
3932
|
|
|
3988
|
-
function loadingShellTarget(): HTMLElement | null {
|
|
3989
|
-
const page = document.getElementById('pnext-page')
|
|
3990
|
-
if (page instanceof HTMLElement) return page
|
|
3991
|
-
const root = document.querySelector('[data-pnext-root]')
|
|
3992
|
-
return root instanceof HTMLElement ? root : document.body
|
|
3993
|
-
}
|
|
3994
|
-
|
|
3995
3933
|
// Choosing the document a navigation commits: cache hit, in-flight prefetch,
|
|
3996
3934
|
// segment reuse or a fresh fetch — and recording what the answer taught us.
|
|
3997
3935
|
|
|
@@ -4040,6 +3978,10 @@ async function pageForNavigation(
|
|
|
4040
3978
|
const paintStaticStage = (html: string) => {
|
|
4041
3979
|
if (staticStagePainted) return
|
|
4042
3980
|
staticStagePainted = true
|
|
3981
|
+
// A cached stage with parallel slots has dynamic continuations beside the page. Those cannot
|
|
3982
|
+
// be reconstructed from a page-only frame, so this navigation needs the whole target document.
|
|
3983
|
+
if (html.includes('data-pnext-slot=') || slotStateSensitive(departureNavState, html))
|
|
3984
|
+
pageFrame.eligible = false
|
|
4043
3985
|
onStaticStage?.(html)
|
|
4044
3986
|
}
|
|
4045
3987
|
const cached = prefetchEntriesForNavigation(key).find(
|
|
@@ -4118,14 +4060,14 @@ async function pageForNavigation(
|
|
|
4118
4060
|
// whole-document fetch below unchanged.
|
|
4119
4061
|
const framed = pageFrame.eligible
|
|
4120
4062
|
? await fetchPageFrameNavigation(url, {
|
|
4121
|
-
navState: options.navState,
|
|
4063
|
+
navState: options.navState ?? departureNavState,
|
|
4122
4064
|
sameUrl: pageFrame.sameUrl,
|
|
4123
4065
|
}).catch(() => null)
|
|
4124
4066
|
: null
|
|
4125
4067
|
const page =
|
|
4126
4068
|
framed ??
|
|
4127
4069
|
(await fetchPage(url.href, {
|
|
4128
|
-
navState: options.navState,
|
|
4070
|
+
navState: options.navState ?? departureNavState,
|
|
4129
4071
|
onShell,
|
|
4130
4072
|
}).catch(() => null))
|
|
4131
4073
|
// Visited-page seeding: the navigation response is as fresh as any prefetch —
|
|
@@ -4515,10 +4457,14 @@ export async function softNavigate(href: string, options: SoftNavigateOptions =
|
|
|
4515
4457
|
// into the live body. Capturing later would save the target's fallback as
|
|
4516
4458
|
// the previous history entry and restore a permanently stuck "Loading...".
|
|
4517
4459
|
if (!options.pop) saveScrollPosition()
|
|
4460
|
+
if (departingBfcacheId) saveFormState(departingBfcacheId)
|
|
4518
4461
|
// Snapshot the departing page's live island roots NOW, before a loading shell
|
|
4519
4462
|
// can paint over (and detach) them below — they are stashed for back/forward
|
|
4520
4463
|
// restore once the incoming document tells us which are preserved vs left.
|
|
4521
4464
|
const departingLiveRoots = collectTopLevelLiveRoots()
|
|
4465
|
+
// Snapshot plain root-layout DOM before a cached/streamed shell can detach it. The final swap
|
|
4466
|
+
// reconciles onto these nodes so server layouts keep the same identity Next's root reconciler does.
|
|
4467
|
+
const departingReusableBody = reusableBodyChildren()
|
|
4522
4468
|
// Painting a prefetched loading shell IS a committed navigation, so push the requested URL
|
|
4523
4469
|
// here and let usePathname() and the address bar reflect the destination while the fetch is
|
|
4524
4470
|
// still in flight; the final commit replaces this entry with the resolved one (or, on a
|
|
@@ -4593,10 +4539,17 @@ export async function softNavigate(href: string, options: SoftNavigateOptions =
|
|
|
4593
4539
|
// Loading-shell reuse: when the navigation must fetch, paint the CACHED shell for the
|
|
4594
4540
|
// target pathname synchronously - search-param independent, so `?a` reuses the shell a `?b`
|
|
4595
4541
|
// prefetch streamed - instead of waiting for the response's shell chunk.
|
|
4542
|
+
// A sibling runtime prefetch may have supplied a reusable PAGE frame while this URL's
|
|
4543
|
+
// param-dependent LAYOUT frame is missing. The route shell is not vary-aware, so do not
|
|
4544
|
+
// paint a sibling's layout while the destination layout is still on the wire.
|
|
4596
4545
|
if (
|
|
4597
4546
|
onShell &&
|
|
4598
4547
|
!refreshLike &&
|
|
4599
4548
|
!segmentHit &&
|
|
4549
|
+
!segmentCachePolicy?.needsLayoutFrameOnly?.({
|
|
4550
|
+
pathname: url.pathname,
|
|
4551
|
+
search: url.search,
|
|
4552
|
+
}) &&
|
|
4600
4553
|
!hasFreshDocument(url) &&
|
|
4601
4554
|
!peekSegmentBody(url.pathname)
|
|
4602
4555
|
) {
|
|
@@ -4625,7 +4578,7 @@ export async function softNavigate(href: string, options: SoftNavigateOptions =
|
|
|
4625
4578
|
eligible:
|
|
4626
4579
|
!options.pop &&
|
|
4627
4580
|
(!refreshLike || options.pageRefresh === true) &&
|
|
4628
|
-
!slotStateSensitive(departureNavState),
|
|
4581
|
+
!slotStateSensitive(departureNavState, segmentHit?.html),
|
|
4629
4582
|
// The DEPARTING URL, not the live one: an optimistic shell paint above may
|
|
4630
4583
|
// already have pushed the destination into the address bar.
|
|
4631
4584
|
sameUrl: url.pathname === previousPathname && url.search === previousSearch,
|
|
@@ -4682,7 +4635,7 @@ export async function softNavigate(href: string, options: SoftNavigateOptions =
|
|
|
4682
4635
|
!options.cachedPage &&
|
|
4683
4636
|
skippedSegmentsUngraftable(
|
|
4684
4637
|
doc,
|
|
4685
|
-
options.freshSegments
|
|
4638
|
+
options.freshSegments || cachedStagePainted || (refreshLike && !options.pageRefresh),
|
|
4686
4639
|
)
|
|
4687
4640
|
) {
|
|
4688
4641
|
const fullPage = await fetchPage(url.href, {
|
|
@@ -4772,7 +4725,10 @@ export async function softNavigate(href: string, options: SoftNavigateOptions =
|
|
|
4772
4725
|
// a preserved segment, and so this pass agrees with the identical dry run in
|
|
4773
4726
|
// skippedSegmentsUngraftable above.
|
|
4774
4727
|
const preservedSegments =
|
|
4775
|
-
options.freshSegments ||
|
|
4728
|
+
options.freshSegments ||
|
|
4729
|
+
cachedStagePainted ||
|
|
4730
|
+
(refreshLike && !options.pageRefresh) ||
|
|
4731
|
+
navSlotsChanged(doc)
|
|
4776
4732
|
? (clearSegmentPreserveTags(doc), [])
|
|
4777
4733
|
: matchPreservedServerSegments(doc)
|
|
4778
4734
|
const preservedIslands = matchPreservedIslands(doc, remountTemplates, remountPageIslands)
|
|
@@ -4782,11 +4738,6 @@ export async function softNavigate(href: string, options: SoftNavigateOptions =
|
|
|
4782
4738
|
remountPageIslands,
|
|
4783
4739
|
)
|
|
4784
4740
|
const targetRouteKey = bfRouteKey(targetUrl.pathname, targetUrl.search)
|
|
4785
|
-
// Browser-back form restoration: snapshot the departing entry's form-control state so a
|
|
4786
|
-
// later popstate can restore what a fresh mount cannot (typed values are DOM state, lost on
|
|
4787
|
-
// remount). Snapshotted for EVERY navigation, not just the ones that mint a new id: a
|
|
4788
|
-
// navigation that KEEPS the bfcacheId must come out the other side with the same values.
|
|
4789
|
-
if (departingBfcacheId) saveFormState(departingBfcacheId)
|
|
4790
4741
|
// Live islands the incoming tree keeps in place (preserved layout/segment
|
|
4791
4742
|
// islands): the departing page's OTHER roots are the ones to stash.
|
|
4792
4743
|
const keptLiveIslands = new Set<Element>([
|
|
@@ -4824,7 +4775,13 @@ export async function softNavigate(href: string, options: SoftNavigateOptions =
|
|
|
4824
4775
|
// across a navigation. pnext's swap DETACHES preserved subtrees to graft them into the new
|
|
4825
4776
|
// body, and detaching blurs - remember the focused node so it can be refocused.
|
|
4826
4777
|
const focusedBeforeSwap = document.activeElement
|
|
4827
|
-
swapBody(
|
|
4778
|
+
swapBody(
|
|
4779
|
+
doc,
|
|
4780
|
+
[...preservedIslands, ...restoredIslands],
|
|
4781
|
+
preservedSegments,
|
|
4782
|
+
preservedPage,
|
|
4783
|
+
departingReusableBody,
|
|
4784
|
+
)
|
|
4828
4785
|
stylesheetReconciler?.(doc)
|
|
4829
4786
|
// The swapped document carries the render's parallel-route state; pin it
|
|
4830
4787
|
// (plus the document itself) to this history entry so back/forward restores
|
|
@@ -4840,9 +4797,16 @@ export async function softNavigate(href: string, options: SoftNavigateOptions =
|
|
|
4840
4797
|
await entryModule?.mountRoute?.()
|
|
4841
4798
|
if (sequence !== navigationSequence) return
|
|
4842
4799
|
restoreSwapFocus(focusedBeforeSwap)
|
|
4800
|
+
if (!options.pop && departingBfcacheId && previousPathname !== targetUrl.pathname) {
|
|
4801
|
+
restoreSharedLayoutFormState(departingBfcacheId, previousPathname, targetUrl.pathname)
|
|
4802
|
+
}
|
|
4843
4803
|
// History traversal restores the popped entry's form state over the freshly
|
|
4844
4804
|
// mounted tree (browser back/forward form restoration semantics).
|
|
4845
|
-
if (options.pop)
|
|
4805
|
+
if (options.pop) {
|
|
4806
|
+
const targetBfcacheId = historyBfcacheId()
|
|
4807
|
+
restoreFormState(targetBfcacheId)
|
|
4808
|
+
if (targetBfcacheId) restoreFormStateWhenMounted(targetBfcacheId, sequence)
|
|
4809
|
+
}
|
|
4846
4810
|
// Same-entry-identity navigation (a search-param nav on the same pathname, a refresh):
|
|
4847
4811
|
// `key={bfcacheId}` subtrees do NOT remount, so typed values must survive. The commit can
|
|
4848
4812
|
// still rebuild the page DOM under them (a PPR route streams its page slot in AFTER the
|
|
@@ -4928,6 +4892,21 @@ function saveFormState(bfcacheId: string) {
|
|
|
4928
4892
|
}
|
|
4929
4893
|
}
|
|
4930
4894
|
|
|
4895
|
+
function restoreControl(
|
|
4896
|
+
control: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement,
|
|
4897
|
+
saved: FormControlState,
|
|
4898
|
+
onlyEmpty: boolean,
|
|
4899
|
+
) {
|
|
4900
|
+
if (control.localName !== saved.tag) return
|
|
4901
|
+
if (
|
|
4902
|
+
control.localName === 'input' &&
|
|
4903
|
+
(control.getAttribute('type')?.toLowerCase() ?? '') !== saved.type
|
|
4904
|
+
)
|
|
4905
|
+
return
|
|
4906
|
+
if (onlyEmpty && control.value !== '') return
|
|
4907
|
+
if (control.value !== saved.value) control.value = saved.value
|
|
4908
|
+
}
|
|
4909
|
+
|
|
4931
4910
|
function restoreFormState(bfcacheId: string | undefined, onlyEmpty = false) {
|
|
4932
4911
|
if (!bfcacheId) return
|
|
4933
4912
|
const state = entryFormStateCache.get(bfcacheId)
|
|
@@ -4935,18 +4914,34 @@ function restoreFormState(bfcacheId: string | undefined, onlyEmpty = false) {
|
|
|
4935
4914
|
const controls = formControls()
|
|
4936
4915
|
if (controls.length !== state.length) return
|
|
4937
4916
|
for (const [index, control] of controls.entries()) {
|
|
4938
|
-
const saved = state[index]!
|
|
4939
|
-
if (control.localName !== saved.tag) continue
|
|
4940
|
-
if (
|
|
4941
|
-
control.localName === 'input' &&
|
|
4942
|
-
(control.getAttribute('type')?.toLowerCase() ?? '') !== saved.type
|
|
4943
|
-
)
|
|
4944
|
-
continue
|
|
4945
4917
|
// `onlyEmpty`: a freshly mounted control is empty, so filling it restores
|
|
4946
4918
|
// what the remount lost — while anything already carrying a value (the app
|
|
4947
4919
|
// set it, or the user typed after the commit) is left alone.
|
|
4948
|
-
|
|
4949
|
-
|
|
4920
|
+
restoreControl(control, state[index]!, onlyEmpty)
|
|
4921
|
+
}
|
|
4922
|
+
}
|
|
4923
|
+
|
|
4924
|
+
/** Restore uncontrolled controls owned by a layout whose URL scope did not change. */
|
|
4925
|
+
function restoreSharedLayoutFormState(
|
|
4926
|
+
bfcacheId: string,
|
|
4927
|
+
departingPathname: string,
|
|
4928
|
+
targetPathname: string,
|
|
4929
|
+
) {
|
|
4930
|
+
const state = entryFormStateCache.get(bfcacheId)
|
|
4931
|
+
if (!state) return
|
|
4932
|
+
const controls = formControls()
|
|
4933
|
+
if (controls.length !== state.length) return
|
|
4934
|
+
const departing = departingPathname.split('/').filter(Boolean)
|
|
4935
|
+
const target = targetPathname.split('/').filter(Boolean)
|
|
4936
|
+
for (const [index, control] of controls.entries()) {
|
|
4937
|
+
if (elementInPageSlot(control)) continue
|
|
4938
|
+
const raw = control
|
|
4939
|
+
.closest('pnext-client[data-pnext-layout-segments]')
|
|
4940
|
+
?.getAttribute('data-pnext-layout-segments')
|
|
4941
|
+
if (!raw) continue
|
|
4942
|
+
const depth = (JSON.parse(raw) as { depth?: number }).depth
|
|
4943
|
+
if (!depth || departing.slice(0, depth).join('/') !== target.slice(0, depth).join('/')) continue
|
|
4944
|
+
restoreControl(control, state[index]!, false)
|
|
4950
4945
|
}
|
|
4951
4946
|
}
|
|
4952
4947
|
|
|
@@ -13,7 +13,7 @@ import { rewriteFacts } from '../../resolve/scan-facts'
|
|
|
13
13
|
import { spliceSource } from '../../runtime/module-transform'
|
|
14
14
|
import { clientReferenceId } from '../../client/reference'
|
|
15
15
|
import { hasUseClientDirective } from '../../client/reference-stub'
|
|
16
|
-
import { isIdentifier, uniqueIdentifier } from '../../utils/code'
|
|
16
|
+
import { esbuildEntryExportNames, isIdentifier, uniqueIdentifier } from '../../utils/code'
|
|
17
17
|
import { fileMemo, fileMemoAsync } from './source-cache'
|
|
18
18
|
|
|
19
19
|
const encoder = new TextEncoder()
|
|
@@ -148,9 +148,17 @@ export function commonJsClientReferencePlugin(): Plugin {
|
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
// Minified ESM keeps every statement on one line, which `isCommonJsModuleSource`'s line-anchored test
|
|
152
|
+
// misses (`…;export{H as RWebShare};`). Such an entry taken for CommonJS gets the facade above, whose
|
|
153
|
+
// `__esModule ? .default` unwrap of esbuild's `__toCommonJS` namespace yields undefined and publishes
|
|
154
|
+
// no named exports at all. Checked HERE rather than in the shared predicate: that one also decides
|
|
155
|
+
// transform bail-outs and vendor entry shape, where the looser test changes unrelated behaviour.
|
|
156
|
+
const MINIFIED_ESM_SYNTAX = /[;}](?:import(?:\s|[{'"*])|export(?:\s|[{*]))/
|
|
157
|
+
|
|
151
158
|
const isCommonJsJavaScriptFile = fileMemo((file: string) => {
|
|
152
159
|
try {
|
|
153
|
-
|
|
160
|
+
const source = readFileSync(file, 'utf8')
|
|
161
|
+
return isCommonJsModuleSource(source, file) && !MINIFIED_ESM_SYNTAX.test(source)
|
|
154
162
|
} catch {
|
|
155
163
|
return true
|
|
156
164
|
}
|
|
@@ -366,15 +374,29 @@ function recoverCommonJsNamedExports(code: string) {
|
|
|
366
374
|
)
|
|
367
375
|
}
|
|
368
376
|
|
|
369
|
-
function commonJsNamespaceBinding(
|
|
377
|
+
function commonJsNamespaceBinding(
|
|
378
|
+
code: string,
|
|
379
|
+
defaultName: string,
|
|
380
|
+
depth = 0,
|
|
381
|
+
): string | undefined {
|
|
370
382
|
const assignment = new RegExp(
|
|
371
383
|
`(?:^|\\n)(?:var|const|let)\\s+${escapeRegExp(defaultName)}\\s*=\\s*([A-Za-z_$][\\w$]*)\\.default\\s*;`,
|
|
372
384
|
).exec(code)
|
|
373
385
|
if (assignment?.[1]) return assignment[1]
|
|
374
386
|
|
|
375
|
-
|
|
387
|
+
const unwrap = new RegExp(
|
|
376
388
|
`(?:^|\\n)(?:var|const|let)\\s+${escapeRegExp(defaultName)}\\s*=\\s*([A-Za-z_$][\\w$]*)\\s*!=\\s*null\\s*&&[\\s\\S]*?\\?\\s*\\1\\.default\\s*:\\s*\\1\\s*;`,
|
|
377
389
|
).exec(code)?.[1]
|
|
390
|
+
if (unwrap) return unwrap
|
|
391
|
+
|
|
392
|
+
// esbuild aliases the interop facade's namespace before exporting it as default. An `__esModule`
|
|
393
|
+
// entry with no `default` unwraps to undefined, so following the alias is what keeps the names
|
|
394
|
+
// being read off the raw `module.exports` rather than off nothing.
|
|
395
|
+
if (depth >= 4) return undefined
|
|
396
|
+
const alias = new RegExp(
|
|
397
|
+
`(?:^|\\n)(?:var|const|let)\\s+${escapeRegExp(defaultName)}\\s*=\\s*([A-Za-z_$][\\w$]*)\\s*;`,
|
|
398
|
+
).exec(code)?.[1]
|
|
399
|
+
return alias ? commonJsNamespaceBinding(code, alias, depth + 1) : undefined
|
|
378
400
|
}
|
|
379
401
|
|
|
380
402
|
function hasNonDefaultExport(code: string) {
|
|
@@ -572,19 +594,3 @@ export const namedFacadeExportNames = fileMemo((file: string): string[] =>
|
|
|
572
594
|
name => name !== 'default' && name !== '__esModule' && !reservedWords.has(name),
|
|
573
595
|
),
|
|
574
596
|
)
|
|
575
|
-
|
|
576
|
-
// Recover named exports from esbuild's `__export(entry, {...}); module.exports = __toCommonJS(entry)` shape.
|
|
577
|
-
function esbuildEntryExportNames(code: string) {
|
|
578
|
-
const toCommonJs = [
|
|
579
|
-
...code.matchAll(/module\.exports\s*=\s*__toCommonJS\w*\(\s*([A-Za-z_$][\w$]*)\s*\)/g),
|
|
580
|
-
]
|
|
581
|
-
const entryVar = toCommonJs.at(-1)?.[1]
|
|
582
|
-
if (!entryVar) return []
|
|
583
|
-
const exportCall = new RegExp(
|
|
584
|
-
`__export\\w*\\(\\s*${entryVar}\\s*,\\s*\\{([\\s\\S]*?)\\}\\s*\\)`,
|
|
585
|
-
).exec(code)
|
|
586
|
-
if (!exportCall?.[1]) return []
|
|
587
|
-
return [...exportCall[1].matchAll(/(?:^|,)\s*([A-Za-z_$][\w$]*)\s*:/g)]
|
|
588
|
-
.map(match => match[1])
|
|
589
|
-
.filter((name): name is string => Boolean(name && isIdentifier(name)))
|
|
590
|
-
}
|
|
@@ -32,5 +32,5 @@ function absoluteHref(link: HTMLLinkElement) {
|
|
|
32
32
|
|
|
33
33
|
function nextStylesheet(href: string) {
|
|
34
34
|
const url = new URL(href, location.href)
|
|
35
|
-
return url.origin === location.origin && url.pathname.startsWith('/
|
|
35
|
+
return url.origin === location.origin && url.pathname.startsWith('/_next/static/')
|
|
36
36
|
}
|
|
@@ -16,29 +16,13 @@ export function nextScrollPlan(
|
|
|
16
16
|
return targetInViewport ? 'preserve' : 'top-then-target'
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
export function firstScrollCandidate<T>(
|
|
20
|
-
candidates: Iterable<T>,
|
|
21
|
-
shouldSkip: (candidate: T) => boolean,
|
|
22
|
-
): T | undefined {
|
|
23
|
-
for (const candidate of candidates) if (!shouldSkip(candidate)) return candidate
|
|
24
|
-
return undefined
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function deepestSingleChild<T>(node: T | undefined, onlyChild: (node: T) => T | undefined) {
|
|
28
|
-
while (node) {
|
|
29
|
-
const child = onlyChild(node)
|
|
30
|
-
if (!child) return node
|
|
31
|
-
node = child
|
|
32
|
-
}
|
|
33
|
-
return undefined
|
|
34
|
-
}
|
|
35
|
-
|
|
36
19
|
export function topOnlyScrollPosition(scrollX: number): [number, number] {
|
|
37
20
|
return [scrollX, 0]
|
|
38
21
|
}
|
|
39
22
|
|
|
40
23
|
export const applyNextNavigationScroll: NavigationScrollAction = (url, options) => {
|
|
41
|
-
const
|
|
24
|
+
const segmentElements = changedSegmentElements()
|
|
25
|
+
const target = scrollFocusTarget(segmentElements)
|
|
42
26
|
const plan = nextScrollPlan(
|
|
43
27
|
url,
|
|
44
28
|
options,
|
|
@@ -56,7 +40,7 @@ export const applyNextNavigationScroll: NavigationScrollAction = (url, options)
|
|
|
56
40
|
// zero-rect sibling there and runs out - the navigation scrolls nowhere and focuses nothing. pnext
|
|
57
41
|
// renders those elements in place, so recognize the leading hoisted element and reproduce the same
|
|
58
42
|
// no-op. Placed after `restore` so history traversal still restores its saved position.
|
|
59
|
-
if (segmentLeadsWithHoistedElement()) return
|
|
43
|
+
if (segmentLeadsWithHoistedElement(segmentElements)) return
|
|
60
44
|
if (plan === 'hash') {
|
|
61
45
|
scrollToHash(url.hash)
|
|
62
46
|
return
|
|
@@ -90,25 +74,13 @@ function targetTopInViewport(element: HTMLElement, viewportHeight: number) {
|
|
|
90
74
|
return top >= 0 && top <= viewportHeight
|
|
91
75
|
}
|
|
92
76
|
|
|
93
|
-
function scrollFocusTarget(): HTMLElement | null {
|
|
94
|
-
const segment = changedSegmentRoot()
|
|
95
|
-
if (!segment) return null
|
|
77
|
+
function scrollFocusTarget(nodes: HTMLElement[]): HTMLElement | null {
|
|
96
78
|
// Next resolves the target with findDOMNode() on the changed segment: the
|
|
97
79
|
// FIRST host element the segment rendered, never a descendant of it. The page
|
|
98
80
|
// container IS that segment, so its first element child is the target as-is.
|
|
99
81
|
// The fallback roots below wrap the segment in pnext's own chrome, so there
|
|
100
82
|
// the single-child chain is walked down to the content that segment rendered.
|
|
101
|
-
|
|
102
|
-
segment.id === PAGE_CONTAINER_ID
|
|
103
|
-
? (segment.firstElementChild ?? undefined)
|
|
104
|
-
: deepestSingleChild(segment.firstElementChild ?? undefined, node =>
|
|
105
|
-
node.nextElementSibling === null ? (node.firstElementChild ?? undefined) : undefined,
|
|
106
|
-
)
|
|
107
|
-
const nodes: HTMLElement[] = []
|
|
108
|
-
for (let node = first; node; node = node.nextElementSibling ?? undefined) {
|
|
109
|
-
if (node instanceof HTMLElement) nodes.push(node)
|
|
110
|
-
}
|
|
111
|
-
return firstScrollCandidate(nodes, shouldSkipScrollElement) ?? null
|
|
83
|
+
return nodes.find(node => !shouldSkipScrollElement(node)) ?? null
|
|
112
84
|
}
|
|
113
85
|
|
|
114
86
|
/** The renderer's page-slot wrapper — the segment a navigation replaced. */
|
|
@@ -121,17 +93,41 @@ const HOISTED_TAGS = new Set(['style', 'link', 'title', 'meta', 'base'])
|
|
|
121
93
|
* Whether the changed segment's first rendered element is one React would have hoisted into <head> -
|
|
122
94
|
* the state in which Next's scroll walk never reaches the page content (see the call site).
|
|
123
95
|
*/
|
|
124
|
-
function segmentLeadsWithHoistedElement(): boolean {
|
|
125
|
-
|
|
126
|
-
const first = segment?.firstElementChild
|
|
127
|
-
return first !== null && first !== undefined && HOISTED_TAGS.has(first.localName)
|
|
96
|
+
function segmentLeadsWithHoistedElement(elements: HTMLElement[]): boolean {
|
|
97
|
+
return elements[0] !== undefined && HOISTED_TAGS.has(elements[0].localName)
|
|
128
98
|
}
|
|
129
99
|
|
|
130
|
-
function
|
|
100
|
+
function changedSegmentElements(): HTMLElement[] {
|
|
131
101
|
const page = document.getElementById(PAGE_CONTAINER_ID)
|
|
132
|
-
if (page instanceof HTMLElement) return page
|
|
102
|
+
if (page instanceof HTMLElement) return Array.from(page.children).filter(isHtmlElement)
|
|
103
|
+
|
|
104
|
+
const walker = document.createTreeWalker(document.body, 128 /* NodeFilter.SHOW_COMMENT */)
|
|
105
|
+
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
|
|
106
|
+
if (!node.nodeValue?.startsWith('pnext-page:')) continue
|
|
107
|
+
const elements: HTMLElement[] = []
|
|
108
|
+
for (let sibling = node.nextSibling; sibling; sibling = sibling.nextSibling) {
|
|
109
|
+
if (sibling.nodeType === 8 /* Node.COMMENT_NODE */ && sibling.nodeValue === '/pnext-page')
|
|
110
|
+
return elements
|
|
111
|
+
if (sibling instanceof HTMLElement) elements.push(sibling)
|
|
112
|
+
}
|
|
113
|
+
break
|
|
114
|
+
}
|
|
115
|
+
|
|
133
116
|
const root = document.querySelector('[data-pnext-root]')
|
|
134
|
-
|
|
117
|
+
const segment = root instanceof HTMLElement ? root : document.body
|
|
118
|
+
let first = segment.firstElementChild instanceof HTMLElement ? segment.firstElementChild : null
|
|
119
|
+
while (first?.nextElementSibling === null && first.firstElementChild instanceof HTMLElement)
|
|
120
|
+
first = first.firstElementChild
|
|
121
|
+
const elements: HTMLElement[] = []
|
|
122
|
+
for (let node = first; node;) {
|
|
123
|
+
elements.push(node)
|
|
124
|
+
node = node.nextElementSibling instanceof HTMLElement ? node.nextElementSibling : null
|
|
125
|
+
}
|
|
126
|
+
return elements
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function isHtmlElement(element: Element): element is HTMLElement {
|
|
130
|
+
return element instanceof HTMLElement
|
|
135
131
|
}
|
|
136
132
|
|
|
137
133
|
function scrollToHash(hash: string) {
|
|
@@ -4,6 +4,7 @@ import path from 'node:path'
|
|
|
4
4
|
import type { Plugin } from 'esbuild'
|
|
5
5
|
import type { ResolvedConfig } from '../../config'
|
|
6
6
|
import { resolveImport } from '../../resolve/imports'
|
|
7
|
+
import { hoistCssImports } from '../../css/build'
|
|
7
8
|
import { maybeLightningcssTransform } from './lightningcss'
|
|
8
9
|
import { escapeRegex } from '../../utils/code'
|
|
9
10
|
|
|
@@ -43,9 +44,12 @@ export function nextCssModuleChunkPlugin(config: ResolvedConfig): Plugin {
|
|
|
43
44
|
setup(build) {
|
|
44
45
|
build.onLoad({ filter: /\.css$/ }, ({ path: file }) => {
|
|
45
46
|
const source = nextCssSource(file, config.root)
|
|
47
|
+
// This onLoad claims every `.css`, so core's directive-order plugin
|
|
48
|
+
// (registered after it) never sees a compat app's stylesheet — hoist here.
|
|
49
|
+
const raw = hoistCssImports(readFileSync(source, 'utf8'))
|
|
46
50
|
const contents = file.endsWith('.module.css')
|
|
47
|
-
? transformCssModule(
|
|
48
|
-
: rewriteTransparentBodyChildren(
|
|
51
|
+
? transformCssModule(raw, source, config.root)
|
|
52
|
+
: rewriteTransparentBodyChildren(raw)
|
|
49
53
|
return {
|
|
50
54
|
// No-op unless next.config enables experimental.useLightningcss.
|
|
51
55
|
contents: maybeLightningcssTransform(contents, source, config.root),
|