@timber-js/app 0.2.0-alpha.4 → 0.2.0-alpha.41
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/LICENSE +8 -0
- package/dist/_chunks/{als-registry-B7DbZ2hS.js → als-registry-Ba7URUIn.js} +1 -1
- package/dist/_chunks/als-registry-Ba7URUIn.js.map +1 -0
- package/dist/_chunks/chunk-DYhsFzuS.js +33 -0
- package/dist/_chunks/debug-ECi_61pb.js +108 -0
- package/dist/_chunks/debug-ECi_61pb.js.map +1 -0
- package/dist/_chunks/define-cookie-BmKbSyp0.js +93 -0
- package/dist/_chunks/define-cookie-BmKbSyp0.js.map +1 -0
- package/dist/_chunks/error-boundary-BAN3751q.js +211 -0
- package/dist/_chunks/error-boundary-BAN3751q.js.map +1 -0
- package/dist/_chunks/{format-CwdaB0_2.js → format-cX7wzEp2.js} +2 -2
- package/dist/_chunks/{format-CwdaB0_2.js.map → format-cX7wzEp2.js.map} +1 -1
- package/dist/_chunks/{interception-BOoWmLUA.js → interception-D2djYaIm.js} +112 -77
- package/dist/_chunks/interception-D2djYaIm.js.map +1 -0
- package/dist/_chunks/{metadata-routes-Cjmvi3rQ.js → metadata-routes-BU684ls2.js} +1 -1
- package/dist/_chunks/{metadata-routes-Cjmvi3rQ.js.map → metadata-routes-BU684ls2.js.map} +1 -1
- package/dist/_chunks/{request-context-CZJi4CuK.js → request-context-BxYIJM24.js} +93 -69
- package/dist/_chunks/request-context-BxYIJM24.js.map +1 -0
- package/dist/_chunks/segment-context-C6byCyZU.js +69 -0
- package/dist/_chunks/segment-context-C6byCyZU.js.map +1 -0
- package/dist/_chunks/stale-reload-C0ValzG7.js +47 -0
- package/dist/_chunks/stale-reload-C0ValzG7.js.map +1 -0
- package/dist/_chunks/{tracing-Cwn7697K.js → tracing-CuXiCP5p.js} +17 -3
- package/dist/_chunks/{tracing-Cwn7697K.js.map → tracing-CuXiCP5p.js.map} +1 -1
- package/dist/_chunks/{use-query-states-D5KaffOK.js → use-query-states-BvW0TKDn.js} +1 -1
- package/dist/_chunks/{use-query-states-D5KaffOK.js.map → use-query-states-BvW0TKDn.js.map} +1 -1
- package/dist/_chunks/wrappers-C6J0nNji.js +331 -0
- package/dist/_chunks/wrappers-C6J0nNji.js.map +1 -0
- package/dist/adapters/compress-module.d.ts.map +1 -1
- package/dist/adapters/nitro.d.ts +17 -1
- package/dist/adapters/nitro.d.ts.map +1 -1
- package/dist/adapters/nitro.js +56 -13
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/cache/fast-hash.d.ts +22 -0
- package/dist/cache/fast-hash.d.ts.map +1 -0
- package/dist/cache/index.d.ts +5 -2
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +88 -18
- package/dist/cache/index.js.map +1 -1
- package/dist/cache/register-cached-function.d.ts.map +1 -1
- package/dist/cache/singleflight.d.ts +18 -1
- package/dist/cache/singleflight.d.ts.map +1 -1
- package/dist/cache/timber-cache.d.ts.map +1 -1
- package/dist/client/error-boundary.d.ts +10 -1
- package/dist/client/error-boundary.d.ts.map +1 -1
- package/dist/client/error-boundary.js +1 -125
- package/dist/client/index.d.ts +3 -2
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +213 -93
- package/dist/client/index.js.map +1 -1
- package/dist/client/link.d.ts +22 -8
- package/dist/client/link.d.ts.map +1 -1
- package/dist/client/navigation-context.d.ts +2 -2
- package/dist/client/router.d.ts +25 -3
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/rsc-fetch.d.ts +23 -2
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/client/segment-cache.d.ts +1 -1
- package/dist/client/segment-cache.d.ts.map +1 -1
- package/dist/client/segment-context.d.ts +1 -1
- package/dist/client/segment-context.d.ts.map +1 -1
- package/dist/client/segment-merger.d.ts.map +1 -1
- package/dist/client/stale-reload.d.ts +15 -0
- package/dist/client/stale-reload.d.ts.map +1 -1
- package/dist/client/top-loader.d.ts +1 -1
- package/dist/client/top-loader.d.ts.map +1 -1
- package/dist/client/transition-root.d.ts +1 -1
- package/dist/client/transition-root.d.ts.map +1 -1
- package/dist/client/use-params.d.ts +2 -2
- package/dist/client/use-params.d.ts.map +1 -1
- package/dist/client/use-query-states.d.ts +1 -1
- package/dist/codec.d.ts +21 -0
- package/dist/codec.d.ts.map +1 -0
- package/dist/cookies/define-cookie.d.ts +33 -12
- package/dist/cookies/define-cookie.d.ts.map +1 -1
- package/dist/cookies/index.js +1 -83
- package/dist/fonts/css.d.ts +1 -0
- package/dist/fonts/css.d.ts.map +1 -1
- package/dist/fonts/local.d.ts +4 -2
- package/dist/fonts/local.d.ts.map +1 -1
- package/dist/index.d.ts +112 -35
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +635 -233
- package/dist/index.js.map +1 -1
- package/dist/params/define.d.ts +76 -0
- package/dist/params/define.d.ts.map +1 -0
- package/dist/params/index.d.ts +8 -0
- package/dist/params/index.d.ts.map +1 -0
- package/dist/params/index.js +104 -0
- package/dist/params/index.js.map +1 -0
- package/dist/plugins/adapter-build.d.ts.map +1 -1
- package/dist/plugins/build-manifest.d.ts.map +1 -1
- package/dist/plugins/client-chunks.d.ts +32 -0
- package/dist/plugins/client-chunks.d.ts.map +1 -0
- package/dist/plugins/dev-error-overlay.d.ts +26 -1
- package/dist/plugins/dev-error-overlay.d.ts.map +1 -1
- package/dist/plugins/entries.d.ts +7 -0
- package/dist/plugins/entries.d.ts.map +1 -1
- package/dist/plugins/fonts.d.ts +9 -1
- package/dist/plugins/fonts.d.ts.map +1 -1
- package/dist/plugins/mdx.d.ts +6 -0
- package/dist/plugins/mdx.d.ts.map +1 -1
- package/dist/plugins/routing.d.ts.map +1 -1
- package/dist/plugins/server-bundle.d.ts.map +1 -1
- package/dist/plugins/static-build.d.ts.map +1 -1
- package/dist/routing/codegen.d.ts +2 -2
- package/dist/routing/codegen.d.ts.map +1 -1
- package/dist/routing/index.js +1 -1
- package/dist/routing/scanner.d.ts.map +1 -1
- package/dist/routing/status-file-lint.d.ts +2 -1
- package/dist/routing/status-file-lint.d.ts.map +1 -1
- package/dist/routing/types.d.ts +6 -4
- package/dist/routing/types.d.ts.map +1 -1
- package/dist/rsc-runtime/rsc.d.ts +1 -1
- package/dist/rsc-runtime/rsc.d.ts.map +1 -1
- package/dist/rsc-runtime/ssr.d.ts +12 -0
- package/dist/rsc-runtime/ssr.d.ts.map +1 -1
- package/dist/search-params/codecs.d.ts +1 -1
- package/dist/search-params/define.d.ts +153 -0
- package/dist/search-params/define.d.ts.map +1 -0
- package/dist/search-params/index.d.ts +4 -5
- package/dist/search-params/index.d.ts.map +1 -1
- package/dist/search-params/index.js +3 -474
- package/dist/search-params/registry.d.ts +1 -1
- package/dist/search-params/wrappers.d.ts +53 -0
- package/dist/search-params/wrappers.d.ts.map +1 -0
- package/dist/server/access-gate.d.ts +4 -0
- package/dist/server/access-gate.d.ts.map +1 -1
- package/dist/server/action-client.d.ts.map +1 -1
- package/dist/server/action-encryption.d.ts +76 -0
- package/dist/server/action-encryption.d.ts.map +1 -0
- package/dist/server/action-handler.d.ts.map +1 -1
- package/dist/server/als-registry.d.ts +18 -4
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/build-manifest.d.ts +2 -2
- package/dist/server/debug.d.ts +46 -15
- package/dist/server/debug.d.ts.map +1 -1
- package/dist/server/default-logger.d.ts +22 -0
- package/dist/server/default-logger.d.ts.map +1 -0
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/early-hints.d.ts +13 -5
- package/dist/server/early-hints.d.ts.map +1 -1
- package/dist/server/error-boundary-wrapper.d.ts +4 -0
- package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
- package/dist/server/flight-injection-state.d.ts +78 -0
- package/dist/server/flight-injection-state.d.ts.map +1 -0
- package/dist/server/flight-scripts.d.ts +39 -0
- package/dist/server/flight-scripts.d.ts.map +1 -0
- package/dist/server/flush.d.ts.map +1 -1
- package/dist/server/form-data.d.ts +29 -0
- package/dist/server/form-data.d.ts.map +1 -1
- package/dist/server/html-injectors.d.ts +5 -11
- package/dist/server/html-injectors.d.ts.map +1 -1
- package/dist/server/index.d.ts +4 -2
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1975 -1649
- package/dist/server/index.js.map +1 -1
- package/dist/server/logger.d.ts +24 -7
- package/dist/server/logger.d.ts.map +1 -1
- package/dist/server/node-stream-transforms.d.ts +77 -0
- package/dist/server/node-stream-transforms.d.ts.map +1 -0
- package/dist/server/pipeline.d.ts +7 -4
- package/dist/server/pipeline.d.ts.map +1 -1
- package/dist/server/primitives.d.ts +30 -3
- package/dist/server/primitives.d.ts.map +1 -1
- package/dist/server/render-timeout.d.ts +51 -0
- package/dist/server/render-timeout.d.ts.map +1 -0
- package/dist/server/request-context.d.ts +65 -38
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/route-element-builder.d.ts +7 -0
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/dist/server/route-handler.d.ts.map +1 -1
- package/dist/server/route-matcher.d.ts +2 -2
- package/dist/server/route-matcher.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 +46 -3
- package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts +6 -1
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-stream.d.ts +9 -0
- package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
- package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
- package/dist/server/slot-resolver.d.ts +1 -1
- package/dist/server/slot-resolver.d.ts.map +1 -1
- package/dist/server/ssr-entry.d.ts +22 -0
- package/dist/server/ssr-entry.d.ts.map +1 -1
- package/dist/server/ssr-render.d.ts +39 -21
- package/dist/server/ssr-render.d.ts.map +1 -1
- package/dist/server/tracing.d.ts +10 -0
- package/dist/server/tracing.d.ts.map +1 -1
- package/dist/server/tree-builder.d.ts +19 -12
- package/dist/server/tree-builder.d.ts.map +1 -1
- package/dist/server/types.d.ts +1 -3
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/version-skew.d.ts +61 -0
- package/dist/server/version-skew.d.ts.map +1 -0
- package/dist/server/waituntil-bridge.d.ts.map +1 -1
- package/dist/shared/merge-search-params.d.ts +22 -0
- package/dist/shared/merge-search-params.d.ts.map +1 -0
- package/dist/shims/navigation-client.d.ts +1 -1
- package/dist/shims/navigation-client.d.ts.map +1 -1
- package/dist/shims/navigation.d.ts +1 -1
- package/dist/shims/navigation.d.ts.map +1 -1
- package/dist/utils/state-machine.d.ts +80 -0
- package/dist/utils/state-machine.d.ts.map +1 -0
- package/package.json +17 -14
- package/src/adapters/compress-module.ts +24 -4
- package/src/adapters/nitro.ts +58 -9
- package/src/cache/fast-hash.ts +34 -0
- package/src/cache/index.ts +5 -2
- package/src/cache/register-cached-function.ts +7 -3
- package/src/cache/singleflight.ts +62 -4
- package/src/cache/timber-cache.ts +34 -26
- package/src/cli.ts +0 -0
- package/src/client/browser-entry.ts +94 -90
- package/src/client/error-boundary.tsx +18 -1
- package/src/client/index.ts +10 -1
- package/src/client/link.tsx +78 -19
- package/src/client/navigation-context.ts +2 -2
- package/src/client/router.ts +105 -60
- package/src/client/rsc-fetch.ts +63 -2
- package/src/client/segment-cache.ts +1 -1
- package/src/client/segment-context.ts +6 -1
- package/src/client/segment-merger.ts +2 -8
- package/src/client/stale-reload.ts +32 -6
- package/src/client/top-loader.tsx +10 -9
- package/src/client/transition-root.tsx +7 -1
- package/src/client/use-params.ts +3 -3
- package/src/client/use-query-states.ts +1 -1
- package/src/codec.ts +21 -0
- package/src/cookies/define-cookie.ts +69 -18
- package/src/fonts/css.ts +2 -1
- package/src/fonts/local.ts +7 -3
- package/src/index.ts +280 -85
- package/src/params/define.ts +260 -0
- package/src/params/index.ts +28 -0
- package/src/plugins/adapter-build.ts +6 -0
- package/src/plugins/build-manifest.ts +11 -0
- package/src/plugins/client-chunks.ts +65 -0
- package/src/plugins/dev-error-overlay.ts +70 -1
- package/src/plugins/dev-server.ts +38 -4
- package/src/plugins/entries.ts +12 -11
- package/src/plugins/fonts.ts +171 -19
- package/src/plugins/mdx.ts +9 -5
- package/src/plugins/routing.ts +40 -14
- package/src/plugins/server-bundle.ts +32 -1
- package/src/plugins/shims.ts +1 -1
- package/src/plugins/static-build.ts +8 -4
- package/src/routing/codegen.ts +109 -88
- package/src/routing/scanner.ts +55 -6
- package/src/routing/status-file-lint.ts +2 -1
- package/src/routing/types.ts +7 -4
- package/src/rsc-runtime/rsc.ts +2 -0
- package/src/rsc-runtime/ssr.ts +50 -0
- package/src/rsc-runtime/vendor-types.d.ts +7 -0
- package/src/search-params/codecs.ts +1 -1
- package/src/search-params/define.ts +504 -0
- package/src/search-params/index.ts +12 -18
- package/src/search-params/registry.ts +1 -1
- package/src/search-params/wrappers.ts +85 -0
- package/src/server/access-gate.tsx +40 -9
- package/src/server/action-client.ts +14 -5
- package/src/server/action-encryption.ts +144 -0
- package/src/server/action-handler.ts +19 -2
- package/src/server/als-registry.ts +18 -4
- package/src/server/build-manifest.ts +4 -4
- package/src/server/compress.ts +25 -7
- package/src/server/debug.ts +55 -17
- package/src/server/default-logger.ts +98 -0
- package/src/server/deny-renderer.ts +2 -1
- package/src/server/early-hints.ts +36 -15
- package/src/server/error-boundary-wrapper.ts +57 -14
- package/src/server/flight-injection-state.ts +152 -0
- package/src/server/flight-scripts.ts +59 -0
- package/src/server/flush.ts +2 -1
- package/src/server/form-data.ts +76 -0
- package/src/server/html-injectors.ts +103 -66
- package/src/server/index.ts +9 -4
- package/src/server/logger.ts +38 -35
- package/src/server/node-stream-transforms.ts +381 -0
- package/src/server/pipeline.ts +131 -39
- package/src/server/primitives.ts +47 -5
- package/src/server/render-timeout.ts +108 -0
- package/src/server/request-context.ts +112 -119
- package/src/server/route-element-builder.ts +106 -114
- package/src/server/route-handler.ts +2 -1
- package/src/server/route-matcher.ts +2 -2
- package/src/server/rsc-entry/error-renderer.ts +5 -3
- package/src/server/rsc-entry/helpers.ts +122 -3
- package/src/server/rsc-entry/index.ts +125 -49
- package/src/server/rsc-entry/rsc-payload.ts +52 -12
- package/src/server/rsc-entry/rsc-stream.ts +33 -8
- package/src/server/rsc-entry/ssr-renderer.ts +40 -13
- package/src/server/slot-resolver.ts +199 -210
- package/src/server/ssr-entry.ts +168 -22
- package/src/server/ssr-render.ts +289 -67
- package/src/server/tracing.ts +23 -0
- package/src/server/tree-builder.ts +91 -57
- package/src/server/types.ts +1 -3
- package/src/server/version-skew.ts +104 -0
- package/src/server/waituntil-bridge.ts +4 -1
- package/src/shared/merge-search-params.ts +48 -0
- package/src/shims/navigation-client.ts +1 -1
- package/src/shims/navigation.ts +1 -1
- package/src/utils/state-machine.ts +111 -0
- package/dist/_chunks/als-registry-B7DbZ2hS.js.map +0 -1
- package/dist/_chunks/debug-B4WUeqJ-.js +0 -75
- package/dist/_chunks/debug-B4WUeqJ-.js.map +0 -1
- package/dist/_chunks/interception-BOoWmLUA.js.map +0 -1
- package/dist/_chunks/request-context-CZJi4CuK.js.map +0 -1
- package/dist/_chunks/ssr-data-MjmprTmO.js +0 -88
- package/dist/_chunks/ssr-data-MjmprTmO.js.map +0 -1
- package/dist/_chunks/use-cookie-DX-l1_5E.js +0 -91
- package/dist/_chunks/use-cookie-DX-l1_5E.js.map +0 -1
- package/dist/client/error-boundary.js.map +0 -1
- package/dist/cookies/index.js.map +0 -1
- package/dist/plugins/dynamic-transform.d.ts +0 -72
- package/dist/plugins/dynamic-transform.d.ts.map +0 -1
- package/dist/search-params/analyze.d.ts +0 -54
- package/dist/search-params/analyze.d.ts.map +0 -1
- package/dist/search-params/builtin-codecs.d.ts +0 -105
- package/dist/search-params/builtin-codecs.d.ts.map +0 -1
- package/dist/search-params/create.d.ts +0 -106
- package/dist/search-params/create.d.ts.map +0 -1
- package/dist/search-params/index.js.map +0 -1
- package/dist/server/prerender.d.ts +0 -77
- package/dist/server/prerender.d.ts.map +0 -1
- package/dist/server/response-cache.d.ts +0 -53
- package/dist/server/response-cache.d.ts.map +0 -1
- package/src/plugins/dynamic-transform.ts +0 -161
- package/src/search-params/analyze.ts +0 -192
- package/src/search-params/builtin-codecs.ts +0 -228
- package/src/search-params/create.ts +0 -321
- package/src/server/prerender.ts +0 -139
- package/src/server/response-cache.ts +0 -277
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { n as useQueryStates } from "./use-query-states-BvW0TKDn.js";
|
|
2
|
+
//#region src/search-params/codecs.ts
|
|
3
|
+
/**
|
|
4
|
+
* Zod v4's ~standard.validate() signature includes Promise in the return union
|
|
5
|
+
* to satisfy the Standard Schema spec, but in practice Zod always validates
|
|
6
|
+
* synchronously for the schema types we use. We assert the result is sync and
|
|
7
|
+
* throw if it isn't — search params parsing must be synchronous.
|
|
8
|
+
*/
|
|
9
|
+
function validateSync(schema, value) {
|
|
10
|
+
const result = schema["~standard"].validate(value);
|
|
11
|
+
if (result instanceof Promise) throw new Error("[timber] fromSchema: schema returned a Promise — only sync schemas are supported for search params.");
|
|
12
|
+
return result;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Bridge a Standard Schema-compatible schema (Zod, Valibot, ArkType) to a
|
|
16
|
+
* SearchParamCodec.
|
|
17
|
+
*
|
|
18
|
+
* Parse: coerces the raw URL string through the schema. On validation failure,
|
|
19
|
+
* parses `undefined` to get the schema's default value (the schema should have
|
|
20
|
+
* a `.default()` call). If that also fails, returns `undefined`.
|
|
21
|
+
*
|
|
22
|
+
* Serialize: uses `String()` for primitives, `null` for null/undefined.
|
|
23
|
+
*
|
|
24
|
+
* ```ts
|
|
25
|
+
* import { fromSchema } from '@timber-js/app/search-params'
|
|
26
|
+
* import { z } from 'zod/v4'
|
|
27
|
+
*
|
|
28
|
+
* const pageCodec = fromSchema(z.coerce.number().int().min(1).default(1))
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
function fromSchema(schema) {
|
|
32
|
+
return {
|
|
33
|
+
parse(value) {
|
|
34
|
+
const result = validateSync(schema, Array.isArray(value) ? value[value.length - 1] : value);
|
|
35
|
+
if (!result.issues) return result.value;
|
|
36
|
+
const defaultResult = validateSync(schema, void 0);
|
|
37
|
+
if (!defaultResult.issues) return defaultResult.value;
|
|
38
|
+
},
|
|
39
|
+
serialize(value) {
|
|
40
|
+
if (value === null || value === void 0) return null;
|
|
41
|
+
return String(value);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Bridge a Standard Schema for array values. Handles both single strings
|
|
47
|
+
* and repeated query keys (`?tag=a&tag=b`).
|
|
48
|
+
*
|
|
49
|
+
* ```ts
|
|
50
|
+
* import { fromArraySchema } from '@timber-js/app/search-params'
|
|
51
|
+
* import { z } from 'zod/v4'
|
|
52
|
+
*
|
|
53
|
+
* const tagsCodec = fromArraySchema(z.array(z.string()).default([]))
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
function fromArraySchema(schema) {
|
|
57
|
+
return {
|
|
58
|
+
parse(value) {
|
|
59
|
+
let input = value;
|
|
60
|
+
if (typeof value === "string") input = [value];
|
|
61
|
+
else if (value === void 0) input = void 0;
|
|
62
|
+
const result = validateSync(schema, input);
|
|
63
|
+
if (!result.issues) return result.value;
|
|
64
|
+
const defaultResult = validateSync(schema, void 0);
|
|
65
|
+
if (!defaultResult.issues) return defaultResult.value;
|
|
66
|
+
},
|
|
67
|
+
serialize(value) {
|
|
68
|
+
if (value === null || value === void 0) return null;
|
|
69
|
+
if (Array.isArray(value)) return value.length === 0 ? null : value.join(",");
|
|
70
|
+
return String(value);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
//#endregion
|
|
75
|
+
//#region src/search-params/define.ts
|
|
76
|
+
/**
|
|
77
|
+
* defineSearchParams — factory for SearchParamsDefinition<T>.
|
|
78
|
+
*
|
|
79
|
+
* Creates a typed, composable definition for a route's search parameters.
|
|
80
|
+
* Accepts both SearchParamCodec values and Standard Schema objects (Zod,
|
|
81
|
+
* Valibot, ArkType) with auto-detection. Supports URL key aliasing via
|
|
82
|
+
* withUrlKey(), default-omission serialization, and composition via
|
|
83
|
+
* .extend() / .pick().
|
|
84
|
+
*
|
|
85
|
+
* Design doc: design/23-search-params.md §"defineSearchParams — The Factory"
|
|
86
|
+
*/
|
|
87
|
+
var _rawSearchParams;
|
|
88
|
+
async function getRawSearchParams() {
|
|
89
|
+
if (!_rawSearchParams) _rawSearchParams = (await import("./request-context-BxYIJM24.js").then((n) => n.l)).rawSearchParams;
|
|
90
|
+
return _rawSearchParams();
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Convert URLSearchParams or a plain record to a normalized record
|
|
94
|
+
* where repeated keys produce arrays.
|
|
95
|
+
*/
|
|
96
|
+
function normalizeRaw(raw) {
|
|
97
|
+
if (raw instanceof URLSearchParams) {
|
|
98
|
+
const result = {};
|
|
99
|
+
for (const key of new Set(raw.keys())) {
|
|
100
|
+
const values = raw.getAll(key);
|
|
101
|
+
result[key] = values.length === 1 ? values[0] : values;
|
|
102
|
+
}
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
return raw;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Compute the serialized default value for a codec. Used for
|
|
109
|
+
* default-omission: when serialize(value) === serialize(parse(undefined)),
|
|
110
|
+
* the field is omitted from the URL.
|
|
111
|
+
*/
|
|
112
|
+
function getDefaultSerialized(codec) {
|
|
113
|
+
return codec.serialize(codec.parse(void 0));
|
|
114
|
+
}
|
|
115
|
+
/** Check if a value is a Standard Schema object. */
|
|
116
|
+
function isStandardSchema(value) {
|
|
117
|
+
return typeof value === "object" && value !== null && "~standard" in value && typeof value["~standard"]?.validate === "function";
|
|
118
|
+
}
|
|
119
|
+
/** Check if a value is a SearchParamCodec. */
|
|
120
|
+
function isCodec(value) {
|
|
121
|
+
return typeof value === "object" && value !== null && typeof value.parse === "function" && typeof value.serialize === "function";
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Resolve a field value to a SearchParamCodec. Auto-detects Standard Schema
|
|
125
|
+
* objects and wraps them with fromSchema. Reads .urlKey from codecs.
|
|
126
|
+
*/
|
|
127
|
+
function resolveField(fieldName, value) {
|
|
128
|
+
if (isCodec(value)) return {
|
|
129
|
+
codec: value,
|
|
130
|
+
urlKey: value.urlKey
|
|
131
|
+
};
|
|
132
|
+
if (isStandardSchema(value)) return { codec: fromSchema(value) };
|
|
133
|
+
throw new Error(`[timber] defineSearchParams: field '${fieldName}' is not a valid codec or Standard Schema. Expected an object with { parse, serialize } methods, or a Standard Schema object (Zod, Valibot, ArkType).`);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Validate that all codecs handle absent params (parse(undefined) doesn't throw).
|
|
137
|
+
* Catches schemas that throw on missing input. `undefined` and `null` are both
|
|
138
|
+
* valid defaults — `undefined` is correct for optional fields (e.g., `z.string().optional()`).
|
|
139
|
+
*/
|
|
140
|
+
function validateDefaults(codecMap) {
|
|
141
|
+
for (const [key, codec] of Object.entries(codecMap)) try {
|
|
142
|
+
codec.parse(void 0);
|
|
143
|
+
} catch {
|
|
144
|
+
throw new Error(`[timber] defineSearchParams: field '${key}' throws when the param is absent.\n Search params are optional — the URL might not contain ?${key}=anything.\n Add .default() or .optional() to your schema, or wrap with withDefault().`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Create a SearchParamsDefinition from a map of codecs and/or Standard Schema
|
|
149
|
+
* objects. Accepts both SearchParamCodec values and raw Zod/Valibot/ArkType
|
|
150
|
+
* schemas with auto-detection.
|
|
151
|
+
*
|
|
152
|
+
* ```ts
|
|
153
|
+
* import { defineSearchParams, withDefault, withUrlKey } from '@timber-js/app/search-params'
|
|
154
|
+
* import { parseAsString, parseAsStringEnum } from 'nuqs'
|
|
155
|
+
* import { z } from 'zod/v4'
|
|
156
|
+
*
|
|
157
|
+
* export const searchParams = defineSearchParams({
|
|
158
|
+
* page: z.coerce.number().int().min(1).default(1), // Standard Schema — auto-wrapped
|
|
159
|
+
* q: withUrlKey(parseAsString, 'search'), // nuqs codec with URL alias
|
|
160
|
+
* sort: withDefault(parseAsStringEnum(['price', 'name']), 'price'),
|
|
161
|
+
* })
|
|
162
|
+
* ```
|
|
163
|
+
*/
|
|
164
|
+
function defineSearchParams(codecs) {
|
|
165
|
+
const resolvedCodecs = {};
|
|
166
|
+
const urlKeys = {};
|
|
167
|
+
for (const [key, value] of Object.entries(codecs)) {
|
|
168
|
+
const resolved = resolveField(key, value);
|
|
169
|
+
resolvedCodecs[key] = resolved.codec;
|
|
170
|
+
if (resolved.urlKey) urlKeys[key] = resolved.urlKey;
|
|
171
|
+
}
|
|
172
|
+
validateDefaults(resolvedCodecs);
|
|
173
|
+
return buildDefinition(resolvedCodecs, urlKeys);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Internal: build a SearchParamsDefinition from a typed codec map and url keys.
|
|
177
|
+
*/
|
|
178
|
+
function buildDefinition(codecMap, urlKeys) {
|
|
179
|
+
const defaultSerialized = {};
|
|
180
|
+
for (const key of Object.keys(codecMap)) defaultSerialized[key] = getDefaultSerialized(codecMap[key]);
|
|
181
|
+
function getUrlKey(prop) {
|
|
182
|
+
return urlKeys[prop] ?? prop;
|
|
183
|
+
}
|
|
184
|
+
function parseSync(raw) {
|
|
185
|
+
const normalized = normalizeRaw(raw);
|
|
186
|
+
const result = {};
|
|
187
|
+
for (const prop of Object.keys(codecMap)) {
|
|
188
|
+
const rawValue = normalized[getUrlKey(prop)];
|
|
189
|
+
result[prop] = codecMap[prop].parse(rawValue);
|
|
190
|
+
}
|
|
191
|
+
return result;
|
|
192
|
+
}
|
|
193
|
+
function parse(raw) {
|
|
194
|
+
if (raw instanceof Promise) return raw.then(parseSync);
|
|
195
|
+
return parseSync(raw);
|
|
196
|
+
}
|
|
197
|
+
function serialize(values) {
|
|
198
|
+
const parts = [];
|
|
199
|
+
for (const prop of Object.keys(codecMap)) {
|
|
200
|
+
if (!(prop in values)) continue;
|
|
201
|
+
const serialized = codecMap[prop].serialize(values[prop]);
|
|
202
|
+
if (serialized === defaultSerialized[prop]) continue;
|
|
203
|
+
if (serialized === null) continue;
|
|
204
|
+
parts.push(`${encodeURIComponent(getUrlKey(prop))}=${encodeURIComponent(serialized)}`);
|
|
205
|
+
}
|
|
206
|
+
return parts.join("&");
|
|
207
|
+
}
|
|
208
|
+
function href(pathname, values) {
|
|
209
|
+
const qs = serialize(values);
|
|
210
|
+
return qs ? `${pathname}?${qs}` : pathname;
|
|
211
|
+
}
|
|
212
|
+
function toSearchParams(values) {
|
|
213
|
+
const usp = new URLSearchParams();
|
|
214
|
+
for (const prop of Object.keys(codecMap)) {
|
|
215
|
+
if (!(prop in values)) continue;
|
|
216
|
+
const serialized = codecMap[prop].serialize(values[prop]);
|
|
217
|
+
if (serialized === defaultSerialized[prop]) continue;
|
|
218
|
+
if (serialized === null) continue;
|
|
219
|
+
usp.set(getUrlKey(prop), serialized);
|
|
220
|
+
}
|
|
221
|
+
return usp;
|
|
222
|
+
}
|
|
223
|
+
function extend(newCodecs) {
|
|
224
|
+
const resolvedNewCodecs = {};
|
|
225
|
+
const newUrlKeys = {};
|
|
226
|
+
for (const [key, value] of Object.entries(newCodecs)) {
|
|
227
|
+
const resolved = resolveField(key, value);
|
|
228
|
+
resolvedNewCodecs[key] = resolved.codec;
|
|
229
|
+
if (resolved.urlKey) newUrlKeys[key] = resolved.urlKey;
|
|
230
|
+
}
|
|
231
|
+
return buildDefinition({
|
|
232
|
+
...codecMap,
|
|
233
|
+
...resolvedNewCodecs
|
|
234
|
+
}, {
|
|
235
|
+
...urlKeys,
|
|
236
|
+
...newUrlKeys
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
function pick(...keys) {
|
|
240
|
+
const pickedCodecs = {};
|
|
241
|
+
const pickedUrlKeys = {};
|
|
242
|
+
for (const key of keys) {
|
|
243
|
+
pickedCodecs[key] = codecMap[key];
|
|
244
|
+
if (key in urlKeys) pickedUrlKeys[key] = urlKeys[key];
|
|
245
|
+
}
|
|
246
|
+
return buildDefinition(pickedCodecs, pickedUrlKeys);
|
|
247
|
+
}
|
|
248
|
+
function useQueryStates$1(options) {
|
|
249
|
+
return useQueryStates(codecMap, options, Object.freeze({ ...urlKeys }));
|
|
250
|
+
}
|
|
251
|
+
async function load() {
|
|
252
|
+
if (typeof window !== "undefined") throw new Error("[timber] searchParams.load() is server-only. Use searchParams.useQueryStates() on the client.");
|
|
253
|
+
return parseSync(await getRawSearchParams());
|
|
254
|
+
}
|
|
255
|
+
return {
|
|
256
|
+
parse,
|
|
257
|
+
load,
|
|
258
|
+
useQueryStates: useQueryStates$1,
|
|
259
|
+
extend,
|
|
260
|
+
pick,
|
|
261
|
+
serialize,
|
|
262
|
+
href,
|
|
263
|
+
toSearchParams,
|
|
264
|
+
codecs: codecMap,
|
|
265
|
+
urlKeys: Object.freeze({ ...urlKeys })
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
//#endregion
|
|
269
|
+
//#region src/search-params/wrappers.ts
|
|
270
|
+
/**
|
|
271
|
+
* Wrap a nullable codec with a default value. When the inner codec returns
|
|
272
|
+
* null, the default is used instead. The output type becomes non-nullable.
|
|
273
|
+
*
|
|
274
|
+
* Works with any codec — nuqs parsers, custom codecs, fromSchema results.
|
|
275
|
+
*
|
|
276
|
+
* ```ts
|
|
277
|
+
* import { parseAsInteger } from 'nuqs'
|
|
278
|
+
* import { withDefault } from '@timber-js/app/search-params'
|
|
279
|
+
*
|
|
280
|
+
* const page = withDefault(parseAsInteger, 1)
|
|
281
|
+
* // page.parse(undefined) → 1 (not null)
|
|
282
|
+
* // page.parse('5') → 5
|
|
283
|
+
* ```
|
|
284
|
+
*/
|
|
285
|
+
function withDefault(codec, defaultValue) {
|
|
286
|
+
return {
|
|
287
|
+
parse(value) {
|
|
288
|
+
const result = codec.parse(value);
|
|
289
|
+
return result === null ? defaultValue : result;
|
|
290
|
+
},
|
|
291
|
+
serialize(value) {
|
|
292
|
+
return codec.serialize(value);
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Attach a URL key alias to a codec. The alias determines what query
|
|
298
|
+
* parameter key is used in the URL, while the TypeScript property name
|
|
299
|
+
* stays descriptive.
|
|
300
|
+
*
|
|
301
|
+
* Aliases travel with codecs through object spread composition — when
|
|
302
|
+
* you spread a bundle containing aliased codecs into defineSearchParams,
|
|
303
|
+
* the aliases come along automatically.
|
|
304
|
+
*
|
|
305
|
+
* ```ts
|
|
306
|
+
* import { parseAsString } from 'nuqs'
|
|
307
|
+
* import { withUrlKey } from '@timber-js/app/search-params'
|
|
308
|
+
*
|
|
309
|
+
* export const searchable = {
|
|
310
|
+
* q: withUrlKey(parseAsString, 'search'),
|
|
311
|
+
* // ?search=shoes → { q: 'shoes' }
|
|
312
|
+
* }
|
|
313
|
+
* ```
|
|
314
|
+
*
|
|
315
|
+
* Composes with withDefault:
|
|
316
|
+
* ```ts
|
|
317
|
+
* import { parseAsInteger } from 'nuqs'
|
|
318
|
+
* withUrlKey(withDefault(parseAsInteger, 1), 'p')
|
|
319
|
+
* ```
|
|
320
|
+
*/
|
|
321
|
+
function withUrlKey(codec, urlKey) {
|
|
322
|
+
return {
|
|
323
|
+
parse: codec.parse.bind(codec),
|
|
324
|
+
serialize: codec.serialize.bind(codec),
|
|
325
|
+
urlKey
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
//#endregion
|
|
329
|
+
export { fromSchema as a, fromArraySchema as i, withUrlKey as n, defineSearchParams as r, withDefault as t };
|
|
330
|
+
|
|
331
|
+
//# sourceMappingURL=wrappers-C6J0nNji.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wrappers-C6J0nNji.js","names":[],"sources":["../../src/search-params/codecs.ts","../../src/search-params/define.ts","../../src/search-params/wrappers.ts"],"sourcesContent":["/**\n * Built-in codecs and the fromSchema bridge for Standard Schema-compatible\n * validation libraries (Zod, Valibot, ArkType).\n *\n * Design doc: design/09-typescript.md §\"The SearchParamCodec Protocol\"\n */\n\nimport type { SearchParamCodec } from './define.js';\n\n// ---------------------------------------------------------------------------\n// Standard Schema interface (subset)\n//\n// Standard Schema (https://github.com/standard-schema/standard-schema) defines\n// a minimal interface that Zod ≥3.24, Valibot ≥1.0, and ArkType all implement.\n// We depend only on `~standard.validate` to avoid coupling to any specific lib.\n// ---------------------------------------------------------------------------\n\ninterface StandardSchemaV1<Output = unknown> {\n '~standard': {\n validate(value: unknown): StandardSchemaResult<Output> | Promise<StandardSchemaResult<Output>>;\n };\n}\n\ntype StandardSchemaResult<Output> =\n | { value: Output; issues?: undefined }\n | { value?: undefined; issues: ReadonlyArray<{ message: string }> };\n\n// ---------------------------------------------------------------------------\n// Sync validate helper\n// ---------------------------------------------------------------------------\n\n/**\n * Zod v4's ~standard.validate() signature includes Promise in the return union\n * to satisfy the Standard Schema spec, but in practice Zod always validates\n * synchronously for the schema types we use. We assert the result is sync and\n * throw if it isn't — search params parsing must be synchronous.\n */\nfunction validateSync<Output>(\n schema: StandardSchemaV1<Output>,\n value: unknown\n): StandardSchemaResult<Output> {\n const result = schema['~standard'].validate(value);\n if (result instanceof Promise) {\n throw new Error(\n '[timber] fromSchema: schema returned a Promise — only sync schemas are supported for search params.'\n );\n }\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// fromSchema — bridge from Standard Schema to SearchParamCodec\n// ---------------------------------------------------------------------------\n\n/**\n * Bridge a Standard Schema-compatible schema (Zod, Valibot, ArkType) to a\n * SearchParamCodec.\n *\n * Parse: coerces the raw URL string through the schema. On validation failure,\n * parses `undefined` to get the schema's default value (the schema should have\n * a `.default()` call). If that also fails, returns `undefined`.\n *\n * Serialize: uses `String()` for primitives, `null` for null/undefined.\n *\n * ```ts\n * import { fromSchema } from '@timber-js/app/search-params'\n * import { z } from 'zod/v4'\n *\n * const pageCodec = fromSchema(z.coerce.number().int().min(1).default(1))\n * ```\n */\nexport function fromSchema<T>(schema: StandardSchemaV1<T>): SearchParamCodec<T> {\n return {\n parse(value: string | string[] | undefined): T {\n // For array inputs, take the last value (consistent with URLSearchParams.get())\n const input = Array.isArray(value) ? value[value.length - 1] : value;\n\n // Try parsing the raw value\n const result = validateSync(schema, input);\n if (!result.issues) {\n return result.value;\n }\n\n // On failure, try parsing undefined to get the default\n const defaultResult = validateSync(schema, undefined);\n if (!defaultResult.issues) {\n return defaultResult.value;\n }\n\n // No default available — return undefined (codec design choice)\n return undefined as T;\n },\n\n serialize(value: T): string | null {\n if (value === null || value === undefined) {\n return null;\n }\n return String(value);\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// fromArraySchema — bridge for array-valued search params\n// ---------------------------------------------------------------------------\n\n/**\n * Bridge a Standard Schema for array values. Handles both single strings\n * and repeated query keys (`?tag=a&tag=b`).\n *\n * ```ts\n * import { fromArraySchema } from '@timber-js/app/search-params'\n * import { z } from 'zod/v4'\n *\n * const tagsCodec = fromArraySchema(z.array(z.string()).default([]))\n * ```\n */\nexport function fromArraySchema<T>(schema: StandardSchemaV1<T>): SearchParamCodec<T> {\n return {\n parse(value: string | string[] | undefined): T {\n // Coerce single string to array for array schemas\n let input: unknown = value;\n if (typeof value === 'string') {\n input = [value];\n } else if (value === undefined) {\n input = undefined;\n }\n\n const result = validateSync(schema, input);\n if (!result.issues) {\n return result.value;\n }\n\n // On failure, try undefined for default\n const defaultResult = validateSync(schema, undefined);\n if (!defaultResult.issues) {\n return defaultResult.value;\n }\n\n return undefined as T;\n },\n\n serialize(value: T): string | null {\n if (value === null || value === undefined) {\n return null;\n }\n if (Array.isArray(value)) {\n return value.length === 0 ? null : value.join(',');\n }\n return String(value);\n },\n };\n}\n","/**\n * defineSearchParams — factory for SearchParamsDefinition<T>.\n *\n * Creates a typed, composable definition for a route's search parameters.\n * Accepts both SearchParamCodec values and Standard Schema objects (Zod,\n * Valibot, ArkType) with auto-detection. Supports URL key aliasing via\n * withUrlKey(), default-omission serialization, and composition via\n * .extend() / .pick().\n *\n * Design doc: design/23-search-params.md §\"defineSearchParams — The Factory\"\n */\n\nimport { useQueryStates as clientUseQueryStates } from '#/client/use-query-states.js';\nimport { fromSchema } from './codecs.js';\nimport type { Codec } from '#/codec.js';\n\n// Lazy import for .load() — avoids pulling server ALS into client bundles.\n// The import is resolved at call time, not at module load time.\n// In client environments, .load() throws before reaching the import.\nlet _rawSearchParams: (() => Promise<URLSearchParams>) | undefined;\nasync function getRawSearchParams(): Promise<URLSearchParams> {\n if (!_rawSearchParams) {\n // Dynamic import to avoid circular dependency and client bundle pollution.\n // request-context.ts is server-only; this path is never reached on the client\n // because load() throws first (see below).\n const mod = await import('#/server/request-context.js');\n _rawSearchParams = mod.rawSearchParams;\n }\n return _rawSearchParams();\n}\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * A codec that converts between URL string values and typed values.\n *\n * nuqs parsers implement this interface natively — no adapter needed.\n * Standard Schema objects (Zod, Valibot, ArkType) are auto-detected\n * by defineSearchParams and wrapped via fromSchema.\n */\nexport interface SearchParamCodec<T> extends Codec<T> {\n /** Optional URL key alias, set by withUrlKey(). */\n urlKey?: string;\n}\n\n/** A codec with a URL key alias attached via withUrlKey(). */\nexport interface SearchParamCodecWithUrlKey<T> extends SearchParamCodec<T> {\n urlKey: string;\n}\n\n/** Infer the output type of a codec. */\nexport type InferCodec<C> = C extends SearchParamCodec<infer T> ? T : never;\n\n/** Map of property names to codecs. */\nexport type CodecMap<T extends Record<string, unknown>> = {\n [K in keyof T]: SearchParamCodec<T[K]>;\n};\n\n/** Options for useQueryStates setter. */\nexport interface SetParamsOptions {\n /** Update URL without server roundtrip (default: false). */\n shallow?: boolean;\n /** Scroll to top after update (default: true). */\n scroll?: boolean;\n /** 'push' (default) or 'replace' for history state. */\n history?: 'push' | 'replace';\n}\n\n/** Setter function returned by useQueryStates. */\nexport type SetParams<T> = (values: Partial<T>, options?: SetParamsOptions) => void;\n\n/** Options for useQueryStates hook. */\nexport interface QueryStatesOptions {\n /** Update URL without server roundtrip (default: false). */\n shallow?: boolean;\n /** Scroll to top after update (default: true). */\n scroll?: boolean;\n /** 'push' (default) or 'replace' for history state. */\n history?: 'push' | 'replace';\n}\n\n/**\n * A fully typed, composable search params definition.\n *\n * Returned by defineSearchParams(). Carries a phantom _type property\n * for build-time type extraction.\n */\nexport interface SearchParamsDefinition<T extends Record<string, unknown>> {\n /** Parse raw URL search params into typed values. */\n parse(raw: URLSearchParams | Record<string, string | string[] | undefined>): T;\n /** Parse a Promise of URLSearchParams (e.g., from the ALS `searchParams()` API). */\n parse(raw: Promise<URLSearchParams | Record<string, string | string[] | undefined>>): Promise<T>;\n\n /**\n * Load typed search params from the current request context (ALS-backed).\n *\n * Server-only — reads rawSearchParams() from ALS and parses through codecs.\n * Throws on client. Eliminates the naming conflict between the definition\n * export and the server helper.\n *\n * ```tsx\n * // app/products/page.tsx\n * import { searchParams } from './params'\n * export default async function Page() {\n * const { page, category } = await searchParams.load()\n * }\n * ```\n */\n load(): Promise<T>;\n\n /** Client hook — reads current URL params and returns typed values + setter. */\n useQueryStates(options?: QueryStatesOptions): [T, SetParams<T>];\n\n /** Extend with additional codecs or Standard Schema objects. */\n extend<U extends Record<string, SearchParamCodec<unknown> | StandardSchemaV1<unknown>>>(\n codecs: U\n ): SearchParamsDefinition<T & { [K in keyof U]: InferField<U[K]> }>;\n\n /** Pick a subset of keys. Preserves codecs and aliases. */\n pick<K extends keyof T & string>(...keys: K[]): SearchParamsDefinition<Pick<T, K>>;\n\n /** Serialize values to a query string (no leading '?'), omitting defaults. */\n serialize(values: Partial<T>): string;\n\n /** Build a full path with query string, omitting defaults. */\n href(pathname: string, values: Partial<T>): string;\n\n /** Build a URLSearchParams instance, omitting defaults. */\n toSearchParams(values: Partial<T>): URLSearchParams;\n\n /** Read-only codec map for spreading into .extend(). */\n codecs: { [K in keyof T]: SearchParamCodec<T[K]> };\n\n /** Read-only URL key alias map. Maps property names to URL query parameter keys. */\n readonly urlKeys: Readonly<Record<string, string>>;\n\n /**\n * Phantom property for build-time type extraction.\n * Never set at runtime — exists only in the type system.\n */\n readonly _type?: T;\n}\n\n// ---------------------------------------------------------------------------\n// Standard Schema interface (subset)\n//\n// Standard Schema (https://github.com/standard-schema/standard-schema) defines\n// a minimal interface that Zod ≥3.24, Valibot ≥1.0, and ArkType all implement.\n// We check for the '~standard' property to auto-detect schemas.\n// ---------------------------------------------------------------------------\n\n/** Minimal Standard Schema interface for auto-detection. */\nexport interface StandardSchemaV1<Output = unknown> {\n '~standard': {\n validate(\n value: unknown\n ):\n | { value: Output; issues?: undefined }\n | { value?: undefined; issues: ReadonlyArray<{ message: string }> }\n | Promise<\n | { value: Output; issues?: undefined }\n | { value?: undefined; issues: ReadonlyArray<{ message: string }> }\n >;\n };\n}\n\n// ---------------------------------------------------------------------------\n// Type-level helpers\n// ---------------------------------------------------------------------------\n\n/** Infer the output type from either a SearchParamCodec or a StandardSchemaV1. */\nexport type InferField<V> =\n V extends SearchParamCodec<infer T> ? T : V extends StandardSchemaV1<infer T> ? T : never;\n\n/** Acceptable field value for defineSearchParams: a codec or a Standard Schema. */\nexport type SearchParamField<T = unknown> = SearchParamCodec<T> | StandardSchemaV1<T>;\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Convert URLSearchParams or a plain record to a normalized record\n * where repeated keys produce arrays.\n */\nfunction normalizeRaw(\n raw: URLSearchParams | Record<string, string | string[] | undefined>\n): Record<string, string | string[] | undefined> {\n if (raw instanceof URLSearchParams) {\n const result: Record<string, string | string[] | undefined> = {};\n for (const key of new Set(raw.keys())) {\n const values = raw.getAll(key);\n result[key] = values.length === 1 ? values[0] : values;\n }\n return result;\n }\n return raw;\n}\n\n/**\n * Compute the serialized default value for a codec. Used for\n * default-omission: when serialize(value) === serialize(parse(undefined)),\n * the field is omitted from the URL.\n */\nfunction getDefaultSerialized<T>(codec: SearchParamCodec<T>): string | null {\n return codec.serialize(codec.parse(undefined));\n}\n\n/** Check if a value is a Standard Schema object. */\nfunction isStandardSchema(value: unknown): value is StandardSchemaV1 {\n return (\n typeof value === 'object' &&\n value !== null &&\n '~standard' in value &&\n typeof (value as StandardSchemaV1)['~standard']?.validate === 'function'\n );\n}\n\n/** Check if a value is a SearchParamCodec. */\nfunction isCodec(value: unknown): value is SearchParamCodec<unknown> {\n return (\n typeof value === 'object' &&\n value !== null &&\n typeof (value as SearchParamCodec<unknown>).parse === 'function' &&\n typeof (value as SearchParamCodec<unknown>).serialize === 'function'\n );\n}\n\n/**\n * Resolve a field value to a SearchParamCodec. Auto-detects Standard Schema\n * objects and wraps them with fromSchema. Reads .urlKey from codecs.\n */\nfunction resolveField(\n fieldName: string,\n value: SearchParamField\n): { codec: SearchParamCodec<unknown>; urlKey?: string } {\n // Check for codec first (codecs may also have '~standard' if they're nuqs parsers)\n if (isCodec(value)) {\n return { codec: value, urlKey: value.urlKey };\n }\n\n // Auto-detect Standard Schema\n if (isStandardSchema(value)) {\n return { codec: fromSchema(value) };\n }\n\n throw new Error(\n `[timber] defineSearchParams: field '${fieldName}' is not a valid codec or Standard Schema. ` +\n `Expected an object with { parse, serialize } methods, or a Standard Schema object ` +\n `(Zod, Valibot, ArkType).`\n );\n}\n\n/**\n * Validate that all codecs handle absent params (parse(undefined) doesn't throw).\n * Catches schemas that throw on missing input. `undefined` and `null` are both\n * valid defaults — `undefined` is correct for optional fields (e.g., `z.string().optional()`).\n */\nfunction validateDefaults(codecMap: Record<string, SearchParamCodec<unknown>>): void {\n for (const [key, codec] of Object.entries(codecMap)) {\n try {\n codec.parse(undefined);\n } catch {\n throw new Error(\n `[timber] defineSearchParams: field '${key}' throws when the param is absent.\\n` +\n ` Search params are optional — the URL might not contain ?${key}=anything.\\n` +\n ` Add .default() or .optional() to your schema, or wrap with withDefault().`\n );\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\n/**\n * Create a SearchParamsDefinition from a map of codecs and/or Standard Schema\n * objects. Accepts both SearchParamCodec values and raw Zod/Valibot/ArkType\n * schemas with auto-detection.\n *\n * ```ts\n * import { defineSearchParams, withDefault, withUrlKey } from '@timber-js/app/search-params'\n * import { parseAsString, parseAsStringEnum } from 'nuqs'\n * import { z } from 'zod/v4'\n *\n * export const searchParams = defineSearchParams({\n * page: z.coerce.number().int().min(1).default(1), // Standard Schema — auto-wrapped\n * q: withUrlKey(parseAsString, 'search'), // nuqs codec with URL alias\n * sort: withDefault(parseAsStringEnum(['price', 'name']), 'price'),\n * })\n * ```\n */\nexport function defineSearchParams<C extends Record<string, SearchParamField>>(\n codecs: C\n): SearchParamsDefinition<{ [K in keyof C]: InferField<C[K]> }> {\n type T = { [K in keyof C]: InferField<C[K]> };\n\n const resolvedCodecs: Record<string, SearchParamCodec<unknown>> = {};\n const urlKeys: Record<string, string> = {};\n\n for (const [key, value] of Object.entries(codecs)) {\n const resolved = resolveField(key, value as SearchParamField);\n resolvedCodecs[key] = resolved.codec;\n if (resolved.urlKey) {\n urlKeys[key] = resolved.urlKey;\n }\n }\n\n // Validate that all codecs handle absent params\n validateDefaults(resolvedCodecs);\n\n return buildDefinition<T>(resolvedCodecs as unknown as CodecMap<T>, urlKeys);\n}\n\n// ---------------------------------------------------------------------------\n// Internal: build the definition object\n// ---------------------------------------------------------------------------\n\n/**\n * Internal: build a SearchParamsDefinition from a typed codec map and url keys.\n */\nfunction buildDefinition<T extends Record<string, unknown>>(\n codecMap: CodecMap<T>,\n urlKeys: Record<string, string>\n): SearchParamsDefinition<T> {\n // Pre-compute default serialized values for omission check\n const defaultSerialized: Record<string, string | null> = {};\n for (const key of Object.keys(codecMap)) {\n defaultSerialized[key] = getDefaultSerialized(codecMap[key as keyof T]);\n }\n\n function getUrlKey(prop: string): string {\n return urlKeys[prop] ?? prop;\n }\n\n // ---- parse ----\n function parseSync(raw: URLSearchParams | Record<string, string | string[] | undefined>): T {\n const normalized = normalizeRaw(raw);\n const result: Record<string, unknown> = {};\n\n for (const prop of Object.keys(codecMap)) {\n const urlKey = getUrlKey(prop);\n const rawValue = normalized[urlKey];\n result[prop] = (codecMap[prop as keyof T] as SearchParamCodec<unknown>).parse(rawValue);\n }\n\n return result as T;\n }\n\n // Overloaded parse: sync when given raw params, async when given a Promise.\n // This enables the ergonomic pattern: await def.parse(searchParams())\n function parse(raw: URLSearchParams | Record<string, string | string[] | undefined>): T;\n function parse(\n raw: Promise<URLSearchParams | Record<string, string | string[] | undefined>>\n ): Promise<T>;\n function parse(\n raw:\n | URLSearchParams\n | Record<string, string | string[] | undefined>\n | Promise<URLSearchParams | Record<string, string | string[] | undefined>>\n ): T | Promise<T> {\n if (raw instanceof Promise) {\n return raw.then(parseSync);\n }\n return parseSync(raw);\n }\n\n // ---- serialize ----\n function serialize(values: Partial<T>): string {\n const parts: string[] = [];\n\n for (const prop of Object.keys(codecMap)) {\n if (!(prop in values)) continue;\n const codec = codecMap[prop as keyof T] as SearchParamCodec<unknown>;\n const serialized = codec.serialize(values[prop as keyof T] as unknown);\n\n // Omit if serialized value matches the default\n if (serialized === defaultSerialized[prop]) continue;\n if (serialized === null) continue;\n\n parts.push(`${encodeURIComponent(getUrlKey(prop))}=${encodeURIComponent(serialized)}`);\n }\n\n return parts.join('&');\n }\n\n // ---- href ----\n function href(pathname: string, values: Partial<T>): string {\n const qs = serialize(values);\n return qs ? `${pathname}?${qs}` : pathname;\n }\n\n // ---- toSearchParams ----\n function toSearchParams(values: Partial<T>): URLSearchParams {\n const usp = new URLSearchParams();\n\n for (const prop of Object.keys(codecMap)) {\n if (!(prop in values)) continue;\n const codec = codecMap[prop as keyof T] as SearchParamCodec<unknown>;\n const serialized = codec.serialize(values[prop as keyof T] as unknown);\n\n if (serialized === defaultSerialized[prop]) continue;\n if (serialized === null) continue;\n\n usp.set(getUrlKey(prop), serialized);\n }\n\n return usp;\n }\n\n // ---- extend ----\n function extend<U extends Record<string, SearchParamCodec<unknown> | StandardSchemaV1<unknown>>>(\n newCodecs: U\n ): SearchParamsDefinition<T & { [K in keyof U]: InferField<U[K]> }> {\n type Combined = T & { [K in keyof U]: InferField<U[K]> };\n\n // Resolve any Standard Schema objects in the extension\n const resolvedNewCodecs: Record<string, SearchParamCodec<unknown>> = {};\n const newUrlKeys: Record<string, string> = {};\n for (const [key, value] of Object.entries(newCodecs)) {\n const resolved = resolveField(key, value as SearchParamField);\n resolvedNewCodecs[key] = resolved.codec;\n if (resolved.urlKey) {\n newUrlKeys[key] = resolved.urlKey;\n }\n }\n\n const combinedCodecs = {\n ...codecMap,\n ...resolvedNewCodecs,\n } as unknown as CodecMap<Combined>;\n\n // Merge URL keys: base keys + new codec urlKeys from withUrlKey\n const combinedUrlKeys: Record<string, string> = { ...urlKeys, ...newUrlKeys };\n\n return buildDefinition<Combined>(combinedCodecs, combinedUrlKeys);\n }\n\n // ---- pick ----\n function pick<K extends keyof T & string>(...keys: K[]): SearchParamsDefinition<Pick<T, K>> {\n const pickedCodecs: Record<string, SearchParamCodec<unknown>> = {};\n const pickedUrlKeys: Record<string, string> = {};\n\n for (const key of keys) {\n pickedCodecs[key] = codecMap[key] as SearchParamCodec<unknown>;\n if (key in urlKeys) {\n pickedUrlKeys[key] = urlKeys[key];\n }\n }\n\n return buildDefinition<Pick<T, K>>(\n pickedCodecs as unknown as CodecMap<Pick<T, K>>,\n pickedUrlKeys\n );\n }\n\n // ---- useQueryStates ----\n // Delegates to the 'use client' implementation from use-query-states.ts.\n //\n // In the RSC environment: use-query-states.ts is transformed by the RSC\n // plugin into a client reference proxy. Calling it throws — correct,\n // because hooks can't run during server component rendering.\n // In SSR: use-query-states.ts is the real nuqs-backed function. Hooks\n // work during SSR's renderToReadableStream, so this works correctly.\n // On the client: same as SSR — the real function is available.\n function useQueryStates(options?: QueryStatesOptions): [T, SetParams<T>] {\n return clientUseQueryStates(codecMap, options, Object.freeze({ ...urlKeys })) as [\n T,\n SetParams<T>,\n ];\n }\n\n // ---- load ----\n // ALS-backed: reads rawSearchParams() from the current request context\n // and parses through codecs. Server-only — throws on client.\n async function load(): Promise<T> {\n if (typeof window !== 'undefined') {\n throw new Error(\n '[timber] searchParams.load() is server-only. ' +\n 'Use searchParams.useQueryStates() on the client.'\n );\n }\n const raw = await getRawSearchParams();\n return parseSync(raw);\n }\n\n const definition: SearchParamsDefinition<T> = {\n parse,\n load,\n useQueryStates,\n extend,\n pick,\n serialize,\n href,\n toSearchParams,\n codecs: codecMap,\n urlKeys: Object.freeze({ ...urlKeys }),\n };\n\n return definition;\n}\n","/**\n * Codec wrappers — withDefault and withUrlKey.\n *\n * These are timber-specific utilities that work with any SearchParamCodec.\n * For actual codecs (string, integer, boolean, etc.), use nuqs parsers\n * or Standard Schema objects (Zod, Valibot, ArkType) with auto-detection.\n *\n * Design doc: design/23-search-params.md\n */\n\nimport type { SearchParamCodec, SearchParamCodecWithUrlKey } from './define.js';\n\n// ---------------------------------------------------------------------------\n// withDefault\n// ---------------------------------------------------------------------------\n\n/**\n * Wrap a nullable codec with a default value. When the inner codec returns\n * null, the default is used instead. The output type becomes non-nullable.\n *\n * Works with any codec — nuqs parsers, custom codecs, fromSchema results.\n *\n * ```ts\n * import { parseAsInteger } from 'nuqs'\n * import { withDefault } from '@timber-js/app/search-params'\n *\n * const page = withDefault(parseAsInteger, 1)\n * // page.parse(undefined) → 1 (not null)\n * // page.parse('5') → 5\n * ```\n */\nexport function withDefault<T>(\n codec: SearchParamCodec<T | null>,\n defaultValue: T\n): SearchParamCodec<T> {\n return {\n parse(value: string | string[] | undefined): T {\n const result = codec.parse(value);\n return result === null ? defaultValue : result;\n },\n serialize(value: T): string | null {\n return codec.serialize(value);\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// withUrlKey\n// ---------------------------------------------------------------------------\n\n/**\n * Attach a URL key alias to a codec. The alias determines what query\n * parameter key is used in the URL, while the TypeScript property name\n * stays descriptive.\n *\n * Aliases travel with codecs through object spread composition — when\n * you spread a bundle containing aliased codecs into defineSearchParams,\n * the aliases come along automatically.\n *\n * ```ts\n * import { parseAsString } from 'nuqs'\n * import { withUrlKey } from '@timber-js/app/search-params'\n *\n * export const searchable = {\n * q: withUrlKey(parseAsString, 'search'),\n * // ?search=shoes → { q: 'shoes' }\n * }\n * ```\n *\n * Composes with withDefault:\n * ```ts\n * import { parseAsInteger } from 'nuqs'\n * withUrlKey(withDefault(parseAsInteger, 1), 'p')\n * ```\n */\nexport function withUrlKey<T>(\n codec: SearchParamCodec<T>,\n urlKey: string\n): SearchParamCodecWithUrlKey<T> {\n return {\n parse: codec.parse.bind(codec),\n serialize: codec.serialize.bind(codec),\n urlKey,\n };\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compress-module.d.ts","sourceRoot":"","sources":["../../src/adapters/compress-module.ts"],"names":[],"mappings":"AAaA;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,
|
|
1
|
+
{"version":3,"file":"compress-module.d.ts","sourceRoot":"","sources":["../../src/adapters/compress-module.ts"],"names":[],"mappings":"AAaA;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,CAmG/C"}
|
package/dist/adapters/nitro.d.ts
CHANGED
|
@@ -28,6 +28,22 @@ export interface NitroAdapterOptions {
|
|
|
28
28
|
* @default 'node-server'
|
|
29
29
|
*/
|
|
30
30
|
preset?: NitroPreset;
|
|
31
|
+
/**
|
|
32
|
+
* Enable application-level gzip compression for HTML and RSC responses.
|
|
33
|
+
*
|
|
34
|
+
* When `true` (default), the origin compresses responses using the Web
|
|
35
|
+
* `CompressionStream` API. This is useful for self-hosted deployments
|
|
36
|
+
* where no reverse proxy or CDN handles compression.
|
|
37
|
+
*
|
|
38
|
+
* Set to `false` when deploying behind a reverse proxy (nginx, caddy)
|
|
39
|
+
* or CDN (Cloudflare, Fastly, Vercel) that compresses at the edge.
|
|
40
|
+
* Disabling origin compression saves CPU on the Node.js event loop —
|
|
41
|
+
* compressing 1MB+ streaming HTML responses takes 10-15ms of main
|
|
42
|
+
* thread time per request, directly reducing throughput under load.
|
|
43
|
+
*
|
|
44
|
+
* @default true
|
|
45
|
+
*/
|
|
46
|
+
compress?: boolean;
|
|
31
47
|
/**
|
|
32
48
|
* Additional Nitro configuration to merge into the generated config.
|
|
33
49
|
* Overrides default values for the selected preset.
|
|
@@ -52,7 +68,7 @@ export interface NitroAdapterOptions {
|
|
|
52
68
|
*/
|
|
53
69
|
export declare function nitro(options?: NitroAdapterOptions): TimberPlatformAdapter;
|
|
54
70
|
/** @internal Exported for testing. */
|
|
55
|
-
export declare function generateNitroEntry(buildDir: string, outDir: string, preset: NitroPreset, hasManifestInit?: boolean): string;
|
|
71
|
+
export declare function generateNitroEntry(buildDir: string, outDir: string, preset: NitroPreset, compress?: boolean, hasManifestInit?: boolean): string;
|
|
56
72
|
/** @internal Exported for testing. */
|
|
57
73
|
export declare function generateNitroConfig(preset: NitroPreset, userConfig?: Record<string, unknown>): string;
|
|
58
74
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nitro.d.ts","sourceRoot":"","sources":["../../src/adapters/nitro.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,qBAAqB,EAAgB,MAAM,SAAS,CAAC;AAsBnE;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GACnB,QAAQ,GACR,aAAa,GACb,SAAS,GACT,cAAc,GACd,YAAY,GACZ,aAAa,GACb,iBAAiB,GACjB,aAAa,GACb,KAAK,CAAC;AAEV,2CAA2C;AAC3C,UAAU,YAAY;IACpB,mDAAmD;IACnD,WAAW,EAAE,MAAM,CAAC;IACpB,kDAAkD;IAClD,SAAS,EAAE,MAAM,CAAC;IAClB,8CAA8C;IAC9C,iBAAiB,EAAE,OAAO,CAAC;IAC3B,sEAAsE;IACtE,kBAAkB,EAAE,OAAO,CAAC;IAC5B,iFAAiF;IACjF,WAAW,EAAE,MAAM,CAAC;IACpB,sDAAsD;IACtD,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACvC;AAgFD,qCAAqC;AACrC,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;IAErB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACvC;AAID;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,KAAK,CAAC,OAAO,GAAE,mBAAwB,GAAG,qBAAqB,
|
|
1
|
+
{"version":3,"file":"nitro.d.ts","sourceRoot":"","sources":["../../src/adapters/nitro.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,qBAAqB,EAAgB,MAAM,SAAS,CAAC;AAsBnE;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GACnB,QAAQ,GACR,aAAa,GACb,SAAS,GACT,cAAc,GACd,YAAY,GACZ,aAAa,GACb,iBAAiB,GACjB,aAAa,GACb,KAAK,CAAC;AAEV,2CAA2C;AAC3C,UAAU,YAAY;IACpB,mDAAmD;IACnD,WAAW,EAAE,MAAM,CAAC;IACpB,kDAAkD;IAClD,SAAS,EAAE,MAAM,CAAC;IAClB,8CAA8C;IAC9C,iBAAiB,EAAE,OAAO,CAAC;IAC3B,sEAAsE;IACtE,kBAAkB,EAAE,OAAO,CAAC;IAC5B,iFAAiF;IACjF,WAAW,EAAE,MAAM,CAAC;IACpB,sDAAsD;IACtD,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACvC;AAgFD,qCAAqC;AACrC,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;IAErB;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACvC;AAID;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,KAAK,CAAC,OAAO,GAAE,mBAAwB,GAAG,qBAAqB,CA2F9E;AAID,sCAAsC;AACtC,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,WAAW,EACnB,QAAQ,UAAO,EACf,eAAe,UAAQ,GACtB,MAAM,CAwFR;AAED,sCAAsC;AACtC,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,WAAW,EACnB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,MAAM,CAyBR;AAOD;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,MAAM,CA6LnF;AAED,wEAAwE;AACxE,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;CACb;AAED,sCAAsC;AACtC,wBAAgB,2BAA2B,CACzC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,WAAW,GAClB,mBAAmB,GAAG,IAAI,CAY5B;AA8DD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW,GAAG,YAAY,CAEjE"}
|
package/dist/adapters/nitro.js
CHANGED
|
@@ -13,9 +13,11 @@ import { execFile } from "node:child_process";
|
|
|
13
13
|
function generateCompressModule() {
|
|
14
14
|
return `// Generated by @timber-js/app — response compression for self-hosted deployments.
|
|
15
15
|
// Do not edit — this file is regenerated on each build.
|
|
16
|
-
// Uses
|
|
17
|
-
//
|
|
18
|
-
//
|
|
16
|
+
// Uses node:zlib createGzip() (C++ native) on Node.js, falls back to
|
|
17
|
+
// CompressionStream (Web API) on other runtimes. Brotli is left to CDNs/reverse
|
|
18
|
+
// proxies — at streaming quality levels its ratio advantage is marginal.
|
|
19
|
+
import { Readable } from 'node:stream';
|
|
20
|
+
import { createGzip, constants } from 'node:zlib';
|
|
19
21
|
|
|
20
22
|
const COMPRESSIBLE_TYPES = new Set([
|
|
21
23
|
'text/html', 'text/css', 'text/plain', 'text/xml', 'text/javascript',
|
|
@@ -62,7 +64,25 @@ function shouldCompress(response) {
|
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
function compressWithGzip(body) {
|
|
65
|
-
|
|
67
|
+
// Use node:zlib (C++ native) for gzip compression. The Web Streams
|
|
68
|
+
// CompressionStream works but every chunk crosses the JS/Promise boundary.
|
|
69
|
+
// node:zlib.createGzip() compresses entirely in C++ with zero per-chunk
|
|
70
|
+
// Promise overhead.
|
|
71
|
+
//
|
|
72
|
+
// Convert: Web ReadableStream → Node Readable → pipe through gzip →
|
|
73
|
+
// Node Readable → Readable.toWeb() → Web ReadableStream
|
|
74
|
+
try {
|
|
75
|
+
const nodeReadable = Readable.fromWeb(body);
|
|
76
|
+
// Z_SYNC_FLUSH ensures each chunk is flushed immediately so the browser
|
|
77
|
+
// receives the HTML shell before Suspense boundaries resolve. Without it,
|
|
78
|
+
// gzip buffers internally and breaks streaming.
|
|
79
|
+
const gzip = createGzip({ flush: constants.Z_SYNC_FLUSH });
|
|
80
|
+
const compressed = nodeReadable.pipe(gzip);
|
|
81
|
+
return Readable.toWeb(compressed);
|
|
82
|
+
} catch {
|
|
83
|
+
// Fallback: CompressionStream (CF Workers, or if node:stream unavailable)
|
|
84
|
+
return body.pipeThrough(new CompressionStream('gzip'));
|
|
85
|
+
}
|
|
66
86
|
}
|
|
67
87
|
|
|
68
88
|
export function compressResponse(request, response) {
|
|
@@ -189,6 +209,7 @@ var PRESET_CONFIGS = {
|
|
|
189
209
|
*/
|
|
190
210
|
function nitro(options = {}) {
|
|
191
211
|
const preset = options.preset ?? "node-server";
|
|
212
|
+
const compress = options.compress ?? true;
|
|
192
213
|
const presetConfig = PRESET_CONFIGS[preset];
|
|
193
214
|
const pendingPromises = [];
|
|
194
215
|
return {
|
|
@@ -213,7 +234,7 @@ function nitro(options = {}) {
|
|
|
213
234
|
const rscContent = await readFile(rscEntry, "utf-8");
|
|
214
235
|
await writeFile(rscEntry, `${config.manifestInit}\n${rscContent}`);
|
|
215
236
|
}
|
|
216
|
-
const entry = generateNitroEntry(buildDir, outDir, preset);
|
|
237
|
+
const entry = generateNitroEntry(buildDir, outDir, preset, compress);
|
|
217
238
|
await writeFile(join(outDir, "entry.ts"), entry);
|
|
218
239
|
await runNitroBuild(outDir, preset, options.nitroConfig);
|
|
219
240
|
},
|
|
@@ -232,7 +253,7 @@ function nitro(options = {}) {
|
|
|
232
253
|
};
|
|
233
254
|
}
|
|
234
255
|
/** @internal Exported for testing. */
|
|
235
|
-
function generateNitroEntry(buildDir, outDir, preset, hasManifestInit = false) {
|
|
256
|
+
function generateNitroEntry(buildDir, outDir, preset, compress = true, hasManifestInit = false) {
|
|
236
257
|
const serverEntryRelative = "./rsc/index.js";
|
|
237
258
|
const runtimeName = PRESET_CONFIGS[preset].runtimeName;
|
|
238
259
|
const earlyHints = PRESET_CONFIGS[preset].supportsEarlyHints;
|
|
@@ -274,8 +295,7 @@ function generateNitroEntry(buildDir, outDir, preset, hasManifestInit = false) {
|
|
|
274
295
|
// Do not edit — this file is regenerated on each build.
|
|
275
296
|
|
|
276
297
|
${hasManifestInit ? "import './_timber-manifest-init.js'\n" : ""}import handler, { runWithEarlyHintsSender${supportsWaitUntil ? ", runWithWaitUntil" : ""} } from '${serverEntryRelative}'
|
|
277
|
-
import { compressResponse } from './_compress.mjs'
|
|
278
|
-
|
|
298
|
+
${compress ? "import { compressResponse } from './_compress.mjs'\n" : ""}
|
|
279
299
|
// Set TIMBER_RUNTIME for instrumentation.ts conditional SDK initialization.
|
|
280
300
|
// See design/25-production-deployments.md §"TIMBER_RUNTIME".
|
|
281
301
|
process.env.TIMBER_RUNTIME = '${runtimeName}'
|
|
@@ -289,7 +309,7 @@ export default async function timberHandler(event) {
|
|
|
289
309
|
// h3 v2: event.req is the Web Request
|
|
290
310
|
const webRequest = event.req
|
|
291
311
|
${handlerCall}
|
|
292
|
-
return compressResponse(webRequest, webResponse)
|
|
312
|
+
return ${compress ? "compressResponse(webRequest, webResponse)" : "webResponse"}
|
|
293
313
|
}
|
|
294
314
|
`;
|
|
295
315
|
}
|
|
@@ -458,14 +478,37 @@ const server = createServer(async (req, res) => {
|
|
|
458
478
|
|
|
459
479
|
if (webResponse.body) {
|
|
460
480
|
const reader = webResponse.body.getReader();
|
|
461
|
-
|
|
481
|
+
|
|
482
|
+
// Cancel the reader when the client disconnects. This causes any
|
|
483
|
+
// pending reader.read() to reject, breaking the pump loop. Critical
|
|
484
|
+
// for SSE and other infinite streams — without this, disconnected
|
|
485
|
+
// clients leak readers.
|
|
486
|
+
let clientDisconnected = false;
|
|
487
|
+
const onClose = () => {
|
|
488
|
+
clientDisconnected = true;
|
|
489
|
+
reader.cancel('Client disconnected').catch(() => {});
|
|
490
|
+
};
|
|
491
|
+
res.on('close', onClose);
|
|
492
|
+
|
|
493
|
+
try {
|
|
462
494
|
while (true) {
|
|
463
495
|
const { done, value } = await reader.read();
|
|
464
|
-
if (done)
|
|
496
|
+
if (done) break;
|
|
465
497
|
res.write(value);
|
|
466
498
|
}
|
|
467
|
-
}
|
|
468
|
-
|
|
499
|
+
} catch (err) {
|
|
500
|
+
// reader.cancel() from the close handler causes read() to reject.
|
|
501
|
+
// This is expected on client disconnect — not an error.
|
|
502
|
+
if (!clientDisconnected) {
|
|
503
|
+
throw err;
|
|
504
|
+
}
|
|
505
|
+
} finally {
|
|
506
|
+
res.off('close', onClose);
|
|
507
|
+
reader.releaseLock();
|
|
508
|
+
if (!res.writableEnded) {
|
|
509
|
+
res.end();
|
|
510
|
+
}
|
|
511
|
+
}
|
|
469
512
|
} else {
|
|
470
513
|
res.end();
|
|
471
514
|
}
|