@timber-js/app 0.2.0-alpha.197 → 0.2.0-alpha.199
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/dist/_chunks/{actions-BS-m5SLv.js → actions-d1hCqnU3.js} +35 -8
- package/dist/_chunks/actions-d1hCqnU3.js.map +1 -0
- package/dist/_chunks/als-registry-C6kcfprT.js.map +1 -1
- package/dist/_chunks/{cache-api-DqzgTEqk.js → cache-api-ByagcC-J.js} +2 -2
- package/dist/_chunks/{cache-api-DqzgTEqk.js.map → cache-api-ByagcC-J.js.map} +1 -1
- package/dist/_chunks/canonicalize-CgHoscYO.js +66 -0
- package/dist/_chunks/canonicalize-CgHoscYO.js.map +1 -0
- package/dist/_chunks/{chains-CZG7E5zg.js → chains-Bpb0W4ax.js} +3 -3
- package/dist/_chunks/{chains-CZG7E5zg.js.map → chains-Bpb0W4ax.js.map} +1 -1
- package/dist/_chunks/{cli-check-bE3H5Bjr.js → cli-check-D6VolrDV.js} +3 -3
- package/dist/_chunks/{cli-check-bE3H5Bjr.js.map → cli-check-D6VolrDV.js.map} +1 -1
- package/dist/_chunks/{cli-schema-sync-DTy_-Msq.js → cli-schema-sync-D6rO-VcS.js} +2 -2
- package/dist/_chunks/{cli-schema-sync-DTy_-Msq.js.map → cli-schema-sync-D6rO-VcS.js.map} +1 -1
- package/dist/_chunks/{convention-lint-n3RJLgww.js → convention-lint-fRkwVwEH.js} +27 -4
- package/dist/_chunks/convention-lint-fRkwVwEH.js.map +1 -0
- package/dist/_chunks/error-boundary-BfPHZjm0.js +1050 -0
- package/dist/_chunks/error-boundary-BfPHZjm0.js.map +1 -0
- package/dist/_chunks/{live-graph-BXDsdzBv.js → live-graph-D_2D32Ad.js} +3 -3
- package/dist/_chunks/{live-graph-BXDsdzBv.js.map → live-graph-D_2D32Ad.js.map} +1 -1
- package/dist/_chunks/{logger-DDirEsn7.js → logger-uLBuGKDI.js} +471 -440
- package/dist/_chunks/logger-uLBuGKDI.js.map +1 -0
- package/dist/_chunks/{navigation-root-B00jjGd5.js → navigation-context-D0TU0Jog.js} +3 -101
- package/dist/_chunks/navigation-context-D0TU0Jog.js.map +1 -0
- package/dist/_chunks/navigation-root-mHSK9psY.js +126 -0
- package/dist/_chunks/{navigation-root-B00jjGd5.js.map → navigation-root-mHSK9psY.js.map} +1 -1
- package/dist/_chunks/{poison-scan-BoDLgbix.js → poison-scan-Bm9Yyqk9.js} +2 -2
- package/dist/_chunks/{poison-scan-BoDLgbix.js.map → poison-scan-Bm9Yyqk9.js.map} +1 -1
- package/dist/_chunks/{scanner-tdFPvDYi.js → scanner-AiazgH_f.js} +6 -5
- package/dist/_chunks/scanner-AiazgH_f.js.map +1 -0
- package/dist/_chunks/{segment-keys-BhqoHiLc.js → segment-keys-lqtdookO.js} +2 -65
- package/dist/_chunks/segment-keys-lqtdookO.js.map +1 -0
- package/dist/_chunks/{ssr-data-BQGhTPAK.js → ssr-data-D6T6Y3ef.js} +4 -26
- package/dist/_chunks/ssr-data-D6T6Y3ef.js.map +1 -0
- package/dist/_chunks/state-FippDgxN.js +52 -0
- package/dist/_chunks/state-FippDgxN.js.map +1 -0
- package/dist/_chunks/status-page-marker-DwQBrLBz.js +496 -0
- package/dist/_chunks/status-page-marker-DwQBrLBz.js.map +1 -0
- package/dist/_chunks/{walkers-DNX05dC0.js → walkers-B6XUtmqK.js} +2 -2
- package/dist/_chunks/{walkers-DNX05dC0.js.map → walkers-B6XUtmqK.js.map} +1 -1
- package/dist/analyze/crawl-entry.js +2 -2
- package/dist/analyze/graph-command.js +2 -2
- package/dist/cache/index.js +1 -1
- package/dist/cli.js +2 -2
- package/dist/client/browser-entry/action-dispatch.d.ts +6 -4
- package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
- package/dist/client/browser-entry/action-queue.d.ts +44 -0
- package/dist/client/browser-entry/action-queue.d.ts.map +1 -0
- package/dist/client/browser-entry/router-init.d.ts.map +1 -1
- package/dist/client/deny-last-resort.d.ts +29 -0
- package/dist/client/deny-last-resort.d.ts.map +1 -0
- package/dist/client/error-boundary.d.ts +48 -2
- package/dist/client/error-boundary.d.ts.map +1 -1
- package/dist/client/error-boundary.js +2 -2
- package/dist/client/history.d.ts +21 -2
- package/dist/client/history.d.ts.map +1 -1
- package/dist/client/index.js +34 -14
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.d.ts +1 -0
- package/dist/client/internal.d.ts.map +1 -1
- package/dist/client/internal.js +272 -1225
- package/dist/client/internal.js.map +1 -1
- package/dist/client/link.d.ts.map +1 -1
- package/dist/client/navigation-commit.d.ts +12 -19
- package/dist/client/navigation-commit.d.ts.map +1 -1
- package/dist/client/navigation-transition.d.ts +62 -11
- package/dist/client/navigation-transition.d.ts.map +1 -1
- package/dist/client/router-effects.d.ts +9 -8
- package/dist/client/router-effects.d.ts.map +1 -1
- package/dist/client/router-lifecycle.d.ts +60 -17
- package/dist/client/router-lifecycle.d.ts.map +1 -1
- package/dist/client/router-pipeline.d.ts +7 -4
- package/dist/client/router-pipeline.d.ts.map +1 -1
- package/dist/client/router-types.d.ts +63 -8
- package/dist/client/router-types.d.ts.map +1 -1
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/rsc-fetch.d.ts +0 -9
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/client/segment-cache.d.ts +23 -8
- package/dist/client/segment-cache.d.ts.map +1 -1
- package/dist/client/state.d.ts +16 -0
- package/dist/client/state.d.ts.map +1 -1
- package/dist/client/status-page-marker.d.ts +25 -0
- package/dist/client/status-page-marker.d.ts.map +1 -0
- package/dist/cookies/index.js +1 -1
- package/dist/dev-tools/holding-server.d.ts +4 -17
- package/dist/dev-tools/holding-server.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +74 -175
- package/dist/index.js.map +1 -1
- package/dist/plugins/dev-server.d.ts.map +1 -1
- package/dist/routing/codegen-write.d.ts.map +1 -1
- package/dist/routing/index.js +2 -2
- package/dist/routing/interception.d.ts +2 -2
- package/dist/routing/slot-placement.d.ts +2 -2
- package/dist/server/access-gate.d.ts +73 -1
- package/dist/server/access-gate.d.ts.map +1 -1
- package/dist/server/action-handler.d.ts.map +1 -1
- package/dist/server/actions.d.ts +16 -1
- package/dist/server/actions.d.ts.map +1 -1
- package/dist/server/als-registry.d.ts +3 -9
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/children-interception.d.ts +1 -1
- package/dist/server/default-status-page.d.ts +2 -2
- package/dist/server/default-status-page.d.ts.map +1 -1
- package/dist/server/deny-boundary.d.ts +15 -9
- package/dist/server/deny-boundary.d.ts.map +1 -1
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/error-boundary-wrapper.d.ts +21 -4
- package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
- package/dist/server/error-response-headers.d.ts +3 -0
- package/dist/server/error-response-headers.d.ts.map +1 -0
- package/dist/server/index.js +3 -3
- package/dist/server/index.js.map +1 -1
- package/dist/server/internal.d.ts +1 -2
- package/dist/server/internal.d.ts.map +1 -1
- package/dist/server/internal.js +2339 -2506
- package/dist/server/internal.js.map +1 -1
- package/dist/server/metadata-collector.d.ts +2 -5
- package/dist/server/metadata-collector.d.ts.map +1 -1
- package/dist/server/param-coercion.d.ts +10 -3
- package/dist/server/param-coercion.d.ts.map +1 -1
- package/dist/server/pipeline-outcome.d.ts.map +1 -1
- package/dist/server/pipeline-phases.d.ts +11 -0
- package/dist/server/pipeline-phases.d.ts.map +1 -1
- package/dist/server/port-resolution.d.ts +3 -89
- package/dist/server/port-resolution.d.ts.map +1 -1
- package/dist/server/primitives.d.ts +38 -10
- package/dist/server/primitives.d.ts.map +1 -1
- package/dist/server/response-cache-policy.d.ts +3 -0
- package/dist/server/response-cache-policy.d.ts.map +1 -0
- package/dist/server/route-element-builder.d.ts +11 -41
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/dist/server/route-element-helpers.d.ts +12 -0
- package/dist/server/route-element-helpers.d.ts.map +1 -0
- package/dist/server/route-module-loader.d.ts +37 -0
- package/dist/server/route-module-loader.d.ts.map +1 -0
- package/dist/server/rsc-cache-key-guard.d.ts.map +1 -1
- package/dist/server/rsc-entry/action-middleware-runner.d.ts.map +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-payload.d.ts +22 -1
- package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-stream.d.ts +4 -11
- package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
- package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
- package/dist/server/rsc-error-envelope.d.ts +11 -0
- package/dist/server/rsc-error-envelope.d.ts.map +1 -0
- package/dist/server/skippable-prefix.d.ts +18 -15
- package/dist/server/skippable-prefix.d.ts.map +1 -1
- package/dist/server/slot-resolver.d.ts.map +1 -1
- package/dist/server/slot-subtree-contain.d.ts +54 -0
- package/dist/server/slot-subtree-contain.d.ts.map +1 -0
- package/dist/server/stream-utils.d.ts.map +1 -1
- package/dist/server/utils/element-type.d.ts +10 -0
- package/dist/server/utils/element-type.d.ts.map +1 -1
- package/dist/shared/rsc-error-envelope.d.ts +0 -9
- package/dist/shared/rsc-error-envelope.d.ts.map +1 -1
- package/dist/shared/status-reason-phrase.d.ts +26 -0
- package/dist/shared/status-reason-phrase.d.ts.map +1 -0
- package/docs/api/30-api-server.mdx +4 -2
- package/docs/api/31-api-client.mdx +5 -1
- package/docs/api/36-cli.mdx +5 -3
- package/docs/learn/12-error-handling.mdx +5 -1
- package/package.json +10 -10
- package/src/client/browser-entry/action-dispatch.ts +166 -99
- package/src/client/browser-entry/action-queue.ts +90 -0
- package/src/client/browser-entry/router-init.ts +60 -35
- package/src/client/deny-last-resort.tsx +54 -0
- package/src/client/error-boundary.tsx +144 -42
- package/src/client/history.ts +52 -3
- package/src/client/internal.ts +1 -0
- package/src/client/link.tsx +70 -35
- package/src/client/navigation-commit.ts +79 -27
- package/src/client/navigation-transition.ts +176 -127
- package/src/client/router-effects.ts +14 -17
- package/src/client/router-lifecycle.ts +181 -115
- package/src/client/router-pipeline.ts +94 -71
- package/src/client/router-types.ts +61 -7
- package/src/client/router.ts +147 -74
- package/src/client/rsc-fetch.ts +0 -13
- package/src/client/segment-cache.ts +43 -10
- package/src/client/state.ts +26 -0
- package/src/client/status-page-marker.tsx +32 -0
- package/src/dev-tools/holding-server.ts +4 -17
- package/src/index.ts +18 -34
- package/src/plugins/dev-server.ts +2 -1
- package/src/react-canary.d.ts +2 -0
- package/src/routing/codegen-write.ts +2 -0
- package/src/routing/interception.ts +2 -2
- package/src/routing/slot-placement.ts +2 -2
- package/src/server/access-gate.tsx +89 -21
- package/src/server/action-client.ts +2 -2
- package/src/server/action-handler.ts +23 -10
- package/src/server/actions.ts +81 -34
- package/src/server/als-registry.ts +3 -9
- package/src/server/children-interception.ts +1 -1
- package/src/server/default-status-page.ts +7 -47
- package/src/server/deny-boundary.ts +45 -28
- package/src/server/deny-renderer.ts +6 -2
- package/src/server/error-boundary-wrapper.ts +23 -4
- package/src/server/error-response-headers.ts +18 -0
- package/src/server/internal.ts +2 -10
- package/src/server/metadata-collector.ts +3 -18
- package/src/server/param-coercion.ts +13 -4
- package/src/server/pipeline-outcome.ts +35 -13
- package/src/server/pipeline-phases.ts +22 -16
- package/src/server/port-resolution.ts +3 -165
- package/src/server/prebuilt-builder.ts +4 -4
- package/src/server/primitives.ts +75 -11
- package/src/server/response-cache-policy.ts +45 -0
- package/src/server/route-element-builder.ts +149 -412
- package/src/server/route-element-helpers.ts +37 -0
- package/src/server/route-handler.ts +2 -2
- package/src/server/route-module-loader.ts +161 -0
- package/src/server/rsc-cache-key-guard.ts +2 -42
- package/src/server/rsc-entry/action-middleware-runner.ts +4 -4
- package/src/server/rsc-entry/api-handler.ts +5 -5
- package/src/server/rsc-entry/error-renderer.ts +3 -4
- package/src/server/rsc-entry/helpers.ts +1 -1
- package/src/server/rsc-entry/index.ts +3 -3
- package/src/server/rsc-entry/render-route.ts +4 -8
- package/src/server/rsc-entry/rsc-payload.ts +59 -42
- package/src/server/rsc-entry/rsc-stream.ts +48 -27
- package/src/server/rsc-entry/ssr-renderer.ts +6 -10
- package/src/server/rsc-error-envelope.ts +18 -0
- package/src/server/skippable-prefix.ts +105 -7
- package/src/server/slot-resolver.ts +43 -12
- package/src/server/slot-subtree-contain.ts +255 -0
- package/src/server/stream-utils.ts +12 -8
- package/src/server/utils/element-type.ts +18 -2
- package/src/shared/rsc-error-envelope.ts +0 -15
- package/src/shared/status-reason-phrase.ts +61 -0
- package/dist/_chunks/actions-BS-m5SLv.js.map +0 -1
- package/dist/_chunks/convention-lint-n3RJLgww.js.map +0 -1
- package/dist/_chunks/error-boundary-BvRCCmbN.js +0 -353
- package/dist/_chunks/error-boundary-BvRCCmbN.js.map +0 -1
- package/dist/_chunks/logger-DDirEsn7.js.map +0 -1
- package/dist/_chunks/mdx-file-CXyHGUpS.js +0 -25
- package/dist/_chunks/mdx-file-CXyHGUpS.js.map +0 -1
- package/dist/_chunks/router-ref-8gr8qsxN.js +0 -28
- package/dist/_chunks/router-ref-8gr8qsxN.js.map +0 -1
- package/dist/_chunks/rsc-error-envelope-tT5PJs4q.js +0 -40
- package/dist/_chunks/rsc-error-envelope-tT5PJs4q.js.map +0 -1
- package/dist/_chunks/scanner-tdFPvDYi.js.map +0 -1
- package/dist/_chunks/segment-keys-BhqoHiLc.js.map +0 -1
- package/dist/_chunks/ssr-data-BQGhTPAK.js.map +0 -1
- package/dist/server/tree-builder.d.ts +0 -150
- package/dist/server/tree-builder.d.ts.map +0 -1
- package/src/server/tree-builder.ts +0 -313
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"navigation-root-B00jjGd5.js","names":[],"sources":["../../src/client/use-pending-navigation.ts","../../src/client/navigation-context.ts","../../src/client/top-loader.tsx","../../src/client/navigation-root.tsx"],"sourcesContent":["import { useSyncExternalStore } from 'react';\nimport { getRouterOrNull } from './router-ref.ts';\n\nfunction subscribe(onStoreChange: () => void): () => void {\n const router = getRouterOrNull();\n if (!router) return () => {};\n return router.onPendingChange(onStoreChange);\n}\n\nfunction getSnapshot(): boolean {\n const router = getRouterOrNull();\n return router ? router.isPending() : false;\n}\n\nconst getServerSnapshot = getSnapshot;\n\n/**\n * Returns true while an RSC navigation is in flight.\n *\n * Reads from the router's external pending store via useSyncExternalStore.\n * Only components that call this hook re-render when pending state\n * changes — no full-tree re-render.\n *\n * ```tsx\n * 'use client'\n * import { usePendingNavigation } from '@timber-js/app/client'\n *\n * export function NavBar() {\n * const isPending = usePendingNavigation()\n * return (\n * <nav className={isPending ? 'opacity-50' : ''}>\n * <Link href=\"/dashboard\">Dashboard</Link>\n * </nav>\n * )\n * }\n * ```\n */\nexport function usePendingNavigation(): boolean {\n return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\n}\n","'use client';\n\n/**\n * NavigationContext — React context for navigation state.\n *\n * Holds the current pathname and search, updated atomically with the RSC\n * tree on each navigation. This replaces the previous useSyncExternalStore\n * approach for usePathname() and useSearchParams(), which suffered from a\n * timing gap: the new tree could commit before the external store\n * re-renders fired, causing a frame where both old and new active states\n * were visible simultaneously.\n *\n * Segment params are NOT here — see the note on NavigationState below.\n *\n * By wrapping the RSC payload element in NavigationProvider inside\n * renderRoot(), the context value and the element tree are passed to\n * reactRoot.render() in the same call — atomic by construction.\n * All consumers (usePathname, useSearchParams) see the new values in the\n * same render pass as the new tree.\n *\n * During SSR, the wrapper chain mounts this same provider with the\n * request's pathname and raw search string (TIM-1424), so the hooks\n * resolve through the identical code path on both sides. Per-request\n * isolation is by construction — the value is part of the request's\n * element tree.\n *\n * This module is `'use client'` and is never evaluated in the RSC\n * environment. The shims plugin resolves next/navigation to\n * navigation-rsc.ts in RSC, which has throwing stubs (TIM-1420).\n *\n * SINGLETON GUARANTEE: All shared mutable state uses globalThis via\n * Symbol.for keys. The RSC client bundler can duplicate this module\n * across chunks (browser-entry graph + client-reference graph). With\n * ESM output, each chunk gets its own module scope — module-level\n * variables would create separate singleton instances per chunk.\n * globalThis guarantees a single instance regardless of duplication.\n *\n * This workaround will be removed when Rolldown ships `format: 'app'`\n * (module registry format that deduplicates like webpack/Turbopack).\n * See design/27-chunking-strategy.md.\n *\n * See design/19-client-navigation.md §\"NavigationContext\"\n */\n\nimport { createContext, useContext, createElement, type ReactNode } from 'react';\nimport type { SlotParamsRecord } from '../shared/slot-params.ts';\n\n// ---------------------------------------------------------------------------\n// Context type\n// ---------------------------------------------------------------------------\n\n/**\n * What the browser knows about the current location.\n *\n * Params are deliberately absent: they are a property of the tree the server\n * rendered, not of the address bar, and they now travel inside the payload as\n * the payload root. Keeping a copy here meant the router had to thread\n * the record from a response header into context on every path — navigation,\n * traversal, prefetch, revalidation — and each of those was a place to drop it\n * (TIM-1294).\n */\nexport interface NavigationState {\n pathname: string;\n search: string;\n}\n\n// ---------------------------------------------------------------------------\n// Lazy context initialization\n// ---------------------------------------------------------------------------\n\n/**\n * Context instances are stored on globalThis (NOT in module-level\n * variables) because the ESM bundler can duplicate this module across\n * chunks. Module-level variables would create separate instances per\n * chunk — the provider in NavigationRoot (index chunk) would use\n * context A while the consumer in usePendingNavigation (shared chunk)\n * reads from context B. globalThis guarantees a single instance.\n *\n * This module is `'use client'` so it is never evaluated in the RSC\n * environment — `createContext` and `useContext` are always available.\n * The shims plugin resolves next/navigation to navigation-rsc.ts in RSC,\n * which has throwing stubs for the hooks (TIM-1420).\n *\n * See design/27-chunking-strategy.md §\"Singleton Safety\"\n */\n\nconst NAV_CTX_KEY = Symbol.for('__timber_nav_ctx');\n\nfunction getOrCreateContext(): import('react').Context<NavigationState | null> {\n const existing = (globalThis as Record<symbol, unknown>)[NAV_CTX_KEY] as\n | import('react').Context<NavigationState | null>\n | undefined;\n if (existing !== undefined) return existing;\n const ctx = createContext<NavigationState | null>(null);\n (globalThis as Record<symbol, unknown>)[NAV_CTX_KEY] = ctx;\n return ctx;\n}\n\n/**\n * Read the navigation context. Returns null only when no provider is above\n * the caller — a component rendered outside the timber app.\n *\n * Internal — used by usePathname() and useSearchParams().\n */\nexport function useNavigationContext(): NavigationState | null {\n return useContext(getOrCreateContext());\n}\n\n// ---------------------------------------------------------------------------\n// Provider component\n// ---------------------------------------------------------------------------\n\nexport interface NavigationProviderProps {\n value: NavigationState;\n children?: ReactNode;\n}\n\n/**\n * Wraps children with NavigationContext.Provider.\n *\n * Used in browser-entry.ts renderRoot to wrap the RSC payload element\n * so that navigation state updates atomically with the tree render.\n */\nexport function NavigationProvider({\n value,\n children,\n}: NavigationProviderProps): import('react').ReactElement {\n return createElement(getOrCreateContext().Provider, { value }, children);\n}\n\n// ---------------------------------------------------------------------------\n// Module-level state for renderRoot to read\n// ---------------------------------------------------------------------------\n\n/**\n * Navigation state communicated between the router and renderRoot.\n *\n * The router calls setNavigationState() before renderRoot(). The\n * renderRoot callback reads via getNavigationState() to create the\n * NavigationProvider with the correct params/pathname.\n *\n * This is NOT used by hooks directly — hooks read from React context.\n *\n * Stored on globalThis (like the context instances above) because the\n * router lives in one chunk while renderRoot lives in another. Module-\n * level variables would be separate per chunk.\n */\nconst NAV_STATE_KEY = Symbol.for('__timber_nav_state');\n\nfunction _getNavStateStore(): { current: NavigationState } {\n const g = globalThis as Record<symbol, unknown>;\n if (!g[NAV_STATE_KEY]) {\n g[NAV_STATE_KEY] = { current: { pathname: '/', search: '' } };\n }\n return g[NAV_STATE_KEY] as { current: NavigationState };\n}\n\nexport function setNavigationState(state: NavigationState): void {\n _getNavStateStore().current = state;\n}\n\nexport function getNavigationState(): NavigationState {\n return _getNavStateStore().current;\n}\n\n// ---------------------------------------------------------------------------\n// Pending navigation state lives in the router, not here\n// ---------------------------------------------------------------------------\n\n/**\n * There was a second React context here — `PendingNavigationContext`, holding\n * the in-flight navigation URL, provided by `NavigationRoot` out of a\n * `useState` — plus a `usePendingNavigationUrl()` reader for it. Both are gone\n * (TIM-1307).\n *\n * Nothing read them. `usePendingNavigation()` (use-pending-navigation.ts) and\n * `TopLoader` both subscribe to the router's external pending store via\n * `useSyncExternalStore`: sync priority, immune to transition entanglement,\n * and cleared by `runNavigation`'s supersession-guarded `finally` (TIM-1034).\n * The context was a parallel representation of the same fact that no consumer\n * ever migrated to, and one that could not even agree with the store — a\n * navigation superseded by a cached popstate replay left its URL set until the\n * next full navigation, because the staleness guard skipped the clear and no\n * other path touched it.\n *\n * One representation, and it is the router's. See\n * design/19-client-navigation.md §\"How Pending State Works\".\n */\n","/**\n * TopLoader — Built-in progress bar for client navigations.\n *\n * Shows an animated progress bar at the top of the viewport while an RSC\n * navigation is in flight. Injected automatically by the framework into\n * NavigationRoot — users never render this component directly.\n *\n * Configuration is via timber.config.ts `topLoader` key. Enabled by default.\n * Users who want a fully custom progress indicator disable the built-in one\n * (`topLoader: { enabled: false }`) and use `usePendingNavigation()` directly.\n *\n * Animation approach: pure CSS @keyframes. The bar crawls from 0% to ~90%\n * width over ~30s using ease-out timing. When navigation completes, the bar\n * snaps to 100% and fades out over 200ms. No JS animation loops (RAF, setInterval).\n *\n * Phase transitions are derived synchronously during render (React's\n * getDerivedStateFromProps pattern) — no useEffect needed for state tracking.\n * The finishing → hidden cleanup uses onTransitionEnd from the CSS transition.\n *\n * When delay > 0, CSS animation-delay + a visibility keyframe ensure the bar\n * stays invisible during the delay period. If navigation finishes before the\n * delay, the bar was never visible so the finish transition is also invisible.\n *\n * See design/19-client-navigation.md §\"usePendingNavigation()\"\n * See LOCAL-336 for design decisions.\n */\n\n'use client';\n\nimport { useState, createElement } from 'react';\nimport { usePendingNavigation } from './use-pending-navigation.ts';\n\n// ─── Types ───────────────────────────────────────────────────────\n\nexport interface TopLoaderConfig {\n /** Whether the top-loader is enabled. Default: true. */\n enabled?: boolean;\n /** Bar color. Default: '#2299DD'. */\n color?: string;\n /** Bar height in pixels. Default: 3. */\n height?: number;\n /** Show subtle glow/shadow effect. Default: false. */\n shadow?: boolean;\n /** Delay in ms before showing the bar. Default: 0. */\n delay?: number;\n /** CSS z-index. Default: 1600. */\n zIndex?: number;\n}\n\n// ─── Defaults ────────────────────────────────────────────────────\n\nconst DEFAULT_COLOR = '#2299DD';\nconst DEFAULT_HEIGHT = 3;\nconst DEFAULT_SHADOW = false;\nconst DEFAULT_DELAY = 0;\nconst DEFAULT_Z_INDEX = 1600;\n\n// ─── Keyframes ───────────────────────────────────────────────────\n\n// Unique keyframes name to avoid collisions with user styles.\nconst CRAWL_KEYFRAMES = '__timber_top_loader_crawl';\nconst APPEAR_KEYFRAMES = '__timber_top_loader_appear';\nconst FINISH_KEYFRAMES = '__timber_top_loader_finish';\n\n// Track whether the @keyframes rules have been injected into the document.\nlet keyframesInjected = false;\n\n/**\n * Inject the @keyframes rules into the document head once.\n * Called during render (idempotent). Uses a <style> tag so the\n * animations are available for inline-styled elements.\n */\nfunction ensureKeyframes(): void {\n if (keyframesInjected) return;\n if (typeof document === 'undefined') return;\n\n const style = document.createElement('style');\n style.textContent = `\n@keyframes ${CRAWL_KEYFRAMES} {\n 0% { width: 0%; }\n 100% { width: 90%; }\n}\n@keyframes ${APPEAR_KEYFRAMES} {\n from { opacity: 0; }\n to { opacity: 1; }\n}\n@keyframes ${FINISH_KEYFRAMES} {\n 0% { width: 90%; opacity: 1; }\n 50% { width: 100%; opacity: 1; }\n 100% { width: 100%; opacity: 0; }\n}\n`;\n document.head.appendChild(style);\n keyframesInjected = true;\n}\n\n// ─── Component ───────────────────────────────────────────────────\n\n/**\n * Internal top-loader component. Injected by NavigationRoot.\n *\n * Reads pending navigation state from the router's external store, via\n * usePendingNavigation() — the one pending representation (TIM-1307).\n * Phase transitions are derived synchronously during render:\n *\n * hidden → crawling: when isPending becomes true\n * crawling → finishing: when isPending becomes false\n * finishing → hidden: when CSS transition ends (onTransitionEnd)\n * finishing → crawling: when isPending becomes true again\n *\n * No useEffect — all state changes are either derived during render\n * (getDerivedStateFromProps pattern) or triggered by DOM events.\n */\nexport function TopLoader({ config }: { config?: TopLoaderConfig }): React.ReactElement | null {\n // Read pending state from the router's external store via\n // useSyncExternalStore (inside usePendingNavigation). Only this\n // component re-renders when pending changes — no full-tree re-render.\n const isPending = usePendingNavigation();\n\n const color = config?.color ?? DEFAULT_COLOR;\n const height = config?.height ?? DEFAULT_HEIGHT;\n const shadow = config?.shadow ?? DEFAULT_SHADOW;\n const delay = config?.delay ?? DEFAULT_DELAY;\n const zIndex = config?.zIndex ?? DEFAULT_Z_INDEX;\n\n const [phase, setPhase] = useState<'hidden' | 'crawling' | 'finishing'>('hidden');\n\n // ─── Synchronous phase derivation (getDerivedStateFromProps) ──\n // React allows setState during render if the value changes — it\n // immediately re-renders with the updated state before committing.\n\n if (isPending && (phase === 'hidden' || phase === 'finishing')) {\n setPhase('crawling');\n }\n if (!isPending && phase === 'crawling') {\n setPhase('finishing');\n }\n\n // Inject keyframes on first visible render (idempotent)\n if (phase !== 'hidden') {\n ensureKeyframes();\n }\n\n if (phase === 'hidden') return null;\n\n // ─── Styles ──────────────────────────────────────────────────\n\n const containerStyle: React.CSSProperties = {\n position: 'fixed',\n top: 0,\n left: 0,\n width: '100%',\n height: `${height}px`,\n zIndex,\n pointerEvents: 'none',\n };\n\n const barStyle: React.CSSProperties = {\n height: '100%',\n backgroundColor: color,\n ...(phase === 'crawling'\n ? {\n // Crawl from 0% to 90% over 30s. When delay > 0, both the crawl\n // and a visibility animation are delayed — the bar stays at width 0%\n // and opacity 0 during the delay, then appears and starts crawling.\n // With delay 0, the appear animation is instant (0s duration, no delay).\n animation: [\n `${CRAWL_KEYFRAMES} 30s ease-out ${delay}ms forwards`,\n `${APPEAR_KEYFRAMES} 0s ${delay}ms both`,\n ].join(', '),\n }\n : {\n // Finishing: fill to 100% then fade out via a keyframe animation.\n // We use a keyframe instead of a CSS transition because the\n // animation-to-transition handoff is unreliable — the browser\n // may not capture the animated width as the transition's \"from\"\n // value when both the animation removal and transition are\n // applied in the same render frame.\n animation: `${FINISH_KEYFRAMES} 400ms ease forwards`,\n }),\n ...(shadow\n ? {\n boxShadow: `0 0 10px ${color}, 0 0 5px ${color}`,\n }\n : {}),\n };\n\n // Clean up the finishing phase when the finish animation completes.\n const handleAnimationEnd =\n phase === 'finishing'\n ? (e: React.AnimationEvent) => {\n if (e.animationName === FINISH_KEYFRAMES) {\n setPhase('hidden');\n }\n }\n : undefined;\n\n return createElement(\n 'div',\n {\n 'style': containerStyle,\n 'aria-hidden': 'true',\n 'data-timber-top-loader': '',\n },\n createElement('div', { style: barStyle, onAnimationEnd: handleAnimationEnd })\n );\n}\n","/**\n * NavigationRoot — the component the router renders the page through.\n *\n * It is stateless. The router owns the displayed tree and drives React with\n * `root.render(<NavigationRoot rendered={{ element, publish }} />)` inside a\n * synchronous `startTransition` (see `client/react-root.ts`). A transition\n * update keeps the committed tree on screen while the incoming one resolves,\n * instead of replacing it with a Suspense fallback — the reason this\n * component exists (TIM-1306).\n *\n * It used to hold the tree in `useState` and register `setState` closures\n * into module globals during render so the router could reach into it, with a\n * second set of stand-in closures for the window before a root existed\n * (TIM-600). Inverting that — router calls React, not the reverse — deleted\n * all of it (TIM-1431). Next.js's `use-action-queue.ts` says it wants this\n * shape and cannot have it because it must decode Flight during render;\n * timber decodes in `router-pipeline.ts`, so nothing stands in the way.\n *\n * What this component does own is **the commit-time publish**: a navigation's\n * state (segment cache, address bar, history stack, pathname) is published\n * from a layout effect keyed on the tree object, so the event that puts a\n * route on screen is the event that makes it current (TIM-1301). Props work\n * for that exactly as state did.\n *\n * This component holds no pending state. The `TopLoader` it renders and the\n * public `usePendingNavigation()` both subscribe to the router's external\n * pending store; NavigationRoot's own `pendingUrl` was a second, unread\n * representation of the same fact and is gone (TIM-1307).\n *\n * Hard navigation guard: When a hard navigation is triggered (500 error,\n * version skew), the component throws an unresolved thenable AFTER all\n * hooks to suspend forever — preventing React from rendering children\n * during page teardown. The throw must come after hooks to satisfy\n * React's rules (same hook count every render) while still preventing\n * child renders that could hit hook count mismatches in components\n * whose positions shift during teardown. This pattern is borrowed from\n * Next.js (app-router.tsx pushRef.mpaNavigation — also after hooks).\n *\n * See design/05-streaming.md §\"deferSuspenseFor\"\n * See design/19-client-navigation.md §\"NavigationContext\"\n */\n\nimport { createElement, Fragment, useLayoutEffect, useRef, type ReactNode } from 'react';\nimport { TopLoader, type TopLoaderConfig } from './top-loader.tsx';\n\n// ─── Rendered Tree ──────────────────────────────────────────────\n\n/**\n * The tree the router has handed to React, and the state publish that belongs\n * to it. They travel as one object so React's commit of the tree is the event\n * that publishes — see the effect in NavigationRoot. Every `render` builds a\n * fresh one, which is what keys that effect: a tree is published once, however\n * many times React re-runs the effect for it.\n */\nexport interface RenderedTree {\n element: ReactNode;\n /**\n * Publishes the navigation's state — segment cache, pathname, address bar,\n * history stack, and the client's record of the mounted tree — and\n * announces it to listeners outside React. Runs when React commits\n * `element`, never before (TIM-1301). Null for a render that has nothing to\n * publish on commit (hydration, the shallow search re-wrap).\n */\n publish: (() => void) | null;\n}\n\n/**\n * Hand a tree to React in a transition. `publish` runs when React commits it.\n *\n * Every path that puts a page on screen goes through one of these — a\n * navigation, a revalidation, a popstate replay, a shallow search re-wrap —\n * and it is always a synchronous `startTransition` around `root.render`.\n * The production one is `createReactRoot().render`.\n */\nexport type NavigationRender = (element: ReactNode, publish: (() => void) | null) => void;\n\n// ─── Hard Navigation Guard ──────────────────────────────────────\n\n/**\n * Module-level flag indicating a hard (MPA) navigation is in progress.\n *\n * When true:\n * - NavigationRoot throws an unresolved thenable to suspend forever,\n * preventing React from rendering children during page teardown\n * (avoids \"Rendered more hooks\" crashes).\n * - The Navigation API handler skips interception, letting the browser\n * perform a full page load (prevents infinite loops where\n * window.location.href → navigate event → router.navigate → 500 →\n * window.location.href → ...).\n *\n * Uses globalThis for singleton guarantee across chunks (same pattern\n * as NavigationContext). See design/19-client-navigation.md §\"Singleton\n * Guarantee via globalThis\".\n */\nconst HARD_NAV_KEY = Symbol.for('__timber_hard_navigating');\n\nfunction getHardNavStore(): { value: boolean } {\n const g = globalThis as Record<symbol, unknown>;\n if (!g[HARD_NAV_KEY]) {\n g[HARD_NAV_KEY] = { value: false };\n }\n return g[HARD_NAV_KEY] as { value: boolean };\n}\n\n/**\n * Set the hard-navigating flag. Call this BEFORE setting\n * window.location.href or window.location.reload() to prevent:\n * 1. React from rendering children during page teardown\n * 2. Navigation API from intercepting the hard navigation\n */\nexport function setHardNavigating(value: boolean): void {\n getHardNavStore().value = value;\n}\n\n/**\n * Check if a hard navigation is in progress.\n * Used by NavigationRoot (throw unresolvedThenable) and by the\n * Navigation API handler (skip interception).\n */\nexport function isHardNavigating(): boolean {\n return getHardNavStore().value;\n}\n\n/**\n * A thenable that never resolves. When thrown during React render,\n * it causes the component to suspend forever — React keeps the\n * old committed tree visible and never attempts to render children.\n *\n * This is the same pattern Next.js uses in app-router.tsx for MPA\n * navigations (pushRef.mpaNavigation → throw unresolvedThenable).\n */\n// for React's Suspense mechanism. Same pattern as Next.js's unresolvedThenable.\n// eslint-disable-next-line unicorn/no-thenable -- Intentionally a never-resolving thenable\nconst unresolvedThenable = { then() {} } as PromiseLike<never>;\n\n// ─── Component ───────────────────────────────────────────────────\n\n/**\n * Root component the router renders the page through.\n *\n * Renders the TopLoader alongside the tree it is given. Neither adds a DOM\n * element on the hydration path, so the tree matches the server HTML.\n *\n * Rendered only by `createReactRoot` (client/react-root.ts): hydration passes\n * `{ element, publish: null }`, and every later render is a synchronous\n * `startTransition(() => root.render(...))` with a fresh `rendered` object.\n */\nexport function NavigationRoot({\n rendered,\n topLoaderConfig,\n}: {\n rendered: RenderedTree;\n topLoaderConfig?: TopLoaderConfig;\n}): ReactNode {\n // Publish the navigation's state when React commits its tree — not when the\n // tree is handed over. A tree React is still waiting on, or one a later\n // navigation replaces before React renders it, has been *given* to React\n // without being on screen; publishing then describes a route the user never\n // saw, which is the defect this ordering exists to prevent (TIM-1301).\n //\n // A **layout** effect, so the publish lands before the browser paints and,\n // more importantly, before every descendant's passive effect. A destination\n // component that navigates from a mount effect — a redirect guard, a\n // `refresh()` on mount — would otherwise start its navigation while the\n // segment cache, address bar and pathname still described the departing\n // route, and send an X-Timber-State-Tree for a tree that is no longer\n // mounted (codex on #998).\n //\n // Descendant *layout* effects still run first: React runs layout effects\n // child-before-parent, and the only way to precede them would be a fiber\n // rendered as an earlier sibling of the payload. That is not free here —\n // `server/ssr-wrappers.tsx` mirrors this component's fiber shape so `useId`\n // agrees across hydration, and an extra sibling shifts every id in the\n // payload subtree. A layout effect that navigates on mount is the price.\n //\n // Keyed on the tree object rather than on the effect run. A publish moves\n // the address bar, which is not idempotent, so any second invocation for the\n // same tree — an effect re-run React is entitled to perform, StrictMode's\n // double-invoke — must be a no-op rather than a second history entry.\n const publishedRef = useRef<RenderedTree | null>(null);\n useLayoutEffect(() => {\n if (publishedRef.current === rendered) return;\n publishedRef.current = rendered;\n rendered.publish?.();\n }, [rendered]);\n\n // ─── Hard navigation guard ─────────────────────────────────\n // When a hard navigation is in progress (500 error, version skew),\n // suspend forever to prevent React from rendering children during\n // page teardown. This avoids \"Rendered more hooks\" crashes in\n // CHILD components whose hook counts may shift during teardown.\n //\n // CRITICAL: This throw MUST come AFTER all hooks (the useRef and\n // useLayoutEffect above). React requires the same hooks to run on every\n // render. If we threw before hooks, React would see 0 hooks on the\n // re-render vs 2 on the initial render — triggering the exact \"Rendered\n // more hooks\" error we're trying to prevent.\n //\n // By placing it after hooks but before the return, all hooks\n // satisfy React's rules, but the thrown thenable prevents any\n // children from rendering. Same pattern as Next.js app-router.tsx\n // (pushRef.mpaNavigation — also placed after all hooks).\n if (isHardNavigating()) {\n throw unresolvedThenable;\n }\n\n // Inject TopLoader alongside the element tree. It subscribes to the router's\n // pending store itself, so it takes no props from here beyond its config,\n // and only it re-renders when a navigation starts or ends. Rendered only\n // when not explicitly disabled via config.\n //\n // This Fragment is a FORK — two children — so it advances React's tree\n // context and shifts every `useId` below it. `server/ssr-wrappers.tsx`\n // mirrors it for exactly that reason; a single-child wrapper would not need\n // a mirror. See tests/ssr-wrapper-parity.test.ts.\n const showTopLoader = topLoaderConfig?.enabled !== false;\n if (!showTopLoader) return rendered.element;\n return createElement(\n Fragment,\n null,\n createElement(TopLoader, { config: topLoaderConfig }),\n rendered.element\n );\n}\n"],"mappings":";;;AAGA,SAAS,UAAU,eAAuC;CACxD,MAAM,SAAS,gBAAgB;CAC/B,IAAI,CAAC,QAAQ,aAAa,CAAC;CAC3B,OAAO,OAAO,gBAAgB,aAAa;AAC7C;AAEA,SAAS,cAAuB;CAC9B,MAAM,SAAS,gBAAgB;CAC/B,OAAO,SAAS,OAAO,UAAU,IAAI;AACvC;AAEA,IAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;AAuB1B,SAAgB,uBAAgC;CAC9C,OAAO,qBAAqB,WAAW,aAAa,iBAAiB;AACvE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC+CA,IAAM,cAAc,OAAO,IAAI,kBAAkB;AAEjD,SAAS,qBAAsE;CAC7E,MAAM,WAAY,WAAuC;CAGzD,IAAI,aAAa,KAAA,GAAW,OAAO;CACnC,MAAM,MAAM,cAAsC,IAAI;CACtD,WAAwC,eAAe;CACvD,OAAO;AACT;;;;;;;AAQA,SAAgB,uBAA+C;CAC7D,OAAO,WAAW,mBAAmB,CAAC;AACxC;;;;;;;AAiBA,SAAgB,mBAAmB,EACjC,OACA,YACwD;CACxD,OAAO,cAAc,mBAAmB,CAAC,CAAC,UAAU,EAAE,MAAM,GAAG,QAAQ;AACzE;;;;;;;;;;;;;;AAmBA,IAAM,gBAAgB,OAAO,IAAI,oBAAoB;AAErD,SAAS,oBAAkD;CACzD,MAAM,IAAI;CACV,IAAI,CAAC,EAAE,gBACL,EAAE,iBAAiB,EAAE,SAAS;EAAE,UAAU;EAAK,QAAQ;CAAG,EAAE;CAE9D,OAAO,EAAE;AACX;AAEA,SAAgB,mBAAmB,OAA8B;CAC/D,kBAAkB,CAAC,CAAC,UAAU;AAChC;AAEA,SAAgB,qBAAsC;CACpD,OAAO,kBAAkB,CAAC,CAAC;AAC7B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AErEA,IAAM,eAAe,OAAO,IAAI,0BAA0B;AAE1D,SAAS,kBAAsC;CAC7C,MAAM,IAAI;CACV,IAAI,CAAC,EAAE,eACL,EAAE,gBAAgB,EAAE,OAAO,MAAM;CAEnC,OAAO,EAAE;AACX;;;;;;;AAQA,SAAgB,kBAAkB,OAAsB;CACtD,gBAAgB,CAAC,CAAC,QAAQ;AAC5B"}
|
|
1
|
+
{"version":3,"file":"navigation-root-mHSK9psY.js","names":[],"sources":["../../src/client/router-ref.ts","../../src/client/use-pending-navigation.ts","../../src/client/top-loader.tsx","../../src/client/navigation-root.tsx"],"sourcesContent":["// Global router reference — shared between browser-entry and client hooks.\n//\n// Delegates to client/state.ts for the actual module-level variable.\n// This ensures singleton semantics regardless of import path — all\n// callers converge on the same state.ts instance via the barrel.\n//\n// See design/18-build-system.md §\"Module Singleton Strategy\"\n\nimport type { RouterInstance } from './router-types.ts';\nimport { globalRouter, _setGlobalRouter } from './state.ts';\n\n/**\n * Set the global router instance. Called once during bootstrap.\n */\nexport function setGlobalRouter(router: RouterInstance): void {\n _setGlobalRouter(router);\n}\n\n/**\n * Get the global router instance. Throws if called before bootstrap.\n * Used by client-side hooks (usePendingNavigation, etc.)\n */\nexport function getRouter(): RouterInstance {\n if (!globalRouter) {\n throw new Error('[timber] Router not initialized. getRouter() was called before bootstrap().');\n }\n return globalRouter;\n}\n\n/**\n * Get the global router instance or null if not yet initialized.\n * Used by useRouter() methods to avoid silent failures — callers\n * can log a meaningful warning instead of silently no-oping.\n */\nexport function getRouterOrNull(): RouterInstance | null {\n return globalRouter;\n}\n\n/**\n * Reset the global router to null. Used only in tests to isolate\n * module-level state between test cases.\n * @internal\n */\nexport function resetGlobalRouter(): void {\n _setGlobalRouter(null);\n}\n","import { useSyncExternalStore } from 'react';\nimport { getRouterOrNull } from './router-ref.ts';\n\nfunction subscribe(onStoreChange: () => void): () => void {\n const router = getRouterOrNull();\n if (!router) return () => {};\n return router.onPendingChange(onStoreChange);\n}\n\nfunction getSnapshot(): boolean {\n const router = getRouterOrNull();\n return router ? router.isPending() : false;\n}\n\nconst getServerSnapshot = getSnapshot;\n\n/**\n * Returns true while an RSC navigation is in flight.\n *\n * Reads from the router's external pending store via useSyncExternalStore.\n * Only components that call this hook re-render when pending state\n * changes — no full-tree re-render.\n *\n * ```tsx\n * 'use client'\n * import { usePendingNavigation } from '@timber-js/app/client'\n *\n * export function NavBar() {\n * const isPending = usePendingNavigation()\n * return (\n * <nav className={isPending ? 'opacity-50' : ''}>\n * <Link href=\"/dashboard\">Dashboard</Link>\n * </nav>\n * )\n * }\n * ```\n */\nexport function usePendingNavigation(): boolean {\n return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\n}\n","/**\n * TopLoader — Built-in progress bar for client navigations.\n *\n * Shows an animated progress bar at the top of the viewport while an RSC\n * navigation is in flight. Injected automatically by the framework into\n * NavigationRoot — users never render this component directly.\n *\n * Configuration is via timber.config.ts `topLoader` key. Enabled by default.\n * Users who want a fully custom progress indicator disable the built-in one\n * (`topLoader: { enabled: false }`) and use `usePendingNavigation()` directly.\n *\n * Animation approach: pure CSS @keyframes. The bar crawls from 0% to ~90%\n * width over ~30s using ease-out timing. When navigation completes, the bar\n * snaps to 100% and fades out over 200ms. No JS animation loops (RAF, setInterval).\n *\n * Phase transitions are derived synchronously during render (React's\n * getDerivedStateFromProps pattern) — no useEffect needed for state tracking.\n * The finishing → hidden cleanup uses onTransitionEnd from the CSS transition.\n *\n * When delay > 0, CSS animation-delay + a visibility keyframe ensure the bar\n * stays invisible during the delay period. If navigation finishes before the\n * delay, the bar was never visible so the finish transition is also invisible.\n *\n * See design/19-client-navigation.md §\"usePendingNavigation()\"\n * See LOCAL-336 for design decisions.\n */\n\n'use client';\n\nimport { useState, createElement } from 'react';\nimport { usePendingNavigation } from './use-pending-navigation.ts';\n\n// ─── Types ───────────────────────────────────────────────────────\n\nexport interface TopLoaderConfig {\n /** Whether the top-loader is enabled. Default: true. */\n enabled?: boolean;\n /** Bar color. Default: '#2299DD'. */\n color?: string;\n /** Bar height in pixels. Default: 3. */\n height?: number;\n /** Show subtle glow/shadow effect. Default: false. */\n shadow?: boolean;\n /** Delay in ms before showing the bar. Default: 0. */\n delay?: number;\n /** CSS z-index. Default: 1600. */\n zIndex?: number;\n}\n\n// ─── Defaults ────────────────────────────────────────────────────\n\nconst DEFAULT_COLOR = '#2299DD';\nconst DEFAULT_HEIGHT = 3;\nconst DEFAULT_SHADOW = false;\nconst DEFAULT_DELAY = 0;\nconst DEFAULT_Z_INDEX = 1600;\n\n// ─── Keyframes ───────────────────────────────────────────────────\n\n// Unique keyframes name to avoid collisions with user styles.\nconst CRAWL_KEYFRAMES = '__timber_top_loader_crawl';\nconst APPEAR_KEYFRAMES = '__timber_top_loader_appear';\nconst FINISH_KEYFRAMES = '__timber_top_loader_finish';\n\n// Track whether the @keyframes rules have been injected into the document.\nlet keyframesInjected = false;\n\n/**\n * Inject the @keyframes rules into the document head once.\n * Called during render (idempotent). Uses a <style> tag so the\n * animations are available for inline-styled elements.\n */\nfunction ensureKeyframes(): void {\n if (keyframesInjected) return;\n if (typeof document === 'undefined') return;\n\n const style = document.createElement('style');\n style.textContent = `\n@keyframes ${CRAWL_KEYFRAMES} {\n 0% { width: 0%; }\n 100% { width: 90%; }\n}\n@keyframes ${APPEAR_KEYFRAMES} {\n from { opacity: 0; }\n to { opacity: 1; }\n}\n@keyframes ${FINISH_KEYFRAMES} {\n 0% { width: 90%; opacity: 1; }\n 50% { width: 100%; opacity: 1; }\n 100% { width: 100%; opacity: 0; }\n}\n`;\n document.head.appendChild(style);\n keyframesInjected = true;\n}\n\n// ─── Component ───────────────────────────────────────────────────\n\n/**\n * Internal top-loader component. Injected by NavigationRoot.\n *\n * Reads pending navigation state from the router's external store, via\n * usePendingNavigation() — the one pending representation (TIM-1307).\n * Phase transitions are derived synchronously during render:\n *\n * hidden → crawling: when isPending becomes true\n * crawling → finishing: when isPending becomes false\n * finishing → hidden: when CSS transition ends (onTransitionEnd)\n * finishing → crawling: when isPending becomes true again\n *\n * No useEffect — all state changes are either derived during render\n * (getDerivedStateFromProps pattern) or triggered by DOM events.\n */\nexport function TopLoader({ config }: { config?: TopLoaderConfig }): React.ReactElement | null {\n // Read pending state from the router's external store via\n // useSyncExternalStore (inside usePendingNavigation). Only this\n // component re-renders when pending changes — no full-tree re-render.\n const isPending = usePendingNavigation();\n\n const color = config?.color ?? DEFAULT_COLOR;\n const height = config?.height ?? DEFAULT_HEIGHT;\n const shadow = config?.shadow ?? DEFAULT_SHADOW;\n const delay = config?.delay ?? DEFAULT_DELAY;\n const zIndex = config?.zIndex ?? DEFAULT_Z_INDEX;\n\n const [phase, setPhase] = useState<'hidden' | 'crawling' | 'finishing'>('hidden');\n\n // ─── Synchronous phase derivation (getDerivedStateFromProps) ──\n // React allows setState during render if the value changes — it\n // immediately re-renders with the updated state before committing.\n\n if (isPending && (phase === 'hidden' || phase === 'finishing')) {\n setPhase('crawling');\n }\n if (!isPending && phase === 'crawling') {\n setPhase('finishing');\n }\n\n // Inject keyframes on first visible render (idempotent)\n if (phase !== 'hidden') {\n ensureKeyframes();\n }\n\n if (phase === 'hidden') return null;\n\n // ─── Styles ──────────────────────────────────────────────────\n\n const containerStyle: React.CSSProperties = {\n position: 'fixed',\n top: 0,\n left: 0,\n width: '100%',\n height: `${height}px`,\n zIndex,\n pointerEvents: 'none',\n };\n\n const barStyle: React.CSSProperties = {\n height: '100%',\n backgroundColor: color,\n ...(phase === 'crawling'\n ? {\n // Crawl from 0% to 90% over 30s. When delay > 0, both the crawl\n // and a visibility animation are delayed — the bar stays at width 0%\n // and opacity 0 during the delay, then appears and starts crawling.\n // With delay 0, the appear animation is instant (0s duration, no delay).\n animation: [\n `${CRAWL_KEYFRAMES} 30s ease-out ${delay}ms forwards`,\n `${APPEAR_KEYFRAMES} 0s ${delay}ms both`,\n ].join(', '),\n }\n : {\n // Finishing: fill to 100% then fade out via a keyframe animation.\n // We use a keyframe instead of a CSS transition because the\n // animation-to-transition handoff is unreliable — the browser\n // may not capture the animated width as the transition's \"from\"\n // value when both the animation removal and transition are\n // applied in the same render frame.\n animation: `${FINISH_KEYFRAMES} 400ms ease forwards`,\n }),\n ...(shadow\n ? {\n boxShadow: `0 0 10px ${color}, 0 0 5px ${color}`,\n }\n : {}),\n };\n\n // Clean up the finishing phase when the finish animation completes.\n const handleAnimationEnd =\n phase === 'finishing'\n ? (e: React.AnimationEvent) => {\n if (e.animationName === FINISH_KEYFRAMES) {\n setPhase('hidden');\n }\n }\n : undefined;\n\n return createElement(\n 'div',\n {\n 'style': containerStyle,\n 'aria-hidden': 'true',\n 'data-timber-top-loader': '',\n },\n createElement('div', { style: barStyle, onAnimationEnd: handleAnimationEnd })\n );\n}\n","/**\n * NavigationRoot — the component the router renders the page through.\n *\n * It is stateless. The router owns the displayed tree and drives React with\n * `root.render(<NavigationRoot rendered={{ element, publish }} />)` inside a\n * synchronous `startTransition` (see `client/react-root.ts`). A transition\n * update keeps the committed tree on screen while the incoming one resolves,\n * instead of replacing it with a Suspense fallback — the reason this\n * component exists (TIM-1306).\n *\n * It used to hold the tree in `useState` and register `setState` closures\n * into module globals during render so the router could reach into it, with a\n * second set of stand-in closures for the window before a root existed\n * (TIM-600). Inverting that — router calls React, not the reverse — deleted\n * all of it (TIM-1431). Next.js's `use-action-queue.ts` says it wants this\n * shape and cannot have it because it must decode Flight during render;\n * timber decodes in `router-pipeline.ts`, so nothing stands in the way.\n *\n * What this component does own is **the commit-time publish**: a navigation's\n * state (segment cache, address bar, history stack, pathname) is published\n * from a layout effect keyed on the tree object, so the event that puts a\n * route on screen is the event that makes it current (TIM-1301). Props work\n * for that exactly as state did.\n *\n * This component holds no pending state. The `TopLoader` it renders and the\n * public `usePendingNavigation()` both subscribe to the router's external\n * pending store; NavigationRoot's own `pendingUrl` was a second, unread\n * representation of the same fact and is gone (TIM-1307).\n *\n * Hard navigation guard: When a hard navigation is triggered (500 error,\n * version skew), the component throws an unresolved thenable AFTER all\n * hooks to suspend forever — preventing React from rendering children\n * during page teardown. The throw must come after hooks to satisfy\n * React's rules (same hook count every render) while still preventing\n * child renders that could hit hook count mismatches in components\n * whose positions shift during teardown. This pattern is borrowed from\n * Next.js (app-router.tsx pushRef.mpaNavigation — also after hooks).\n *\n * See design/05-streaming.md §\"deferSuspenseFor\"\n * See design/19-client-navigation.md §\"NavigationContext\"\n */\n\nimport { createElement, Fragment, useLayoutEffect, useRef, type ReactNode } from 'react';\nimport { TopLoader, type TopLoaderConfig } from './top-loader.tsx';\n\n// ─── Rendered Tree ──────────────────────────────────────────────\n\n/**\n * The tree the router has handed to React, and the state publish that belongs\n * to it. They travel as one object so React's commit of the tree is the event\n * that publishes — see the effect in NavigationRoot. Every `render` builds a\n * fresh one, which is what keys that effect: a tree is published once, however\n * many times React re-runs the effect for it.\n */\nexport interface RenderedTree {\n element: ReactNode;\n /**\n * Publishes the navigation's state — segment cache, pathname, address bar,\n * history stack, and the client's record of the mounted tree — and\n * announces it to listeners outside React. Runs when React commits\n * `element`, never before (TIM-1301). Null for a render that has nothing to\n * publish on commit (hydration, the shallow search re-wrap).\n */\n publish: (() => void) | null;\n}\n\n/**\n * Hand a tree to React in a transition. `publish` runs when React commits it.\n *\n * Every path that puts a page on screen goes through one of these — a\n * navigation, a revalidation, a popstate replay, a shallow search re-wrap —\n * and it is always a synchronous `startTransition` around `root.render`.\n * The production one is `createReactRoot().render`.\n */\nexport type NavigationRender = (element: ReactNode, publish: (() => void) | null) => void;\n\n// ─── Hard Navigation Guard ──────────────────────────────────────\n\n/**\n * Module-level flag indicating a hard (MPA) navigation is in progress.\n *\n * When true:\n * - NavigationRoot throws an unresolved thenable to suspend forever,\n * preventing React from rendering children during page teardown\n * (avoids \"Rendered more hooks\" crashes).\n * - The Navigation API handler skips interception, letting the browser\n * perform a full page load (prevents infinite loops where\n * window.location.href → navigate event → router.navigate → 500 →\n * window.location.href → ...).\n *\n * Uses globalThis for singleton guarantee across chunks (same pattern\n * as NavigationContext). See design/19-client-navigation.md §\"Singleton\n * Guarantee via globalThis\".\n */\nconst HARD_NAV_KEY = Symbol.for('__timber_hard_navigating');\n\nfunction getHardNavStore(): { value: boolean } {\n const g = globalThis as Record<symbol, unknown>;\n if (!g[HARD_NAV_KEY]) {\n g[HARD_NAV_KEY] = { value: false };\n }\n return g[HARD_NAV_KEY] as { value: boolean };\n}\n\n/**\n * Set the hard-navigating flag. Call this BEFORE setting\n * window.location.href or window.location.reload() to prevent:\n * 1. React from rendering children during page teardown\n * 2. Navigation API from intercepting the hard navigation\n */\nexport function setHardNavigating(value: boolean): void {\n getHardNavStore().value = value;\n}\n\n/**\n * Check if a hard navigation is in progress.\n * Used by NavigationRoot (throw unresolvedThenable) and by the\n * Navigation API handler (skip interception).\n */\nexport function isHardNavigating(): boolean {\n return getHardNavStore().value;\n}\n\n/**\n * A thenable that never resolves. When thrown during React render,\n * it causes the component to suspend forever — React keeps the\n * old committed tree visible and never attempts to render children.\n *\n * This is the same pattern Next.js uses in app-router.tsx for MPA\n * navigations (pushRef.mpaNavigation → throw unresolvedThenable).\n */\n// for React's Suspense mechanism. Same pattern as Next.js's unresolvedThenable.\n// eslint-disable-next-line unicorn/no-thenable -- Intentionally a never-resolving thenable\nconst unresolvedThenable = { then() {} } as PromiseLike<never>;\n\n// ─── Component ───────────────────────────────────────────────────\n\n/**\n * Root component the router renders the page through.\n *\n * Renders the TopLoader alongside the tree it is given. Neither adds a DOM\n * element on the hydration path, so the tree matches the server HTML.\n *\n * Rendered only by `createReactRoot` (client/react-root.ts): hydration passes\n * `{ element, publish: null }`, and every later render is a synchronous\n * `startTransition(() => root.render(...))` with a fresh `rendered` object.\n */\nexport function NavigationRoot({\n rendered,\n topLoaderConfig,\n}: {\n rendered: RenderedTree;\n topLoaderConfig?: TopLoaderConfig;\n}): ReactNode {\n // Publish the navigation's state when React commits its tree — not when the\n // tree is handed over. A tree React is still waiting on, or one a later\n // navigation replaces before React renders it, has been *given* to React\n // without being on screen; publishing then describes a route the user never\n // saw, which is the defect this ordering exists to prevent (TIM-1301).\n //\n // A **layout** effect, so the publish lands before the browser paints and,\n // more importantly, before every descendant's passive effect. A destination\n // component that navigates from a mount effect — a redirect guard, a\n // `refresh()` on mount — would otherwise start its navigation while the\n // segment cache, address bar and pathname still described the departing\n // route, and send an X-Timber-State-Tree for a tree that is no longer\n // mounted (codex on #998).\n //\n // Descendant *layout* effects still run first: React runs layout effects\n // child-before-parent, and the only way to precede them would be a fiber\n // rendered as an earlier sibling of the payload. That is not free here —\n // `server/ssr-wrappers.tsx` mirrors this component's fiber shape so `useId`\n // agrees across hydration, and an extra sibling shifts every id in the\n // payload subtree. A layout effect that navigates on mount is the price.\n //\n // Keyed on the tree object rather than on the effect run. A publish moves\n // the address bar, which is not idempotent, so any second invocation for the\n // same tree — an effect re-run React is entitled to perform, StrictMode's\n // double-invoke — must be a no-op rather than a second history entry.\n const publishedRef = useRef<RenderedTree | null>(null);\n useLayoutEffect(() => {\n if (publishedRef.current === rendered) return;\n publishedRef.current = rendered;\n rendered.publish?.();\n }, [rendered]);\n\n // ─── Hard navigation guard ─────────────────────────────────\n // When a hard navigation is in progress (500 error, version skew),\n // suspend forever to prevent React from rendering children during\n // page teardown. This avoids \"Rendered more hooks\" crashes in\n // CHILD components whose hook counts may shift during teardown.\n //\n // CRITICAL: This throw MUST come AFTER all hooks (the useRef and\n // useLayoutEffect above). React requires the same hooks to run on every\n // render. If we threw before hooks, React would see 0 hooks on the\n // re-render vs 2 on the initial render — triggering the exact \"Rendered\n // more hooks\" error we're trying to prevent.\n //\n // By placing it after hooks but before the return, all hooks\n // satisfy React's rules, but the thrown thenable prevents any\n // children from rendering. Same pattern as Next.js app-router.tsx\n // (pushRef.mpaNavigation — also placed after all hooks).\n if (isHardNavigating()) {\n throw unresolvedThenable;\n }\n\n // Inject TopLoader alongside the element tree. It subscribes to the router's\n // pending store itself, so it takes no props from here beyond its config,\n // and only it re-renders when a navigation starts or ends. Rendered only\n // when not explicitly disabled via config.\n //\n // This Fragment is a FORK — two children — so it advances React's tree\n // context and shifts every `useId` below it. `server/ssr-wrappers.tsx`\n // mirrors it for exactly that reason; a single-child wrapper would not need\n // a mirror. See tests/ssr-wrapper-parity.test.ts.\n const showTopLoader = topLoaderConfig?.enabled !== false;\n if (!showTopLoader) return rendered.element;\n return createElement(\n Fragment,\n null,\n createElement(TopLoader, { config: topLoaderConfig }),\n rendered.element\n );\n}\n"],"mappings":";;;;;;AAcA,SAAgB,gBAAgB,QAA8B;CAC5D,iBAAiB,MAAM;AACzB;;;;;AAMA,SAAgB,YAA4B;CAC1C,IAAI,CAAC,cACH,MAAM,IAAI,MAAM,6EAA6E;CAE/F,OAAO;AACT;;;;;;AAOA,SAAgB,kBAAyC;CACvD,OAAO;AACT;;;ACjCA,SAAS,UAAU,eAAuC;CACxD,MAAM,SAAS,gBAAgB;CAC/B,IAAI,CAAC,QAAQ,aAAa,CAAC;CAC3B,OAAO,OAAO,gBAAgB,aAAa;AAC7C;AAEA,SAAS,cAAuB;CAC9B,MAAM,SAAS,gBAAgB;CAC/B,OAAO,SAAS,OAAO,UAAU,IAAI;AACvC;AAEA,IAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;AAuB1B,SAAgB,uBAAgC;CAC9C,OAAO,qBAAqB,WAAW,aAAa,iBAAiB;AACvE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AEuDA,IAAM,eAAe,OAAO,IAAI,0BAA0B;AAE1D,SAAS,kBAAsC;CAC7C,MAAM,IAAI;CACV,IAAI,CAAC,EAAE,eACL,EAAE,gBAAgB,EAAE,OAAO,MAAM;CAEnC,OAAO,EAAE;AACX;;;;;;;AAQA,SAAgB,kBAAkB,OAAsB;CACtD,gBAAgB,CAAC,CAAC,QAAQ;AAC5B"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as INFERRED_RULES, t as EXPLICIT_MARKERS } from "./poison-rules-DoEhbqaY.js";
|
|
2
|
-
import { o as langForFile } from "./chains-
|
|
2
|
+
import { o as langForFile } from "./chains-Bpb0W4ax.js";
|
|
3
3
|
import { parseAst } from "vite";
|
|
4
4
|
//#region src/analyze/poison-scan.ts
|
|
5
5
|
/**
|
|
@@ -98,4 +98,4 @@ function poisoningsFor(kind, pending, signals) {
|
|
|
98
98
|
//#endregion
|
|
99
99
|
export { importSignals as n, poisoningsFor as r, collectImportSpecifiers as t };
|
|
100
100
|
|
|
101
|
-
//# sourceMappingURL=poison-scan-
|
|
101
|
+
//# sourceMappingURL=poison-scan-Bm9Yyqk9.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"poison-scan-
|
|
1
|
+
{"version":3,"file":"poison-scan-Bm9Yyqk9.js","names":[],"sources":["../../src/analyze/poison-scan.ts"],"sourcesContent":["/**\n * poison-scan — report-only poisoning detection for `timber graph`.\n *\n * Matches a module's static import specifiers against the shared rule\n * lists in poison-rules.ts (the single source of truth — design/47 §3)\n * and reports a poisoning only when the implied exclusivity CONFLICTS\n * with where the module actually runs: a server-only signal in a\n * client-reachable module, or a client-only signal in a server-reachable\n * one. A `node:fs` import in a file only the RSC environment loads is\n * working code, not a finding.\n *\n * Scope: import-specifier signals only (explicit markers + the inferred\n * rules' `matchesImport`). The inferred browser-global identifier rules\n * need shadow-safe allowlisted-position AST matching and are a separate\n * ticket — reporting them from a text scan would flag legitimate code.\n *\n * Design docs: 47-module-environment-tooling.md §3.\n */\n\nimport { parseAst } from 'vite';\nimport { EXPLICIT_MARKERS, INFERRED_RULES } from './poison-rules.ts';\nimport { langForFile, type ModuleKind } from './classify.ts';\n\n/** One poisoning finding on a module, in the JSON-contract shape. */\nexport interface Poisoning {\n /** Rule id — an inferred rule's id, or the explicit marker specifier. */\n rule: string;\n /** The import specifier that matched. */\n specifier: string;\n /** Which exclusivity the signal implies for the containing module. */\n implies: 'server-only' | 'client-only';\n /** Human-readable description for reporter output. */\n description: string;\n}\n\n/** File extensions the specifier scan can parse. */\nconst PARSEABLE_EXTENSIONS = /\\.(?:ts|tsx|js|jsx|mjs|cjs|mts|cts)$/;\n\n/**\n * A NON-EMPTY specifier list where every entry is type-only. Empty or\n * absent lists return false — a bare `import 'pkg'` is a side-effect\n * import and very much runs.\n */\nfunction isAllSpecifiersTypeOnly(\n specifiers: Array<{ importKind?: string; exportKind?: string }> | undefined\n): boolean {\n if (!specifiers || specifiers.length === 0) return false;\n return specifiers.every((s) => s.importKind === 'type' || s.exportKind === 'type');\n}\n\n/**\n * Collect every static import/export source and string-literal dynamic\n * import from a module's original source. Sources that fail to parse\n * (or are not JS/TS at all) return [] — no specifiers is a safe\n * default for a report-only scan.\n */\nexport function collectImportSpecifiers(code: string, file: string): string[] {\n if (!PARSEABLE_EXTENSIONS.test(file)) return [];\n let program;\n try {\n // Language from the extension, never forced tsx — see langForFile.\n program = parseAst(code, { lang: langForFile(file) });\n } catch {\n return [];\n }\n const specifiers: string[] = [];\n const visit = (node: unknown): void => {\n if (Array.isArray(node)) {\n for (const child of node) visit(child);\n return;\n }\n if (typeof node !== 'object' || node === null) return;\n const record = node as Record<string, unknown> & {\n type?: string;\n importKind?: string;\n exportKind?: string;\n specifiers?: Array<{ importKind?: string; exportKind?: string }>;\n };\n if (\n (record.type === 'ImportDeclaration' ||\n record.type === 'ExportNamedDeclaration' ||\n record.type === 'ExportAllDeclaration') &&\n // Type-only imports/exports are erased at runtime — `import type\n // { Stats } from 'node:fs'` in client code is working code, and\n // flagging it would be a false positive in a report-only scan.\n // Covers both the declaration-level form (`import type {…}`,\n // importKind on the declaration) and the inline form\n // (`import { type Stats }`, where the declaration stays 'value'\n // and each SPECIFIER carries the kind) — a declaration whose\n // specifiers are all type-only is erased just the same.\n record.importKind !== 'type' &&\n record.exportKind !== 'type' &&\n !isAllSpecifiersTypeOnly(record.specifiers) &&\n typeof (record.source as { value?: unknown } | null)?.value === 'string'\n ) {\n specifiers.push((record.source as { value: string }).value);\n } else if (\n record.type === 'ImportExpression' &&\n (record.source as { type?: string; value?: unknown })?.type === 'Literal' &&\n typeof (record.source as { value?: unknown }).value === 'string'\n ) {\n specifiers.push((record.source as { value: string }).value);\n }\n for (const key of Object.keys(record)) {\n if (key === 'type') continue;\n visit(record[key]);\n }\n };\n visit(program.body);\n return specifiers;\n}\n\n/** Exclusivity signals a module's import specifiers carry. */\nexport function importSignals(specifiers: string[]): Poisoning[] {\n const signals: Poisoning[] = [];\n for (const specifier of specifiers) {\n for (const marker of EXPLICIT_MARKERS) {\n if (specifier === marker.specifier) {\n signals.push({\n rule: marker.specifier,\n specifier,\n implies: marker.specifier === 'server-only' ? 'server-only' : 'client-only',\n description: `imports the ${marker.specifier} poison-pill marker`,\n });\n }\n }\n for (const rule of INFERRED_RULES) {\n if (rule.matchesImport?.(specifier)) {\n signals.push({\n rule: rule.id,\n specifier,\n implies: rule.implies,\n description: rule.description,\n });\n }\n }\n }\n return signals;\n}\n\n/**\n * Filter a module's signals down to actual conflicts with its\n * classification. Environment reach follows the taxonomy: boundary /\n * client-internal / shared modules run client-side; server / shared /\n * server-action modules run server-side. Pending (provisional) modules\n * run nowhere confirmed, so nothing conflicts yet.\n */\nexport function poisoningsFor(\n kind: ModuleKind,\n pending: boolean,\n signals: Poisoning[]\n): Poisoning[] {\n if (pending || signals.length === 0) return [];\n const clientReachable =\n kind === 'client-boundary' || kind === 'client-internal' || kind === 'shared';\n const serverReachable = kind === 'server' || kind === 'shared' || kind === 'server-action';\n return signals.filter(\n (signal) =>\n (signal.implies === 'server-only' && clientReachable) ||\n (signal.implies === 'client-only' && serverReachable)\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAoCA,IAAM,uBAAuB;;;;;;AAO7B,SAAS,wBACP,YACS;CACT,IAAI,CAAC,cAAc,WAAW,WAAW,GAAG,OAAO;CACnD,OAAO,WAAW,OAAO,MAAM,EAAE,eAAe,UAAU,EAAE,eAAe,MAAM;AACnF;;;;;;;AAQA,SAAgB,wBAAwB,MAAc,MAAwB;CAC5E,IAAI,CAAC,qBAAqB,KAAK,IAAI,GAAG,OAAO,CAAC;CAC9C,IAAI;CACJ,IAAI;EAEF,UAAU,SAAS,MAAM,EAAE,MAAM,YAAY,IAAI,EAAE,CAAC;CACtD,QAAQ;EACN,OAAO,CAAC;CACV;CACA,MAAM,aAAuB,CAAC;CAC9B,MAAM,SAAS,SAAwB;EACrC,IAAI,MAAM,QAAQ,IAAI,GAAG;GACvB,KAAK,MAAM,SAAS,MAAM,MAAM,KAAK;GACrC;EACF;EACA,IAAI,OAAO,SAAS,YAAY,SAAS,MAAM;EAC/C,MAAM,SAAS;EAMf,KACG,OAAO,SAAS,uBACf,OAAO,SAAS,4BAChB,OAAO,SAAS,2BASlB,OAAO,eAAe,UACtB,OAAO,eAAe,UACtB,CAAC,wBAAwB,OAAO,UAAU,KAC1C,OAAQ,OAAO,QAAuC,UAAU,UAEhE,WAAW,KAAM,OAAO,OAA6B,KAAK;OACrD,IACL,OAAO,SAAS,sBACf,OAAO,QAA+C,SAAS,aAChE,OAAQ,OAAO,OAA+B,UAAU,UAExD,WAAW,KAAM,OAAO,OAA6B,KAAK;EAE5D,KAAK,MAAM,OAAO,OAAO,KAAK,MAAM,GAAG;GACrC,IAAI,QAAQ,QAAQ;GACpB,MAAM,OAAO,IAAI;EACnB;CACF;CACA,MAAM,QAAQ,IAAI;CAClB,OAAO;AACT;;AAGA,SAAgB,cAAc,YAAmC;CAC/D,MAAM,UAAuB,CAAC;CAC9B,KAAK,MAAM,aAAa,YAAY;EAClC,KAAK,MAAM,UAAU,kBACnB,IAAI,cAAc,OAAO,WACvB,QAAQ,KAAK;GACX,MAAM,OAAO;GACb;GACA,SAAS,OAAO,cAAc,gBAAgB,gBAAgB;GAC9D,aAAa,eAAe,OAAO,UAAU;EAC/C,CAAC;EAGL,KAAK,MAAM,QAAQ,gBACjB,IAAI,KAAK,gBAAgB,SAAS,GAChC,QAAQ,KAAK;GACX,MAAM,KAAK;GACX;GACA,SAAS,KAAK;GACd,aAAa,KAAK;EACpB,CAAC;CAGP;CACA,OAAO;AACT;;;;;;;;AASA,SAAgB,cACd,MACA,SACA,SACa;CACb,IAAI,WAAW,QAAQ,WAAW,GAAG,OAAO,CAAC;CAC7C,MAAM,kBACJ,SAAS,qBAAqB,SAAS,qBAAqB,SAAS;CACvE,MAAM,kBAAkB,SAAS,YAAY,SAAS,YAAY,SAAS;CAC3E,OAAO,QAAQ,QACZ,WACE,OAAO,YAAY,iBAAiB,mBACpC,OAAO,YAAY,iBAAiB,eACzC;AACF"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { h as swallow } from "./logger-uLBuGKDI.js";
|
|
2
|
+
import { i as classifyMetadataRoute, n as treePathDepth, s as isDynamicMetadataExtension, t as computeSegmentTreePaths } from "./segment-keys-lqtdookO.js";
|
|
3
|
+
import { n as NULL_BYTE_RE, t as ENCODED_SEPARATOR_RE } from "./canonicalize-CgHoscYO.js";
|
|
2
4
|
import { i as DEFAULT_PAGE_EXTENSIONS, n as classifyUrlSegment, r as effectiveUrlSegment, t as classifySegment } from "./segment-classify-C539Pa2O.js";
|
|
3
|
-
import { h as swallow } from "./logger-DDirEsn7.js";
|
|
4
5
|
import { basename, extname, join } from "node:path";
|
|
5
6
|
import { readdirSync, statSync } from "node:fs";
|
|
6
7
|
//#region src/server/tree-match.ts
|
|
@@ -1306,8 +1307,8 @@ function childInterceptedBase(child, base) {
|
|
|
1306
1307
|
* Reject every parallel slot that no renderer reaches.
|
|
1307
1308
|
*
|
|
1308
1309
|
* **Where slots are resolved.** Two places read `segment.slots`, and both walk
|
|
1309
|
-
* the main route chain: `route-element-builder.ts` (the RSC render)
|
|
1310
|
-
*
|
|
1310
|
+
* the main route chain: `route-element-builder.ts` (the RSC render).
|
|
1311
|
+
* It is not reachable from inside a slot. The slot's own
|
|
1311
1312
|
* subtree is rendered by `slot-resolver.ts`, which walks the slot's matched
|
|
1312
1313
|
* chain to apply that chain's layouts, error boundaries and access gates
|
|
1313
1314
|
* (`wrapWithIntermediateSegments`, `wrapWithLayout`) and never reads `.slots`
|
|
@@ -1767,4 +1768,4 @@ function findPageExtFile(dirPath, name, extSet) {
|
|
|
1767
1768
|
//#endregion
|
|
1768
1769
|
export { matchUrlParts as i, scanRoutes as n, collectInterceptionRewrites as r, isRouteConventionFile as t };
|
|
1769
1770
|
|
|
1770
|
-
//# sourceMappingURL=scanner-
|
|
1771
|
+
//# sourceMappingURL=scanner-AiazgH_f.js.map
|