@timber-js/app 0.2.0-alpha.186 → 0.2.0-alpha.188
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-C-Rw9vPc.js → actions-35jnMdeJ.js} +4 -3
- package/dist/_chunks/{actions-C-Rw9vPc.js.map → actions-35jnMdeJ.js.map} +1 -1
- package/dist/_chunks/als-registry-C6kcfprT.js +41 -0
- package/dist/_chunks/als-registry-C6kcfprT.js.map +1 -0
- package/dist/_chunks/{als-slots-mFweg276.js → als-slots-BEEIPKYm.js} +3 -4
- package/dist/_chunks/{als-slots-mFweg276.js.map → als-slots-BEEIPKYm.js.map} +1 -1
- package/dist/_chunks/{cache-api-Cd0VZ_Pd.js → cache-api-DjNrIWRR.js} +7 -13
- package/dist/_chunks/cache-api-DjNrIWRR.js.map +1 -0
- package/dist/_chunks/cli-check-CpmN7Nh-.js +256 -0
- package/dist/_chunks/cli-check-CpmN7Nh-.js.map +1 -0
- package/dist/_chunks/cli-schema-sync-CGMp_Psg.js +298 -0
- package/dist/_chunks/cli-schema-sync-CGMp_Psg.js.map +1 -0
- package/dist/_chunks/{cloudflare-Cs0uZXea.js → cloudflare-CGP6BZKO.js} +4 -3
- package/dist/_chunks/{cloudflare-Cs0uZXea.js.map → cloudflare-CGP6BZKO.js.map} +1 -1
- package/dist/_chunks/convention-lint-kXsgc_-7.js +784 -0
- package/dist/_chunks/convention-lint-kXsgc_-7.js.map +1 -0
- package/dist/_chunks/{error-boundary-DpYRI_I1.js → error-boundary-D-ODYX41.js} +25 -2
- package/dist/_chunks/error-boundary-D-ODYX41.js.map +1 -0
- package/dist/_chunks/file-cache-DmX7OqZP.js +454 -0
- package/dist/_chunks/file-cache-DmX7OqZP.js.map +1 -0
- package/dist/_chunks/{logger-N7e5auP0.js → logger-D8xJZXIN.js} +27 -40
- package/dist/_chunks/logger-D8xJZXIN.js.map +1 -0
- package/dist/_chunks/mdx-file-C005ay-P.js +25 -0
- package/dist/_chunks/mdx-file-C005ay-P.js.map +1 -0
- package/dist/_chunks/{plugin-context-DEGLSJs3.js → plugin-context-rCinWLiE.js} +7 -2
- package/dist/_chunks/{plugin-context-DEGLSJs3.js.map → plugin-context-rCinWLiE.js.map} +1 -1
- package/dist/_chunks/purge-store-Byr8XOjU.js +14 -0
- package/dist/_chunks/purge-store-Byr8XOjU.js.map +1 -0
- package/dist/_chunks/{resolve-schema-Dz3fcFUo.js → resolve-schema-5ma5pp1b.js} +2 -2
- package/dist/_chunks/{resolve-schema-Dz3fcFUo.js.map → resolve-schema-5ma5pp1b.js.map} +1 -1
- package/dist/_chunks/rsc-media-type-DRqE_lD_.js +46 -0
- package/dist/_chunks/rsc-media-type-DRqE_lD_.js.map +1 -0
- package/dist/_chunks/{cli-schema-sync-CKgHC2MB.js → scanner-C8b0Gcw3.js} +5 -298
- package/dist/_chunks/scanner-C8b0Gcw3.js.map +1 -0
- package/dist/_chunks/schema-bridge-Cc2Gngu1.js +199 -0
- package/dist/_chunks/schema-bridge-Cc2Gngu1.js.map +1 -0
- package/dist/_chunks/segment-classify-C539Pa2O.js.map +1 -1
- package/dist/_chunks/{param-value-C8TNYchQ.js → segment-context-CjOlyB8Y.js} +33 -2
- package/dist/_chunks/segment-context-CjOlyB8Y.js.map +1 -0
- package/dist/_chunks/segment-keys-BawYuNFO.js.map +1 -1
- package/dist/_chunks/{use-query-states-DFvWd-EA.js → use-query-states-BbU5Ge1V.js} +74 -26
- package/dist/_chunks/use-query-states-BbU5Ge1V.js.map +1 -0
- package/dist/_chunks/{navigation-root-B29qg0_T.js → use-segment-params-C4r4BD9T.js} +129 -5
- package/dist/_chunks/use-segment-params-C4r4BD9T.js.map +1 -0
- package/dist/_chunks/walkers-RzN6AFjr.js +141 -0
- package/dist/_chunks/walkers-RzN6AFjr.js.map +1 -0
- package/dist/adapters/cloudflare-dev.js +1 -1
- package/dist/adapters/cloudflare-kv-cache.js +1 -1
- package/dist/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +1 -1
- package/dist/adapters/compress-module.d.ts +12 -0
- package/dist/adapters/compress-module.d.ts.map +1 -1
- package/dist/adapters/nitro.js +54 -2
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/cache/index.js +1 -1
- package/dist/cdn/cloudflare-purge.js +30 -0
- package/dist/cdn/cloudflare-purge.js.map +1 -0
- package/dist/cdn/fastly-purge.js +33 -0
- package/dist/cdn/fastly-purge.js.map +1 -0
- package/dist/cdn/index.js +94 -0
- package/dist/cdn/index.js.map +1 -0
- package/dist/cdn/workers-cache-purge.js +35 -0
- package/dist/cdn/workers-cache-purge.js.map +1 -0
- package/dist/cli-check.d.ts +153 -0
- package/dist/cli-check.d.ts.map +1 -0
- package/dist/cli.d.ts +34 -8
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +46 -21
- package/dist/cli.js.map +1 -1
- package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
- package/dist/client/browser-entry/index.d.ts +1 -1
- package/dist/client/browser-entry/index.d.ts.map +1 -1
- package/dist/client/error-boundary.d.ts +6 -0
- package/dist/client/error-boundary.d.ts.map +1 -1
- package/dist/client/error-boundary.js +1 -1
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +27 -33
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.js +33 -17
- package/dist/client/internal.js.map +1 -1
- package/dist/client/link.d.ts +1 -7
- package/dist/client/link.d.ts.map +1 -1
- package/dist/client/navigation-commit.d.ts +16 -0
- package/dist/client/navigation-commit.d.ts.map +1 -1
- package/dist/client/router-pipeline.d.ts.map +1 -1
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/rsc-fetch.d.ts +9 -1
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/client/segment-cache.d.ts +14 -0
- package/dist/client/segment-cache.d.ts.map +1 -1
- package/dist/client/use-query-states.d.ts +9 -3
- package/dist/client/use-query-states.d.ts.map +1 -1
- package/dist/codec.js +1 -1
- package/dist/cookies/define-cookie.d.ts.map +1 -1
- package/dist/cookies/index.js +2 -2
- package/dist/cookies/index.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +142 -477
- package/dist/index.js.map +1 -1
- package/dist/params/index.js +1 -1
- package/dist/plugin-context.d.ts +15 -0
- package/dist/plugin-context.d.ts.map +1 -1
- package/dist/plugins/routing.d.ts +0 -9
- package/dist/plugins/routing.d.ts.map +1 -1
- package/dist/plugins/shims.d.ts.map +1 -1
- package/dist/plugins/static-build.d.ts +24 -0
- package/dist/plugins/static-build.d.ts.map +1 -1
- package/dist/routing/codegen-shared.d.ts +3 -44
- package/dist/routing/codegen-shared.d.ts.map +1 -1
- package/dist/routing/codegen-types.d.ts +10 -31
- package/dist/routing/codegen-types.d.ts.map +1 -1
- package/dist/routing/codegen-write.d.ts +51 -0
- package/dist/routing/codegen-write.d.ts.map +1 -0
- package/dist/routing/codegen.d.ts.map +1 -1
- package/dist/routing/convention-lint.d.ts +18 -4
- package/dist/routing/convention-lint.d.ts.map +1 -1
- package/dist/routing/export-detect.d.ts +16 -0
- package/dist/routing/export-detect.d.ts.map +1 -1
- package/dist/routing/index.js +3 -2
- package/dist/routing/link-codegen.d.ts +19 -4
- package/dist/routing/link-codegen.d.ts.map +1 -1
- package/dist/routing/manifest-codegen.d.ts +1 -7
- package/dist/routing/manifest-codegen.d.ts.map +1 -1
- package/dist/routing/segment-keys.d.ts +22 -0
- package/dist/routing/segment-keys.d.ts.map +1 -1
- package/dist/routing/types.d.ts +0 -6
- package/dist/routing/types.d.ts.map +1 -1
- package/dist/schema-bridge.d.ts +60 -9
- package/dist/schema-bridge.d.ts.map +1 -1
- package/dist/search-params/define.d.ts +62 -8
- package/dist/search-params/define.d.ts.map +1 -1
- package/dist/search-params/index.d.ts +0 -1
- package/dist/search-params/index.d.ts.map +1 -1
- package/dist/search-params/index.js +66 -29
- package/dist/search-params/index.js.map +1 -1
- package/dist/search-params/parse-total.d.ts +70 -0
- package/dist/search-params/parse-total.d.ts.map +1 -0
- package/dist/search-params/wrappers.d.ts +26 -3
- package/dist/search-params/wrappers.d.ts.map +1 -1
- package/dist/server/access-gate.d.ts +19 -8
- package/dist/server/access-gate.d.ts.map +1 -1
- package/dist/server/action-handler.d.ts.map +1 -1
- package/dist/server/als-registry.d.ts +16 -0
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/chain-url-parts.d.ts +2 -3
- package/dist/server/chain-url-parts.d.ts.map +1 -1
- package/dist/server/compress.d.ts.map +1 -1
- package/dist/server/deny-boundary.d.ts +148 -15
- package/dist/server/deny-boundary.d.ts.map +1 -1
- package/dist/server/deny-renderer.d.ts +2 -2
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/error-boundary-wrapper.d.ts +85 -15
- package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
- package/dist/server/index.d.ts +0 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +3 -2
- package/dist/server/index.js.map +1 -1
- package/dist/server/internal.d.ts +3 -1
- package/dist/server/internal.d.ts.map +1 -1
- package/dist/server/internal.js +344 -237
- package/dist/server/internal.js.map +1 -1
- package/dist/server/metadata-collector.d.ts +52 -0
- package/dist/server/metadata-collector.d.ts.map +1 -0
- package/dist/server/param-coercion.d.ts +12 -5
- package/dist/server/param-coercion.d.ts.map +1 -1
- package/dist/server/pipeline-helpers.d.ts.map +1 -1
- package/dist/server/pipeline-outcome.d.ts.map +1 -1
- package/dist/server/pipeline-phases.d.ts.map +1 -1
- package/dist/server/primitives.d.ts +23 -0
- package/dist/server/primitives.d.ts.map +1 -1
- package/dist/server/route-element-builder.d.ts +1 -12
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/dist/server/rsc-cache-key-guard.d.ts.map +1 -1
- package/dist/server/rsc-entry/deny-fallback.d.ts.map +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/helpers.d.ts +0 -7
- package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts +0 -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/revalidate-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-payload.d.ts +1 -3
- package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-stream.d.ts +12 -0
- package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
- package/dist/server/rsc-entry/ssr-renderer.d.ts +0 -2
- package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
- package/dist/server/skippable-prefix.d.ts +3 -0
- package/dist/server/skippable-prefix.d.ts.map +1 -1
- package/dist/server/slot-resolver.d.ts +18 -19
- package/dist/server/slot-resolver.d.ts.map +1 -1
- package/dist/server/ssr-bridge-types.d.ts +11 -0
- package/dist/server/ssr-bridge-types.d.ts.map +1 -1
- package/dist/server/ssr-entry.d.ts +0 -1
- package/dist/server/ssr-entry.d.ts.map +1 -1
- package/dist/server/state-tree-diff.d.ts +9 -16
- package/dist/server/state-tree-diff.d.ts.map +1 -1
- package/dist/server/static-generator.d.ts.map +1 -1
- package/dist/server/status-code-resolver.d.ts +8 -1
- package/dist/server/status-code-resolver.d.ts.map +1 -1
- package/dist/server/tree-builder.d.ts +28 -43
- package/dist/server/tree-builder.d.ts.map +1 -1
- package/dist/server/types.d.ts +12 -7
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/utils/element-type.d.ts +40 -0
- package/dist/server/utils/element-type.d.ts.map +1 -0
- package/dist/shared/rsc-media-type.d.ts +40 -0
- package/dist/shared/rsc-media-type.d.ts.map +1 -0
- package/dist/shared/segment-info.d.ts +7 -0
- package/dist/shared/segment-info.d.ts.map +1 -1
- package/docs/api/30-api-server.mdx +1 -1
- package/docs/api/33-api-search-params.mdx +38 -16
- package/docs/api/35-api-typescript.mdx +3 -3
- package/docs/api/36-cli.mdx +34 -7
- package/docs/learn/00-introduction.mdx +1 -1
- package/docs/learn/02-pages-and-layouts.mdx +1 -1
- package/docs/learn/05-typed-params.mdx +8 -8
- package/docs/learn/07-typed-routes.mdx +12 -7
- package/docs/learn/11-error-handling.mdx +16 -0
- package/docs/more/01-advanced-routing.mdx +1 -1
- package/docs/more/03-coming-from-nextjs.mdx +2 -2
- package/docs/more/50-ai-agent-instructions.mdx +6 -4
- package/package.json +8 -5
- package/src/adapters/cloudflare.ts +4 -1
- package/src/adapters/compress-module.ts +79 -1
- package/src/cli-check.ts +458 -0
- package/src/cli.ts +59 -24
- package/src/client/browser-entry/action-dispatch.ts +2 -1
- package/src/client/browser-entry/index.ts +0 -5
- package/src/client/error-boundary.tsx +65 -1
- package/src/client/index.ts +14 -3
- package/src/client/link.tsx +65 -64
- package/src/client/navigation-commit.ts +27 -4
- package/src/client/params-context.ts +4 -4
- package/src/client/router-pipeline.ts +4 -0
- package/src/client/router.ts +1 -0
- package/src/client/rsc-fetch.ts +14 -4
- package/src/client/segment-cache.ts +26 -5
- package/src/client/use-query-states.ts +102 -39
- package/src/cookies/define-cookie.ts +6 -1
- package/src/index.ts +20 -3
- package/src/plugin-context.ts +26 -0
- package/src/plugins/routing.ts +84 -146
- package/src/plugins/shims.ts +0 -1
- package/src/plugins/static-build.ts +78 -24
- package/src/routing/codegen-shared.ts +3 -79
- package/src/routing/codegen-types.ts +10 -31
- package/src/routing/codegen-write.ts +139 -0
- package/src/routing/codegen.ts +56 -182
- package/src/routing/convention-lint.ts +139 -40
- package/src/routing/export-detect.ts +151 -7
- package/src/routing/link-codegen.ts +32 -65
- package/src/routing/manifest-codegen.ts +1 -59
- package/src/routing/scanner.ts +3 -3
- package/src/routing/segment-keys.ts +37 -0
- package/src/routing/types.ts +0 -6
- package/src/schema-bridge.ts +180 -58
- package/src/search-params/define.ts +102 -37
- package/src/search-params/index.ts +0 -1
- package/src/search-params/parse-total.ts +78 -0
- package/src/search-params/wrappers.ts +60 -11
- package/src/server/access-gate.tsx +61 -68
- package/src/server/action-handler.ts +1 -4
- package/src/server/als-registry.ts +16 -0
- package/src/server/chain-url-parts.ts +2 -3
- package/src/server/compress.ts +9 -1
- package/src/server/deny-boundary.ts +269 -41
- package/src/server/deny-renderer.ts +32 -21
- package/src/server/error-boundary-wrapper.ts +166 -79
- package/src/server/index.ts +1 -3
- package/src/server/internal.ts +2 -2
- package/src/server/metadata-collector.ts +115 -0
- package/src/server/param-coercion.ts +13 -61
- package/src/server/pipeline-helpers.ts +2 -2
- package/src/server/pipeline-outcome.ts +2 -1
- package/src/server/pipeline-phases.ts +9 -9
- package/src/server/primitives.ts +25 -0
- package/src/server/route-element-builder.ts +169 -194
- package/src/server/rsc-cache-key-guard.ts +2 -8
- package/src/server/rsc-entry/deny-fallback.ts +3 -2
- package/src/server/rsc-entry/error-renderer.ts +31 -11
- package/src/server/rsc-entry/helpers.ts +3 -12
- package/src/server/rsc-entry/index.ts +0 -5
- package/src/server/rsc-entry/render-route.ts +14 -11
- package/src/server/rsc-entry/revalidate-renderer.ts +2 -1
- package/src/server/rsc-entry/rsc-payload.ts +104 -20
- package/src/server/rsc-entry/rsc-stream.ts +25 -2
- package/src/server/rsc-entry/ssr-renderer.ts +28 -16
- package/src/server/skippable-prefix.ts +21 -39
- package/src/server/slot-resolver.ts +69 -189
- package/src/server/ssr-bridge-types.ts +12 -0
- package/src/server/ssr-entry.ts +8 -6
- package/src/server/state-tree-diff.ts +11 -62
- package/src/server/static-generator.ts +3 -2
- package/src/server/status-code-resolver.ts +28 -11
- package/src/server/tree-builder.ts +35 -228
- package/src/server/types.ts +12 -7
- package/src/server/utils/element-type.ts +72 -0
- package/src/shared/rsc-media-type.ts +43 -0
- package/src/shared/segment-info.ts +7 -0
- package/dist/_chunks/cache-api-Cd0VZ_Pd.js.map +0 -1
- package/dist/_chunks/cli-schema-sync-CKgHC2MB.js.map +0 -1
- package/dist/_chunks/error-boundary-DpYRI_I1.js.map +0 -1
- package/dist/_chunks/logger-N7e5auP0.js.map +0 -1
- package/dist/_chunks/navigation-root-B29qg0_T.js.map +0 -1
- package/dist/_chunks/param-value-C8TNYchQ.js.map +0 -1
- package/dist/_chunks/registry-DbJPKoBp.js +0 -20
- package/dist/_chunks/registry-DbJPKoBp.js.map +0 -1
- package/dist/_chunks/schema-bridge-DT_Tn0Xf.js +0 -119
- package/dist/_chunks/schema-bridge-DT_Tn0Xf.js.map +0 -1
- package/dist/_chunks/segment-context-ZDnXDkbz.js +0 -34
- package/dist/_chunks/segment-context-ZDnXDkbz.js.map +0 -1
- package/dist/_chunks/use-query-states-DFvWd-EA.js.map +0 -1
- package/dist/_chunks/use-segment-params-ClyUNq4d.js +0 -128
- package/dist/_chunks/use-segment-params-ClyUNq4d.js.map +0 -1
- package/dist/_chunks/walkers-BhhwI9TD.js +0 -936
- package/dist/_chunks/walkers-BhhwI9TD.js.map +0 -1
- package/dist/search-params/registry.d.ts +0 -20
- package/dist/search-params/registry.d.ts.map +0 -1
- package/dist/segment-params/define.d.ts +0 -83
- package/dist/segment-params/define.d.ts.map +0 -1
- package/dist/segment-params/index.d.ts +0 -3
- package/dist/segment-params/index.d.ts.map +0 -1
- package/dist/segment-params/index.js +0 -70
- package/dist/segment-params/index.js.map +0 -1
- package/src/search-params/registry.ts +0 -31
- package/src/segment-params/define.ts +0 -226
- package/src/segment-params/index.ts +0 -9
package/src/routing/codegen.ts
CHANGED
|
@@ -12,14 +12,8 @@ import { existsSync } from 'node:fs';
|
|
|
12
12
|
import { join, relative } from 'node:path';
|
|
13
13
|
import type { RouteTree, SegmentNode } from './types.js';
|
|
14
14
|
import type { ParamEntry, RouteEntry } from './codegen-types.js';
|
|
15
|
-
import {
|
|
16
|
-
buildCodecChainType,
|
|
17
|
-
buildSchemaParamType,
|
|
18
|
-
emitResolveSegmentFieldHelper,
|
|
19
|
-
formatSearchParamsType,
|
|
20
|
-
} from './codegen-shared.js';
|
|
15
|
+
import { buildSchemaParamType } from './codegen-shared.js';
|
|
21
16
|
import { effectiveUrlSegment, type UrlSegmentIdentity } from './segment-classify.js';
|
|
22
|
-
import { fileHasExport } from './export-detect.js';
|
|
23
17
|
import { formatLinkCatchAllOverloads, formatTypedLinkOverloads } from './link-codegen.js';
|
|
24
18
|
|
|
25
19
|
/** Options for route map generation. */
|
|
@@ -50,7 +44,7 @@ export function generateRouteMap(tree: RouteTree, options: CodegenOptions = {}):
|
|
|
50
44
|
const hasSchema = options.hasSchema ?? detectSchema(options.appDir);
|
|
51
45
|
|
|
52
46
|
const routes: RouteEntry[] = [];
|
|
53
|
-
collectRoutes(tree.root, [],
|
|
47
|
+
collectRoutes(tree.root, [], '', routes, hasSchema, false);
|
|
54
48
|
|
|
55
49
|
// Sort routes alphabetically for deterministic output
|
|
56
50
|
routes.sort((a, b) => a.urlPath.localeCompare(b.urlPath));
|
|
@@ -81,7 +75,6 @@ function detectSchema(appDir: string | undefined): boolean {
|
|
|
81
75
|
function collectRoutes(
|
|
82
76
|
node: SegmentNode,
|
|
83
77
|
ancestorParams: ParamEntry[],
|
|
84
|
-
ancestorParamsFiles: string[],
|
|
85
78
|
parentTreePath: string,
|
|
86
79
|
routes: RouteEntry[],
|
|
87
80
|
hasSchema: boolean,
|
|
@@ -91,23 +84,7 @@ function collectRoutes(
|
|
|
91
84
|
// Build the tree path (includes groups, slots — uniquely identifies this segment).
|
|
92
85
|
// Root node has empty segmentName, producing treePath = ''.
|
|
93
86
|
const treePath = node.segmentName ? `${parentTreePath}/${node.segmentName}` : parentTreePath;
|
|
94
|
-
//
|
|
95
|
-
// segmentParams export). The full chain of params.ts files in the
|
|
96
|
-
// route ancestry is threaded down via `ancestorParamsFiles`; codec
|
|
97
|
-
// resolution for each ParamEntry is deferred until leaf time so that
|
|
98
|
-
// descendant params.ts files can override ancestor codecs (closest-
|
|
99
|
-
// to-leaf wins, matching the runtime semantics of
|
|
100
|
-
// coerceSegmentParams which walks segments top-down and overwrites
|
|
101
|
-
// earlier coercions).
|
|
102
|
-
const ownParamsFile =
|
|
103
|
-
node.params && fileHasExport(node.params.filePath, 'segmentParams')
|
|
104
|
-
? node.params.filePath
|
|
105
|
-
: undefined;
|
|
106
|
-
|
|
107
|
-
// Accumulate params from this segment. We attach `codecFilePaths`
|
|
108
|
-
// later (at leaf time) using the FULL chain so descendant overrides
|
|
109
|
-
// are visible. The legacy layout/page fallback is recorded now
|
|
110
|
-
// because it is per-segment (and does not participate in inheritance).
|
|
87
|
+
// Accumulate params from this segment.
|
|
111
88
|
//
|
|
112
89
|
// The segment is read through `effectiveUrlSegment` for the same reason the
|
|
113
90
|
// runtime coercer is: an intercepting node's own `segmentType` is
|
|
@@ -120,72 +97,44 @@ function collectRoutes(
|
|
|
120
97
|
const urlSegment = effectiveUrlSegment(node);
|
|
121
98
|
const params = [...ancestorParams];
|
|
122
99
|
if (urlSegment.paramName) {
|
|
123
|
-
const legacyFallback = ownParamsFile ? undefined : findLegacyParamsExport(node);
|
|
124
100
|
params.push({
|
|
125
101
|
name: urlSegment.paramName,
|
|
126
102
|
bracketName: bracketNameForSegment(urlSegment),
|
|
127
103
|
type: paramTypeForSegment(urlSegment.segmentType),
|
|
128
|
-
// Codec chain populated at leaf time. We carry the per-segment
|
|
129
|
-
// legacy fallback (if any) so leaf-time resolution can fall back
|
|
130
|
-
// to it when no params.ts in the chain declares this key.
|
|
131
|
-
legacyCodecFilePath: legacyFallback,
|
|
132
104
|
});
|
|
133
105
|
}
|
|
134
106
|
|
|
135
|
-
// Extend the chain for descendants of this segment.
|
|
136
|
-
const nextAncestorFiles = ownParamsFile
|
|
137
|
-
? [...ancestorParamsFiles, ownParamsFile]
|
|
138
|
-
: ancestorParamsFiles;
|
|
139
|
-
|
|
140
107
|
// Check if this segment is a leaf route (has page or route file)
|
|
141
108
|
const isPage = !!node.page;
|
|
142
109
|
const isApiRoute = !!node.route;
|
|
143
110
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
: undefined;
|
|
159
|
-
return {
|
|
160
|
-
name: p.name,
|
|
161
|
-
bracketName: p.bracketName,
|
|
162
|
-
type: p.type,
|
|
163
|
-
codecFilePaths,
|
|
164
|
-
};
|
|
165
|
-
});
|
|
111
|
+
// A segment that hosts code but is not itself addressable — a layout,
|
|
112
|
+
// middleware, or access check above the leaf — still gets a `$segment`
|
|
113
|
+
// module, so `getSegmentParams(SEGMENT_PATH)` is callable from it and needs
|
|
114
|
+
// a matching typed overload. Emitting only leaves left those call sites on
|
|
115
|
+
// the broad `Record<string, string | string[]>` signature, which since
|
|
116
|
+
// TIM-1342 (which deleted `defineSegmentParams`) is their ONLY typed path.
|
|
117
|
+
// design/41-global-params.md's Type Summary promises layouts and middleware
|
|
118
|
+
// exact accumulated params.
|
|
119
|
+
//
|
|
120
|
+
// `isRoutable: false` keeps them out of the urlPath-keyed emissions — the
|
|
121
|
+
// `Routes` interface and `<Link>` variants — where they would publish a URL
|
|
122
|
+
// that no request can reach. They belong only to the segmentPath-keyed
|
|
123
|
+
// APIs: `getSegmentParams`, `useSegmentParams`, `TimberSegmentParams`.
|
|
124
|
+
const hostsCode = !!node.layout || !!node.middleware || !!node.access;
|
|
166
125
|
|
|
126
|
+
if (isPage || isApiRoute || hostsCode) {
|
|
167
127
|
const entry: RouteEntry = {
|
|
168
128
|
urlPath: node.urlPath,
|
|
169
129
|
segmentPath: treePath || '/',
|
|
170
|
-
params
|
|
171
|
-
hasSearchParams: false,
|
|
130
|
+
params,
|
|
172
131
|
isApiRoute,
|
|
132
|
+
isRoutable: isPage || isApiRoute,
|
|
173
133
|
isSlotRoute: insideSlot,
|
|
174
134
|
isInterceptingRoute: insideIntercepting,
|
|
175
135
|
hasSchema,
|
|
176
136
|
};
|
|
177
137
|
|
|
178
|
-
// Detect searchParams export from params.ts (primary) or page.tsx (fallback)
|
|
179
|
-
if (isPage) {
|
|
180
|
-
if (node.params && fileHasExport(node.params.filePath, 'searchParams')) {
|
|
181
|
-
entry.hasSearchParams = true;
|
|
182
|
-
entry.searchParamsPagePath = node.params.filePath;
|
|
183
|
-
} else if (node.page && fileHasExport(node.page.filePath, 'searchParams')) {
|
|
184
|
-
entry.hasSearchParams = true;
|
|
185
|
-
entry.searchParamsPagePath = node.page.filePath;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
138
|
routes.push(entry);
|
|
190
139
|
}
|
|
191
140
|
|
|
@@ -195,44 +144,30 @@ function collectRoutes(
|
|
|
195
144
|
// alone was not enough.
|
|
196
145
|
for (const child of node.children) {
|
|
197
146
|
const childIntercepting = insideIntercepting || child.segmentType === 'intercepting';
|
|
198
|
-
collectRoutes(
|
|
199
|
-
child,
|
|
200
|
-
params,
|
|
201
|
-
nextAncestorFiles,
|
|
202
|
-
treePath,
|
|
203
|
-
routes,
|
|
204
|
-
hasSchema,
|
|
205
|
-
insideSlot,
|
|
206
|
-
childIntercepting
|
|
207
|
-
);
|
|
147
|
+
collectRoutes(child, params, treePath, routes, hasSchema, insideSlot, childIntercepting);
|
|
208
148
|
}
|
|
209
149
|
|
|
210
150
|
// Recurse into slots (they share the parent's URL path, but may have their own pages)
|
|
211
151
|
for (const slot of Object.values(node.slots)) {
|
|
212
|
-
collectRoutes(
|
|
213
|
-
slot,
|
|
214
|
-
params,
|
|
215
|
-
nextAncestorFiles,
|
|
216
|
-
treePath,
|
|
217
|
-
routes,
|
|
218
|
-
hasSchema,
|
|
219
|
-
true,
|
|
220
|
-
insideIntercepting
|
|
221
|
-
);
|
|
152
|
+
collectRoutes(slot, params, treePath, routes, hasSchema, true, insideIntercepting);
|
|
222
153
|
}
|
|
223
154
|
}
|
|
224
155
|
|
|
225
156
|
/**
|
|
226
157
|
* Dedupe route entries by urlPath for urlPath-keyed emissions (the Routes
|
|
227
|
-
* interface
|
|
158
|
+
* interface and typed Link variants).
|
|
228
159
|
*
|
|
229
160
|
* Parallel-slot pages share their parent's urlPath; emitting one entry per
|
|
230
161
|
* slot page produced duplicate interface keys (TS2300/TS2717 under
|
|
231
162
|
* skipLibCheck:false) and duplicate overloads. The non-slot entry is the
|
|
232
163
|
* canonical one; when only slot entries exist (parent has no page), the
|
|
233
164
|
* lexicographically smallest segmentPath wins so output is independent of
|
|
234
|
-
* slot scan order.
|
|
235
|
-
*
|
|
165
|
+
* slot scan order.
|
|
166
|
+
*
|
|
167
|
+
* The loser is discarded outright. It used to donate its `searchParams`
|
|
168
|
+
* definition to the winner, because a slot could carry one the parent page
|
|
169
|
+
* lacked; since TIM-1343 no route entry carries a searchParams definition at
|
|
170
|
+
* all, there is nothing left to merge.
|
|
236
171
|
*
|
|
237
172
|
* segmentPath-keyed emissions (getSegmentParams, useSegmentParams,
|
|
238
173
|
* TimberSegmentParams) must NOT use this — segmentPaths are already unique
|
|
@@ -247,25 +182,11 @@ function dedupeRoutesByUrlPath(routes: RouteEntry[]): RouteEntry[] {
|
|
|
247
182
|
continue;
|
|
248
183
|
}
|
|
249
184
|
|
|
250
|
-
let winner = existing;
|
|
251
|
-
let loser = route;
|
|
252
185
|
const routeWins =
|
|
253
186
|
(existing.isSlotRoute && !route.isSlotRoute) ||
|
|
254
187
|
(!!existing.isSlotRoute === !!route.isSlotRoute &&
|
|
255
188
|
route.segmentPath.localeCompare(existing.segmentPath) < 0);
|
|
256
|
-
|
|
257
|
-
winner = route;
|
|
258
|
-
loser = existing;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
if (!winner.hasSearchParams && loser.hasSearchParams) {
|
|
262
|
-
winner = {
|
|
263
|
-
...winner,
|
|
264
|
-
hasSearchParams: true,
|
|
265
|
-
searchParamsPagePath: loser.searchParamsPagePath,
|
|
266
|
-
};
|
|
267
|
-
}
|
|
268
|
-
byPath.set(route.urlPath, winner);
|
|
189
|
+
byPath.set(route.urlPath, routeWins ? route : existing);
|
|
269
190
|
}
|
|
270
191
|
return [...byPath.values()];
|
|
271
192
|
}
|
|
@@ -296,23 +217,6 @@ function paramTypeForSegment(segmentType: string): ParamEntry['type'] {
|
|
|
296
217
|
}
|
|
297
218
|
}
|
|
298
219
|
|
|
299
|
-
/**
|
|
300
|
-
* Find a legacy `segmentParams` export on layout.tsx or page.tsx.
|
|
301
|
-
*
|
|
302
|
-
* Backward-compat shim: TIM-508 made params.ts the canonical location
|
|
303
|
-
* for `segmentParams`. Layout/page exports are still accepted for the
|
|
304
|
-
* OWN segment only (not inherited by descendants — see TIM-834).
|
|
305
|
-
*/
|
|
306
|
-
function findLegacyParamsExport(node: SegmentNode): string | undefined {
|
|
307
|
-
if (node.layout && fileHasExport(node.layout.filePath, 'segmentParams')) {
|
|
308
|
-
return node.layout.filePath;
|
|
309
|
-
}
|
|
310
|
-
if (node.page && fileHasExport(node.page.filePath, 'segmentParams')) {
|
|
311
|
-
return node.page.filePath;
|
|
312
|
-
}
|
|
313
|
-
return undefined;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
220
|
/**
|
|
317
221
|
* Emit the schema-based type exports: TimberSegmentParams, TimberRoute,
|
|
318
222
|
* AllSegmentParams, RoutesWithParams.
|
|
@@ -436,14 +340,6 @@ function formatDeclarationFile(
|
|
|
436
340
|
lines.push('');
|
|
437
341
|
}
|
|
438
342
|
|
|
439
|
-
if (!hasSchema) {
|
|
440
|
-
// TIM-834 P2: emit the shared codec-resolution helper type ONCE so the
|
|
441
|
-
// per-param chain conditionals reference it instead of inlining the
|
|
442
|
-
// fallback in both branches (which grows O(2^N) in chain depth).
|
|
443
|
-
lines.push(emitResolveSegmentFieldHelper());
|
|
444
|
-
lines.push('');
|
|
445
|
-
}
|
|
446
|
-
|
|
447
343
|
// urlPath-keyed emissions use the deduped list (slot pages share their
|
|
448
344
|
// parent's urlPath); segmentPath-keyed emissions below keep all entries.
|
|
449
345
|
//
|
|
@@ -451,18 +347,23 @@ function formatDeclarationFile(
|
|
|
451
347
|
// computed urlPath is not a route any request can reach, so a path that
|
|
452
348
|
// happens NOT to collide with a real one would otherwise be published as a
|
|
453
349
|
// real route. See `RouteEntry.isInterceptingRoute`.
|
|
454
|
-
const uniqueByUrlPath = dedupeRoutesByUrlPath(
|
|
350
|
+
const uniqueByUrlPath = dedupeRoutesByUrlPath(
|
|
351
|
+
routes.filter((r) => r.isRoutable && !r.isInterceptingRoute)
|
|
352
|
+
);
|
|
455
353
|
|
|
456
354
|
lines.push("declare module '@timber-js/app' {");
|
|
457
355
|
lines.push(' interface Routes {');
|
|
458
356
|
|
|
357
|
+
// TIM-1343: no `searchParams` member. With `params.ts` gone there is no
|
|
358
|
+
// convention file to read a definition from, so the framework has nothing
|
|
359
|
+
// to type it with — and an always-`{}` member is a claim that every route
|
|
360
|
+
// has no search params, which is false. Search-param types now come from
|
|
361
|
+
// the definition object itself, wherever the app chooses to keep it.
|
|
459
362
|
for (const route of uniqueByUrlPath) {
|
|
460
|
-
const paramsType = formatParamsType(route.params,
|
|
461
|
-
const searchParamsType = formatSearchParamsType(route, importBase);
|
|
363
|
+
const paramsType = formatParamsType(route.params, hasSchema);
|
|
462
364
|
|
|
463
365
|
lines.push(` '${route.urlPath}': {`);
|
|
464
366
|
lines.push(` segmentParams: ${paramsType}`);
|
|
465
|
-
lines.push(` searchParams: ${searchParamsType}`);
|
|
466
367
|
lines.push(` }`);
|
|
467
368
|
}
|
|
468
369
|
|
|
@@ -493,7 +394,7 @@ function formatDeclarationFile(
|
|
|
493
394
|
lines.push(" import type { AllSegmentParams } from '@timber-js/app'");
|
|
494
395
|
}
|
|
495
396
|
for (const route of dynamicRoutes) {
|
|
496
|
-
const paramsType = formatParamsType(route.params,
|
|
397
|
+
const paramsType = formatParamsType(route.params, hasSchema);
|
|
497
398
|
lines.push(
|
|
498
399
|
` export function getSegmentParams(segmentPath: '${route.segmentPath}'): ${paramsType}`
|
|
499
400
|
);
|
|
@@ -511,9 +412,7 @@ function formatDeclarationFile(
|
|
|
511
412
|
|
|
512
413
|
if (dynamicRoutes.length > 0 || pageRoutes.length > 0) {
|
|
513
414
|
lines.push("declare module '@timber-js/app/client' {");
|
|
514
|
-
lines.push(
|
|
515
|
-
" import type { SearchParamsDefinition, SetParams, QueryStatesOptions, SearchParamCodec } from '@timber-js/app/search-params'"
|
|
516
|
-
);
|
|
415
|
+
lines.push(" import type { SearchParamsDefinition } from '@timber-js/app/search-params'");
|
|
517
416
|
if (hasSchema) {
|
|
518
417
|
lines.push(" import type { AllSegmentParams } from '@timber-js/app'");
|
|
519
418
|
}
|
|
@@ -522,7 +421,7 @@ function formatDeclarationFile(
|
|
|
522
421
|
// useSegmentParams overloads
|
|
523
422
|
if (dynamicRoutes.length > 0) {
|
|
524
423
|
for (const route of dynamicRoutes) {
|
|
525
|
-
const paramsType = formatParamsType(route.params,
|
|
424
|
+
const paramsType = formatParamsType(route.params, hasSchema);
|
|
526
425
|
lines.push(
|
|
527
426
|
` export function useSegmentParams(segmentPath: '${route.segmentPath}'): ${paramsType}`
|
|
528
427
|
);
|
|
@@ -535,12 +434,6 @@ function formatDeclarationFile(
|
|
|
535
434
|
lines.push('');
|
|
536
435
|
}
|
|
537
436
|
|
|
538
|
-
// useQueryStates overloads
|
|
539
|
-
if (pageRoutes.length > 0) {
|
|
540
|
-
lines.push(...formatUseQueryStatesOverloads(pageRoutes, importBase));
|
|
541
|
-
lines.push('');
|
|
542
|
-
}
|
|
543
|
-
|
|
544
437
|
// Typed Link overloads — per-route with DIRECT types (no conditionals).
|
|
545
438
|
// Direct types preserve TypeScript's excess property checking.
|
|
546
439
|
//
|
|
@@ -555,7 +448,7 @@ function formatDeclarationFile(
|
|
|
555
448
|
// confusing "Type 'string' is not assignable to type 'never'" cascade.
|
|
556
449
|
if (pageRoutes.length > 0) {
|
|
557
450
|
lines.push(' // Typed Link overloads — per-route (block 1 / emitted first)');
|
|
558
|
-
lines.push(...formatTypedLinkOverloads(pageRoutes,
|
|
451
|
+
lines.push(...formatTypedLinkOverloads(pageRoutes, hasSchema));
|
|
559
452
|
lines.push('');
|
|
560
453
|
}
|
|
561
454
|
|
|
@@ -580,48 +473,29 @@ function formatDeclarationFile(
|
|
|
580
473
|
/**
|
|
581
474
|
* Format the params type for a route entry.
|
|
582
475
|
*/
|
|
583
|
-
function formatParamsType(params: ParamEntry[],
|
|
476
|
+
function formatParamsType(params: ParamEntry[], hasSchema?: boolean): string {
|
|
584
477
|
if (params.length === 0) {
|
|
585
478
|
return '{}';
|
|
586
479
|
}
|
|
587
480
|
|
|
481
|
+
// Without a schema a URL segment is an uninterpreted string — that IS
|
|
482
|
+
// its type. Per-segment codec chains used to narrow it here; TIM-1342
|
|
483
|
+
// deleted them along with params.ts.
|
|
588
484
|
const fields = params.map((p) => {
|
|
589
|
-
const codecType = hasSchema
|
|
590
|
-
? buildSchemaParamType(p)
|
|
591
|
-
: buildCodecChainType(p, importBase, p.type);
|
|
485
|
+
const codecType = hasSchema ? buildSchemaParamType(p) : p.type;
|
|
592
486
|
return `${p.name}: ${codecType}`;
|
|
593
487
|
});
|
|
594
488
|
return `{ ${fields.join('; ')} }`;
|
|
595
489
|
}
|
|
596
490
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
*/
|
|
606
|
-
function formatUseQueryStatesOverloads(routes: RouteEntry[], importBase?: string): string[] {
|
|
607
|
-
const lines: string[] = [];
|
|
608
|
-
|
|
609
|
-
for (const route of routes) {
|
|
610
|
-
const searchParamsType = route.hasSearchParams
|
|
611
|
-
? formatSearchParamsType(route, importBase)
|
|
612
|
-
: '{}';
|
|
613
|
-
lines.push(
|
|
614
|
-
` export function useQueryStates<R extends '${route.urlPath}'>(route: R, options?: QueryStatesOptions): [${searchParamsType}, SetParams<${searchParamsType}>]`
|
|
615
|
-
);
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
// Fallback: standalone codecs (existing API)
|
|
619
|
-
lines.push(
|
|
620
|
-
' export function useQueryStates<T extends Record<string, unknown>>(codecs: { [K in keyof T]: SearchParamCodec<T[K]> }, options?: QueryStatesOptions): [T, SetParams<T>]'
|
|
621
|
-
);
|
|
622
|
-
|
|
623
|
-
return lines;
|
|
624
|
-
}
|
|
491
|
+
// TIM-1341: `useQueryStates` no longer has a route-string form, so the
|
|
492
|
+
// generated `.d.ts` emits no overloads for it. Its source signature —
|
|
493
|
+
// `useQueryStates(codecs, options?)` — is fully generic over the codec map
|
|
494
|
+
// and needs no per-route augmentation. Reaching another route's codecs is
|
|
495
|
+
// done by importing that route's `searchParams` definition from its
|
|
496
|
+
// `params.ts` and calling `definition.useQueryStates()`, which is typed by
|
|
497
|
+
// inference and cannot fail at runtime on an unpopulated registry.
|
|
498
|
+
// See design/23-search-params.md §"Client Access".
|
|
625
499
|
|
|
626
500
|
// Link overload formatters and helpers (`formatTypedLinkOverloads`,
|
|
627
501
|
// `formatLinkCatchAllOverloads`, `formatLinkParamsType`,
|
|
@@ -4,17 +4,25 @@
|
|
|
4
4
|
* Runs at scan time (build and dev startup). Each check produces a warning
|
|
5
5
|
* with the file path, what's wrong, and what to do about it.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* Most checks are warnings — they don't block the build. The goal is to catch
|
|
8
|
+
* issues that would otherwise produce cryptic runtime behavior (silent 404s,
|
|
9
|
+
* empty pages, confusing React errors).
|
|
10
|
+
*
|
|
11
|
+
* A check emits `level: 'error'` only when the misconfiguration is *proven*
|
|
12
|
+
* (never inferred from an absence the analysis cannot see through) and fatal
|
|
13
|
+
* to rendering. `plugins/routing.ts` turns those into a thrown build error,
|
|
14
|
+
* so a false positive there refuses a working app — see
|
|
15
|
+
* `classifyClientBoundaryFile` for how that bar is met.
|
|
10
16
|
*
|
|
11
17
|
* Design doc: 07-routing.md, 10-error-handling.md
|
|
12
18
|
*/
|
|
13
19
|
|
|
14
20
|
import { existsSync } from 'node:fs';
|
|
15
|
-
import type { RouteTree, SegmentNode } from './types.js';
|
|
21
|
+
import type { RouteFile, RouteTree, SegmentNode } from './types.js';
|
|
16
22
|
import { swallow } from '../server/logger.js';
|
|
23
|
+
import { isMdxFilePath } from '../server/utils/mdx-file.js';
|
|
17
24
|
import {
|
|
25
|
+
fileDefaultExportIsLocalValue,
|
|
18
26
|
fileHasAnyExport,
|
|
19
27
|
fileHasDefaultExport,
|
|
20
28
|
fileHasDirective,
|
|
@@ -61,10 +69,10 @@ export function lintConventions(tree: RouteTree, appDir: string): ConventionWarn
|
|
|
61
69
|
// Check 5: page.tsx / layout.tsx without default export
|
|
62
70
|
checkDefaultExports(tree.root, warnings);
|
|
63
71
|
|
|
64
|
-
// Check 6: error.tsx /
|
|
65
|
-
|
|
66
|
-
if (tree.globalError
|
|
67
|
-
checkFileUseClientDirective(tree.globalError
|
|
72
|
+
// Check 6: error.tsx / 404.tsx / 4xx.tsx / … without 'use client' directive
|
|
73
|
+
checkClientBoundaryFiles(tree.root, warnings);
|
|
74
|
+
if (tree.globalError) {
|
|
75
|
+
checkFileUseClientDirective(tree.globalError, 'global-error', warnings);
|
|
68
76
|
}
|
|
69
77
|
|
|
70
78
|
return warnings;
|
|
@@ -272,54 +280,132 @@ function checkFileDefaultExport(
|
|
|
272
280
|
}
|
|
273
281
|
}
|
|
274
282
|
|
|
275
|
-
// ─── Check:
|
|
283
|
+
// ─── Check: Client Boundary Directive ───────────────────────────────────────
|
|
276
284
|
|
|
277
285
|
/**
|
|
278
|
-
*
|
|
286
|
+
* Check every file the renderer hands to `TimberErrorBoundary` as a component
|
|
287
|
+
* prop: `error.tsx`, `global-error.tsx`, and the status-code family
|
|
288
|
+
* (`404.tsx`, `4xx.tsx`, `503.tsx`, …).
|
|
289
|
+
*
|
|
290
|
+
* `TimberErrorBoundary` is a client component, so a server component passed as
|
|
291
|
+
* `fallbackComponent` is a function crossing the RSC → client boundary. React
|
|
292
|
+
* Flight refuses to serialize it, and because the boundary wraps the segment's
|
|
293
|
+
* *children*, the failure hits every page under that segment — not just the
|
|
294
|
+
* error path. The raw message ("Functions cannot be passed directly to Client
|
|
295
|
+
* Components") names neither the file nor the fix (TIM-1329).
|
|
279
296
|
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
297
|
+
* MDX status files are exempt: they are server components by design and the
|
|
298
|
+
* renderer pre-renders them into `fallbackElement` instead. See TIM-503 and
|
|
299
|
+
* `server/error-boundary-wrapper.ts`.
|
|
300
|
+
*
|
|
301
|
+
* `.json` status files never reach React at all, and are not in `statusFiles`.
|
|
283
302
|
*/
|
|
284
|
-
function
|
|
285
|
-
if (node.error
|
|
286
|
-
checkFileUseClientDirective(node.error
|
|
303
|
+
function checkClientBoundaryFiles(node: SegmentNode, warnings: ConventionWarning[]): void {
|
|
304
|
+
if (node.error) {
|
|
305
|
+
checkFileUseClientDirective(node.error, 'error', warnings);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
for (const [key, file] of Object.entries(node.statusFiles ?? {})) {
|
|
309
|
+
checkFileUseClientDirective(file, key, warnings);
|
|
287
310
|
}
|
|
288
311
|
|
|
289
312
|
for (const child of node.children) {
|
|
290
|
-
|
|
313
|
+
checkClientBoundaryFiles(child, warnings);
|
|
291
314
|
}
|
|
292
315
|
for (const slot of Object.values(node.slots)) {
|
|
293
|
-
|
|
316
|
+
checkClientBoundaryFiles(slot, warnings);
|
|
294
317
|
}
|
|
295
318
|
}
|
|
296
319
|
|
|
320
|
+
/**
|
|
321
|
+
* `'client'` — carries the directive.
|
|
322
|
+
* `'server'` — proven server component: no directive, and the default export is
|
|
323
|
+
* a function or class built by this very module, so no other file's directive
|
|
324
|
+
* can be governing it.
|
|
325
|
+
* `'unknown'` — no directive, but the exported value may come from elsewhere
|
|
326
|
+
* (`export { default } from './client-thing'`), or the file could not be
|
|
327
|
+
* read or parsed. Never escalated to a build error.
|
|
328
|
+
*/
|
|
329
|
+
type ClientBoundaryVerdict = 'client' | 'server' | 'unknown';
|
|
330
|
+
|
|
331
|
+
function classifyClientBoundaryFile(filePath: string): ClientBoundaryVerdict {
|
|
332
|
+
if (fileHasDirective(filePath, 'use client')) return 'client';
|
|
333
|
+
if (fileDefaultExportIsLocalValue(filePath)) return 'server';
|
|
334
|
+
return 'unknown';
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** Example component name for the fix snippet, keyed by convention name. */
|
|
338
|
+
function exampleComponentName(convention: string): string {
|
|
339
|
+
if (convention === 'global-error') return 'GlobalError';
|
|
340
|
+
if (convention === 'error') return 'ErrorBoundary';
|
|
341
|
+
if (convention === '404') return 'NotFound';
|
|
342
|
+
return 'StatusPage';
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* @param file the route file (its extension decides whether the check applies)
|
|
347
|
+
* @param convention the convention name — `error`, `global-error`, `404`, `4xx`, …
|
|
348
|
+
*/
|
|
297
349
|
function checkFileUseClientDirective(
|
|
298
|
-
|
|
299
|
-
|
|
350
|
+
file: RouteFile,
|
|
351
|
+
convention: string,
|
|
300
352
|
warnings: ConventionWarning[]
|
|
301
353
|
): void {
|
|
354
|
+
// MDX/markdown status and error files are server components by design — the
|
|
355
|
+
// renderer pre-renders them into `fallbackElement`, so they never cross the
|
|
356
|
+
// boundary as a function.
|
|
357
|
+
//
|
|
358
|
+
// The exemption is the *complement* of the renderer's own test, using the
|
|
359
|
+
// renderer's own helper: everything `error-boundary-wrapper.ts` and
|
|
360
|
+
// `error-renderer.ts` do not treat as MDX, they pass as a component. An
|
|
361
|
+
// independent "is this a script extension?" list here would be a second
|
|
362
|
+
// expression of that fact, free to drift into a gap where a file is a
|
|
363
|
+
// component to the renderer and exempt to the linter.
|
|
364
|
+
if (isMdxFilePath(file.filePath)) return;
|
|
365
|
+
|
|
366
|
+
const filePath = file.filePath;
|
|
367
|
+
const label = `${convention}.${file.extension}`;
|
|
368
|
+
let verdict: ClientBoundaryVerdict;
|
|
302
369
|
try {
|
|
303
|
-
|
|
304
|
-
warnings.push({
|
|
305
|
-
id: `ERROR_NO_USE_CLIENT:${filePath}`,
|
|
306
|
-
summary: `${fileType}.tsx is missing 'use client': ${filePath}`,
|
|
307
|
-
details:
|
|
308
|
-
` File: ${filePath}\n\n` +
|
|
309
|
-
` ${fileType}.tsx must start with 'use client' because React error\n` +
|
|
310
|
-
' boundaries require client components. Without it, the error boundary\n' +
|
|
311
|
-
' silently fails and errors are uncaught.\n\n' +
|
|
312
|
-
" To fix: Add 'use client' as the first line of the file:\n\n" +
|
|
313
|
-
" 'use client';\n\n" +
|
|
314
|
-
` export default function ${fileType === 'global-error' ? 'GlobalError' : 'ErrorBoundary'}({ error, reset }) {\n` +
|
|
315
|
-
' return <div><h2>Something went wrong</h2><button onClick={reset}>Try again</button></div>;\n' +
|
|
316
|
-
' }\n',
|
|
317
|
-
level: 'warn',
|
|
318
|
-
});
|
|
319
|
-
}
|
|
370
|
+
verdict = classifyClientBoundaryFile(filePath);
|
|
320
371
|
} catch (err) {
|
|
321
|
-
swallow(err, `convention-lint: unreadable ${
|
|
372
|
+
swallow(err, `convention-lint: unreadable ${label} file ${filePath}`);
|
|
373
|
+
return;
|
|
322
374
|
}
|
|
375
|
+
if (verdict === 'client') return;
|
|
376
|
+
|
|
377
|
+
const isError = verdict === 'server';
|
|
378
|
+
const name = exampleComponentName(convention);
|
|
379
|
+
const props =
|
|
380
|
+
convention === 'error' || convention === 'global-error' ? '{ error, reset }' : '{ status }';
|
|
381
|
+
|
|
382
|
+
warnings.push({
|
|
383
|
+
id: `NEEDS_USE_CLIENT:${filePath}`,
|
|
384
|
+
summary: isError
|
|
385
|
+
? `${label} is a server component and must be a client component: ${filePath}`
|
|
386
|
+
: `${label} may be missing 'use client': ${filePath}`,
|
|
387
|
+
details:
|
|
388
|
+
` File: ${filePath}\n\n` +
|
|
389
|
+
` ${label} is handed to timber's error boundary — a client\n` +
|
|
390
|
+
' component — as a prop. A server component cannot cross that boundary:\n' +
|
|
391
|
+
' React fails with "Functions cannot be passed directly to Client\n' +
|
|
392
|
+
' Components", and every page under this segment fails to render.\n\n' +
|
|
393
|
+
(isError
|
|
394
|
+
? " To fix: Add 'use client' as the first line of the file:\n\n" +
|
|
395
|
+
" 'use client';\n\n" +
|
|
396
|
+
` export default function ${name}(${props}) {\n` +
|
|
397
|
+
' return <h1>…</h1>;\n' +
|
|
398
|
+
' }\n'
|
|
399
|
+
: // The default export comes from somewhere this analysis cannot follow
|
|
400
|
+
// — a re-export, an imported binding, a wrapper call. It may well
|
|
401
|
+
// already be a client component, so say what was checked rather than
|
|
402
|
+
// assert a break.
|
|
403
|
+
" This file has no 'use client' directive, and its default export\n" +
|
|
404
|
+
' comes from elsewhere, so timber cannot tell which it is. If the\n' +
|
|
405
|
+
" module it re-exports starts with 'use client', nothing is wrong\n" +
|
|
406
|
+
' and you can ignore this. Otherwise add the directive there.\n'),
|
|
407
|
+
level: isError ? 'error' : 'warn',
|
|
408
|
+
});
|
|
323
409
|
}
|
|
324
410
|
|
|
325
411
|
// ─── Check: App Directory Exists ────────────────────────────────────────────
|
|
@@ -352,14 +438,27 @@ import { styleText } from 'node:util';
|
|
|
352
438
|
|
|
353
439
|
type Format = InspectColor | readonly InspectColor[];
|
|
354
440
|
const noValidate = { validateStream: false } as const;
|
|
355
|
-
|
|
441
|
+
|
|
442
|
+
export interface FormatOptions {
|
|
443
|
+
/**
|
|
444
|
+
* Emit ANSI escapes. Off when the text becomes an `Error` message —
|
|
445
|
+
* whoever prints it decides its formatting, and colored escapes inside
|
|
446
|
+
* an exception leak into logs and stack traces.
|
|
447
|
+
*/
|
|
448
|
+
color?: boolean;
|
|
449
|
+
}
|
|
356
450
|
|
|
357
451
|
/**
|
|
358
452
|
* Format warnings for terminal output.
|
|
359
453
|
*
|
|
360
454
|
* Groups by severity, uses colors, and includes fix suggestions.
|
|
361
455
|
*/
|
|
362
|
-
export function formatConventionWarnings(
|
|
456
|
+
export function formatConventionWarnings(
|
|
457
|
+
warnings: ConventionWarning[],
|
|
458
|
+
options: FormatOptions = {}
|
|
459
|
+
): string {
|
|
460
|
+
const { color = true } = options;
|
|
461
|
+
const style = (fmt: Format, text: string) => (color ? styleText(fmt, text, noValidate) : text);
|
|
363
462
|
if (warnings.length === 0) return '';
|
|
364
463
|
|
|
365
464
|
const errors = warnings.filter((w) => w.level === 'error');
|