@timber-js/app 0.2.0-alpha.146 → 0.2.0-alpha.148
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-OIZ8siQu.js → actions-a0RczDnB.js} +8 -34
- package/dist/_chunks/{actions-OIZ8siQu.js.map → actions-a0RczDnB.js.map} +1 -1
- package/dist/_chunks/cache-api-Co-yI-gP.js +424 -0
- package/dist/_chunks/cache-api-Co-yI-gP.js.map +1 -0
- package/dist/_chunks/{metadata-routes-BjepjefT.js → canonicalize-P41GR6tY.js} +65 -2
- package/dist/_chunks/canonicalize-P41GR6tY.js.map +1 -0
- package/dist/_chunks/{cli-schema-sync-D2IpXicg.js → cli-schema-sync-DJDdyEae.js} +157 -313
- package/dist/_chunks/cli-schema-sync-DJDdyEae.js.map +1 -0
- package/dist/_chunks/cli-schema-sync-D_IhHEUW.js +2 -0
- package/dist/_chunks/{define-DMVhsipD.js → define-Bssfp6ot.js} +3 -33
- package/dist/_chunks/define-Bssfp6ot.js.map +1 -0
- package/dist/_chunks/{define-C3RvHQPK.js → define-CFmvb4Bt.js} +15 -7
- package/dist/_chunks/define-CFmvb4Bt.js.map +1 -0
- package/dist/_chunks/{href-validation-DyZIzfMJ.js → href-validation-FiA_rToW.js} +2 -1
- package/dist/_chunks/{href-validation-DyZIzfMJ.js.map → href-validation-FiA_rToW.js.map} +1 -1
- package/dist/_chunks/{tracing-M_zKubhk.js → logger-CM0clugc.js} +394 -107
- package/dist/_chunks/logger-CM0clugc.js.map +1 -0
- package/dist/_chunks/navigation-root-bUDZLw74.js.map +1 -1
- package/dist/_chunks/plugin-context-DurqFBuY.js +2 -0
- package/dist/_chunks/plugin-context-GfDWsdd2.js +268 -0
- package/dist/_chunks/plugin-context-GfDWsdd2.js.map +1 -0
- package/dist/_chunks/{resolve-schema-Dj8tQloi.js → resolve-schema-3iUvBV5T.js} +3 -30
- package/dist/_chunks/resolve-schema-3iUvBV5T.js.map +1 -0
- package/dist/_chunks/{schema-bridge-DK_89h8O.js → schema-bridge-BY3QLBL7.js} +35 -2
- package/dist/_chunks/schema-bridge-BY3QLBL7.js.map +1 -0
- package/dist/_chunks/tree-match-D2l830j2.js +102 -0
- package/dist/_chunks/tree-match-D2l830j2.js.map +1 -0
- package/dist/_chunks/use-segment-params-D1CzlgBo.js.map +1 -1
- package/dist/_chunks/{walkers-7FBPM-hX.js → walkers-DvWDj453.js} +154 -88
- package/dist/_chunks/walkers-DvWDj453.js.map +1 -0
- package/dist/adapters/cloudflare-kv-cache.d.ts.map +1 -1
- package/dist/adapters/cloudflare-kv-cache.js +36 -14
- package/dist/adapters/cloudflare-kv-cache.js.map +1 -1
- package/dist/adapters/compress-module.d.ts +5 -4
- package/dist/adapters/compress-module.d.ts.map +1 -1
- package/dist/adapters/nitro.d.ts +1 -3
- package/dist/adapters/nitro.d.ts.map +1 -1
- package/dist/adapters/nitro.js +14 -134
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/cache/cache-api.d.ts.map +1 -1
- package/dist/cache/index.d.ts +3 -1
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +20 -235
- package/dist/cache/index.js.map +1 -1
- package/dist/cache/invalidation-epoch.d.ts +43 -0
- package/dist/cache/invalidation-epoch.d.ts.map +1 -0
- package/dist/cache/redis-handler.d.ts +13 -0
- package/dist/cache/redis-handler.d.ts.map +1 -1
- package/dist/cache/timber-cache.d.ts +17 -9
- package/dist/cache/timber-cache.d.ts.map +1 -1
- package/dist/cli-schema-sync.d.ts +21 -1
- package/dist/cli-schema-sync.d.ts.map +1 -1
- package/dist/cli.d.ts +12 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +26 -31
- package/dist/cli.js.map +1 -1
- package/dist/client/browser-entry/hmr.d.ts.map +1 -1
- package/dist/client/browser-entry/hydrate.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.map +1 -1
- package/dist/client/error-boundary.d.ts.map +1 -1
- package/dist/client/error-boundary.js +13 -4
- package/dist/client/error-boundary.js.map +1 -1
- package/dist/client/error-reconstituter.d.ts +8 -1
- package/dist/client/error-reconstituter.d.ts.map +1 -1
- package/dist/client/history.d.ts +10 -17
- package/dist/client/history.d.ts.map +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.js +30 -33
- package/dist/client/internal.js.map +1 -1
- package/dist/client/navigation-api.d.ts +9 -2
- package/dist/client/navigation-api.d.ts.map +1 -1
- package/dist/client/navigation-context.d.ts +0 -11
- package/dist/client/navigation-context.d.ts.map +1 -1
- package/dist/client/navigation-root.d.ts +0 -5
- package/dist/client/navigation-root.d.ts.map +1 -1
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/client/use-search-params.d.ts +10 -5
- package/dist/client/use-search-params.d.ts.map +1 -1
- package/dist/codec.js +1 -1
- package/dist/config-validation.d.ts.map +1 -1
- package/dist/cookies/define-cookie.d.ts.map +1 -1
- package/dist/cookies/index.js +2 -4
- package/dist/cookies/index.js.map +1 -1
- package/dist/dev-tools/dev-page-shell.d.ts +1 -0
- package/dist/dev-tools/dev-page-shell.d.ts.map +1 -1
- package/dist/dev-tools/hydration-overlay.d.ts +11 -0
- package/dist/dev-tools/hydration-overlay.d.ts.map +1 -0
- package/dist/dev-tools/index.d.ts +1 -2
- package/dist/dev-tools/index.d.ts.map +1 -1
- package/dist/fonts/ast-walk.d.ts +68 -0
- package/dist/fonts/ast-walk.d.ts.map +1 -0
- package/dist/fonts/ast.d.ts +23 -11
- package/dist/fonts/ast.d.ts.map +1 -1
- package/dist/fonts/bundle.d.ts +3 -1
- package/dist/fonts/bundle.d.ts.map +1 -1
- package/dist/fonts/dev-middleware.d.ts +1 -1
- package/dist/fonts/dev-middleware.d.ts.map +1 -1
- package/dist/fonts/fallbacks.d.ts +0 -7
- package/dist/fonts/fallbacks.d.ts.map +1 -1
- package/dist/fonts/google.d.ts +4 -2
- package/dist/fonts/google.d.ts.map +1 -1
- package/dist/fonts/local.d.ts +3 -3
- package/dist/fonts/local.d.ts.map +1 -1
- package/dist/fonts/paths.d.ts +43 -0
- package/dist/fonts/paths.d.ts.map +1 -0
- package/dist/fonts/pipeline.d.ts +7 -21
- package/dist/fonts/pipeline.d.ts.map +1 -1
- package/dist/fonts/transform.d.ts.map +1 -1
- package/dist/fonts/virtual-modules.d.ts +1 -18
- package/dist/fonts/virtual-modules.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +609 -406
- package/dist/index.js.map +1 -1
- package/dist/params/index.js +1 -1
- package/dist/params/resolve-schema.d.ts.map +1 -1
- package/dist/plugin-context.d.ts +28 -5
- package/dist/plugin-context.d.ts.map +1 -1
- package/dist/plugins/cache.d.ts +13 -5
- package/dist/plugins/cache.d.ts.map +1 -1
- package/dist/plugins/dev-server.d.ts.map +1 -1
- package/dist/plugins/fonts.d.ts +1 -2
- package/dist/plugins/fonts.d.ts.map +1 -1
- package/dist/plugins/routing.d.ts.map +1 -1
- package/dist/routing/codegen-types.d.ts +4 -0
- package/dist/routing/codegen-types.d.ts.map +1 -1
- package/dist/routing/codegen.d.ts.map +1 -1
- package/dist/routing/convention-lint.d.ts.map +1 -1
- package/dist/routing/file-cache.d.ts +8 -0
- package/dist/routing/file-cache.d.ts.map +1 -0
- package/dist/routing/index.js +2 -2
- package/dist/routing/interception.d.ts +0 -5
- package/dist/routing/interception.d.ts.map +1 -1
- package/dist/routing/manifest-codegen.d.ts +32 -0
- package/dist/routing/manifest-codegen.d.ts.map +1 -0
- package/dist/routing/scanner.d.ts.map +1 -1
- package/dist/schema-bridge.d.ts +15 -0
- package/dist/schema-bridge.d.ts.map +1 -1
- package/dist/search-params/define.d.ts.map +1 -1
- package/dist/search-params/index.js +7 -3
- package/dist/search-params/index.js.map +1 -1
- package/dist/search-params/wrappers.d.ts.map +1 -1
- package/dist/segment-params/define.d.ts.map +1 -1
- package/dist/segment-params/index.js +1 -1
- package/dist/server/access-gate.d.ts.map +1 -1
- package/dist/server/actions.d.ts +0 -14
- package/dist/server/actions.d.ts.map +1 -1
- package/dist/server/build-manifest.d.ts +0 -29
- package/dist/server/build-manifest.d.ts.map +1 -1
- package/dist/server/canonicalize.d.ts +10 -0
- package/dist/server/canonicalize.d.ts.map +1 -1
- package/dist/server/deny-boundary.d.ts +27 -2
- package/dist/server/deny-boundary.d.ts.map +1 -1
- package/dist/server/deny-renderer.d.ts +1 -1
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
- package/dist/server/html-injector-core.d.ts +8 -0
- package/dist/server/html-injector-core.d.ts.map +1 -1
- package/dist/server/html-injectors.d.ts.map +1 -1
- package/dist/server/index.js +2 -2
- package/dist/server/internal.d.ts +1 -5
- package/dist/server/internal.d.ts.map +1 -1
- package/dist/server/internal.js +238 -349
- package/dist/server/internal.js.map +1 -1
- package/dist/server/node-stream-transforms.d.ts +0 -11
- package/dist/server/node-stream-transforms.d.ts.map +1 -1
- package/dist/server/render-utils.d.ts +25 -0
- package/dist/server/render-utils.d.ts.map +1 -0
- package/dist/server/route-element-builder.d.ts +11 -25
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/helpers.d.ts +2 -18
- package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.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.map +1 -1
- package/dist/server/ssr-render.d.ts.map +1 -1
- package/dist/server/status-code-resolver.d.ts +6 -0
- package/dist/server/status-code-resolver.d.ts.map +1 -1
- package/dist/server/utils/mdx-file.d.ts +15 -0
- package/dist/server/utils/mdx-file.d.ts.map +1 -0
- package/dist/shared/href-validation.d.ts.map +1 -1
- package/dist/shims/image.d.ts +15 -15
- package/package.json +3 -3
- package/src/adapters/cloudflare-kv-cache.ts +56 -19
- package/src/adapters/compress-module.ts +27 -118
- package/src/adapters/nitro.ts +3 -41
- package/src/adapters/raw-imports.d.ts +5 -0
- package/src/cache/cache-api.ts +9 -1
- package/src/cache/index.ts +5 -2
- package/src/cache/invalidation-epoch.ts +93 -0
- package/src/cache/redis-handler.ts +33 -2
- package/src/cache/timber-cache.ts +109 -53
- package/src/cli-schema-sync.ts +123 -30
- package/src/cli.ts +33 -37
- package/src/client/browser-entry/hmr.ts +0 -11
- package/src/client/browser-entry/hydrate.ts +39 -5
- package/src/client/browser-entry/post-hydration.ts +6 -15
- package/src/client/browser-entry/router-init.ts +98 -3
- package/src/client/error-boundary.tsx +14 -0
- package/src/client/error-reconstituter.tsx +15 -1
- package/src/client/history.ts +10 -24
- package/src/client/navigation-api.ts +14 -9
- package/src/client/navigation-context.ts +1 -21
- package/src/client/navigation-root.tsx +0 -8
- package/src/client/router.ts +18 -8
- package/src/client/rsc-fetch.ts +7 -2
- package/src/client/use-search-params.ts +10 -5
- package/src/config-validation.ts +33 -25
- package/src/cookies/define-cookie.ts +2 -18
- package/src/dev-tools/dev-page-shell.ts +6 -2
- package/src/dev-tools/hydration-overlay.ts +158 -0
- package/src/dev-tools/index.ts +3 -16
- package/src/fonts/ast-walk.ts +235 -0
- package/src/fonts/ast.ts +91 -249
- package/src/fonts/bundle.ts +16 -9
- package/src/fonts/dev-middleware.ts +16 -5
- package/src/fonts/fallbacks.ts +0 -23
- package/src/fonts/google.ts +7 -3
- package/src/fonts/local.ts +7 -6
- package/src/fonts/paths.ts +58 -0
- package/src/fonts/pipeline.ts +27 -48
- package/src/fonts/transform.ts +27 -0
- package/src/fonts/virtual-modules.ts +1 -57
- package/src/index.ts +5 -11
- package/src/params/resolve-schema.ts +2 -34
- package/src/plugin-context.ts +95 -17
- package/src/plugins/cache.ts +64 -17
- package/src/plugins/dev-server.ts +61 -9
- package/src/plugins/fonts.ts +21 -6
- package/src/plugins/routing.ts +5 -348
- package/src/routing/codegen-types.ts +4 -0
- package/src/routing/codegen.ts +66 -8
- package/src/routing/convention-lint.ts +4 -3
- package/src/routing/file-cache.ts +23 -0
- package/src/routing/interception.ts +2 -21
- package/src/routing/manifest-codegen.ts +290 -0
- package/src/routing/scanner.ts +85 -80
- package/src/routing/schema-validation.ts +2 -2
- package/src/schema-bridge.ts +63 -2
- package/src/search-params/define.ts +34 -2
- package/src/search-params/wrappers.ts +21 -3
- package/src/segment-params/define.ts +2 -62
- package/src/server/access-gate.tsx +8 -0
- package/src/server/actions.ts +8 -31
- package/src/server/build-manifest.ts +0 -56
- package/src/server/canonicalize.ts +6 -3
- package/src/server/deny-boundary.ts +106 -13
- package/src/server/deny-renderer.ts +46 -10
- package/src/server/error-boundary-wrapper.ts +1 -14
- package/src/server/html-injector-core.ts +19 -0
- package/src/server/html-injectors.ts +14 -4
- package/src/server/internal.ts +1 -18
- package/src/server/node-stream-transforms.ts +5 -72
- package/src/server/render-utils.ts +31 -0
- package/src/server/route-element-builder.ts +77 -47
- package/src/server/route-matcher.ts +22 -6
- package/src/server/rsc-entry/error-renderer.ts +2 -12
- package/src/server/rsc-entry/helpers.ts +6 -34
- package/src/server/rsc-entry/index.ts +11 -0
- package/src/server/rsc-entry/render-route.ts +7 -9
- package/src/server/rsc-entry/rsc-payload.ts +2 -1
- package/src/server/rsc-entry/ssr-renderer.ts +2 -2
- package/src/server/ssr-render.ts +1 -11
- package/src/server/status-code-resolver.ts +3 -2
- package/src/server/tree-match.ts +15 -5
- package/src/server/utils/mdx-file.ts +22 -0
- package/src/shared/href-validation.ts +8 -0
- package/dist/_chunks/cli-schema-sync-D2IpXicg.js.map +0 -1
- package/dist/_chunks/cli-schema-sync-DH-M8Poy.js +0 -2
- package/dist/_chunks/define-C3RvHQPK.js.map +0 -1
- package/dist/_chunks/define-DMVhsipD.js.map +0 -1
- package/dist/_chunks/handler-store-BVDgyTh7.js +0 -78
- package/dist/_chunks/handler-store-BVDgyTh7.js.map +0 -1
- package/dist/_chunks/logger-Dt-jQaBO.js +0 -291
- package/dist/_chunks/logger-Dt-jQaBO.js.map +0 -1
- package/dist/_chunks/metadata-routes-BjepjefT.js.map +0 -1
- package/dist/_chunks/resolve-schema-Dj8tQloi.js.map +0 -1
- package/dist/_chunks/schema-bridge-DK_89h8O.js.map +0 -1
- package/dist/_chunks/tracing-M_zKubhk.js.map +0 -1
- package/dist/_chunks/walkers-7FBPM-hX.js.map +0 -1
- package/dist/_chunks/warnings-CNPXaypM.js +0 -125
- package/dist/_chunks/warnings-CNPXaypM.js.map +0 -1
- package/dist/client/link-pending-store.d.ts +0 -65
- package/dist/client/link-pending-store.d.ts.map +0 -1
- package/dist/client/nav-link-store.d.ts +0 -27
- package/dist/client/nav-link-store.d.ts.map +0 -1
- package/dist/dev-tools/warnings.d.ts +0 -109
- package/dist/dev-tools/warnings.d.ts.map +0 -1
- package/dist/server/flush.d.ts +0 -74
- package/dist/server/flush.d.ts.map +0 -1
- package/src/client/link-pending-store.ts +0 -116
- package/src/client/nav-link-store.ts +0 -37
- package/src/dev-tools/warnings.ts +0 -238
- package/src/server/flush.ts +0 -183
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
//#region src/server/tree-match.ts
|
|
2
|
+
/**
|
|
3
|
+
* Match URL path parts against a segment tree.
|
|
4
|
+
*
|
|
5
|
+
* Walks the tree depth-first with backtracking, applying priority
|
|
6
|
+
* ordering globally across group boundaries. Returns the matched
|
|
7
|
+
* segment chain and collected params, or null if no route matches.
|
|
8
|
+
*
|
|
9
|
+
* By default, a match requires a terminal node (one with `page` or
|
|
10
|
+
* `route`) at the leaf. Pass `{ requireTerminal: false }` to match
|
|
11
|
+
* any segment (used by metadata route matching).
|
|
12
|
+
*
|
|
13
|
+
* @param root The root segment node to start matching from.
|
|
14
|
+
* @param parts URL path parts (e.g. ['blog', 'hello-world']).
|
|
15
|
+
* Empty array matches the root itself (if terminal).
|
|
16
|
+
*/
|
|
17
|
+
function matchUrlParts(root, parts, options) {
|
|
18
|
+
const chain = [];
|
|
19
|
+
const params = {};
|
|
20
|
+
if (matchNode(root, parts, 0, chain, params, options?.requireTerminal ?? true)) return {
|
|
21
|
+
chain,
|
|
22
|
+
params
|
|
23
|
+
};
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Flatten children through group segments.
|
|
28
|
+
*
|
|
29
|
+
* Groups are transparent for URL matching — their non-group descendants
|
|
30
|
+
* are returned with the chain of group nodes leading to them. This lets
|
|
31
|
+
* the caller apply priority ordering (static > dynamic > catch-all >
|
|
32
|
+
* optional-catch-all) across all groups uniformly instead of per-group.
|
|
33
|
+
*/
|
|
34
|
+
function collectEffectiveChildren(node, groupChain = []) {
|
|
35
|
+
const result = [];
|
|
36
|
+
for (const child of node.children) if (child.segmentType === "group") {
|
|
37
|
+
const nested = collectEffectiveChildren(child, [...groupChain, child]);
|
|
38
|
+
result.push(...nested);
|
|
39
|
+
} else result.push({
|
|
40
|
+
child,
|
|
41
|
+
groupChain
|
|
42
|
+
});
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
/** A node is terminal if it can serve a response (has a page or route handler). */
|
|
46
|
+
function isTerminal(node) {
|
|
47
|
+
return !!(node.page || node.route);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Recursive depth-first matcher with backtracking.
|
|
51
|
+
*
|
|
52
|
+
* Mutates `chain` and `params` in place for performance; backtracks
|
|
53
|
+
* (restores previous state) when a candidate doesn't lead to a match.
|
|
54
|
+
*/
|
|
55
|
+
function matchNode(node, parts, index, chain, params, requireTerminal) {
|
|
56
|
+
chain.push(node);
|
|
57
|
+
if (index >= parts.length) {
|
|
58
|
+
if (!requireTerminal || isTerminal(node)) return true;
|
|
59
|
+
for (const child of node.children) if (child.segmentType === "group") {
|
|
60
|
+
if (matchNode(child, parts, index, chain, params, requireTerminal)) return true;
|
|
61
|
+
}
|
|
62
|
+
const effective = collectEffectiveChildren(node);
|
|
63
|
+
for (const { child, groupChain } of effective) if (child.segmentType === "optional-catch-all" && (!requireTerminal || isTerminal(child))) {
|
|
64
|
+
chain.push(...groupChain, child);
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
chain.pop();
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
const part = parts[index];
|
|
71
|
+
const effective = collectEffectiveChildren(node);
|
|
72
|
+
for (const { child, groupChain } of effective) if (child.segmentType === "static" && child.segmentName === part) {
|
|
73
|
+
chain.push(...groupChain);
|
|
74
|
+
if (matchNode(child, parts, index + 1, chain, params, requireTerminal)) return true;
|
|
75
|
+
chain.length -= groupChain.length;
|
|
76
|
+
}
|
|
77
|
+
for (const { child, groupChain } of effective) if (child.segmentType === "dynamic" && child.paramName) {
|
|
78
|
+
chain.push(...groupChain);
|
|
79
|
+
const prev = params[child.paramName];
|
|
80
|
+
params[child.paramName] = part;
|
|
81
|
+
if (matchNode(child, parts, index + 1, chain, params, requireTerminal)) return true;
|
|
82
|
+
chain.length -= groupChain.length;
|
|
83
|
+
if (prev !== void 0) params[child.paramName] = prev;
|
|
84
|
+
else delete params[child.paramName];
|
|
85
|
+
}
|
|
86
|
+
for (const { child, groupChain } of effective) if (child.segmentType === "catch-all" && child.paramName && (!requireTerminal || isTerminal(child))) {
|
|
87
|
+
chain.push(...groupChain, child);
|
|
88
|
+
params[child.paramName] = parts.slice(index);
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
for (const { child, groupChain } of effective) if (child.segmentType === "optional-catch-all" && child.paramName && (!requireTerminal || isTerminal(child))) {
|
|
92
|
+
chain.push(...groupChain, child);
|
|
93
|
+
params[child.paramName] = parts.slice(index);
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
chain.pop();
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
//#endregion
|
|
100
|
+
export { matchUrlParts as t };
|
|
101
|
+
|
|
102
|
+
//# sourceMappingURL=tree-match-D2l830j2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tree-match-D2l830j2.js","names":[],"sources":["../../src/server/tree-match.ts"],"sourcesContent":["/**\n * Shared URL-matching tree walker for segment trees.\n *\n * Both the main route matcher (`route-matcher.ts`) and slot resolver\n * (`slot-resolver.ts`) need to walk a SegmentNode tree, matching URL\n * parts against children with the same priority semantics:\n *\n * static > dynamic > catch-all > optional-catch-all\n *\n * Before this module, each had its own walker with subtle divergences\n * (different group handling, missing backtracking, incomplete\n * optional-catch-all support). This shared walker is the single\n * source of truth for URL-to-segment matching.\n *\n * See design/07-routing.md §\"Request Lifecycle\" and TIM-940.\n */\n\nimport type { SegmentNode } from '../routing/types.js';\n\n// ─── Public API ───────────────────────────────────────────────────────────\n\n/** Result of matching URL parts against a segment tree. */\nexport interface TreeMatchResult<TFile> {\n /** Matched segment chain from root to leaf, inclusive. */\n chain: SegmentNode<TFile>[];\n /** Collected params, keyed by param name. Catch-all params are string[]. */\n params: Record<string, string | string[]>;\n}\n\nexport interface TreeMatchOptions {\n /**\n * When true (default), only match if the leaf node has a page or route\n * handler. Set to false for metadata route matching, where we just need\n * to find the owning segment regardless of whether it has a page.\n */\n requireTerminal?: boolean;\n}\n\n/**\n * Match URL path parts against a segment tree.\n *\n * Walks the tree depth-first with backtracking, applying priority\n * ordering globally across group boundaries. Returns the matched\n * segment chain and collected params, or null if no route matches.\n *\n * By default, a match requires a terminal node (one with `page` or\n * `route`) at the leaf. Pass `{ requireTerminal: false }` to match\n * any segment (used by metadata route matching).\n *\n * @param root The root segment node to start matching from.\n * @param parts URL path parts (e.g. ['blog', 'hello-world']).\n * Empty array matches the root itself (if terminal).\n */\nexport function matchUrlParts<TFile>(\n root: SegmentNode<TFile>,\n parts: string[],\n options?: TreeMatchOptions\n): TreeMatchResult<TFile> | null {\n const chain: SegmentNode<TFile>[] = [];\n const params: Record<string, string | string[]> = {};\n const requireTerminal = options?.requireTerminal ?? true;\n\n if (matchNode(root, parts, 0, chain, params, requireTerminal)) {\n return { chain, params };\n }\n return null;\n}\n\n// ─── Internal ─────────────────────────────────────────────────────────────\n\n/**\n * A child node flattened through group segments, along with the\n * chain of group nodes that must appear in the segment chain\n * before the child to preserve the tree path.\n */\ninterface EffectiveChild<TFile> {\n child: SegmentNode<TFile>;\n groupChain: SegmentNode<TFile>[];\n}\n\n/**\n * Flatten children through group segments.\n *\n * Groups are transparent for URL matching — their non-group descendants\n * are returned with the chain of group nodes leading to them. This lets\n * the caller apply priority ordering (static > dynamic > catch-all >\n * optional-catch-all) across all groups uniformly instead of per-group.\n */\nfunction collectEffectiveChildren<TFile>(\n node: SegmentNode<TFile>,\n groupChain: SegmentNode<TFile>[] = []\n): EffectiveChild<TFile>[] {\n const result: EffectiveChild<TFile>[] = [];\n for (const child of node.children) {\n if (child.segmentType === 'group') {\n const nested = collectEffectiveChildren(child, [...groupChain, child]);\n result.push(...nested);\n } else {\n result.push({ child, groupChain });\n }\n }\n return result;\n}\n\n/** A node is terminal if it can serve a response (has a page or route handler). */\nfunction isTerminal<TFile>(node: SegmentNode<TFile>): boolean {\n return !!(node.page || node.route);\n}\n\n/**\n * Recursive depth-first matcher with backtracking.\n *\n * Mutates `chain` and `params` in place for performance; backtracks\n * (restores previous state) when a candidate doesn't lead to a match.\n */\nfunction matchNode<TFile>(\n node: SegmentNode<TFile>,\n parts: string[],\n index: number,\n chain: SegmentNode<TFile>[],\n params: Record<string, string | string[]>,\n requireTerminal: boolean\n): boolean {\n chain.push(node);\n\n // All parts consumed — check if this node is terminal (or any node if !requireTerminal)\n if (index >= parts.length) {\n if (!requireTerminal || isTerminal(node)) return true;\n\n // Groups don't consume URL segments, so a group child's page can\n // match at the same URL depth. Recurse into groups transparently.\n for (const child of node.children) {\n if (child.segmentType === 'group') {\n if (matchNode(child, parts, index, chain, params, requireTerminal)) {\n return true;\n }\n }\n }\n\n // Optional catch-all with zero segments (direct and through groups).\n // The param is NOT set — matching Next.js semantics for [[...slug]]\n // with no path segments.\n const effective = collectEffectiveChildren(node);\n for (const { child, groupChain } of effective) {\n if (child.segmentType === 'optional-catch-all' && (!requireTerminal || isTerminal(child))) {\n chain.push(...groupChain, child);\n return true;\n }\n }\n\n chain.pop();\n return false;\n }\n\n const part = parts[index];\n\n // Flatten children through groups so priority ordering applies globally.\n const effective = collectEffectiveChildren(node);\n\n // 1. Static segments (exact name match)\n for (const { child, groupChain } of effective) {\n if (child.segmentType === 'static' && child.segmentName === part) {\n chain.push(...groupChain);\n if (matchNode(child, parts, index + 1, chain, params, requireTerminal)) {\n return true;\n }\n chain.length -= groupChain.length;\n }\n }\n\n // 2. Dynamic segments ([param])\n for (const { child, groupChain } of effective) {\n if (child.segmentType === 'dynamic' && child.paramName) {\n chain.push(...groupChain);\n const prev = params[child.paramName];\n params[child.paramName] = part;\n if (matchNode(child, parts, index + 1, chain, params, requireTerminal)) {\n return true;\n }\n // Backtrack\n chain.length -= groupChain.length;\n if (prev !== undefined) {\n params[child.paramName] = prev;\n } else {\n delete params[child.paramName];\n }\n }\n }\n\n // 3. Catch-all segments ([...param]) — must be terminal unless the\n // caller passed requireTerminal:false (metadata route matching needs to\n // find the owning segment even when it has no page).\n for (const { child, groupChain } of effective) {\n if (\n child.segmentType === 'catch-all' &&\n child.paramName &&\n (!requireTerminal || isTerminal(child))\n ) {\n chain.push(...groupChain, child);\n params[child.paramName] = parts.slice(index);\n return true;\n }\n }\n\n // 4. Optional catch-all segments ([[...param]]) — same terminal rule\n for (const { child, groupChain } of effective) {\n if (\n child.segmentType === 'optional-catch-all' &&\n child.paramName &&\n (!requireTerminal || isTerminal(child))\n ) {\n chain.push(...groupChain, child);\n params[child.paramName] = parts.slice(index);\n return true;\n }\n }\n\n chain.pop();\n return false;\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAqDA,SAAgB,cACd,MACA,OACA,SAC+B;CAC/B,MAAM,QAA8B,CAAC;CACrC,MAAM,SAA4C,CAAC;CAGnD,IAAI,UAAU,MAAM,OAAO,GAAG,OAAO,QAFb,SAAS,mBAAmB,IAEQ,GAC1D,OAAO;EAAE;EAAO;CAAO;CAEzB,OAAO;AACT;;;;;;;;;AAsBA,SAAS,yBACP,MACA,aAAmC,CAAC,GACX;CACzB,MAAM,SAAkC,CAAC;CACzC,KAAK,MAAM,SAAS,KAAK,UACvB,IAAI,MAAM,gBAAgB,SAAS;EACjC,MAAM,SAAS,yBAAyB,OAAO,CAAC,GAAG,YAAY,KAAK,CAAC;EACrE,OAAO,KAAK,GAAG,MAAM;CACvB,OACE,OAAO,KAAK;EAAE;EAAO;CAAW,CAAC;CAGrC,OAAO;AACT;;AAGA,SAAS,WAAkB,MAAmC;CAC5D,OAAO,CAAC,EAAE,KAAK,QAAQ,KAAK;AAC9B;;;;;;;AAQA,SAAS,UACP,MACA,OACA,OACA,OACA,QACA,iBACS;CACT,MAAM,KAAK,IAAI;CAGf,IAAI,SAAS,MAAM,QAAQ;EACzB,IAAI,CAAC,mBAAmB,WAAW,IAAI,GAAG,OAAO;EAIjD,KAAK,MAAM,SAAS,KAAK,UACvB,IAAI,MAAM,gBAAgB;OACpB,UAAU,OAAO,OAAO,OAAO,OAAO,QAAQ,eAAe,GAC/D,OAAO;EAAA;EAQb,MAAM,YAAY,yBAAyB,IAAI;EAC/C,KAAK,MAAM,EAAE,OAAO,gBAAgB,WAClC,IAAI,MAAM,gBAAgB,yBAAyB,CAAC,mBAAmB,WAAW,KAAK,IAAI;GACzF,MAAM,KAAK,GAAG,YAAY,KAAK;GAC/B,OAAO;EACT;EAGF,MAAM,IAAI;EACV,OAAO;CACT;CAEA,MAAM,OAAO,MAAM;CAGnB,MAAM,YAAY,yBAAyB,IAAI;CAG/C,KAAK,MAAM,EAAE,OAAO,gBAAgB,WAClC,IAAI,MAAM,gBAAgB,YAAY,MAAM,gBAAgB,MAAM;EAChE,MAAM,KAAK,GAAG,UAAU;EACxB,IAAI,UAAU,OAAO,OAAO,QAAQ,GAAG,OAAO,QAAQ,eAAe,GACnE,OAAO;EAET,MAAM,UAAU,WAAW;CAC7B;CAIF,KAAK,MAAM,EAAE,OAAO,gBAAgB,WAClC,IAAI,MAAM,gBAAgB,aAAa,MAAM,WAAW;EACtD,MAAM,KAAK,GAAG,UAAU;EACxB,MAAM,OAAO,OAAO,MAAM;EAC1B,OAAO,MAAM,aAAa;EAC1B,IAAI,UAAU,OAAO,OAAO,QAAQ,GAAG,OAAO,QAAQ,eAAe,GACnE,OAAO;EAGT,MAAM,UAAU,WAAW;EAC3B,IAAI,SAAS,KAAA,GACX,OAAO,MAAM,aAAa;OAE1B,OAAO,OAAO,MAAM;CAExB;CAMF,KAAK,MAAM,EAAE,OAAO,gBAAgB,WAClC,IACE,MAAM,gBAAgB,eACtB,MAAM,cACL,CAAC,mBAAmB,WAAW,KAAK,IACrC;EACA,MAAM,KAAK,GAAG,YAAY,KAAK;EAC/B,OAAO,MAAM,aAAa,MAAM,MAAM,KAAK;EAC3C,OAAO;CACT;CAIF,KAAK,MAAM,EAAE,OAAO,gBAAgB,WAClC,IACE,MAAM,gBAAgB,wBACtB,MAAM,cACL,CAAC,mBAAmB,WAAW,KAAK,IACrC;EACA,MAAM,KAAK,GAAG,YAAY,KAAK;EAC/B,OAAO,MAAM,aAAa,MAAM,MAAM,KAAK;EAC3C,OAAO;CACT;CAGF,MAAM,IAAI;CACV,OAAO;AACT"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-segment-params-D1CzlgBo.js","names":[],"sources":["../../src/client/navigation-context.ts","../../src/client/use-segment-params.ts"],"sourcesContent":["'use client';\n\n/**\n * NavigationContext — React context for navigation state.\n *\n * Holds the current route params and pathname, updated atomically\n * with the RSC tree on each navigation. This replaces the previous\n * useSyncExternalStore approach for useSegmentParams() and usePathname(),\n * which suffered from a timing gap: the new tree could commit before\n * the external store re-renders fired, causing a frame where both\n * old and new active states were visible simultaneously.\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 (useParams, usePathname) see the new values in the\n * same render pass as the new tree.\n *\n * During SSR, no NavigationProvider is mounted. Hooks fall back to\n * the ALS-backed getSsrData() for per-request isolation.\n *\n * IMPORTANT: createContext and useContext are NOT available in the RSC\n * environment (React Server Components use a stripped-down React).\n * The context is lazily initialized on first access, and all functions\n * that depend on these APIs are safe to call from any environment —\n * they return null or no-op when the APIs aren't available.\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 React, { createElement, type ReactNode } from 'react';\n\n// ---------------------------------------------------------------------------\n// Context type\n// ---------------------------------------------------------------------------\n\nexport interface NavigationState {\n params: Record<string, string | string[]>;\n pathname: string;\n search: string;\n}\n\n// ---------------------------------------------------------------------------\n// Lazy context initialization\n// ---------------------------------------------------------------------------\n\n/**\n * The context is created lazily to avoid calling createContext at module\n * level. In the RSC environment, React.createContext doesn't exist —\n * calling it at import time would crash the server.\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 * See design/27-chunking-strategy.md §\"Singleton Safety\"\n *\n * NOTE: Despite similar naming, `usePendingNavigationUrl()` here is an\n * internal helper — the public hook is `usePendingNavigation()` in\n * use-pending-navigation.ts.\n */\n\n// Symbol keys for globalThis storage — prevents collisions with user code\nconst NAV_CTX_KEY = Symbol.for('__timber_nav_ctx');\nconst PENDING_CTX_KEY = Symbol.for('__timber_pending_nav_ctx');\n\nfunction getOrCreateContext(): React.Context<NavigationState | null> | undefined {\n const existing = (globalThis as Record<symbol, unknown>)[NAV_CTX_KEY] as\n | React.Context<NavigationState | null>\n | undefined;\n if (existing !== undefined) return existing;\n // createContext may not exist in the RSC environment\n if (typeof React.createContext === 'function') {\n const ctx = React.createContext<NavigationState | null>(null);\n (globalThis as Record<symbol, unknown>)[NAV_CTX_KEY] = ctx;\n return ctx;\n }\n return undefined;\n}\n\n/**\n * Read the navigation context. Returns null during SSR (no provider)\n * or in the RSC environment (no context available).\n * Internal — used by useSegmentParams() and usePathname().\n */\nexport function useNavigationContext(): NavigationState | null {\n const ctx = getOrCreateContext();\n if (!ctx) return null;\n // useContext may not exist in the RSC environment — caller wraps in try/catch\n if (typeof React.useContext !== 'function') return null;\n // eslint-disable-next-line rules-of-hooks -- conditional on environment, not render path\n return React.useContext(ctx);\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): React.ReactElement {\n const ctx = getOrCreateContext();\n if (!ctx) {\n // RSC environment — no context available. Return children as-is.\n return children as React.ReactElement;\n }\n return createElement(ctx.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: { params: {}, 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 Context (same module for singleton guarantee)\n// ---------------------------------------------------------------------------\n\n/**\n * Separate context for the in-flight navigation URL. Provided by\n * NavigationRoot (urgent useState), consumed by usePendingNavigation\n * and TopLoader. Per-link pending state uses useOptimistic instead\n * (see link-pending-store.ts).\n *\n * Uses globalThis via Symbol.for for the same reason as NavigationContext\n * above — the bundler may duplicate this module across chunks, and module-\n * level variables would create separate context instances.\n */\n\nfunction getOrCreatePendingContext(): React.Context<string | null> | undefined {\n const existing = (globalThis as Record<symbol, unknown>)[PENDING_CTX_KEY] as\n | React.Context<string | null>\n | undefined;\n if (existing !== undefined) return existing;\n if (typeof React.createContext === 'function') {\n const ctx = React.createContext<string | null>(null);\n (globalThis as Record<symbol, unknown>)[PENDING_CTX_KEY] = ctx;\n return ctx;\n }\n return undefined;\n}\n\n/**\n * Read the pending navigation URL from context.\n * Returns null during SSR (no provider) or in the RSC environment.\n */\nexport function usePendingNavigationUrl(): string | null {\n const ctx = getOrCreatePendingContext();\n if (!ctx) return null;\n if (typeof React.useContext !== 'function') return null;\n // eslint-disable-next-line react-hooks/rules-of-hooks -- conditional on environment, not render path\n return React.useContext(ctx);\n}\n\n/**\n * Provider for the pending navigation URL. Wraps children with\n * the pending context Provider.\n */\nexport function PendingNavigationProvider({\n value,\n children,\n}: {\n value: string | null;\n children?: ReactNode;\n}): React.ReactElement {\n const ctx = getOrCreatePendingContext();\n if (!ctx) {\n return children as React.ReactElement;\n }\n return createElement(ctx.Provider, { value }, children);\n}\n\n// ---------------------------------------------------------------------------\n// Navigation API transition state (optional progressive enhancement)\n// ---------------------------------------------------------------------------\n\n/**\n * Check if the browser's Navigation API has an active transition.\n *\n * When the Navigation API is available and a navigation has been intercepted\n * via event.intercept(), `navigation.transition` is non-null until the\n * handler resolves. This provides browser-native progress tracking that\n * can be used alongside the existing pendingUrl mechanism.\n *\n * Returns false when Navigation API is unavailable or no transition is active.\n */\nexport function hasNativeNavigationTransition(): boolean {\n if (typeof window === 'undefined') return false;\n const nav = (window as unknown as { navigation?: { transition?: unknown } }).navigation;\n return nav?.transition != null;\n}\n","/**\n * useParams() — client-side hook for accessing route params.\n *\n * Returns the dynamic route parameters for the current URL.\n * When called with a route pattern argument, TypeScript narrows\n * the return type to the exact params shape for that route.\n *\n * Two layers of type narrowing work together:\n * 1. The generic overload here uses the Routes interface directly —\n * `useParams<R>()` returns `Routes[R]['segmentParams']`.\n * 2. Build-time codegen generates per-route string-literal overloads\n * in the .d.ts file for IDE autocomplete (see routing/codegen.ts).\n *\n * When the Routes interface is empty (no codegen yet), the generic\n * overload has `keyof Routes = never`, so only the fallback matches.\n *\n * During SSR, params are read from the ALS-backed SSR data context\n * (populated by ssr-entry.ts) to ensure correct per-request isolation\n * across concurrent requests with streaming Suspense.\n *\n * Reactivity: On the client, useParams() reads from NavigationContext\n * which is updated atomically with the RSC tree render. This replaces\n * the previous useSyncExternalStore approach that suffered from a\n * timing gap between tree render and store notification — causing\n * preserved layout components to briefly show stale active state.\n *\n * All mutable state is delegated to client/state.ts for singleton guarantees.\n * See design/18-build-system.md §\"Singleton State Registry\"\n *\n * Design doc: design/09-typescript.md §\"Typed Routes\"\n */\n\nimport type { Routes } from '../index.js';\nimport { getSsrData } from './ssr-data.js';\nimport { currentParams, _setCurrentParams, paramsListeners } from './state.js';\nimport { useNavigationContext } from './navigation-context.js';\n\n// ---------------------------------------------------------------------------\n// Module-level subscribe/notify pattern — kept for backward compat and tests\n// ---------------------------------------------------------------------------\n\n/**\n * Subscribe to params changes.\n * Retained for backward compatibility with tests that verify the\n * subscribe/notify contract. On the client, useParams() reads from\n * NavigationContext instead.\n */\nexport function subscribe(callback: () => void): () => void {\n paramsListeners.add(callback);\n return () => paramsListeners.delete(callback);\n}\n\n/**\n * Get the current params snapshot (module-level fallback).\n * Used by tests and by the hook when called outside a React component.\n */\nexport function getSnapshot(): Record<string, string | string[]> {\n return currentParams;\n}\n\n// ---------------------------------------------------------------------------\n// Framework API — called by the segment router on each navigation\n// ---------------------------------------------------------------------------\n\n/**\n * Set the current route params in the module-level store.\n *\n * Called by the router on each navigation. This updates the fallback\n * snapshot used by tests and by the hook when called outside a React\n * component (no NavigationContext available).\n *\n * On the client, the primary reactivity path is NavigationContext —\n * the router calls setNavigationState() then renderRoot() which wraps\n * the element in NavigationProvider. setCurrentParams is still called\n * for the module-level fallback.\n *\n * During SSR, params are also available via getSsrData().params\n * (ALS-backed).\n */\nexport function setCurrentParams(params: Record<string, string | string[]>): void {\n _setCurrentParams(params);\n}\n\n/**\n * Notify all legacy subscribers that params have changed.\n *\n * Retained for backward compatibility with tests. On the client,\n * the NavigationContext + renderRoot pattern replaces this — params\n * update atomically with the tree render, so explicit notification\n * is no longer needed.\n */\nexport function notifyParamsListeners(): void {\n for (const listener of paramsListeners) {\n listener();\n }\n}\n\n// ---------------------------------------------------------------------------\n// Public hook\n// ---------------------------------------------------------------------------\n\n/**\n * Read the current route's dynamic params.\n *\n * The optional `_route` argument exists only for TypeScript narrowing —\n * it does not affect the runtime return value.\n *\n * On the client, reads from NavigationContext (provided by\n * NavigationProvider in renderRoot). This ensures params update\n * atomically with the RSC tree — no timing gap.\n *\n * During SSR, reads from the ALS-backed SSR data context to ensure\n * per-request isolation across concurrent requests with streaming Suspense.\n *\n * When called outside a React component (e.g., in test assertions),\n * falls back to the module-level snapshot.\n *\n * @overload Typed — when a known segment path is passed, returns the\n * exact params shape from the generated Routes interface.\n * @overload Fallback — returns the generic params record.\n */\nexport function useSegmentParams<R extends keyof Routes>(\n segmentPath: R\n): Routes[R] extends { segmentParams: infer P } ? P : Record<string, string | string[]>;\nexport function useSegmentParams(segmentPath?: string): Record<string, string | string[]>;\nexport function useSegmentParams(_segmentPath?: string): Record<string, string | string[]> {\n // Try reading from NavigationContext (client-side, inside React tree).\n // During SSR, no NavigationProvider is mounted, so this returns null.\n // When called outside a React component, useContext throws — caught below.\n try {\n // eslint-disable-next-line react-hooks/rules-of-hooks -- conditional on environment, not render path\n const navContext = useNavigationContext();\n if (navContext !== null) {\n return navContext.params;\n }\n } catch {\n // No React dispatcher available (called outside a component).\n // Fall through to module-level snapshot below.\n }\n\n // SSR path: read from ALS-backed SSR data context.\n // Falls back to module-level currentParams for tests.\n return getSsrData()?.params ?? currentParams;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6EA,IAAM,cAAc,OAAO,IAAI,kBAAkB;AAGjD,SAAS,qBAAwE;CAC/E,MAAM,WAAY,WAAuC;CAGzD,IAAI,aAAa,KAAA,GAAW,OAAO;CAEnC,IAAI,OAAO,MAAM,kBAAkB,YAAY;EAC7C,MAAM,MAAM,MAAM,cAAsC,IAAI;EAC5D,WAAwC,eAAe;EACvD,OAAO;CACT;AAEF;;;;;;AAOA,SAAgB,uBAA+C;CAC7D,MAAM,MAAM,mBAAmB;CAC/B,IAAI,CAAC,KAAK,OAAO;CAEjB,IAAI,OAAO,MAAM,eAAe,YAAY,OAAO;CAEnD,OAAO,MAAM,WAAW,GAAG;AAC7B;;;;;;;AAiBA,SAAgB,mBAAmB,EACjC,OACA,YAC8C;CAC9C,MAAM,MAAM,mBAAmB;CAC/B,IAAI,CAAC,KAEH,OAAO;CAET,OAAO,cAAc,IAAI,UAAU,EAAE,MAAM,GAAG,QAAQ;AACxD;;;;;;;;;;;;;;AAmBA,IAAM,gBAAgB,OAAO,IAAI,oBAAoB;AAErD,SAAS,oBAAkD;CACzD,MAAM,IAAI;CACV,IAAI,CAAC,EAAE,gBACL,EAAE,iBAAiB,EAAE,SAAS;EAAE,QAAQ,CAAC;EAAG,UAAU;EAAK,QAAQ;CAAG,EAAE;CAE1E,OAAO,EAAE;AACX;AAEA,SAAgB,mBAAmB,OAA8B;CAC/D,kBAAkB,EAAE,UAAU;AAChC;AAEA,SAAgB,qBAAsC;CACpD,OAAO,kBAAkB,EAAE;AAC7B;;;;;;;;;;;;;;;;;;ACzFA,SAAgB,iBAAiB,QAAiD;CAChF,kBAAkB,MAAM;AAC1B;AA4CA,SAAgB,iBAAiB,cAA0D;CAIzF,IAAI;EAEF,MAAM,aAAa,qBAAqB;EACxC,IAAI,eAAe,MACjB,OAAO,WAAW;CAEtB,QAAQ,CAGR;CAIA,OAAO,WAAW,GAAG,UAAU;AACjC"}
|
|
1
|
+
{"version":3,"file":"use-segment-params-D1CzlgBo.js","names":[],"sources":["../../src/client/navigation-context.ts","../../src/client/use-segment-params.ts"],"sourcesContent":["'use client';\n\n/**\n * NavigationContext — React context for navigation state.\n *\n * Holds the current route params and pathname, updated atomically\n * with the RSC tree on each navigation. This replaces the previous\n * useSyncExternalStore approach for useSegmentParams() and usePathname(),\n * which suffered from a timing gap: the new tree could commit before\n * the external store re-renders fired, causing a frame where both\n * old and new active states were visible simultaneously.\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 (useParams, usePathname) see the new values in the\n * same render pass as the new tree.\n *\n * During SSR, no NavigationProvider is mounted. Hooks fall back to\n * the ALS-backed getSsrData() for per-request isolation.\n *\n * IMPORTANT: createContext and useContext are NOT available in the RSC\n * environment (React Server Components use a stripped-down React).\n * The context is lazily initialized on first access, and all functions\n * that depend on these APIs are safe to call from any environment —\n * they return null or no-op when the APIs aren't available.\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 React, { createElement, type ReactNode } from 'react';\n\n// ---------------------------------------------------------------------------\n// Context type\n// ---------------------------------------------------------------------------\n\nexport interface NavigationState {\n params: Record<string, string | string[]>;\n pathname: string;\n search: string;\n}\n\n// ---------------------------------------------------------------------------\n// Lazy context initialization\n// ---------------------------------------------------------------------------\n\n/**\n * The context is created lazily to avoid calling createContext at module\n * level. In the RSC environment, React.createContext doesn't exist —\n * calling it at import time would crash the server.\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 * See design/27-chunking-strategy.md §\"Singleton Safety\"\n *\n * NOTE: Despite similar naming, `usePendingNavigationUrl()` here is an\n * internal helper — the public hook is `usePendingNavigation()` in\n * use-pending-navigation.ts.\n */\n\n// Symbol keys for globalThis storage — prevents collisions with user code\nconst NAV_CTX_KEY = Symbol.for('__timber_nav_ctx');\nconst PENDING_CTX_KEY = Symbol.for('__timber_pending_nav_ctx');\n\nfunction getOrCreateContext(): React.Context<NavigationState | null> | undefined {\n const existing = (globalThis as Record<symbol, unknown>)[NAV_CTX_KEY] as\n | React.Context<NavigationState | null>\n | undefined;\n if (existing !== undefined) return existing;\n // createContext may not exist in the RSC environment\n if (typeof React.createContext === 'function') {\n const ctx = React.createContext<NavigationState | null>(null);\n (globalThis as Record<symbol, unknown>)[NAV_CTX_KEY] = ctx;\n return ctx;\n }\n return undefined;\n}\n\n/**\n * Read the navigation context. Returns null during SSR (no provider)\n * or in the RSC environment (no context available).\n * Internal — used by useSegmentParams() and usePathname().\n */\nexport function useNavigationContext(): NavigationState | null {\n const ctx = getOrCreateContext();\n if (!ctx) return null;\n // useContext may not exist in the RSC environment — caller wraps in try/catch\n if (typeof React.useContext !== 'function') return null;\n // eslint-disable-next-line rules-of-hooks -- conditional on environment, not render path\n return React.useContext(ctx);\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): React.ReactElement {\n const ctx = getOrCreateContext();\n if (!ctx) {\n // RSC environment — no context available. Return children as-is.\n return children as React.ReactElement;\n }\n return createElement(ctx.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: { params: {}, 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 Context (same module for singleton guarantee)\n// ---------------------------------------------------------------------------\n\n/**\n * Separate context for the in-flight navigation URL. Provided by\n * NavigationRoot (urgent useState), consumed by usePendingNavigation\n * and TopLoader. Per-link pending state uses useOptimistic instead\n * (see design/19-client-navigation.md §\"Per-Link Pending State\").\n *\n * Uses globalThis via Symbol.for for the same reason as NavigationContext\n * above — the bundler may duplicate this module across chunks, and module-\n * level variables would create separate context instances.\n */\n\nfunction getOrCreatePendingContext(): React.Context<string | null> | undefined {\n const existing = (globalThis as Record<symbol, unknown>)[PENDING_CTX_KEY] as\n | React.Context<string | null>\n | undefined;\n if (existing !== undefined) return existing;\n if (typeof React.createContext === 'function') {\n const ctx = React.createContext<string | null>(null);\n (globalThis as Record<symbol, unknown>)[PENDING_CTX_KEY] = ctx;\n return ctx;\n }\n return undefined;\n}\n\n/**\n * Read the pending navigation URL from context.\n * Returns null during SSR (no provider) or in the RSC environment.\n */\nexport function usePendingNavigationUrl(): string | null {\n const ctx = getOrCreatePendingContext();\n if (!ctx) return null;\n if (typeof React.useContext !== 'function') return null;\n // eslint-disable-next-line react-hooks/rules-of-hooks -- conditional on environment, not render path\n return React.useContext(ctx);\n}\n\n/**\n * Provider for the pending navigation URL. Wraps children with\n * the pending context Provider.\n */\nexport function PendingNavigationProvider({\n value,\n children,\n}: {\n value: string | null;\n children?: ReactNode;\n}): React.ReactElement {\n const ctx = getOrCreatePendingContext();\n if (!ctx) {\n return children as React.ReactElement;\n }\n return createElement(ctx.Provider, { value }, children);\n}\n","/**\n * useParams() — client-side hook for accessing route params.\n *\n * Returns the dynamic route parameters for the current URL.\n * When called with a route pattern argument, TypeScript narrows\n * the return type to the exact params shape for that route.\n *\n * Two layers of type narrowing work together:\n * 1. The generic overload here uses the Routes interface directly —\n * `useParams<R>()` returns `Routes[R]['segmentParams']`.\n * 2. Build-time codegen generates per-route string-literal overloads\n * in the .d.ts file for IDE autocomplete (see routing/codegen.ts).\n *\n * When the Routes interface is empty (no codegen yet), the generic\n * overload has `keyof Routes = never`, so only the fallback matches.\n *\n * During SSR, params are read from the ALS-backed SSR data context\n * (populated by ssr-entry.ts) to ensure correct per-request isolation\n * across concurrent requests with streaming Suspense.\n *\n * Reactivity: On the client, useParams() reads from NavigationContext\n * which is updated atomically with the RSC tree render. This replaces\n * the previous useSyncExternalStore approach that suffered from a\n * timing gap between tree render and store notification — causing\n * preserved layout components to briefly show stale active state.\n *\n * All mutable state is delegated to client/state.ts for singleton guarantees.\n * See design/18-build-system.md §\"Singleton State Registry\"\n *\n * Design doc: design/09-typescript.md §\"Typed Routes\"\n */\n\nimport type { Routes } from '../index.js';\nimport { getSsrData } from './ssr-data.js';\nimport { currentParams, _setCurrentParams, paramsListeners } from './state.js';\nimport { useNavigationContext } from './navigation-context.js';\n\n// ---------------------------------------------------------------------------\n// Module-level subscribe/notify pattern — kept for backward compat and tests\n// ---------------------------------------------------------------------------\n\n/**\n * Subscribe to params changes.\n * Retained for backward compatibility with tests that verify the\n * subscribe/notify contract. On the client, useParams() reads from\n * NavigationContext instead.\n */\nexport function subscribe(callback: () => void): () => void {\n paramsListeners.add(callback);\n return () => paramsListeners.delete(callback);\n}\n\n/**\n * Get the current params snapshot (module-level fallback).\n * Used by tests and by the hook when called outside a React component.\n */\nexport function getSnapshot(): Record<string, string | string[]> {\n return currentParams;\n}\n\n// ---------------------------------------------------------------------------\n// Framework API — called by the segment router on each navigation\n// ---------------------------------------------------------------------------\n\n/**\n * Set the current route params in the module-level store.\n *\n * Called by the router on each navigation. This updates the fallback\n * snapshot used by tests and by the hook when called outside a React\n * component (no NavigationContext available).\n *\n * On the client, the primary reactivity path is NavigationContext —\n * the router calls setNavigationState() then renderRoot() which wraps\n * the element in NavigationProvider. setCurrentParams is still called\n * for the module-level fallback.\n *\n * During SSR, params are also available via getSsrData().params\n * (ALS-backed).\n */\nexport function setCurrentParams(params: Record<string, string | string[]>): void {\n _setCurrentParams(params);\n}\n\n/**\n * Notify all legacy subscribers that params have changed.\n *\n * Retained for backward compatibility with tests. On the client,\n * the NavigationContext + renderRoot pattern replaces this — params\n * update atomically with the tree render, so explicit notification\n * is no longer needed.\n */\nexport function notifyParamsListeners(): void {\n for (const listener of paramsListeners) {\n listener();\n }\n}\n\n// ---------------------------------------------------------------------------\n// Public hook\n// ---------------------------------------------------------------------------\n\n/**\n * Read the current route's dynamic params.\n *\n * The optional `_route` argument exists only for TypeScript narrowing —\n * it does not affect the runtime return value.\n *\n * On the client, reads from NavigationContext (provided by\n * NavigationProvider in renderRoot). This ensures params update\n * atomically with the RSC tree — no timing gap.\n *\n * During SSR, reads from the ALS-backed SSR data context to ensure\n * per-request isolation across concurrent requests with streaming Suspense.\n *\n * When called outside a React component (e.g., in test assertions),\n * falls back to the module-level snapshot.\n *\n * @overload Typed — when a known segment path is passed, returns the\n * exact params shape from the generated Routes interface.\n * @overload Fallback — returns the generic params record.\n */\nexport function useSegmentParams<R extends keyof Routes>(\n segmentPath: R\n): Routes[R] extends { segmentParams: infer P } ? P : Record<string, string | string[]>;\nexport function useSegmentParams(segmentPath?: string): Record<string, string | string[]>;\nexport function useSegmentParams(_segmentPath?: string): Record<string, string | string[]> {\n // Try reading from NavigationContext (client-side, inside React tree).\n // During SSR, no NavigationProvider is mounted, so this returns null.\n // When called outside a React component, useContext throws — caught below.\n try {\n // eslint-disable-next-line react-hooks/rules-of-hooks -- conditional on environment, not render path\n const navContext = useNavigationContext();\n if (navContext !== null) {\n return navContext.params;\n }\n } catch {\n // No React dispatcher available (called outside a component).\n // Fall through to module-level snapshot below.\n }\n\n // SSR path: read from ALS-backed SSR data context.\n // Falls back to module-level currentParams for tests.\n return getSsrData()?.params ?? currentParams;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6EA,IAAM,cAAc,OAAO,IAAI,kBAAkB;AAGjD,SAAS,qBAAwE;CAC/E,MAAM,WAAY,WAAuC;CAGzD,IAAI,aAAa,KAAA,GAAW,OAAO;CAEnC,IAAI,OAAO,MAAM,kBAAkB,YAAY;EAC7C,MAAM,MAAM,MAAM,cAAsC,IAAI;EAC5D,WAAwC,eAAe;EACvD,OAAO;CACT;AAEF;;;;;;AAOA,SAAgB,uBAA+C;CAC7D,MAAM,MAAM,mBAAmB;CAC/B,IAAI,CAAC,KAAK,OAAO;CAEjB,IAAI,OAAO,MAAM,eAAe,YAAY,OAAO;CAEnD,OAAO,MAAM,WAAW,GAAG;AAC7B;;;;;;;AAiBA,SAAgB,mBAAmB,EACjC,OACA,YAC8C;CAC9C,MAAM,MAAM,mBAAmB;CAC/B,IAAI,CAAC,KAEH,OAAO;CAET,OAAO,cAAc,IAAI,UAAU,EAAE,MAAM,GAAG,QAAQ;AACxD;;;;;;;;;;;;;;AAmBA,IAAM,gBAAgB,OAAO,IAAI,oBAAoB;AAErD,SAAS,oBAAkD;CACzD,MAAM,IAAI;CACV,IAAI,CAAC,EAAE,gBACL,EAAE,iBAAiB,EAAE,SAAS;EAAE,QAAQ,CAAC;EAAG,UAAU;EAAK,QAAQ;CAAG,EAAE;CAE1E,OAAO,EAAE;AACX;AAEA,SAAgB,mBAAmB,OAA8B;CAC/D,kBAAkB,EAAE,UAAU;AAChC;AAEA,SAAgB,qBAAsC;CACpD,OAAO,kBAAkB,EAAE;AAC7B;;;;;;;;;;;;;;;;;;ACzFA,SAAgB,iBAAiB,QAAiD;CAChF,kBAAkB,MAAM;AAC1B;AA4CA,SAAgB,iBAAiB,cAA0D;CAIzF,IAAI;EAEF,MAAM,aAAa,qBAAqB;EACxC,IAAI,eAAe,MACjB,OAAO,WAAW;CAEtB,QAAQ,CAGR;CAIA,OAAO,WAAW,GAAG,UAAU;AACjC"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { n as classifyUrlSegment } from "./segment-classify-CDDRVKs7.js";
|
|
2
|
-
import {
|
|
2
|
+
import { a as parseExistingSchemaKeys, r as findSchemaFile } from "./cli-schema-sync-DJDdyEae.js";
|
|
3
3
|
import { join, posix, relative } from "node:path";
|
|
4
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
5
5
|
//#region src/routing/codegen-shared.ts
|
|
6
6
|
/**
|
|
7
7
|
* Shared codegen helpers — import-path computation, codec chain type
|
|
@@ -92,6 +92,32 @@ function formatSearchParamsType(route, importBase) {
|
|
|
92
92
|
return "{}";
|
|
93
93
|
}
|
|
94
94
|
//#endregion
|
|
95
|
+
//#region src/routing/file-cache.ts
|
|
96
|
+
var cache = /* @__PURE__ */ new Map();
|
|
97
|
+
/**
|
|
98
|
+
* Read a file's text content, returning a cached copy when the file
|
|
99
|
+
* hasn't been modified since the last read (checked via mtime + size).
|
|
100
|
+
*
|
|
101
|
+
* Falls back to a fresh read on any stat/read error.
|
|
102
|
+
*/
|
|
103
|
+
function readFileCached(filePath) {
|
|
104
|
+
try {
|
|
105
|
+
const stat = statSync(filePath);
|
|
106
|
+
const entry = cache.get(filePath);
|
|
107
|
+
if (entry && entry.mtimeMs === stat.mtimeMs && entry.size === stat.size) return entry.content;
|
|
108
|
+
const content = readFileSync(filePath, "utf-8");
|
|
109
|
+
cache.set(filePath, {
|
|
110
|
+
mtimeMs: stat.mtimeMs,
|
|
111
|
+
size: stat.size,
|
|
112
|
+
content
|
|
113
|
+
});
|
|
114
|
+
return content;
|
|
115
|
+
} catch {
|
|
116
|
+
cache.delete(filePath);
|
|
117
|
+
return readFileSync(filePath, "utf-8");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
//#endregion
|
|
95
121
|
//#region src/routing/link-codegen.ts
|
|
96
122
|
/** Shared Link base-props type literal used in every emitted call signature. */
|
|
97
123
|
var LINK_BASE_PROPS_TYPE = "Omit<import('react').AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> & { prefetch?: boolean; scroll?: boolean; preserveSearchParams?: true | string[]; onNavigate?: import('./client/link.js').OnNavigateHandler; children?: import('react').ReactNode }";
|
|
@@ -241,7 +267,7 @@ function formatTypedLinkOverloads(routes, importBase, hasSchema) {
|
|
|
241
267
|
function generateRouteMap(tree, options = {}) {
|
|
242
268
|
const hasSchema = options.hasSchema ?? detectSchema(options.appDir);
|
|
243
269
|
const routes = [];
|
|
244
|
-
collectRoutes(tree.root, [], [], "", routes, hasSchema);
|
|
270
|
+
collectRoutes(tree.root, [], [], "", routes, hasSchema, false);
|
|
245
271
|
routes.sort((a, b) => a.urlPath.localeCompare(b.urlPath));
|
|
246
272
|
return formatDeclarationFile(routes, options.outputDir ?? options.appDir, hasSchema, options.appDir);
|
|
247
273
|
}
|
|
@@ -265,7 +291,7 @@ function detectSchema(appDir) {
|
|
|
265
291
|
* A route entry is created for any segment that has a `page` or `route` file.
|
|
266
292
|
* Params accumulate from ancestor dynamic segments.
|
|
267
293
|
*/
|
|
268
|
-
function collectRoutes(node, ancestorParams, ancestorParamsFiles, parentTreePath, routes, hasSchema) {
|
|
294
|
+
function collectRoutes(node, ancestorParams, ancestorParamsFiles, parentTreePath, routes, hasSchema, insideSlot) {
|
|
269
295
|
const treePath = node.segmentName ? `${parentTreePath}/${node.segmentName}` : parentTreePath;
|
|
270
296
|
const ownParamsFile = node.params && fileHasExport(node.params.filePath, "segmentParams") ? node.params.filePath : void 0;
|
|
271
297
|
const params = [...ancestorParams];
|
|
@@ -298,6 +324,7 @@ function collectRoutes(node, ancestorParams, ancestorParamsFiles, parentTreePath
|
|
|
298
324
|
params: resolvedParams,
|
|
299
325
|
hasSearchParams: false,
|
|
300
326
|
isApiRoute,
|
|
327
|
+
isSlotRoute: insideSlot,
|
|
301
328
|
hasSchema
|
|
302
329
|
};
|
|
303
330
|
if (isPage) {
|
|
@@ -311,8 +338,47 @@ function collectRoutes(node, ancestorParams, ancestorParamsFiles, parentTreePath
|
|
|
311
338
|
}
|
|
312
339
|
routes.push(entry);
|
|
313
340
|
}
|
|
314
|
-
for (const child of node.children) collectRoutes(child, params, nextAncestorFiles, treePath, routes, hasSchema);
|
|
315
|
-
for (const slot of Object.values(node.slots)) collectRoutes(slot, params, nextAncestorFiles, treePath, routes, hasSchema);
|
|
341
|
+
for (const child of node.children) collectRoutes(child, params, nextAncestorFiles, treePath, routes, hasSchema, insideSlot);
|
|
342
|
+
for (const slot of Object.values(node.slots)) collectRoutes(slot, params, nextAncestorFiles, treePath, routes, hasSchema, true);
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Dedupe route entries by urlPath for urlPath-keyed emissions (the Routes
|
|
346
|
+
* interface, useQueryStates overloads, and typed Link variants).
|
|
347
|
+
*
|
|
348
|
+
* Parallel-slot pages share their parent's urlPath; emitting one entry per
|
|
349
|
+
* slot page produced duplicate interface keys (TS2300/TS2717 under
|
|
350
|
+
* skipLibCheck:false) and duplicate overloads. The non-slot entry is the
|
|
351
|
+
* canonical one; when only slot entries exist (parent has no page), the
|
|
352
|
+
* lexicographically smallest segmentPath wins so output is independent of
|
|
353
|
+
* slot scan order. A slot's searchParams definition is carried onto the
|
|
354
|
+
* merged entry when the canonical entry has none — never silently dropped.
|
|
355
|
+
*
|
|
356
|
+
* segmentPath-keyed emissions (getSegmentParams, useSegmentParams,
|
|
357
|
+
* TimberSegmentParams) must NOT use this — segmentPaths are already unique
|
|
358
|
+
* and slot entries feed those APIs directly.
|
|
359
|
+
*/
|
|
360
|
+
function dedupeRoutesByUrlPath(routes) {
|
|
361
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
362
|
+
for (const route of routes) {
|
|
363
|
+
const existing = byPath.get(route.urlPath);
|
|
364
|
+
if (!existing) {
|
|
365
|
+
byPath.set(route.urlPath, route);
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
let winner = existing;
|
|
369
|
+
let loser = route;
|
|
370
|
+
if (existing.isSlotRoute && !route.isSlotRoute || !!existing.isSlotRoute === !!route.isSlotRoute && route.segmentPath.localeCompare(existing.segmentPath) < 0) {
|
|
371
|
+
winner = route;
|
|
372
|
+
loser = existing;
|
|
373
|
+
}
|
|
374
|
+
if (!winner.hasSearchParams && loser.hasSearchParams) winner = {
|
|
375
|
+
...winner,
|
|
376
|
+
hasSearchParams: true,
|
|
377
|
+
searchParamsPagePath: loser.searchParamsPagePath
|
|
378
|
+
};
|
|
379
|
+
byPath.set(route.urlPath, winner);
|
|
380
|
+
}
|
|
381
|
+
return [...byPath.values()];
|
|
316
382
|
}
|
|
317
383
|
/**
|
|
318
384
|
* Determine the TypeScript type for a segment's param.
|
|
@@ -333,7 +399,7 @@ function paramTypeForSegment(segmentType) {
|
|
|
333
399
|
function fileHasExport(filePath, exportName) {
|
|
334
400
|
if (!existsSync(filePath)) return false;
|
|
335
401
|
try {
|
|
336
|
-
const content =
|
|
402
|
+
const content = readFileCached(filePath);
|
|
337
403
|
const e = exportName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
338
404
|
return new RegExp(`export\\s+(const|let|var)\\s+${e}\\b`).test(content) || new RegExp(`export\\s*\\{[^}]*\\b${e}\\b[^}]*\\}`).test(content);
|
|
339
405
|
} catch {
|
|
@@ -425,9 +491,10 @@ function formatDeclarationFile(routes, importBase, hasSchema, appDir) {
|
|
|
425
491
|
lines.push(emitResolveSegmentFieldHelper());
|
|
426
492
|
lines.push("");
|
|
427
493
|
}
|
|
494
|
+
const uniqueByUrlPath = dedupeRoutesByUrlPath(routes);
|
|
428
495
|
lines.push("declare module '@timber-js/app' {");
|
|
429
496
|
lines.push(" interface Routes {");
|
|
430
|
-
for (const route of
|
|
497
|
+
for (const route of uniqueByUrlPath) {
|
|
431
498
|
const paramsType = formatParamsType(route.params, importBase, hasSchema);
|
|
432
499
|
const searchParamsType = formatSearchParamsType(route, importBase);
|
|
433
500
|
lines.push(` '${route.urlPath}': {`);
|
|
@@ -442,7 +509,7 @@ function formatDeclarationFile(routes, importBase, hasSchema, appDir) {
|
|
|
442
509
|
}
|
|
443
510
|
lines.push("}");
|
|
444
511
|
lines.push("");
|
|
445
|
-
const pageRoutes =
|
|
512
|
+
const pageRoutes = uniqueByUrlPath.filter((r) => !r.isApiRoute);
|
|
446
513
|
const dynamicRoutes = routes.filter((r) => r.params.length > 0);
|
|
447
514
|
if (dynamicRoutes.length > 0) {
|
|
448
515
|
lines.push("declare module '@timber-js/app/server' {");
|
|
@@ -517,82 +584,6 @@ function formatUseQueryStatesOverloads(routes, importBase) {
|
|
|
517
584
|
return lines;
|
|
518
585
|
}
|
|
519
586
|
//#endregion
|
|
520
|
-
//#region src/routing/interception.ts
|
|
521
|
-
/**
|
|
522
|
-
* Collect all interception rewrite rules from the route tree.
|
|
523
|
-
*
|
|
524
|
-
* Walks the tree recursively. For each intercepting segment, computes the
|
|
525
|
-
* intercepted URL based on the marker and the segment's position.
|
|
526
|
-
*/
|
|
527
|
-
function collectInterceptionRewrites(root) {
|
|
528
|
-
const rewrites = [];
|
|
529
|
-
walkForInterceptions(root, [root], rewrites);
|
|
530
|
-
return rewrites;
|
|
531
|
-
}
|
|
532
|
-
/**
|
|
533
|
-
* Recursively walk the segment tree to find intercepting routes.
|
|
534
|
-
*/
|
|
535
|
-
function walkForInterceptions(node, ancestors, rewrites) {
|
|
536
|
-
for (const child of node.children) if (child.segmentType === "intercepting" && child.interceptionMarker) collectFromInterceptingNode(child, ancestors, rewrites);
|
|
537
|
-
else walkForInterceptions(child, [...ancestors, child], rewrites);
|
|
538
|
-
for (const slot of Object.values(node.slots)) walkForInterceptions(slot, ancestors, rewrites);
|
|
539
|
-
}
|
|
540
|
-
/**
|
|
541
|
-
* For an intercepting segment, find all leaf pages in its sub-tree and
|
|
542
|
-
* generate rewrite rules for each.
|
|
543
|
-
*/
|
|
544
|
-
function collectFromInterceptingNode(interceptingNode, ancestors, rewrites) {
|
|
545
|
-
const marker = interceptingNode.interceptionMarker;
|
|
546
|
-
const segmentName = interceptingNode.interceptedSegmentName;
|
|
547
|
-
const parentUrlPath = ancestors[ancestors.length - 1].urlPath;
|
|
548
|
-
const interceptedBase = computeInterceptedBase(parentUrlPath, marker);
|
|
549
|
-
collectLeavesWithRewrites(interceptingNode, interceptedBase === "/" ? `/${segmentName}` : `${interceptedBase}/${segmentName}`, parentUrlPath, [...ancestors, interceptingNode], rewrites);
|
|
550
|
-
}
|
|
551
|
-
/**
|
|
552
|
-
* Recursively find leaf pages in an intercepting sub-tree and generate
|
|
553
|
-
* rewrite rules for each.
|
|
554
|
-
*/
|
|
555
|
-
function collectLeavesWithRewrites(node, interceptedUrlPath, interceptingPrefix, segmentPath, rewrites) {
|
|
556
|
-
if (node.page) rewrites.push({
|
|
557
|
-
interceptedPattern: interceptedUrlPath,
|
|
558
|
-
interceptingPrefix,
|
|
559
|
-
segmentPath: [...segmentPath]
|
|
560
|
-
});
|
|
561
|
-
for (const child of node.children) collectLeavesWithRewrites(child, child.segmentType === "group" ? interceptedUrlPath : `${interceptedUrlPath}/${child.segmentName}`, interceptingPrefix, [...segmentPath, child], rewrites);
|
|
562
|
-
}
|
|
563
|
-
/**
|
|
564
|
-
* Compute the base URL that an intercepting route intercepts, given the
|
|
565
|
-
* parent's URL path and the interception marker.
|
|
566
|
-
*
|
|
567
|
-
* - (.) — same level: parent's URL path
|
|
568
|
-
* - (..) — one level up: parent's parent URL path
|
|
569
|
-
* - (...) — root level: /
|
|
570
|
-
* - (..)(..) — two levels up: parent's grandparent URL path
|
|
571
|
-
*
|
|
572
|
-
* Level counting operates on URL path segments, NOT filesystem directories.
|
|
573
|
-
* Route groups and parallel slots are already excluded from urlPath (they
|
|
574
|
-
* don't add URL depth), so (..) correctly climbs visible segments. This
|
|
575
|
-
* avoids the Vinext bug where path.dirname() on filesystem paths would
|
|
576
|
-
* waste climbs on invisible route groups.
|
|
577
|
-
*/
|
|
578
|
-
function computeInterceptedBase(parentUrlPath, marker) {
|
|
579
|
-
switch (marker) {
|
|
580
|
-
case "(.)": return parentUrlPath;
|
|
581
|
-
case "(..)": {
|
|
582
|
-
const parts = parentUrlPath.split("/").filter(Boolean);
|
|
583
|
-
parts.pop();
|
|
584
|
-
return parts.length === 0 ? "/" : `/${parts.join("/")}`;
|
|
585
|
-
}
|
|
586
|
-
case "(...)": return "/";
|
|
587
|
-
case "(..)(..)": {
|
|
588
|
-
const parts = parentUrlPath.split("/").filter(Boolean);
|
|
589
|
-
parts.pop();
|
|
590
|
-
parts.pop();
|
|
591
|
-
return parts.length === 0 ? "/" : `/${parts.join("/")}`;
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
//#endregion
|
|
596
587
|
//#region src/routing/schema-validation.ts
|
|
597
588
|
/**
|
|
598
589
|
* Schema key/value validation against the filesystem route tree.
|
|
@@ -638,7 +629,7 @@ function extractParamName(bracketKey) {
|
|
|
638
629
|
function validateSchemaAgainstRoutes(tree, appDir) {
|
|
639
630
|
const schemaPath = findSchemaFile(appDir);
|
|
640
631
|
if (!schemaPath) return [];
|
|
641
|
-
const schemaKeys = parseExistingSchemaKeys(
|
|
632
|
+
const schemaKeys = parseExistingSchemaKeys(readFileCached(schemaPath));
|
|
642
633
|
if (schemaKeys.length === 0) return [];
|
|
643
634
|
return validateKeys(schemaKeys, collectDynamicSegmentsFromTree(tree));
|
|
644
635
|
}
|
|
@@ -683,6 +674,81 @@ function validateKeys(schemaKeys, filesystemSegments) {
|
|
|
683
674
|
return warnings;
|
|
684
675
|
}
|
|
685
676
|
//#endregion
|
|
677
|
+
//#region src/routing/interception.ts
|
|
678
|
+
/**
|
|
679
|
+
* Collect all interception rewrite rules from the route tree.
|
|
680
|
+
*
|
|
681
|
+
* Walks the tree recursively. For each intercepting segment, computes the
|
|
682
|
+
* intercepted URL based on the marker and the segment's position.
|
|
683
|
+
*/
|
|
684
|
+
function collectInterceptionRewrites(root) {
|
|
685
|
+
const rewrites = [];
|
|
686
|
+
walkForInterceptions(root, [root], rewrites);
|
|
687
|
+
return rewrites;
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* Recursively walk the segment tree to find intercepting routes.
|
|
691
|
+
*/
|
|
692
|
+
function walkForInterceptions(node, ancestors, rewrites) {
|
|
693
|
+
for (const child of node.children) if (child.segmentType === "intercepting" && child.interceptionMarker) collectFromInterceptingNode(child, ancestors, rewrites);
|
|
694
|
+
else walkForInterceptions(child, [...ancestors, child], rewrites);
|
|
695
|
+
for (const slot of Object.values(node.slots)) walkForInterceptions(slot, ancestors, rewrites);
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* For an intercepting segment, find all leaf pages in its sub-tree and
|
|
699
|
+
* generate rewrite rules for each.
|
|
700
|
+
*/
|
|
701
|
+
function collectFromInterceptingNode(interceptingNode, ancestors, rewrites) {
|
|
702
|
+
const marker = interceptingNode.interceptionMarker;
|
|
703
|
+
const segmentName = interceptingNode.interceptedSegmentName;
|
|
704
|
+
const parentUrlPath = ancestors[ancestors.length - 1].urlPath;
|
|
705
|
+
const interceptedBase = computeInterceptedBase(parentUrlPath, marker);
|
|
706
|
+
collectLeavesWithRewrites(interceptingNode, interceptedBase === "/" ? `/${segmentName}` : `${interceptedBase}/${segmentName}`, parentUrlPath, rewrites);
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Recursively find leaf pages in an intercepting sub-tree and generate
|
|
710
|
+
* rewrite rules for each.
|
|
711
|
+
*/
|
|
712
|
+
function collectLeavesWithRewrites(node, interceptedUrlPath, interceptingPrefix, rewrites) {
|
|
713
|
+
if (node.page) rewrites.push({
|
|
714
|
+
interceptedPattern: interceptedUrlPath,
|
|
715
|
+
interceptingPrefix
|
|
716
|
+
});
|
|
717
|
+
for (const child of node.children) collectLeavesWithRewrites(child, child.segmentType === "group" ? interceptedUrlPath : `${interceptedUrlPath}/${child.segmentName}`, interceptingPrefix, rewrites);
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Compute the base URL that an intercepting route intercepts, given the
|
|
721
|
+
* parent's URL path and the interception marker.
|
|
722
|
+
*
|
|
723
|
+
* - (.) — same level: parent's URL path
|
|
724
|
+
* - (..) — one level up: parent's parent URL path
|
|
725
|
+
* - (...) — root level: /
|
|
726
|
+
* - (..)(..) — two levels up: parent's grandparent URL path
|
|
727
|
+
*
|
|
728
|
+
* Level counting operates on URL path segments, NOT filesystem directories.
|
|
729
|
+
* Route groups and parallel slots are already excluded from urlPath (they
|
|
730
|
+
* don't add URL depth), so (..) correctly climbs visible segments. This
|
|
731
|
+
* avoids the Vinext bug where path.dirname() on filesystem paths would
|
|
732
|
+
* waste climbs on invisible route groups.
|
|
733
|
+
*/
|
|
734
|
+
function computeInterceptedBase(parentUrlPath, marker) {
|
|
735
|
+
switch (marker) {
|
|
736
|
+
case "(.)": return parentUrlPath;
|
|
737
|
+
case "(..)": {
|
|
738
|
+
const parts = parentUrlPath.split("/").filter(Boolean);
|
|
739
|
+
parts.pop();
|
|
740
|
+
return parts.length === 0 ? "/" : `/${parts.join("/")}`;
|
|
741
|
+
}
|
|
742
|
+
case "(...)": return "/";
|
|
743
|
+
case "(..)(..)": {
|
|
744
|
+
const parts = parentUrlPath.split("/").filter(Boolean);
|
|
745
|
+
parts.pop();
|
|
746
|
+
parts.pop();
|
|
747
|
+
return parts.length === 0 ? "/" : `/${parts.join("/")}`;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
//#endregion
|
|
686
752
|
//#region src/routing/walkers.ts
|
|
687
753
|
/**
|
|
688
754
|
* Walk a segment tree and collect every leaf with a `page` or `route`
|
|
@@ -719,6 +785,6 @@ function walk(node, chain, result, includeSlots) {
|
|
|
719
785
|
if (includeSlots) for (const slotNode of Object.values(node.slots)) walk(slotNode, currentChain, result, includeSlots);
|
|
720
786
|
}
|
|
721
787
|
//#endregion
|
|
722
|
-
export { generateRouteMap as a,
|
|
788
|
+
export { generateRouteMap as a, validateSchemaAgainstRoutes as i, collectInterceptionRewrites as n, readFileCached as o, collectDynamicSegmentsFromTree as r, collectLeafRoutes as t };
|
|
723
789
|
|
|
724
|
-
//# sourceMappingURL=walkers-
|
|
790
|
+
//# sourceMappingURL=walkers-DvWDj453.js.map
|