@timber-js/app 0.2.0-alpha.196 → 0.2.0-alpha.198
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-CWYtq6ii.js → actions-BS-m5SLv.js} +3 -3
- package/dist/_chunks/{actions-CWYtq6ii.js.map → actions-BS-m5SLv.js.map} +1 -1
- package/dist/_chunks/als-registry-C6kcfprT.js.map +1 -1
- package/dist/_chunks/{build-manifest-DWppEdLB.js → build-manifest-DTmSGLRz.js} +51 -2
- package/dist/_chunks/build-manifest-DTmSGLRz.js.map +1 -0
- package/dist/_chunks/{cache-api-CQeYzA5g.js → cache-api-DqzgTEqk.js} +4 -49
- package/dist/_chunks/cache-api-DqzgTEqk.js.map +1 -0
- package/dist/_chunks/{chains-h7EO-u3n.js → chains-CZG7E5zg.js} +2 -2
- package/dist/_chunks/{chains-h7EO-u3n.js.map → chains-CZG7E5zg.js.map} +1 -1
- package/dist/_chunks/{cli-check-BVthpfLS.js → cli-check-dVDi1GQz.js} +3 -3
- package/dist/_chunks/{cli-check-BVthpfLS.js.map → cli-check-dVDi1GQz.js.map} +1 -1
- package/dist/_chunks/{cli-schema-sync-3Wutm8pH.js → cli-schema-sync-DTy_-Msq.js} +2 -2
- package/dist/_chunks/{cli-schema-sync-3Wutm8pH.js.map → cli-schema-sync-DTy_-Msq.js.map} +1 -1
- package/dist/_chunks/{cloudflare-BKJC3SC_.js → cloudflare-BFb__LYG.js} +2 -2
- package/dist/_chunks/{cloudflare-BKJC3SC_.js.map → cloudflare-BFb__LYG.js.map} +1 -1
- package/dist/_chunks/{convention-lint-DO10_pVl.js → convention-lint-Ph6luW4c.js} +4 -2
- package/dist/_chunks/convention-lint-Ph6luW4c.js.map +1 -0
- package/dist/_chunks/{error-boundary-D-lkwyaD.js → error-boundary-BvRCCmbN.js} +3 -3
- package/dist/_chunks/{error-boundary-D-lkwyaD.js.map → error-boundary-BvRCCmbN.js.map} +1 -1
- package/dist/_chunks/{href-validation-CMc5JRls.js → href-validation-BIrxavIy.js} +74 -2
- package/dist/_chunks/href-validation-BIrxavIy.js.map +1 -0
- package/dist/_chunks/{live-graph-Bx4HodF1.js → live-graph-BXDsdzBv.js} +3 -3
- package/dist/_chunks/{live-graph-Bx4HodF1.js.map → live-graph-BXDsdzBv.js.map} +1 -1
- package/dist/_chunks/{logger-pumCm3Il.js → logger-DDirEsn7.js} +3 -4
- package/dist/_chunks/{logger-pumCm3Il.js.map → logger-DDirEsn7.js.map} +1 -1
- package/dist/_chunks/navigation-root-B00jjGd5.js +233 -0
- package/dist/_chunks/navigation-root-B00jjGd5.js.map +1 -0
- package/dist/_chunks/{segment-context-CjOlyB8Y.js → param-value-C8TNYchQ.js} +2 -33
- package/dist/_chunks/param-value-C8TNYchQ.js.map +1 -0
- package/dist/_chunks/{poison-scan-BAxfTT5L.js → poison-scan-BoDLgbix.js} +2 -2
- package/dist/_chunks/{poison-scan-BAxfTT5L.js.map → poison-scan-BoDLgbix.js.map} +1 -1
- package/dist/_chunks/{router-ref-BzqbPwYC.js → router-ref-8gr8qsxN.js} +2 -2
- package/dist/_chunks/{router-ref-BzqbPwYC.js.map → router-ref-8gr8qsxN.js.map} +1 -1
- package/dist/_chunks/{rsc-cache-key-DD0fl_-s.js → rsc-cache-key-ClUiXQnK.js} +2 -2
- package/dist/_chunks/{rsc-cache-key-DD0fl_-s.js.map → rsc-cache-key-ClUiXQnK.js.map} +1 -1
- package/dist/_chunks/{scanner-BRIOmHE2.js → scanner-tdFPvDYi.js} +174 -7
- package/dist/_chunks/scanner-tdFPvDYi.js.map +1 -0
- package/dist/_chunks/segment-context-D9_89u34.js +34 -0
- package/dist/_chunks/segment-context-D9_89u34.js.map +1 -0
- package/dist/_chunks/singleflight-2lUWfcAk.js +54 -0
- package/dist/_chunks/singleflight-2lUWfcAk.js.map +1 -0
- package/dist/_chunks/{ssr-data-Ya2HJPFp.js → ssr-data-BQGhTPAK.js} +2 -17
- package/dist/_chunks/ssr-data-BQGhTPAK.js.map +1 -0
- package/dist/_chunks/{walkers-BU6z9xRV.js → walkers-DNX05dC0.js} +2 -2
- package/dist/_chunks/{walkers-BU6z9xRV.js.map → walkers-DNX05dC0.js.map} +1 -1
- package/dist/adapters/cloudflare-dev.js +1 -1
- package/dist/adapters/cloudflare-kv-cache.js +1 -1
- package/dist/adapters/cloudflare.js +1 -1
- package/dist/adapters/nitro.d.ts +1 -1
- package/dist/adapters/nitro.d.ts.map +1 -1
- package/dist/adapters/nitro.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/cache/singleflight.d.ts +2 -0
- package/dist/cache/singleflight.d.ts.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/client/browser-entry/hydrate.d.ts +21 -15
- package/dist/client/browser-entry/hydrate.d.ts.map +1 -1
- package/dist/client/browser-entry/index.d.ts +4 -3
- package/dist/client/browser-entry/index.d.ts.map +1 -1
- package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
- package/dist/client/browser-entry/router-init.d.ts +17 -1
- package/dist/client/browser-entry/router-init.d.ts.map +1 -1
- package/dist/client/error-boundary.js +1 -1
- package/dist/client/global-context.d.ts +15 -0
- package/dist/client/global-context.d.ts.map +1 -0
- package/dist/client/index.js +138 -35
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.d.ts +0 -1
- package/dist/client/internal.d.ts.map +1 -1
- package/dist/client/internal.js +206 -55
- package/dist/client/internal.js.map +1 -1
- package/dist/client/link.d.ts.map +1 -1
- package/dist/client/location-search.d.ts +12 -0
- package/dist/client/location-search.d.ts.map +1 -0
- package/dist/client/navigation-api.d.ts.map +1 -1
- package/dist/client/navigation-commit.d.ts +18 -0
- package/dist/client/navigation-commit.d.ts.map +1 -1
- package/dist/client/navigation-context.d.ts +13 -11
- package/dist/client/navigation-context.d.ts.map +1 -1
- package/dist/client/navigation-root.d.ts +47 -108
- package/dist/client/navigation-root.d.ts.map +1 -1
- package/dist/client/navigation-transition.d.ts +136 -0
- package/dist/client/navigation-transition.d.ts.map +1 -0
- package/dist/client/nuqs-adapter.d.ts.map +1 -1
- package/dist/client/params-context.d.ts +4 -5
- package/dist/client/params-context.d.ts.map +1 -1
- package/dist/client/react-root.d.ts +44 -0
- package/dist/client/react-root.d.ts.map +1 -0
- package/dist/client/router-pipeline.d.ts +2 -2
- package/dist/client/router-pipeline.d.ts.map +1 -1
- package/dist/client/router-types.d.ts +12 -2
- package/dist/client/router-types.d.ts.map +1 -1
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/segment-cache.d.ts +39 -0
- package/dist/client/segment-cache.d.ts.map +1 -1
- package/dist/client/segment-context.d.ts.map +1 -1
- package/dist/client/segment-outlet.d.ts +25 -14
- package/dist/client/segment-outlet.d.ts.map +1 -1
- package/dist/client/segment-update-context.d.ts +3 -9
- package/dist/client/segment-update-context.d.ts.map +1 -1
- package/dist/client/slot-content-cache-context.d.ts +35 -0
- package/dist/client/slot-content-cache-context.d.ts.map +1 -0
- package/dist/client/ssr-data.d.ts +8 -2
- package/dist/client/ssr-data.d.ts.map +1 -1
- package/dist/client/state.d.ts +0 -15
- package/dist/client/state.d.ts.map +1 -1
- package/dist/client/use-pathname.d.ts +13 -11
- package/dist/client/use-pathname.d.ts.map +1 -1
- package/dist/client/use-search-params.d.ts +13 -13
- package/dist/client/use-search-params.d.ts.map +1 -1
- package/dist/client/use-segment-params.d.ts +18 -68
- package/dist/client/use-segment-params.d.ts.map +1 -1
- package/dist/config-types.d.ts +17 -0
- package/dist/config-types.d.ts.map +1 -1
- package/dist/config-validation.d.ts.map +1 -1
- package/dist/cookies/index.js +1 -1
- package/dist/dev-tools/holding-server.d.ts +15 -10
- package/dist/dev-tools/holding-server.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +44 -43
- package/dist/index.js.map +1 -1
- package/dist/plugins/dev-server.d.ts.map +1 -1
- package/dist/plugins/entries.d.ts.map +1 -1
- package/dist/plugins/shims.d.ts.map +1 -1
- package/dist/plugins/static-build.d.ts +2 -2
- package/dist/plugins/static-build.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-overlap.d.ts +35 -0
- package/dist/routing/interception-overlap.d.ts.map +1 -0
- package/dist/routing/interception.d.ts.map +1 -1
- package/dist/rsc-runtime/ssr.d.ts +3 -1
- package/dist/rsc-runtime/ssr.d.ts.map +1 -1
- package/dist/server/als-registry.d.ts +6 -0
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/csp-nonce.d.ts +45 -0
- package/dist/server/csp-nonce.d.ts.map +1 -0
- package/dist/server/default-status-page.d.ts.map +1 -1
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/flight-scripts.d.ts +5 -2
- package/dist/server/flight-scripts.d.ts.map +1 -1
- package/dist/server/html-injector-core.d.ts +17 -2
- package/dist/server/html-injector-core.d.ts.map +1 -1
- package/dist/server/html-injectors.d.ts +3 -2
- package/dist/server/html-injectors.d.ts.map +1 -1
- package/dist/server/index.js +2 -2
- package/dist/server/internal.js +86 -37
- package/dist/server/internal.js.map +1 -1
- package/dist/server/metadata-render.d.ts.map +1 -1
- package/dist/server/node-stream-transforms.d.ts +3 -17
- package/dist/server/node-stream-transforms.d.ts.map +1 -1
- package/dist/server/nuqs-ssr-provider.d.ts +7 -3
- package/dist/server/nuqs-ssr-provider.d.ts.map +1 -1
- package/dist/server/pipeline-phases.d.ts.map +1 -1
- package/dist/server/prebuilt/key-discipline.d.ts +32 -3
- package/dist/server/prebuilt/key-discipline.d.ts.map +1 -1
- package/dist/server/primitives.d.ts.map +1 -1
- package/dist/server/render-utils.d.ts +4 -3
- package/dist/server/render-utils.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/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
- package/dist/server/ssr-bridge-types.d.ts +22 -2
- package/dist/server/ssr-bridge-types.d.ts.map +1 -1
- package/dist/server/ssr-entry.d.ts.map +1 -1
- package/dist/server/ssr-render.d.ts +5 -1
- package/dist/server/ssr-render.d.ts.map +1 -1
- package/dist/server/ssr-wrappers.d.ts +59 -27
- package/dist/server/ssr-wrappers.d.ts.map +1 -1
- package/dist/server/types.d.ts +10 -0
- package/dist/server/types.d.ts.map +1 -1
- package/dist/shims/navigation-rsc.d.ts +21 -0
- package/dist/shims/navigation-rsc.d.ts.map +1 -0
- package/docs/api/30-api-server.mdx +1 -0
- package/docs/api/35-api-typescript.mdx +4 -83
- package/docs/learn/03-fetching-data.mdx +1 -1
- package/docs/learn/{03b-access-control.mdx → 04-access-control.mdx} +2 -17
- package/docs/learn/05-the-flush-point.mdx +175 -0
- package/docs/learn/{05-typed-params.mdx → 06-typed-params.mdx} +1 -1
- package/docs/learn/07-typed-routes.mdx +25 -49
- package/docs/learn/{08-streaming.mdx → 09-streaming.mdx} +1 -7
- package/docs/learn/{10-middleware.mdx → 11-middleware.mdx} +1 -0
- package/package.json +3 -3
- package/src/adapters/nitro.ts +7 -7
- package/src/cache/singleflight.ts +5 -0
- package/src/client/browser-entry/hydrate.ts +54 -104
- package/src/client/browser-entry/index.ts +16 -6
- package/src/client/browser-entry/post-hydration.ts +3 -2
- package/src/client/browser-entry/router-init.ts +84 -33
- package/src/client/global-context.ts +31 -0
- package/src/client/internal.ts +1 -2
- package/src/client/link.tsx +18 -18
- package/src/client/location-search.ts +15 -0
- package/src/client/navigation-api.ts +4 -2
- package/src/client/navigation-commit.ts +48 -2
- package/src/client/navigation-context.ts +25 -37
- package/src/client/navigation-root.tsx +55 -411
- package/src/client/navigation-transition.ts +278 -0
- package/src/client/nuqs-adapter.tsx +4 -5
- package/src/client/params-context.ts +13 -18
- package/src/client/react-root.ts +72 -0
- package/src/client/router-lifecycle.ts +1 -1
- package/src/client/router-pipeline.ts +96 -22
- package/src/client/router-types.ts +12 -2
- package/src/client/router.ts +48 -36
- package/src/client/segment-cache.ts +70 -2
- package/src/client/segment-context.ts +7 -4
- package/src/client/segment-outlet.tsx +41 -86
- package/src/client/segment-update-context.ts +7 -26
- package/src/client/slot-content-cache-context.ts +43 -0
- package/src/client/ssr-data.ts +8 -2
- package/src/client/state.ts +0 -26
- package/src/client/use-pathname.ts +21 -31
- package/src/client/use-search-params.ts +31 -29
- package/src/client/use-segment-params.ts +27 -126
- package/src/config-types.ts +17 -0
- package/src/config-validation.ts +17 -0
- package/src/dev-tools/holding-server.ts +23 -12
- package/src/index.ts +26 -11
- package/src/plugins/dev-server.ts +9 -12
- package/src/plugins/entries.ts +3 -0
- package/src/plugins/shims.ts +8 -7
- package/src/plugins/static-build.ts +9 -5
- package/src/react-canary.d.ts +2 -0
- package/src/routing/codegen-write.ts +2 -0
- package/src/routing/interception-overlap.ts +141 -0
- package/src/routing/interception.ts +118 -5
- package/src/rsc-runtime/ssr.ts +3 -2
- package/src/server/als-registry.ts +6 -0
- package/src/server/csp-nonce.ts +70 -0
- package/src/server/default-status-page.ts +1 -0
- package/src/server/deny-renderer.ts +7 -3
- package/src/server/flight-scripts.ts +9 -4
- package/src/server/html-injector-core.ts +26 -9
- package/src/server/html-injectors.ts +8 -8
- package/src/server/metadata-render.ts +26 -4
- package/src/server/node-stream-transforms.ts +7 -20
- package/src/server/nuqs-ssr-provider.tsx +8 -7
- package/src/server/pipeline-phases.ts +5 -0
- package/src/server/prebuilt/key-discipline.ts +82 -13
- package/src/server/prebuilt-runtime.ts +2 -2
- package/src/server/primitives.ts +4 -4
- package/src/server/render-utils.ts +8 -4
- package/src/server/rsc-entry/action-middleware-runner.ts +7 -0
- package/src/server/rsc-entry/error-renderer.ts +5 -2
- package/src/server/rsc-entry/index.ts +8 -0
- package/src/server/rsc-entry/ssr-renderer.ts +11 -4
- package/src/server/ssr-bridge-types.ts +22 -2
- package/src/server/ssr-entry.ts +35 -28
- package/src/server/ssr-render.ts +13 -4
- package/src/server/ssr-wrappers.tsx +81 -61
- package/src/server/types.ts +10 -0
- package/src/shared/slot-params.ts +3 -4
- package/src/shims/navigation-rsc.ts +47 -0
- package/dist/_chunks/build-manifest-DWppEdLB.js.map +0 -1
- package/dist/_chunks/cache-api-CQeYzA5g.js.map +0 -1
- package/dist/_chunks/convention-lint-DO10_pVl.js.map +0 -1
- package/dist/_chunks/href-validation-CMc5JRls.js.map +0 -1
- package/dist/_chunks/scanner-BRIOmHE2.js.map +0 -1
- package/dist/_chunks/segment-context-CjOlyB8Y.js.map +0 -1
- package/dist/_chunks/slot-params-BCTmZkQB.js +0 -76
- package/dist/_chunks/slot-params-BCTmZkQB.js.map +0 -1
- package/dist/_chunks/ssr-data-Ya2HJPFp.js.map +0 -1
- package/dist/_chunks/use-segment-params-DzTBpkvj.js +0 -398
- package/dist/_chunks/use-segment-params-DzTBpkvj.js.map +0 -1
- package/docs/learn/04-loading-states.mdx +0 -67
- package/docs/learn/04b-the-flush-point.mdx +0 -115
- package/docs/learn/12-client-navigation.mdx +0 -176
- package/docs/learn/13-configuration.mdx +0 -166
- package/docs/more/01-advanced-routing.mdx +0 -344
- package/docs/more/02-advanced-forms.mdx +0 -137
- package/docs/more/03-coming-from-nextjs.mdx +0 -186
- package/docs/more/04-metadata-and-fonts.mdx +0 -193
- package/docs/more/04b-mdx.mdx +0 -229
- package/docs/more/05-content-collections.mdx +0 -90
- package/docs/more/06-instrumentation.mdx +0 -214
- package/docs/more/07-security.mdx +0 -129
- package/docs/more/08-developer-experience.mdx +0 -134
- package/docs/more/40-why-timber.mdx +0 -50
- package/docs/more/41-timber-vs-nextjs.mdx +0 -81
- package/docs/more/42-timber-vs-others.mdx +0 -68
- package/docs/more/50-ai-agent-instructions.mdx +0 -171
- /package/docs/learn/{06-forms-and-actions.mdx → 08-forms-and-actions.mdx} +0 -0
- /package/docs/learn/{09-caching.mdx → 10-caching.mdx} +0 -0
- /package/docs/learn/{11-error-handling.mdx → 12-error-handling.mdx} +0 -0
- /package/docs/learn/{14-deploying.mdx → 13-deploying.mdx} +0 -0
|
@@ -4,48 +4,38 @@
|
|
|
4
4
|
* Returns the pathname portion of the current URL (e.g. '/dashboard/settings').
|
|
5
5
|
* Updates when client-side navigation changes the URL.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
* with the RSC tree render. This replaces the previous useSyncExternalStore
|
|
9
|
-
* approach which only subscribed to popstate events — meaning usePathname()
|
|
10
|
-
* did NOT re-render on forward navigation (pushState). The context approach
|
|
11
|
-
* fixes this: pathname updates in the same render pass as the new tree.
|
|
7
|
+
* One unconditional read of NavigationContext, on every side (TIM-1425):
|
|
12
8
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
9
|
+
* - In the browser, the provider wraps the RSC payload in renderRoot(), so
|
|
10
|
+
* the pathname updates in the same render pass as the new tree.
|
|
11
|
+
* - During SSR, the wrapper chain mounts the same provider with the request's
|
|
12
|
+
* pathname (TIM-1424), so this is the identical code path — no ALS read,
|
|
13
|
+
* no fallback tiers.
|
|
14
|
+
* - In the RSC environment, this module is never evaluated — the shims plugin
|
|
15
|
+
* resolves next/navigation to navigation-rsc.ts, which has a throwing stub
|
|
16
|
+
* (TIM-1420).
|
|
17
|
+
* - Called outside a component entirely, React itself throws its
|
|
18
|
+
* invalid-hook-call error — loud, not guessed-at from window.location.
|
|
15
19
|
*
|
|
16
20
|
* Compatible with Next.js's `usePathname()` from `next/navigation`.
|
|
17
21
|
*/
|
|
18
22
|
|
|
19
|
-
import { getSsrData } from './ssr-data.ts';
|
|
20
23
|
import { useNavigationContext } from './navigation-context.ts';
|
|
21
24
|
|
|
22
25
|
/**
|
|
23
26
|
* Read the current URL pathname.
|
|
24
27
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* ALS-backed SSR data context. Falls back to window.location.pathname
|
|
28
|
-
* when called outside a React component (e.g., in tests).
|
|
28
|
+
* Throws when no NavigationProvider is above the caller (a component rendered
|
|
29
|
+
* outside the timber app — in tests, render inside the timber providers).
|
|
29
30
|
*/
|
|
30
31
|
export function usePathname(): string {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
} catch {
|
|
40
|
-
// No React dispatcher available (called outside a component).
|
|
41
|
-
// Fall through to SSR/fallback below.
|
|
32
|
+
const nav = useNavigationContext();
|
|
33
|
+
if (nav === null) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
'[timber] usePathname() was called outside the timber app tree ' +
|
|
36
|
+
'(no NavigationProvider found). In tests, render the component ' +
|
|
37
|
+
'inside the timber providers.'
|
|
38
|
+
);
|
|
42
39
|
}
|
|
43
|
-
|
|
44
|
-
// SSR path: read from ALS-backed SSR data context.
|
|
45
|
-
const ssrData = getSsrData();
|
|
46
|
-
if (ssrData) return ssrData.pathname ?? '/';
|
|
47
|
-
|
|
48
|
-
// Final fallback: window.location (tests, edge cases).
|
|
49
|
-
if (typeof window !== 'undefined') return window.location.pathname;
|
|
50
|
-
return '/';
|
|
40
|
+
return nav.pathname;
|
|
51
41
|
}
|
|
@@ -4,25 +4,23 @@
|
|
|
4
4
|
* Returns a read-only URLSearchParams instance reflecting the current
|
|
5
5
|
* URL's query string. Updates when client-side navigation changes the URL.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
* with the RSC tree render during full navigations, AND by
|
|
9
|
-
* syncShallowSearch() for shallow URL updates (nuqs shallow: true,
|
|
10
|
-
* replaceUrl, or any external pushState/replaceState that changes the
|
|
11
|
-
* query string). See router-init.ts.
|
|
7
|
+
* One unconditional read of NavigationContext, on every side (TIM-1425):
|
|
12
8
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
9
|
+
* - In the browser, the provider is updated atomically with the RSC tree
|
|
10
|
+
* during full navigations, AND by syncShallowSearch() for shallow URL
|
|
11
|
+
* updates (nuqs shallow: true, replaceUrl, or any external
|
|
12
|
+
* pushState/replaceState that changes the query string). See router-init.ts.
|
|
13
|
+
* - During SSR, the wrapper chain mounts the same provider carrying the
|
|
14
|
+
* request's RAW query string (TIM-1424), so repeated keys
|
|
15
|
+
* (`?tag=a&tag=b`) survive — the collapsed record the ALS context holds
|
|
16
|
+
* cannot represent them.
|
|
17
|
+
* - In the RSC environment, this module is never evaluated — the shims plugin
|
|
18
|
+
* resolves next/navigation to navigation-rsc.ts (TIM-1420).
|
|
18
19
|
*
|
|
19
20
|
* Unlike Next.js's ReadonlyURLSearchParams, this returns a standard
|
|
20
21
|
* URLSearchParams. Mutation methods (set, delete, append) work on the
|
|
21
22
|
* local copy but do NOT affect the URL — use the router or nuqs for that.
|
|
22
23
|
*
|
|
23
|
-
* During SSR, reads the request search params from the SSR ALS context
|
|
24
|
-
* (populated by ssr-entry.ts) instead of window.location.
|
|
25
|
-
*
|
|
26
24
|
* Compatible with Next.js's `useSearchParams()` from `next/navigation`.
|
|
27
25
|
*/
|
|
28
26
|
|
|
@@ -42,24 +40,28 @@ function getSearchParams(search: string): URLSearchParams {
|
|
|
42
40
|
/**
|
|
43
41
|
* Read the current URL search params.
|
|
44
42
|
*
|
|
43
|
+
* Throws when no NavigationProvider is above the caller.
|
|
44
|
+
*
|
|
45
45
|
* Compatible with Next.js's `useSearchParams()` from `next/navigation`.
|
|
46
46
|
*/
|
|
47
47
|
export function useSearchParams(): URLSearchParams {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
// No React dispatcher available (called outside a component).
|
|
48
|
+
const nav = useNavigationContext();
|
|
49
|
+
if (nav === null) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
'[timber] useSearchParams() was called outside the timber app tree ' +
|
|
52
|
+
'(no NavigationProvider found). In tests, render the component ' +
|
|
53
|
+
'inside the timber providers.'
|
|
54
|
+
);
|
|
56
55
|
}
|
|
57
|
-
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
56
|
+
// On the server, return a fresh instance per call. The module-level cache
|
|
57
|
+
// below exists to give client code stable identity for effect deps; on the
|
|
58
|
+
// server it would be shared mutable state across concurrent requests
|
|
59
|
+
// (design/13-security.md, "no global fallback state"). Both halves of the
|
|
60
|
+
// check matter: `window` is absent in a plain Node SSR render, and
|
|
61
|
+
// getSsrData() catches SSR environments that do define `window` (handleSsr
|
|
62
|
+
// always runs inside the ALS scope). Pinned by tests/ssr-provider-hooks.test.ts.
|
|
63
|
+
if (typeof window === 'undefined' || getSsrData()) {
|
|
64
|
+
return new URLSearchParams(nav.search);
|
|
65
|
+
}
|
|
66
|
+
return getSearchParams(nav.search);
|
|
65
67
|
}
|
|
@@ -14,126 +14,39 @@
|
|
|
14
14
|
* When the Routes interface is empty (no codegen yet), the generic
|
|
15
15
|
* overload has `keyof Routes = never`, so only the fallback matches.
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
17
|
+
* One unconditional read of ParamsContext, on every side (TIM-1425):
|
|
18
|
+
*
|
|
19
|
+
* - In the browser, `PayloadRoot` publishes the payload's params above the
|
|
20
|
+
* merge point on every render path. Params update atomically with the RSC
|
|
21
|
+
* tree — no timing gap (TIM-1294, TIM-1297).
|
|
22
|
+
* - During SSR, the wrapper chain mounts the same `PayloadRoot`, fed the
|
|
23
|
+
* `params` half of `splitPayloadRoot(root)` — the identical derivation
|
|
24
|
+
* the browser performs at hydration (TIM-1424).
|
|
25
|
+
* - In the RSC environment, this module is never evaluated — the shims plugin
|
|
26
|
+
* resolves next/navigation to navigation-rsc.ts (TIM-1420).
|
|
27
|
+
* - Called outside a component entirely, React itself throws its
|
|
28
|
+
* invalid-hook-call error.
|
|
29
|
+
*
|
|
30
|
+
* The module-level subscribe/notify machinery and the `currentParams`
|
|
31
|
+
* snapshot that used to back a fourth fallback tier are gone (TIM-1425):
|
|
32
|
+
* the provider is unconditional on every render path, so nothing read them.
|
|
29
33
|
*
|
|
30
34
|
* Design doc: design/09-typescript.md §"Typed Routes"
|
|
31
35
|
*/
|
|
32
36
|
|
|
33
37
|
import type { CoercedParams } from '../shared/param-value.ts';
|
|
34
38
|
import type { Routes } from '../index.ts';
|
|
35
|
-
import {
|
|
36
|
-
import {
|
|
37
|
-
currentParams,
|
|
38
|
-
currentSlotParams,
|
|
39
|
-
_setCurrentParams,
|
|
40
|
-
_setCurrentSlotParams,
|
|
41
|
-
paramsListeners,
|
|
42
|
-
} from './state.ts';
|
|
43
|
-
import { resolveSegmentParams, type SlotParamsRecord } from '../shared/slot-params.ts';
|
|
39
|
+
import { resolveSegmentParams } from '../shared/slot-params.ts';
|
|
44
40
|
import { useParamsContext } from './params-context.ts';
|
|
45
41
|
|
|
46
|
-
// ---------------------------------------------------------------------------
|
|
47
|
-
// Module-level subscribe/notify pattern — kept for backward compat and tests
|
|
48
|
-
// ---------------------------------------------------------------------------
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Subscribe to params changes.
|
|
52
|
-
* Retained for backward compatibility with tests that verify the
|
|
53
|
-
* subscribe/notify contract. On the client, useParams() reads from
|
|
54
|
-
* NavigationContext instead.
|
|
55
|
-
*/
|
|
56
|
-
export function subscribe(callback: () => void): () => void {
|
|
57
|
-
paramsListeners.add(callback);
|
|
58
|
-
return () => paramsListeners.delete(callback);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Get the current params snapshot (module-level fallback).
|
|
63
|
-
* Used by tests and by the hook when called outside a React component.
|
|
64
|
-
*/
|
|
65
|
-
export function getSnapshot(): CoercedParams {
|
|
66
|
-
return currentParams;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// ---------------------------------------------------------------------------
|
|
70
|
-
// Framework API — called by the segment router on each navigation
|
|
71
|
-
// ---------------------------------------------------------------------------
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Set the current route params in the module-level store.
|
|
75
|
-
*
|
|
76
|
-
* Called by the router on each navigation. This updates the fallback
|
|
77
|
-
* snapshot used by tests and by the hook when called outside a React
|
|
78
|
-
* component (no NavigationContext available).
|
|
79
|
-
*
|
|
80
|
-
* On the client, the primary reactivity path is NavigationContext —
|
|
81
|
-
* the router calls setNavigationState() then renderRoot() which wraps
|
|
82
|
-
* the element in NavigationProvider. setCurrentParams is still called
|
|
83
|
-
* for the module-level fallback.
|
|
84
|
-
*
|
|
85
|
-
* During SSR, params are also available via getSsrData().params
|
|
86
|
-
* (ALS-backed).
|
|
87
|
-
*/
|
|
88
|
-
export function setCurrentParams(params: CoercedParams): void {
|
|
89
|
-
_setCurrentParams(params);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Set the per-slot params snapshot in the module-level store.
|
|
94
|
-
*
|
|
95
|
-
* Paired with `setCurrentParams`: the router calls both on every navigation,
|
|
96
|
-
* including with `null` when a response carries no slot params, so a slot's
|
|
97
|
-
* params from the *previous* route cannot be read on the next one. Fill and
|
|
98
|
-
* serve are paired; so are fill and clear. See TIM-1285.
|
|
99
|
-
*/
|
|
100
|
-
export function setCurrentSlotParams(slotParams: SlotParamsRecord | null): void {
|
|
101
|
-
_setCurrentSlotParams(slotParams);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Notify all legacy subscribers that params have changed.
|
|
106
|
-
*
|
|
107
|
-
* Retained for backward compatibility with tests. On the client,
|
|
108
|
-
* the NavigationContext + renderRoot pattern replaces this — params
|
|
109
|
-
* update atomically with the tree render, so explicit notification
|
|
110
|
-
* is no longer needed.
|
|
111
|
-
*/
|
|
112
|
-
export function notifyParamsListeners(): void {
|
|
113
|
-
for (const listener of paramsListeners) {
|
|
114
|
-
listener();
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// ---------------------------------------------------------------------------
|
|
119
|
-
// Public hook
|
|
120
|
-
// ---------------------------------------------------------------------------
|
|
121
|
-
|
|
122
42
|
/**
|
|
123
43
|
* Read the current route's dynamic params.
|
|
124
44
|
*
|
|
125
45
|
* The optional `_route` argument exists only for TypeScript narrowing —
|
|
126
46
|
* it does not affect the runtime return value.
|
|
127
47
|
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
* tree — no timing gap.
|
|
131
|
-
*
|
|
132
|
-
* During SSR, reads from the ALS-backed SSR data context to ensure
|
|
133
|
-
* per-request isolation across concurrent requests with streaming Suspense.
|
|
134
|
-
*
|
|
135
|
-
* When called outside a React component (e.g., in test assertions),
|
|
136
|
-
* falls back to the module-level snapshot.
|
|
48
|
+
* Throws when no `PayloadRoot` is above the caller (a component rendered
|
|
49
|
+
* outside the timber app — in tests, render inside the timber providers).
|
|
137
50
|
*
|
|
138
51
|
* @overload Typed — when a known segment path is passed, returns the
|
|
139
52
|
* exact params shape from the generated Routes interface.
|
|
@@ -144,25 +57,13 @@ export function useSegmentParams<R extends keyof Routes>(
|
|
|
144
57
|
): Routes[R] extends { segmentParams: infer P } ? P : CoercedParams;
|
|
145
58
|
export function useSegmentParams(segmentPath?: string): CoercedParams;
|
|
146
59
|
export function useSegmentParams(segmentPath?: string): CoercedParams {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const paramsContext = useParamsContext();
|
|
155
|
-
if (paramsContext !== null) {
|
|
156
|
-
return resolveSegmentParams(paramsContext.params, paramsContext.slotParams, segmentPath);
|
|
157
|
-
}
|
|
158
|
-
} catch {
|
|
159
|
-
// No React dispatcher available (called outside a component).
|
|
160
|
-
// Fall through to module-level snapshot below.
|
|
60
|
+
const paramsContext = useParamsContext();
|
|
61
|
+
if (paramsContext === null) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
'[timber] useSegmentParams() was called outside the timber app tree ' +
|
|
64
|
+
'(no params provider found). In tests, render the component inside ' +
|
|
65
|
+
'the timber providers.'
|
|
66
|
+
);
|
|
161
67
|
}
|
|
162
|
-
|
|
163
|
-
// SSR path: read from ALS-backed SSR data context.
|
|
164
|
-
// Falls back to module-level currentParams for tests.
|
|
165
|
-
const ssrData = getSsrData();
|
|
166
|
-
if (ssrData) return resolveSegmentParams(ssrData.params, ssrData.slotParams, segmentPath);
|
|
167
|
-
return resolveSegmentParams(currentParams, currentSlotParams, segmentPath);
|
|
68
|
+
return resolveSegmentParams(paramsContext.params, paramsContext.slotParams, segmentPath);
|
|
168
69
|
}
|
package/src/config-types.ts
CHANGED
|
@@ -288,6 +288,23 @@ export interface TimberUserConfig {
|
|
|
288
288
|
* See design/19-client-navigation.md §"X-Timber-State-Tree Header"
|
|
289
289
|
*/
|
|
290
290
|
clientSegmentCache?: boolean;
|
|
291
|
+
/**
|
|
292
|
+
* Cache behavior configuration.
|
|
293
|
+
*/
|
|
294
|
+
cache?: {
|
|
295
|
+
/**
|
|
296
|
+
* Promote the runtime-tier cache.component key-discipline warning to a
|
|
297
|
+
* hard error. When true, a cached component that reads request context
|
|
298
|
+
* (cookies, headers, searchParams) during a capture render throws
|
|
299
|
+
* instead of logging — the tainted payload is never cached.
|
|
300
|
+
*
|
|
301
|
+
* Default: false (structured error-level diagnostic, rate-limited per
|
|
302
|
+
* component). Recommended true for any app serving authenticated content.
|
|
303
|
+
*
|
|
304
|
+
* See design/45-cache-lifetimes.md §Enforcement.
|
|
305
|
+
*/
|
|
306
|
+
strictKeyDiscipline?: boolean;
|
|
307
|
+
};
|
|
291
308
|
topLoader?: {
|
|
292
309
|
/** Whether the top-loader is enabled. Default: true. */
|
|
293
310
|
enabled?: boolean;
|
package/src/config-validation.ts
CHANGED
|
@@ -46,6 +46,7 @@ const KNOWN_CONFIG_KEYS = {
|
|
|
46
46
|
reactCompiler: true,
|
|
47
47
|
sitemap: true,
|
|
48
48
|
buildDir: true,
|
|
49
|
+
cache: true,
|
|
49
50
|
clientSegmentCache: true,
|
|
50
51
|
topLoader: true,
|
|
51
52
|
budget: true,
|
|
@@ -195,6 +196,22 @@ export function validateConfig(config: TimberUserConfig): ConfigError[] {
|
|
|
195
196
|
}
|
|
196
197
|
}
|
|
197
198
|
|
|
199
|
+
// cache
|
|
200
|
+
if (config.cache != null && typeof config.cache === 'object') {
|
|
201
|
+
if (
|
|
202
|
+
config.cache.strictKeyDiscipline !== undefined &&
|
|
203
|
+
typeof config.cache.strictKeyDiscipline !== 'boolean'
|
|
204
|
+
) {
|
|
205
|
+
errors.push({
|
|
206
|
+
field: 'cache.strictKeyDiscipline',
|
|
207
|
+
message: `cache.strictKeyDiscipline must be a boolean (got ${JSON.stringify(config.cache.strictKeyDiscipline)}).`,
|
|
208
|
+
value: config.cache.strictKeyDiscipline,
|
|
209
|
+
suggestion:
|
|
210
|
+
'Use cache: { strictKeyDiscipline: true } to promote key-discipline warnings to errors.',
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
198
215
|
const MOVED_TO_CACHE_FILE = new Set(['cacheHandler', 'cdnPurge']);
|
|
199
216
|
|
|
200
217
|
for (const key of Object.keys(config)) {
|
|
@@ -13,8 +13,10 @@
|
|
|
13
13
|
*
|
|
14
14
|
* The holding server is started in rootSync's config() hook (the
|
|
15
15
|
* earliest Vite plugin lifecycle point where we know the port) and
|
|
16
|
-
* closed in timber-dev-server's configureServer() hook
|
|
17
|
-
* plugin,
|
|
16
|
+
* closed directly in timber-dev-server's configureServer() hook
|
|
17
|
+
* (the last plugin, fire-and-forget). configureServer runs during
|
|
18
|
+
* createServer(), well before Vite's port probe and bind, so the
|
|
19
|
+
* port is free by the time httpServerStart() runs. (TIM-1419)
|
|
18
20
|
*
|
|
19
21
|
* Browser requests (Accept: text/html) get the HTML holding page.
|
|
20
22
|
* API requests (Accept: application/json) get a 503 JSON response.
|
|
@@ -29,10 +31,12 @@ import http from 'node:http';
|
|
|
29
31
|
|
|
30
32
|
export interface HoldingServer {
|
|
31
33
|
/**
|
|
32
|
-
* Start listening on the given port.
|
|
34
|
+
* Start listening on the given port and host.
|
|
35
|
+
* The host must match Vite's resolved `server.host` so the probe
|
|
36
|
+
* faithfully detects conflicts on the same address Vite will bind.
|
|
33
37
|
* Returns the actual bound port (useful when port is 0 for OS assignment).
|
|
34
38
|
*/
|
|
35
|
-
listen(port: number): Promise<number>;
|
|
39
|
+
listen(port: number, host?: string): Promise<number>;
|
|
36
40
|
|
|
37
41
|
/** Gracefully close the server and stop accepting connections. */
|
|
38
42
|
close(): Promise<void>;
|
|
@@ -125,12 +129,13 @@ const HOLDING_PAGE_HTML = [
|
|
|
125
129
|
* const holding = createHoldingServer();
|
|
126
130
|
* await bindWithBump((p) => holding.listen(p), { startPort: 3000, autoBump: true });
|
|
127
131
|
*
|
|
128
|
-
* // In last plugin's configureServer() —
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
132
|
+
* // In last plugin's configureServer() — close directly so Vite 8's
|
|
133
|
+
* // isPortAvailable() probe and httpServer.listen() both see a free
|
|
134
|
+
* // port. SO_REUSEADDR allows immediate rebind. (TIM-1419)
|
|
135
|
+
* if (ctx.holdingServer) {
|
|
136
|
+
* ctx.holdingServer.close().catch(() => {});
|
|
137
|
+
* ctx.holdingServer = null;
|
|
138
|
+
* }
|
|
134
139
|
* ```
|
|
135
140
|
*/
|
|
136
141
|
export function createHoldingServer(): HoldingServer {
|
|
@@ -163,10 +168,10 @@ export function createHoldingServer(): HoldingServer {
|
|
|
163
168
|
});
|
|
164
169
|
|
|
165
170
|
return {
|
|
166
|
-
listen(port: number): Promise<number> {
|
|
171
|
+
listen(port: number, host?: string): Promise<number> {
|
|
167
172
|
return new Promise((resolve, reject) => {
|
|
168
173
|
server.once('error', reject);
|
|
169
|
-
server.listen(port, () => {
|
|
174
|
+
server.listen(port, host, () => {
|
|
170
175
|
server.removeListener('error', reject);
|
|
171
176
|
const addr = server.address();
|
|
172
177
|
const boundPort = typeof addr === 'object' && addr ? addr.port : port;
|
|
@@ -176,6 +181,12 @@ export function createHoldingServer(): HoldingServer {
|
|
|
176
181
|
},
|
|
177
182
|
|
|
178
183
|
close(): Promise<void> {
|
|
184
|
+
// Kill lingering keep-alive sockets immediately — the holding
|
|
185
|
+
// page's meta-refresh keeps browser connections alive, and
|
|
186
|
+
// server.close() waits for all sockets to drain before the
|
|
187
|
+
// 'close' event fires. Without this, the close can stall for
|
|
188
|
+
// the full keepAliveTimeout (~5s), blocking the port handoff.
|
|
189
|
+
server.closeAllConnections();
|
|
179
190
|
return new Promise((resolve, reject) => {
|
|
180
191
|
server.close((err) => {
|
|
181
192
|
if (err) reject(err);
|
package/src/index.ts
CHANGED
|
@@ -343,6 +343,16 @@ export function timber(config?: TimberUserConfig): PluginOption[] {
|
|
|
343
343
|
// that closes the holding server never runs — starting it would
|
|
344
344
|
// occupy the default port for the embedding's lifetime.
|
|
345
345
|
const isMiddlewareMode = Boolean(userConfig.server?.middlewareMode);
|
|
346
|
+
// Resolve the host Vite will bind so the holding server probes the
|
|
347
|
+
// same address. Vite defaults to 'localhost' when server.host is
|
|
348
|
+
// unset; binding wildcard (no host) would mask conflicts on
|
|
349
|
+
// localhost due to SO_REUSEADDR address-family differences. TIM-1419.
|
|
350
|
+
const resolvedHost =
|
|
351
|
+
typeof userConfig.server?.host === 'string'
|
|
352
|
+
? userConfig.server.host
|
|
353
|
+
: userConfig.server?.host === true
|
|
354
|
+
? undefined
|
|
355
|
+
: 'localhost';
|
|
346
356
|
if (command === 'serve' && !isPreview && !isMiddlewareMode) {
|
|
347
357
|
ctx.holdingServer = createHoldingServer();
|
|
348
358
|
const holdingRef = ctx.holdingServer;
|
|
@@ -350,7 +360,7 @@ export function timber(config?: TimberUserConfig): PluginOption[] {
|
|
|
350
360
|
configPort:
|
|
351
361
|
typeof userConfig.server?.port === 'number' ? userConfig.server.port : undefined,
|
|
352
362
|
envPort: process.env.PORT,
|
|
353
|
-
listen: (p) => holdingRef.listen(p),
|
|
363
|
+
listen: (p) => holdingRef.listen(p, resolvedHost),
|
|
354
364
|
});
|
|
355
365
|
if (!result.bound) {
|
|
356
366
|
// Holding server failed to bind. Drop the reference so the
|
|
@@ -358,13 +368,17 @@ export function timber(config?: TimberUserConfig): PluginOption[] {
|
|
|
358
368
|
// never listening.
|
|
359
369
|
ctx.holdingServer = null;
|
|
360
370
|
}
|
|
361
|
-
if (result.bound
|
|
362
|
-
//
|
|
363
|
-
//
|
|
364
|
-
//
|
|
365
|
-
// When unbound and implicit, skip — let Vite auto-bump.
|
|
371
|
+
if (result.bound) {
|
|
372
|
+
// Forward the probed port so Vite starts there. strictPort
|
|
373
|
+
// matches explicit — explicit ports fail loudly on a race
|
|
374
|
+
// between close and bind; implicit ones let Vite re-bump.
|
|
366
375
|
resolvedDevPort = result.port;
|
|
367
376
|
resolvedDevPortExplicit = result.explicit;
|
|
377
|
+
} else if (result.explicit) {
|
|
378
|
+
// Explicit port failed to bind (another process holds it).
|
|
379
|
+
// Forward it as the starting point but without strictPort
|
|
380
|
+
// so Vite auto-bumps to the next free port. TIM-1419.
|
|
381
|
+
resolvedDevPort = result.port;
|
|
368
382
|
}
|
|
369
383
|
} else if (command === 'serve' && isPreview) {
|
|
370
384
|
// Preview mode: resolve the starting port from config/env/
|
|
@@ -431,10 +445,11 @@ export function timber(config?: TimberUserConfig): PluginOption[] {
|
|
|
431
445
|
// Dev/preview mode: set outDir so dev-time references are
|
|
432
446
|
// consistent, and surface the resolved port to Vite.
|
|
433
447
|
//
|
|
434
|
-
// For dev, we set strictPort: true so Vite
|
|
435
|
-
//
|
|
436
|
-
//
|
|
437
|
-
//
|
|
448
|
+
// For dev with an explicit port, we set strictPort: true so Vite
|
|
449
|
+
// fails loudly on conflict. For auto-bumped (implicit) ports, we
|
|
450
|
+
// leave strictPort false so Vite can re-bump if the holding server's
|
|
451
|
+
// port becomes unavailable between close and bind (e.g., a second
|
|
452
|
+
// app instance starting on the same default port). TIM-1419.
|
|
438
453
|
//
|
|
439
454
|
// For `vite preview`, we set `preview.strictPort` based on
|
|
440
455
|
// whether the user explicitly set the port. Implicit (default)
|
|
@@ -451,7 +466,7 @@ export function timber(config?: TimberUserConfig): PluginOption[] {
|
|
|
451
466
|
if (resolvedDevPort != null) {
|
|
452
467
|
if (!isPreview) {
|
|
453
468
|
serverConfig.port = resolvedDevPort;
|
|
454
|
-
serverConfig.strictPort =
|
|
469
|
+
serverConfig.strictPort = resolvedDevPortExplicit;
|
|
455
470
|
}
|
|
456
471
|
previewConfig.port = resolvedDevPort;
|
|
457
472
|
previewConfig.strictPort = resolvedDevPortExplicit;
|
|
@@ -118,7 +118,7 @@ export function timberDevServer(ctx: PluginContext): Plugin {
|
|
|
118
118
|
* rewritten /index.html. Vite-internal and asset requests are
|
|
119
119
|
* filtered out explicitly and passed through to Vite.
|
|
120
120
|
*/
|
|
121
|
-
configureServer(server: ViteDevServer) {
|
|
121
|
+
async configureServer(server: ViteDevServer) {
|
|
122
122
|
// Ensure config files are in the watch set. Vite's watcher covers
|
|
123
123
|
// the project root, but `server.watcher.add()` is a no-op for files
|
|
124
124
|
// already watched, and ensures newly-created config files (e.g. from
|
|
@@ -191,19 +191,16 @@ export function timberDevServer(ctx: PluginContext): Plugin {
|
|
|
191
191
|
// discover it. See design/47-module-environment-tooling.md §5.
|
|
192
192
|
registerDevDiscovery(server.httpServer, ctx.root, server.config);
|
|
193
193
|
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
//
|
|
194
|
+
// Close the holding server and await the port release so Vite's
|
|
195
|
+
// port probe and bind see a free port. configureServer is awaited
|
|
196
|
+
// by Vite during createServer(), before startServer() runs
|
|
197
|
+
// isPortAvailable() and httpServer.listen().
|
|
198
|
+
// closeAllConnections() (in HoldingServer.close) kills keep-alive
|
|
199
|
+
// sockets from the meta-refresh page so close() resolves promptly.
|
|
200
|
+
// See design/21-dev-server.md §"Startup Holding Server", TIM-1419.
|
|
199
201
|
if (ctx.holdingServer) {
|
|
200
|
-
|
|
201
|
-
const holdingRef = ctx.holdingServer;
|
|
202
|
+
await ctx.holdingServer.close().catch(() => {});
|
|
202
203
|
ctx.holdingServer = null;
|
|
203
|
-
server.listen = async (port?: number, isRestart?: boolean) => {
|
|
204
|
-
await holdingRef.close().catch(() => {});
|
|
205
|
-
return originalListen(port, isRestart);
|
|
206
|
-
};
|
|
207
204
|
}
|
|
208
205
|
|
|
209
206
|
// Log startup timing summary. configureServer runs on all plugins
|
package/src/plugins/entries.ts
CHANGED
|
@@ -159,6 +159,9 @@ function generateConfigModule(ctx: PluginContext): string {
|
|
|
159
159
|
// Null in dev mode — HMR handles code updates without full reloads.
|
|
160
160
|
deploymentId: ctx.deploymentId ?? null,
|
|
161
161
|
clientSegmentCache: ctx.config.clientSegmentCache ?? false,
|
|
162
|
+
cache: {
|
|
163
|
+
strictKeyDiscipline: ctx.config.cache?.strictKeyDiscipline ?? false,
|
|
164
|
+
},
|
|
162
165
|
// Browser log forwarding threshold — used by buildClientScripts to
|
|
163
166
|
// decide whether /@vite/client is needed when clientJavascript is
|
|
164
167
|
// disabled. See TIM-1152.
|
package/src/plugins/shims.ts
CHANGED
|
@@ -198,14 +198,15 @@ export function timberShims(_ctx: PluginContext): Plugin {
|
|
|
198
198
|
const cleanId = stripJsExtension(id);
|
|
199
199
|
|
|
200
200
|
// Check next/* shim map.
|
|
201
|
-
//
|
|
202
|
-
// navigation-
|
|
203
|
-
//
|
|
204
|
-
//
|
|
201
|
+
// next/navigation resolves per environment (TIM-1420):
|
|
202
|
+
// RSC → navigation-rsc.ts (throwing stubs — no useContext)
|
|
203
|
+
// SSR → navigation.ts (real hooks + server functions)
|
|
204
|
+
// client → navigation-client.ts (real hooks, no server imports)
|
|
205
205
|
if (cleanId in SHIM_MAP) {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
return resolve(SHIMS_DIR, 'navigation-client.ts');
|
|
206
|
+
if (cleanId === 'next/navigation') {
|
|
207
|
+
const env = envName(this);
|
|
208
|
+
if (env === 'client') return resolve(SHIMS_DIR, 'navigation-client.ts');
|
|
209
|
+
if (env === 'rsc') return resolve(SHIMS_DIR, 'navigation-rsc.ts');
|
|
209
210
|
}
|
|
210
211
|
return SHIM_MAP[cleanId];
|
|
211
212
|
}
|
|
@@ -228,7 +228,11 @@ export class StaticOutputPaths {
|
|
|
228
228
|
* asked, not assumed. Folding unconditionally would reject `/Foo` beside
|
|
229
229
|
* `/foo` on Linux, where both are perfectly servable files.
|
|
230
230
|
*/
|
|
231
|
-
|
|
231
|
+
private readonly identity: FsIdentity;
|
|
232
|
+
|
|
233
|
+
constructor(identity: FsIdentity = EXACT_FS_IDENTITY) {
|
|
234
|
+
this.identity = identity;
|
|
235
|
+
}
|
|
232
236
|
|
|
233
237
|
/** The identity under which the filesystem will compare this path. */
|
|
234
238
|
private key(relPath: string): string {
|
|
@@ -309,10 +313,10 @@ export class StaticSiteWriter {
|
|
|
309
313
|
readonly contentTypes: Record<string, string> = {};
|
|
310
314
|
private readonly paths: StaticOutputPaths;
|
|
311
315
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
+
private readonly staticDir: string;
|
|
317
|
+
|
|
318
|
+
constructor(staticDir: string, identity: FsIdentity = EXACT_FS_IDENTITY) {
|
|
319
|
+
this.staticDir = staticDir;
|
|
316
320
|
this.paths = new StaticOutputPaths(identity);
|
|
317
321
|
}
|
|
318
322
|
|