@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
|
@@ -84,23 +84,23 @@ Add `rootDirs` so TypeScript can resolve `$segment` imports:
|
|
|
84
84
|
|
|
85
85
|
## Typed Search Params
|
|
86
86
|
|
|
87
|
-
|
|
87
|
+
Define search params with `defineSearchParams()` in any module and import it — there is no convention file. Search params are then fully typed throughout the stack:
|
|
88
88
|
|
|
89
|
-
```ts title="app/products/params.ts"
|
|
89
|
+
```ts title="app/products/search-params.ts"
|
|
90
90
|
import { defineSearchParams, withDefault, withUrlKey } from '@timber-js/app/search-params';
|
|
91
|
-
import { parseAsInteger,
|
|
91
|
+
import { parseAsInteger, parseAsStringEnum } from 'nuqs';
|
|
92
92
|
import { z } from 'zod/v4';
|
|
93
93
|
|
|
94
94
|
export const searchParams = defineSearchParams({
|
|
95
95
|
page: withDefault(parseAsInteger, 1),
|
|
96
|
-
q: withUrlKey(
|
|
96
|
+
q: withUrlKey(z.string().nullable().default(null), 'search'),
|
|
97
97
|
category: z.string().nullable().default(null),
|
|
98
98
|
sort: withDefault(parseAsStringEnum(['price-asc', 'price-desc', 'newest']), 'newest'),
|
|
99
99
|
});
|
|
100
100
|
```
|
|
101
101
|
|
|
102
102
|
```tsx title="app/products/page.tsx"
|
|
103
|
-
import { searchParams } from './params';
|
|
103
|
+
import { searchParams } from './search-params';
|
|
104
104
|
|
|
105
105
|
export default async function ProductsPage() {
|
|
106
106
|
const { page, q, category, sort } = await searchParams.get();
|
|
@@ -108,16 +108,21 @@ export default async function ProductsPage() {
|
|
|
108
108
|
}
|
|
109
109
|
```
|
|
110
110
|
|
|
111
|
-
`<Link>`
|
|
111
|
+
Pass them to `<Link>` by building them from the definition. `buildSearchParams` type-checks the values and applies the definition's codecs and URL-key aliases:
|
|
112
112
|
|
|
113
113
|
```tsx
|
|
114
|
+
import { searchParams } from './search-params';
|
|
115
|
+
|
|
114
116
|
<Link
|
|
115
117
|
href="/products/[id]"
|
|
116
118
|
segmentParams={{ id }}
|
|
117
|
-
searchParams={{ sort: 'newest', q: 'boots' }}
|
|
119
|
+
searchParams={searchParams.buildSearchParams({ sort: 'newest', q: 'boots' })}
|
|
118
120
|
/>
|
|
121
|
+
// → /products/42?sort=newest&search=boots (q is aliased to search)
|
|
119
122
|
```
|
|
120
123
|
|
|
124
|
+
A plain object also works as an untyped escape hatch for one-off params with no definition — but it skips codecs and aliases, so do not use it for keys a definition owns.
|
|
125
|
+
|
|
121
126
|
See [Typed Params](/docs/typed-params) for the full search params API.
|
|
122
127
|
|
|
123
128
|
## Codegen
|
|
@@ -73,6 +73,18 @@ Priority order (innermost to outermost):
|
|
|
73
73
|
3. `error.tsx` — catches anything unmatched
|
|
74
74
|
4. Parent segments → framework default
|
|
75
75
|
|
|
76
|
+
Component status files (`.tsx`, `.jsx`, `.ts`, `.js`) and `error.tsx` **must start with `'use client'`**:
|
|
77
|
+
|
|
78
|
+
```tsx title="app/404.tsx"
|
|
79
|
+
'use client';
|
|
80
|
+
|
|
81
|
+
export default function NotFound({ status }: { status: number }) {
|
|
82
|
+
return <h1>Page not found</h1>;
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
timber hands these components to its error boundary — a client component — as a prop, so a server component there cannot cross the RSC boundary. It is not a degraded error page: every page under that segment fails to render. The build catches it and names the file, so you will not discover it as a serialization error on a page you did not touch. MDX status files (`404.mdx`) are the exception — they are server components, rendered ahead of time.
|
|
87
|
+
|
|
76
88
|
If nothing in the chain matches, timber renders a built-in status page: a plain document with the status code and its reason phrase, and nothing else — no request URL, no error message. It is what a static build writes to `404.html` when your app has no not-found page, so a static deploy always ships one. Add a root `app/404.tsx` (or `4xx.tsx` / `error.tsx`) to replace it.
|
|
77
89
|
|
|
78
90
|
## `error.tsx`
|
|
@@ -101,6 +113,10 @@ export default function DashboardError({
|
|
|
101
113
|
|
|
102
114
|
Error boundaries wrap page content inside layouts — the layout shell (nav, sidebar) stays visible when an error fires.
|
|
103
115
|
|
|
116
|
+
A status page renders inside the layouts **above and including the segment it lives in**, and none below it. A root `404.tsx` serving a `deny(404)` from `app/docs/[slug]/page.tsx` renders inside the root layout only — `docs/layout.tsx` and `[slug]/layout.tsx` do not appear, because the file that matched lives above them. Co-locate the status file with the segment whose chrome you want kept: move that `404.tsx` to `app/docs/` and the docs layout comes with it.
|
|
117
|
+
|
|
118
|
+
One exception: **the segment that denied never contributes its own layout**, even when it owns the matched status file. If its `access.ts` denied, that layout sits behind the failed auth check, so running it would let it fetch and serialize data the request was not allowed to see. If the layout itself called `deny()`, it threw instead of returning its chrome, so there is nothing to render. A `deny()` from a page is unaffected — the page's own layout still renders.
|
|
119
|
+
|
|
104
120
|
When a `deny()` has no matching status-code file, it falls through to the nearest `error.tsx`. The component still receives a real `error` (its message is `Access denied with status <N>`), and additionally receives `status` and `dangerouslyPassData` so you can render denial-specific UI:
|
|
105
121
|
|
|
106
122
|
```tsx title="app/error.tsx"
|
|
@@ -67,7 +67,7 @@ This is useful for CDN-cacheable file-extension routes (e.g. image generation en
|
|
|
67
67
|
- One param per segment — `[foo]-[bar]` is not supported
|
|
68
68
|
- Affixes cannot be used with catch-all (`[...slug].png`) or optional catch-all segments
|
|
69
69
|
- Matching is greedy: `[id].png` matching `a.png.png` yields `id = "a.png"`
|
|
70
|
-
- `
|
|
70
|
+
- `app/schema.ts` codecs use the bare bracket form (`[id]`), not the full segment pattern
|
|
71
71
|
|
|
72
72
|
**Priority:** static → affixed dynamic → bare dynamic → catch-all → optional catch-all. A static `foo.png` always wins over `[id].png`.
|
|
73
73
|
|
|
@@ -64,9 +64,9 @@ app/
|
|
|
64
64
|
|
|
65
65
|
**Next.js:** `useSearchParams()` hook returns a `URLSearchParams` object.
|
|
66
66
|
|
|
67
|
-
**timber:** Typed search params via `defineSearchParams` in
|
|
67
|
+
**timber:** Typed search params via `defineSearchParams` in any module you import. Server reads via `.get()`, client reads/writes via `.useQueryStates()`. Segment params are defined globally in `app/schema.ts` via `defineSchema()`, accessed with `getSegmentParams(SEGMENT_PATH)` using the `$segment` module.
|
|
68
68
|
|
|
69
|
-
```ts title="app/products/params.ts"
|
|
69
|
+
```ts title="app/products/search-params.ts"
|
|
70
70
|
import { defineSearchParams } from '@timber-js/app/search-params';
|
|
71
71
|
import { z } from 'zod/v4';
|
|
72
72
|
|
|
@@ -59,7 +59,7 @@ These are the things you will get wrong if you assume Next.js behavior.
|
|
|
59
59
|
| Middleware | Single global `middleware.ts` with matchers | Per-segment `middleware.ts` files + global `proxy.ts`. One-arg signature: `middleware(ctx)`. |
|
|
60
60
|
| `notFound()` | `notFound()` from `next/navigation` | `deny(404)` from `@timber-js/app/server`. Sends a real HTTP 404. |
|
|
61
61
|
| `redirect()` | `redirect()` from `next/navigation` | `redirect()` from `@timber-js/app/server`. Same concept, different import. |
|
|
62
|
-
| Search params | Untyped `useSearchParams()` | Typed `defineSearchParams()` with Zod schemas in
|
|
62
|
+
| Search params | Untyped `useSearchParams()` | Typed `defineSearchParams()` with Zod schemas, in any module you import. |
|
|
63
63
|
| Auth pattern | In middleware or components | `access.ts` per segment with slot degradation. |
|
|
64
64
|
| Forms | `useFormState` / `useFormStatus` | `createActionClient` + `useActionState` returns a 4-tuple. Forms work without JS. |
|
|
65
65
|
|
|
@@ -71,7 +71,7 @@ These are the things you will get wrong if you assume Next.js behavior.
|
|
|
71
71
|
| `Link`, `useRouter`, `usePathname`, `useActionState` | `@timber-js/app/client` |
|
|
72
72
|
| `cache`, `CacheHandler` | `@timber-js/app/cache` |
|
|
73
73
|
| `defineSearchParams` | `@timber-js/app/search-params` |
|
|
74
|
-
| `defineSchema`, `
|
|
74
|
+
| `defineSchema`, `InferCodecType` | `@timber-js/app/params` |
|
|
75
75
|
| Content collection data | `content-collections` |
|
|
76
76
|
| `Metadata` type | `@timber-js/app/server` |
|
|
77
77
|
|
|
@@ -90,7 +90,7 @@ app/ # Route tree — file-system routing
|
|
|
90
90
|
access.ts # Per-segment authorization
|
|
91
91
|
denied.tsx # Rendered when access.ts denies
|
|
92
92
|
error.tsx # Error boundary for this segment
|
|
93
|
-
params.ts
|
|
93
|
+
search-params.ts # Search param definitions (ordinary module, not a convention)
|
|
94
94
|
schema.ts # Segment param schemas (from @timber-js/app/params)
|
|
95
95
|
content/ # Content collections (outside route tree)
|
|
96
96
|
timber.config.ts # Framework configuration
|
|
@@ -104,6 +104,8 @@ content-collections.ts # Collection definitions (optional)
|
|
|
104
104
|
|
|
105
105
|
2. **Adding `'use client'` to everything** — components are server components by default. Only add `'use client'` when you need browser APIs, event handlers, or React hooks like `useState`/`useEffect`.
|
|
106
106
|
|
|
107
|
+
The exception: `error.tsx`, `global-error.tsx`, and component status files (`404.tsx`, `4xx.tsx`, `503.tsx`, …) **always** need `'use client'`. timber passes them to a client error boundary as a prop, so a server component there breaks every page under that segment. The build rejects it and names the file. MDX status files (`404.mdx`) are exempt.
|
|
108
|
+
|
|
107
109
|
3. **Using `fetch()` and expecting caching** — timber never patches `fetch`. Wrap data-fetching functions in `cache.data()` from `@timber-js/app/cache` when you want caching.
|
|
108
110
|
|
|
109
111
|
4. **Creating `loading.tsx` files** — this convention does not exist. Use `<Suspense>` with an explicit fallback where you want streaming.
|
|
@@ -114,7 +116,7 @@ content-collections.ts # Collection definitions (optional)
|
|
|
114
116
|
|
|
115
117
|
7. **Putting all middleware in one file** — middleware is per-segment. Place `middleware.ts` at the route level that matches the concern. Use `proxy.ts` for global concerns.
|
|
116
118
|
|
|
117
|
-
8. **Assuming `useSearchParams()` exists** — use `defineSearchParams()` in
|
|
119
|
+
8. **Assuming `useSearchParams()` exists** — use `defineSearchParams()` in any module with Zod schemas, then access via `.get()` (server) or `.useQueryStates()` (client).
|
|
118
120
|
|
|
119
121
|
## Quick Patterns
|
|
120
122
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@timber-js/app",
|
|
3
|
-
"version": "0.2.0-alpha.
|
|
3
|
+
"version": "0.2.0-alpha.188",
|
|
4
4
|
"description": "Vite-native React framework built for Servers and Serverless Platforms — correct HTTP semantics, real status codes, pages that work without JavaScript",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudflare-workers",
|
|
@@ -113,10 +113,6 @@
|
|
|
113
113
|
"types": "./dist/search-params/index.d.ts",
|
|
114
114
|
"import": "./dist/search-params/index.js"
|
|
115
115
|
},
|
|
116
|
-
"./segment-params": {
|
|
117
|
-
"types": "./dist/segment-params/index.d.ts",
|
|
118
|
-
"import": "./dist/segment-params/index.js"
|
|
119
|
-
},
|
|
120
116
|
"./params": {
|
|
121
117
|
"types": "./dist/params/index.d.ts",
|
|
122
118
|
"import": "./dist/params/index.js"
|
|
@@ -165,6 +161,7 @@
|
|
|
165
161
|
"@content-collections/mdx": "^0.2.0",
|
|
166
162
|
"@content-collections/vite": "^0.2.0 || ^0.3.0",
|
|
167
163
|
"@mdx-js/rollup": "^3.0.0",
|
|
164
|
+
"@typescript/native-preview": "^7.0.0-dev.0",
|
|
168
165
|
"@vitejs/plugin-react": "^6.0.0",
|
|
169
166
|
"@vitejs/plugin-rsc": ">=0.5.28",
|
|
170
167
|
"nuqs": "^2.0.0",
|
|
@@ -187,6 +184,9 @@
|
|
|
187
184
|
"@mdx-js/rollup": {
|
|
188
185
|
"optional": true
|
|
189
186
|
},
|
|
187
|
+
"@typescript/native-preview": {
|
|
188
|
+
"optional": true
|
|
189
|
+
},
|
|
190
190
|
"wrangler": {
|
|
191
191
|
"optional": true
|
|
192
192
|
},
|
|
@@ -194,6 +194,9 @@
|
|
|
194
194
|
"optional": true
|
|
195
195
|
}
|
|
196
196
|
},
|
|
197
|
+
"devDependencies": {
|
|
198
|
+
"@typescript/native-preview": "7.0.0-dev.20260707.2"
|
|
199
|
+
},
|
|
197
200
|
"engines": {
|
|
198
201
|
"node": ">=22.18.0"
|
|
199
202
|
},
|
|
@@ -20,6 +20,9 @@ import { adapterBase, generateHeadersFile } from './shared.js';
|
|
|
20
20
|
// native ESM loader evaluates before Vite's resolver exists. Same precedent as
|
|
21
21
|
// `adapters/shared.ts` importing `../shared/base-path.js`.
|
|
22
22
|
import { RSC_KEY_HEADERS } from '../shared/rsc-cache-key.js';
|
|
23
|
+
// Same reasoning: the media type the origin's own RSC gate matches on, so the
|
|
24
|
+
// two predicates cannot drift apart. See `shared/rsc-media-type.ts`.
|
|
25
|
+
import { RSC_CONTENT_TYPE } from '../shared/rsc-media-type.js';
|
|
23
26
|
|
|
24
27
|
// ─── Bindings passthrough ─────────────────────────────────────────────────
|
|
25
28
|
// ALS stores the env object per-request so server components and middleware
|
|
@@ -486,7 +489,7 @@ ${handlersSpread} async fetch(request, env, ctx) {
|
|
|
486
489
|
// This exclusion is load-bearing in the other direction too: the store
|
|
487
490
|
// predicate below treats "Vary: Accept" as honoured *because* of it
|
|
488
491
|
// (see VARY_HONORED). Removing it would make that allowlist unsound.
|
|
489
|
-
const isRsc = request.headers.get('Accept')?.includes(
|
|
492
|
+
const isRsc = request.headers.get('Accept')?.includes(${JSON.stringify(RSC_CONTENT_TYPE)})
|
|
490
493
|
const reqCc = request.headers.get('Cache-Control') || ''
|
|
491
494
|
const bypassCache = reqCc.includes('no-cache') || reqCc.includes('max-age=0')
|
|
492
495
|
|| request.headers.has('Pragma')
|
|
@@ -7,12 +7,90 @@
|
|
|
7
7
|
// hand-written string template, any fix to server/compress.ts automatically
|
|
8
8
|
// flows to the production module. See design/18-build-system.md ("entry
|
|
9
9
|
// modules are real TypeScript files, not codegen strings").
|
|
10
|
+
//
|
|
11
|
+
// The transform strips types; it does NOT bundle. The emitted file sits
|
|
12
|
+
// alone in the build output, so any import specifier that survives into it
|
|
13
|
+
// is a module resolution failure at runtime on the deployed server. Shared
|
|
14
|
+
// constants are therefore interpolated here — the same treatment the
|
|
15
|
+
// Cloudflare worker template gives `RSC_KEY_HEADERS` — and anything else
|
|
16
|
+
// that reaches the output as an import is rejected with a build error
|
|
17
|
+
// rather than shipped.
|
|
10
18
|
|
|
11
19
|
import { transformWithOxc } from 'vite';
|
|
12
20
|
// Vite's ?raw import embeds the file as a string at build time — works
|
|
13
21
|
// in both dev (Vite resolves the TS source) and the published package
|
|
14
22
|
// (the string is inlined into the compiled adapter entry).
|
|
15
23
|
import compressSource from '../server/compress.ts?raw';
|
|
24
|
+
import { RSC_CONTENT_TYPE } from '../shared/rsc-media-type.js';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The one import `server/compress.ts` is allowed to carry, and its inlined
|
|
28
|
+
* form.
|
|
29
|
+
*
|
|
30
|
+
* The binding list is matched exactly rather than as `{…}`. A second binding
|
|
31
|
+
* added to that import would otherwise be erased along with the statement and
|
|
32
|
+
* become a `ReferenceError` in the deployed `_compress.mjs` — the failure this
|
|
33
|
+
* whole module exists to make impossible. Matching exactly means such an
|
|
34
|
+
* import falls through to the rejection below instead.
|
|
35
|
+
*/
|
|
36
|
+
const INLINED_IMPORTS: ReadonlyArray<{ pattern: RegExp; replacement: string }> = [
|
|
37
|
+
{
|
|
38
|
+
pattern:
|
|
39
|
+
/^import\s*\{\s*RSC_CONTENT_TYPE\s*,?\s*\}\s*from\s*["'][^"']*shared\/rsc-media-type\.js["'];?[ \t]*$/m,
|
|
40
|
+
replacement: `const RSC_CONTENT_TYPE = ${JSON.stringify(RSC_CONTENT_TYPE)};`,
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Every way a module specifier can reach the emitted file, minus Node
|
|
46
|
+
* builtins.
|
|
47
|
+
*
|
|
48
|
+
* `node:*` is exempt because it resolves on the deployed server exactly as it
|
|
49
|
+
* does here — `_compress.mjs` already imports `node:zlib` and `node:stream`,
|
|
50
|
+
* and self-hosted (Node/Bun) is the only place this file runs. Everything
|
|
51
|
+
* else — relative paths, bare packages — has no resolution target next to a
|
|
52
|
+
* lone `.mjs` in the build output.
|
|
53
|
+
*
|
|
54
|
+
* All three forms are listed rather than the one `server/compress.ts` happens
|
|
55
|
+
* to use, because a re-export or a lazy `await import()` fails in exactly the
|
|
56
|
+
* same place and is exactly as invisible until deploy.
|
|
57
|
+
*/
|
|
58
|
+
const MODULE_SPECIFIERS: readonly RegExp[] = [
|
|
59
|
+
// import … from '…' / export … from '…'
|
|
60
|
+
/^[ \t]*(?:import|export)\b[^\n]*\bfrom\s*["'](?!node:)[^"']+["']/m,
|
|
61
|
+
// import '…' (side effect only)
|
|
62
|
+
/^[ \t]*import\s*["'](?!node:)[^"']+["']/m,
|
|
63
|
+
// import('…') (dynamic, literal specifier)
|
|
64
|
+
/\bimport\s*\(\s*["'](?!node:)[^"']+["']/,
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Replace the shared-module imports with their values and reject anything
|
|
69
|
+
* else that would leave a dangling specifier in `_compress.mjs`.
|
|
70
|
+
*
|
|
71
|
+
* Exported separately from `generateCompressModule` so both branches can be
|
|
72
|
+
* driven with fixtures: the source it normally compiles is fixed, so a test
|
|
73
|
+
* of the generator alone can only ever exercise whichever branch that source
|
|
74
|
+
* happens to take today.
|
|
75
|
+
*
|
|
76
|
+
* @internal Exported for testing.
|
|
77
|
+
*/
|
|
78
|
+
export function inlineCompressImports(code: string): string {
|
|
79
|
+
let out = code;
|
|
80
|
+
for (const { pattern, replacement } of INLINED_IMPORTS) {
|
|
81
|
+
out = out.replace(pattern, replacement);
|
|
82
|
+
}
|
|
83
|
+
const surviving = MODULE_SPECIFIERS.map((pattern) => pattern.exec(out)).find(Boolean);
|
|
84
|
+
if (surviving) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`[timber] server/compress.ts is emitted as a standalone _compress.mjs (type-stripped, ` +
|
|
87
|
+
`not bundled), so it cannot import anything at runtime. Found: ${surviving[0].trim()}\n` +
|
|
88
|
+
` Either inline the value in adapters/compress-module.ts (see INLINED_IMPORTS) or ` +
|
|
89
|
+
`keep the dependency out of server/compress.ts.`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
16
94
|
|
|
17
95
|
/**
|
|
18
96
|
* Compile server/compress.ts into a standalone ESM module.
|
|
@@ -31,6 +109,6 @@ export async function generateCompressModule(): Promise<string> {
|
|
|
31
109
|
return (
|
|
32
110
|
'// Generated by @timber-js/app — compiled from server/compress.ts.\n' +
|
|
33
111
|
'// Do not edit — this file is regenerated on each build.\n' +
|
|
34
|
-
result.code
|
|
112
|
+
inlineCompressImports(result.code)
|
|
35
113
|
);
|
|
36
114
|
}
|