@timber-js/app 0.2.0-alpha.164 → 0.2.0-alpha.166
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-CSDD6x7U.js → actions-CDPfMp_I.js} +3 -3
- package/dist/_chunks/{actions-CSDD6x7U.js.map → actions-CDPfMp_I.js.map} +1 -1
- package/dist/_chunks/{cache-api-eb1gydM7.js → cache-api-DygSeKCB.js} +2 -2
- package/dist/_chunks/{cache-api-eb1gydM7.js.map → cache-api-DygSeKCB.js.map} +1 -1
- package/dist/_chunks/{cli-schema-sync-mGfRbjh2.js → cli-schema-sync-EXGYPhI2.js} +3 -3
- package/dist/_chunks/{cli-schema-sync-mGfRbjh2.js.map → cli-schema-sync-EXGYPhI2.js.map} +1 -1
- package/dist/_chunks/{define-CFmvb4Bt.js → define-COtkxMRT.js} +16 -28
- package/dist/_chunks/define-COtkxMRT.js.map +1 -0
- package/dist/_chunks/{define-Bssfp6ot.js → define-c4au4I9R.js} +2 -2
- package/dist/_chunks/{define-Bssfp6ot.js.map → define-c4au4I9R.js.map} +1 -1
- package/dist/_chunks/{logger-B_O6-mdJ.js → logger-t3uxAmbX.js} +3 -3
- package/dist/_chunks/{logger-B_O6-mdJ.js.map → logger-t3uxAmbX.js.map} +1 -1
- package/dist/_chunks/{plugin-context-BnaiU_cF.js → plugin-context---kTF5v8.js} +2 -2
- package/dist/_chunks/{plugin-context-BnaiU_cF.js.map → plugin-context---kTF5v8.js.map} +1 -1
- package/dist/_chunks/{resolve-schema-3iUvBV5T.js → resolve-schema-Dz3fcFUo.js} +2 -2
- package/dist/_chunks/{resolve-schema-3iUvBV5T.js.map → resolve-schema-Dz3fcFUo.js.map} +1 -1
- package/dist/_chunks/{schema-bridge-BY3QLBL7.js → schema-bridge-DT_Tn0Xf.js} +2 -2
- package/dist/_chunks/{schema-bridge-BY3QLBL7.js.map → schema-bridge-DT_Tn0Xf.js.map} +1 -1
- package/dist/_chunks/{use-query-states-CbeQmext.js → use-query-states-DFvWd-EA.js} +65 -7
- package/dist/_chunks/use-query-states-DFvWd-EA.js.map +1 -0
- package/dist/_chunks/{walkers-BL3MCMgO.js → walkers-DBVzXuWc.js} +3 -3
- package/dist/_chunks/{walkers-BL3MCMgO.js.map → walkers-DBVzXuWc.js.map} +1 -1
- package/dist/adapters/nitro.d.ts +29 -0
- package/dist/adapters/nitro.d.ts.map +1 -1
- package/dist/adapters/nitro.js +57 -10
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/cache/index.js +1 -1
- package/dist/cli.js +2 -2
- package/dist/client/internal.js +1 -1
- package/dist/client/use-query-states.d.ts.map +1 -1
- package/dist/codec.js +1 -1
- package/dist/cookies/index.js +1 -1
- package/dist/dev-tools/logs.d.ts +15 -1
- package/dist/dev-tools/logs.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +104 -20
- package/dist/index.js.map +1 -1
- package/dist/params/index.js +1 -1
- package/dist/routing/index.js +2 -2
- package/dist/schema-bridge.d.ts.map +1 -1
- package/dist/search-params/define.d.ts +35 -4
- package/dist/search-params/define.d.ts.map +1 -1
- package/dist/search-params/index.js +3 -7
- package/dist/search-params/index.js.map +1 -1
- package/dist/search-params/wrappers.d.ts +2 -2
- package/dist/search-params/wrappers.d.ts.map +1 -1
- package/dist/segment-params/index.js +1 -1
- package/dist/server/index.js +2 -2
- package/dist/server/internal.js +8 -6
- package/dist/server/internal.js.map +1 -1
- package/dist/server/stream-utils.d.ts.map +1 -1
- package/docs/api/33-api-search-params.mdx +13 -0
- package/docs/learn/00-introduction.mdx +1 -2
- package/package.json +8 -7
- package/src/adapters/nitro.ts +106 -4
- package/src/cli.ts +0 -0
- package/src/client/use-query-states.ts +99 -7
- package/src/dev-tools/logs.ts +119 -34
- package/src/index.ts +14 -1
- package/src/schema-bridge.ts +4 -3
- package/src/search-params/define.ts +57 -51
- package/src/search-params/wrappers.ts +17 -26
- package/src/server/stream-utils.ts +8 -6
- package/LICENSE +0 -8
- package/dist/_chunks/define-CFmvb4Bt.js.map +0 -1
- package/dist/_chunks/use-query-states-CbeQmext.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stream-utils.d.ts","sourceRoot":"","sources":["../../src/server/stream-utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAKH,MAAM,WAAW,UAAU;IACzB;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,EAClC,OAAO,CAAC,EAAE,UAAU,GACnB,CAAC,cAAc,CAAC,UAAU,CAAC,EAAE,cAAc,CAAC,UAAU,CAAC,CAAC,CAmK1D;AAED;;;;;;;;;GASG;AACH,wBAAgB,4BAA4B,CAC1C,IAAI,EAAE,cAAc,CAAC,UAAU,CAAC,EAChC,SAAS,EAAE,MAAM,GAChB,cAAc,CAAC,UAAU,CAAC,
|
|
1
|
+
{"version":3,"file":"stream-utils.d.ts","sourceRoot":"","sources":["../../src/server/stream-utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAKH,MAAM,WAAW,UAAU;IACzB;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,EAClC,OAAO,CAAC,EAAE,UAAU,GACnB,CAAC,cAAc,CAAC,UAAU,CAAC,EAAE,cAAc,CAAC,UAAU,CAAC,CAAC,CAmK1D;AAED;;;;;;;;;GASG;AACH,wBAAgB,4BAA4B,CAC1C,IAAI,EAAE,cAAc,CAAC,UAAU,CAAC,EAChC,SAAS,EAAE,MAAM,GAChB,cAAc,CAAC,UAAU,CAAC,CA4D5B"}
|
|
@@ -23,6 +23,19 @@ export const searchParams = defineSearchParams({
|
|
|
23
23
|
});
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
+
### Implicit optionality
|
|
27
|
+
|
|
28
|
+
Search params are optional by nature — the URL might not contain them. A schema without `.default()` or `.optional()` (e.g. bare `z.string()`) is treated as implicitly optional: absent or invalid input parses to `undefined`, and the field's inferred type widens to `T | undefined`. Definitions never throw at module-eval time, and schema-backed parsing never throws at request time.
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
const def = defineSearchParams({ name: z.string() });
|
|
32
|
+
def.parse(new URLSearchParams('')); // { name: undefined }
|
|
33
|
+
def.parse(new URLSearchParams('name=x')); // { name: 'x' }
|
|
34
|
+
// Inferred type: { name: string | undefined }
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Note: `z.coerce.*` schemas declare input `unknown`, so a coerce schema without `.default()` keeps its narrow type even though absent input yields `undefined` at runtime — add `.default()` to coerce schemas for accurate types.
|
|
38
|
+
|
|
26
39
|
### Returns: `SearchParamsDefinition<T>`
|
|
27
40
|
|
|
28
41
|
| Method / Property | Description |
|
|
@@ -61,8 +61,7 @@ export const searchParams = defineSearchParams({
|
|
|
61
61
|
const sp = searchParams.get();
|
|
62
62
|
|
|
63
63
|
// or on the client
|
|
64
|
-
|
|
65
|
-
const [sp, setSearchParams] = searchParams.useQueryState();
|
|
64
|
+
const [sp, setSearchParams] = searchParams.useQueryStates();
|
|
66
65
|
```
|
|
67
66
|
|
|
68
67
|
### Typed Routes
|
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.166",
|
|
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",
|
|
@@ -150,6 +150,12 @@
|
|
|
150
150
|
"publishConfig": {
|
|
151
151
|
"access": "public"
|
|
152
152
|
},
|
|
153
|
+
"scripts": {
|
|
154
|
+
"copy-docs": "node scripts/copy-docs.js",
|
|
155
|
+
"build": "vite build --config vite.lib.config.ts && tsc --emitDeclarationOnly --project tsconfig.json --outDir dist && pnpm run copy-docs",
|
|
156
|
+
"typecheck": "tsgo --noEmit",
|
|
157
|
+
"prepublishOnly": "pnpm run build"
|
|
158
|
+
},
|
|
153
159
|
"dependencies": {
|
|
154
160
|
"@opentelemetry/api": "^1.9.1",
|
|
155
161
|
"@opentelemetry/context-async-hooks": "^2.8.0",
|
|
@@ -195,10 +201,5 @@
|
|
|
195
201
|
},
|
|
196
202
|
"engines": {
|
|
197
203
|
"node": ">=22.18.0"
|
|
198
|
-
},
|
|
199
|
-
"scripts": {
|
|
200
|
-
"copy-docs": "node scripts/copy-docs.js",
|
|
201
|
-
"build": "vite build --config vite.lib.config.ts && tsc --emitDeclarationOnly --project tsconfig.json --outDir dist && pnpm run copy-docs",
|
|
202
|
-
"typecheck": "tsgo --noEmit"
|
|
203
204
|
}
|
|
204
|
-
}
|
|
205
|
+
}
|
package/src/adapters/nitro.ts
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
// compression, graceful shutdown, static file serving, and platform quirks.
|
|
6
6
|
// See design/11-platform.md and design/25-production-deployments.md.
|
|
7
7
|
|
|
8
|
-
import { writeFile, readFile } from 'node:fs/promises';
|
|
8
|
+
import { writeFile, readFile, cp, glob } from 'node:fs/promises';
|
|
9
9
|
import { execFile } from 'node:child_process';
|
|
10
|
-
import { join, relative } from 'node:path';
|
|
10
|
+
import { join, relative, dirname, basename } from 'node:path';
|
|
11
11
|
import type { TimberPlatformAdapter, TimberConfig } from './types';
|
|
12
12
|
import { generateCompressModule } from './compress-module.js';
|
|
13
13
|
import { IMMUTABLE_CACHE } from './shared.js';
|
|
@@ -38,6 +38,12 @@ interface PresetConfig {
|
|
|
38
38
|
nitroPreset: string;
|
|
39
39
|
/** Output directory name within the build dir. */
|
|
40
40
|
outputDir: string;
|
|
41
|
+
/**
|
|
42
|
+
* Path to the server bundle directory relative to outputDir.
|
|
43
|
+
* Nitro places the bundled server entry + _chunks/ here.
|
|
44
|
+
* Most presets use `server`; Vercel uses `functions/__server.func`.
|
|
45
|
+
*/
|
|
46
|
+
serverBundleDir: string;
|
|
41
47
|
/** Whether the runtime supports waitUntil. */
|
|
42
48
|
supportsWaitUntil: boolean;
|
|
43
49
|
/** Whether the runtime supports application-level 103 Early Hints. */
|
|
@@ -52,6 +58,7 @@ const PRESET_CONFIGS: Record<NitroPreset, PresetConfig> = {
|
|
|
52
58
|
'vercel': {
|
|
53
59
|
nitroPreset: 'vercel',
|
|
54
60
|
outputDir: '.vercel/output',
|
|
61
|
+
serverBundleDir: 'functions/__server.func',
|
|
55
62
|
supportsWaitUntil: true,
|
|
56
63
|
supportsEarlyHints: false,
|
|
57
64
|
runtimeName: 'vercel',
|
|
@@ -60,6 +67,7 @@ const PRESET_CONFIGS: Record<NitroPreset, PresetConfig> = {
|
|
|
60
67
|
'vercel-edge': {
|
|
61
68
|
nitroPreset: 'vercel-edge',
|
|
62
69
|
outputDir: '.vercel/output',
|
|
70
|
+
serverBundleDir: 'functions/__server.func',
|
|
63
71
|
supportsWaitUntil: true,
|
|
64
72
|
supportsEarlyHints: false,
|
|
65
73
|
runtimeName: 'vercel-edge',
|
|
@@ -67,6 +75,7 @@ const PRESET_CONFIGS: Record<NitroPreset, PresetConfig> = {
|
|
|
67
75
|
'netlify': {
|
|
68
76
|
nitroPreset: 'netlify',
|
|
69
77
|
outputDir: '.netlify/functions-internal',
|
|
78
|
+
serverBundleDir: 'server',
|
|
70
79
|
supportsWaitUntil: false,
|
|
71
80
|
supportsEarlyHints: false,
|
|
72
81
|
runtimeName: 'netlify',
|
|
@@ -74,6 +83,7 @@ const PRESET_CONFIGS: Record<NitroPreset, PresetConfig> = {
|
|
|
74
83
|
'netlify-edge': {
|
|
75
84
|
nitroPreset: 'netlify-edge',
|
|
76
85
|
outputDir: '.netlify/edge-functions',
|
|
86
|
+
serverBundleDir: 'server',
|
|
77
87
|
supportsWaitUntil: true,
|
|
78
88
|
supportsEarlyHints: false,
|
|
79
89
|
runtimeName: 'netlify-edge',
|
|
@@ -81,6 +91,7 @@ const PRESET_CONFIGS: Record<NitroPreset, PresetConfig> = {
|
|
|
81
91
|
'aws-lambda': {
|
|
82
92
|
nitroPreset: 'aws-lambda',
|
|
83
93
|
outputDir: '.output',
|
|
94
|
+
serverBundleDir: 'server',
|
|
84
95
|
supportsWaitUntil: false,
|
|
85
96
|
supportsEarlyHints: false,
|
|
86
97
|
runtimeName: 'aws-lambda',
|
|
@@ -88,6 +99,7 @@ const PRESET_CONFIGS: Record<NitroPreset, PresetConfig> = {
|
|
|
88
99
|
'deno-deploy': {
|
|
89
100
|
nitroPreset: 'deno-deploy',
|
|
90
101
|
outputDir: '.output',
|
|
102
|
+
serverBundleDir: 'server',
|
|
91
103
|
supportsWaitUntil: true,
|
|
92
104
|
supportsEarlyHints: false,
|
|
93
105
|
runtimeName: 'deno-deploy',
|
|
@@ -95,6 +107,7 @@ const PRESET_CONFIGS: Record<NitroPreset, PresetConfig> = {
|
|
|
95
107
|
'azure-functions': {
|
|
96
108
|
nitroPreset: 'azure-functions',
|
|
97
109
|
outputDir: '.output',
|
|
110
|
+
serverBundleDir: 'server',
|
|
98
111
|
supportsWaitUntil: false,
|
|
99
112
|
supportsEarlyHints: false,
|
|
100
113
|
runtimeName: 'azure-functions',
|
|
@@ -102,6 +115,7 @@ const PRESET_CONFIGS: Record<NitroPreset, PresetConfig> = {
|
|
|
102
115
|
'node-server': {
|
|
103
116
|
nitroPreset: 'node-server',
|
|
104
117
|
outputDir: '.output',
|
|
118
|
+
serverBundleDir: 'server',
|
|
105
119
|
supportsWaitUntil: true,
|
|
106
120
|
// Disabled by default: most node-server deployments sit behind a
|
|
107
121
|
// reverse proxy (nginx, caddy, traefik) that doesn't support 103
|
|
@@ -116,6 +130,7 @@ const PRESET_CONFIGS: Record<NitroPreset, PresetConfig> = {
|
|
|
116
130
|
'bun': {
|
|
117
131
|
nitroPreset: 'bun',
|
|
118
132
|
outputDir: '.output',
|
|
133
|
+
serverBundleDir: 'server',
|
|
119
134
|
supportsWaitUntil: true,
|
|
120
135
|
// Disabled for same reason as node-server — reverse proxies choke on 103.
|
|
121
136
|
// Link headers on the 200 response are converted to 103 by CDNs.
|
|
@@ -220,7 +235,29 @@ export function nitro(options: NitroAdapterOptions = {}): TimberPlatformAdapter
|
|
|
220
235
|
// Run the Nitro build to produce a production-ready server bundle.
|
|
221
236
|
// The output goes to dist/nitro/.output/server/index.mjs (for node-server preset).
|
|
222
237
|
// Config is passed programmatically — no nitro.config.ts file needed.
|
|
223
|
-
await runNitroBuild(outDir, preset, options.nitroConfig);
|
|
238
|
+
const { serverDir } = await runNitroBuild(outDir, preset, options.nitroConfig);
|
|
239
|
+
|
|
240
|
+
// Copy prebuilt flight payloads from the canonical build source
|
|
241
|
+
// (buildDir/prebuilt, cleaned by the prebuilt plugin each build) into
|
|
242
|
+
// the Nitro output. The RSC entry resolves `../prebuilt/` relative to
|
|
243
|
+
// its bundled chunk (<serverDir>/_chunks/), so prebuilt/ must live at
|
|
244
|
+
// <serverDir>/prebuilt/. serverDir is the resolved path from Nitro
|
|
245
|
+
// (honors user overrides via nitroConfig.output.serverDir).
|
|
246
|
+
const prebuiltSrc = join(buildDir, 'prebuilt');
|
|
247
|
+
await cp(prebuiltSrc, join(serverDir, 'prebuilt'), {
|
|
248
|
+
recursive: true,
|
|
249
|
+
}).catch((e: NodeJS.ErrnoException) => {
|
|
250
|
+
if (e.code !== 'ENOENT') throw e;
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// Vercel with functionRules clones __server.func to per-route .func
|
|
254
|
+
// directories during the Nitro build. Copy prebuilt into those clones
|
|
255
|
+
// so requests routed to them also resolve ../prebuilt correctly.
|
|
256
|
+
if (preset === 'vercel' || preset === 'vercel-edge') {
|
|
257
|
+
const functionsDir = dirname(serverDir);
|
|
258
|
+
const baseFuncName = basename(serverDir);
|
|
259
|
+
await copyPrebuiltToVercelFuncClones(functionsDir, prebuiltSrc, baseFuncName);
|
|
260
|
+
}
|
|
224
261
|
},
|
|
225
262
|
|
|
226
263
|
// Only presets that produce a locally-runnable server get preview().
|
|
@@ -598,11 +635,16 @@ export function generateNitroPreviewCommand(
|
|
|
598
635
|
* Externalizes the timber RSC/SSR output — those files are pre-built
|
|
599
636
|
* by timber and have internal references that nitro's bundler can't follow.
|
|
600
637
|
*/
|
|
638
|
+
interface NitroBuildResult {
|
|
639
|
+
/** Resolved output.serverDir — where the server bundle + _chunks/ live. */
|
|
640
|
+
serverDir: string;
|
|
641
|
+
}
|
|
642
|
+
|
|
601
643
|
async function runNitroBuild(
|
|
602
644
|
nitroDir: string,
|
|
603
645
|
preset: NitroPreset,
|
|
604
646
|
userConfig?: Record<string, unknown>
|
|
605
|
-
): Promise<
|
|
647
|
+
): Promise<NitroBuildResult> {
|
|
606
648
|
const presetConfig = PRESET_CONFIGS[preset];
|
|
607
649
|
const {
|
|
608
650
|
createNitro,
|
|
@@ -633,10 +675,18 @@ async function runNitroBuild(
|
|
|
633
675
|
...userConfig,
|
|
634
676
|
});
|
|
635
677
|
|
|
678
|
+
// Read the resolved serverDir AFTER config merging — userConfig may
|
|
679
|
+
// override output.dir or output.serverDir. Nitro appends a trailing
|
|
680
|
+
// slash during resolution; strip it for join() compatibility.
|
|
681
|
+
const raw = nitro.options.output.serverDir;
|
|
682
|
+
const serverDir = raw.endsWith('/') ? raw.slice(0, -1) : raw;
|
|
683
|
+
|
|
636
684
|
await prepare(nitro);
|
|
637
685
|
await copyPublicAssets(nitro);
|
|
638
686
|
await nitroBuild(nitro);
|
|
639
687
|
await nitro.close();
|
|
688
|
+
|
|
689
|
+
return { serverDir };
|
|
640
690
|
}
|
|
641
691
|
|
|
642
692
|
/** Spawn a Nitro preview process and pipe stdio. */
|
|
@@ -729,3 +779,55 @@ export function sendNodeResponse(nodeRes, webRes) {
|
|
|
729
779
|
export function getPresetConfig(preset: NitroPreset): PresetConfig {
|
|
730
780
|
return PRESET_CONFIGS[preset];
|
|
731
781
|
}
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* Resolve the destination path for prebuilt flight payloads in the Nitro
|
|
785
|
+
* output directory. The RSC entry resolves `../prebuilt/` relative to its
|
|
786
|
+
* bundled chunk location (`<serverBundleDir>/_chunks/`), so prebuilt/ must
|
|
787
|
+
* live at `<serverBundleDir>/prebuilt/`.
|
|
788
|
+
*
|
|
789
|
+
* The serverBundleDir varies by preset — Nitro's Vercel preset uses
|
|
790
|
+
* `functions/__server.func` while most others use `server`.
|
|
791
|
+
*
|
|
792
|
+
* @internal Exported for testing.
|
|
793
|
+
*/
|
|
794
|
+
export function resolvePrebuiltOutputPath(outDir: string, preset: NitroPreset): string {
|
|
795
|
+
const presetConfig = PRESET_CONFIGS[preset];
|
|
796
|
+
return join(outDir, presetConfig.outputDir, presetConfig.serverBundleDir, 'prebuilt');
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* Copy prebuilt payloads into cloned Vercel function directories.
|
|
801
|
+
*
|
|
802
|
+
* When users configure `vercel.functionRules`, Nitro's compiled hook clones
|
|
803
|
+
* the base __server.func to per-route .func directories. These clones are
|
|
804
|
+
* created during `runNitroBuild()` — before our post-build prebuilt copy.
|
|
805
|
+
* Each clone resolves `../prebuilt` independently, so they each need a copy.
|
|
806
|
+
*
|
|
807
|
+
* @internal Exported for testing.
|
|
808
|
+
*/
|
|
809
|
+
export async function copyPrebuiltToVercelFuncClones(
|
|
810
|
+
functionsDir: string,
|
|
811
|
+
prebuiltSrc: string,
|
|
812
|
+
baseFuncName: string
|
|
813
|
+
): Promise<void> {
|
|
814
|
+
const funcDirs: string[] = [];
|
|
815
|
+
try {
|
|
816
|
+
for await (const match of glob('**/*.func', { cwd: functionsDir })) {
|
|
817
|
+
if (!match.endsWith(baseFuncName)) {
|
|
818
|
+
funcDirs.push(join(functionsDir, match));
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
} catch {
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
await Promise.all(
|
|
825
|
+
funcDirs.map((dir) =>
|
|
826
|
+
cp(prebuiltSrc, join(dir, 'prebuilt'), { recursive: true }).catch(
|
|
827
|
+
(e: NodeJS.ErrnoException) => {
|
|
828
|
+
if (e.code !== 'ENOENT') throw e;
|
|
829
|
+
}
|
|
830
|
+
)
|
|
831
|
+
)
|
|
832
|
+
);
|
|
833
|
+
}
|
package/src/cli.ts
CHANGED
|
File without changes
|
|
@@ -22,19 +22,71 @@ import { getSearchParamsDefinition } from '../search-params/registry.js';
|
|
|
22
22
|
|
|
23
23
|
// ─── Codec Bridge ─────────────────────────────────────────────────
|
|
24
24
|
|
|
25
|
+
// nuqs's parser contract conflates values timber codecs distinguish:
|
|
26
|
+
// parse() returning null means "unparseable, substitute defaultValue",
|
|
27
|
+
// and undefined entries are skipped entirely. Timber codecs can
|
|
28
|
+
// legitimately produce both — bare z.string() yields undefined for absent
|
|
29
|
+
// params (implicit optionality), and a codec may map a present value to
|
|
30
|
+
// null. Wrap those two values in sentinels across the nuqs boundary and
|
|
31
|
+
// unwrap them before handing values back to the caller, so the client
|
|
32
|
+
// hook returns exactly what server-side parse() returns.
|
|
33
|
+
// Unique object references compared by identity — a codec can never
|
|
34
|
+
// produce these from URL input, so user-controlled strings cannot collide
|
|
35
|
+
// with them (unlike string sentinels), and unlike Symbols they survive
|
|
36
|
+
// nuqs's internal string coercion without throwing.
|
|
37
|
+
const NULL_SENTINEL: object = { timberSentinel: 'null' };
|
|
38
|
+
const UNDEFINED_SENTINEL: object = { timberSentinel: 'undefined' };
|
|
39
|
+
|
|
40
|
+
function wrapNuqsValue(value: unknown): unknown {
|
|
41
|
+
if (value === null) return NULL_SENTINEL;
|
|
42
|
+
if (value === undefined) return UNDEFINED_SENTINEL;
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function unwrapNuqsValue(value: unknown): unknown {
|
|
47
|
+
if (value === NULL_SENTINEL) return null;
|
|
48
|
+
if (value === UNDEFINED_SENTINEL) return undefined;
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
|
|
25
52
|
/**
|
|
26
53
|
* Bridge a timber SearchParamCodec to a nuqs-compatible SingleParser.
|
|
27
54
|
*
|
|
28
55
|
* nuqs parsers: { parse(string) → T|null, serialize?(T) → string, eq?, defaultValue? }
|
|
29
56
|
* timber codecs: { parse(string|string[]|undefined) → T, serialize(T) → string|null }
|
|
57
|
+
*
|
|
58
|
+
* The defaultValue is computed eagerly. Codecs are documented to return a
|
|
59
|
+
* default rather than throw, but a throwing codec must not crash every
|
|
60
|
+
* component that mounts the hook — treat its default as undefined and let
|
|
61
|
+
* its error surface from server-side parse() instead.
|
|
30
62
|
*/
|
|
31
63
|
function bridgeCodec<T>(codec: SearchParamCodec<T>): SingleParser<T> & { defaultValue: T } {
|
|
64
|
+
let defaultValue: unknown;
|
|
65
|
+
try {
|
|
66
|
+
defaultValue = codec.parse(undefined);
|
|
67
|
+
} catch {
|
|
68
|
+
defaultValue = undefined;
|
|
69
|
+
}
|
|
32
70
|
return {
|
|
33
|
-
parse: (v: string) => codec.parse(v),
|
|
34
|
-
serialize: (v:
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
71
|
+
parse: (v: string) => wrapNuqsValue(codec.parse(v)),
|
|
72
|
+
serialize: (v: unknown) => {
|
|
73
|
+
const value = unwrapNuqsValue(v);
|
|
74
|
+
// Delegate null to the codec — some codecs encode null as a real
|
|
75
|
+
// query value. undefined has no encoding; nuqs requires a string.
|
|
76
|
+
return value === undefined ? '' : (codec.serialize(value as T) ?? '');
|
|
77
|
+
},
|
|
78
|
+
defaultValue: wrapNuqsValue(defaultValue),
|
|
79
|
+
eq: (a: unknown, b: unknown) => {
|
|
80
|
+
if (a === b) return true;
|
|
81
|
+
try {
|
|
82
|
+
return (
|
|
83
|
+
codec.serialize(unwrapNuqsValue(a) as T) === codec.serialize(unwrapNuqsValue(b) as T)
|
|
84
|
+
);
|
|
85
|
+
} catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
} as SingleParser<T> & { defaultValue: T };
|
|
38
90
|
}
|
|
39
91
|
|
|
40
92
|
/**
|
|
@@ -124,6 +176,19 @@ export function useQueryStates<T extends Record<string, unknown>>(
|
|
|
124
176
|
throw err;
|
|
125
177
|
}
|
|
126
178
|
|
|
179
|
+
// Unwrap the null/undefined sentinels the bridge injected (see Codec
|
|
180
|
+
// Bridge above) so callers see exactly what server-side parse() returns.
|
|
181
|
+
// Copy-on-write preserves the identity of nuqs's memoized values object
|
|
182
|
+
// when nothing needs unwrapping.
|
|
183
|
+
let normalized = values;
|
|
184
|
+
for (const key of Object.keys(bridged)) {
|
|
185
|
+
const value = normalized[key];
|
|
186
|
+
if (value === NULL_SENTINEL || value === UNDEFINED_SENTINEL) {
|
|
187
|
+
if (normalized === values) normalized = { ...values };
|
|
188
|
+
normalized[key] = unwrapNuqsValue(value);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
127
192
|
// Wrap the nuqs setter to match timber's SetParams<T> signature.
|
|
128
193
|
// nuqs's setter accepts Partial<Nullable<Values>> | UpdaterFn | null.
|
|
129
194
|
// timber's setter accepts Partial<T> with optional SetParamsOptions.
|
|
@@ -132,11 +197,38 @@ export function useQueryStates<T extends Record<string, unknown>>(
|
|
|
132
197
|
if (setOptions?.shallow !== undefined) nuqsSetOptions.shallow = setOptions.shallow;
|
|
133
198
|
if (setOptions?.scroll !== undefined) nuqsSetOptions.scroll = setOptions.scroll;
|
|
134
199
|
if (setOptions?.history !== undefined) nuqsSetOptions.history = setOptions.history;
|
|
200
|
+
// nuqs's update loop skips undefined entries and treats null as a
|
|
201
|
+
// key deletion before serialize runs. Timber semantics:
|
|
202
|
+
// - setParams({ q: undefined }) must clear ?q= (absent = undefined),
|
|
203
|
+
// so explicit undefined maps to a null deletion.
|
|
204
|
+
// - setParams({ q: null }) clears the key only when the codec encodes
|
|
205
|
+
// null as "omit" (serialize(null) === null). If the codec encodes
|
|
206
|
+
// null as a real query value, forward the sentinel so the bridged
|
|
207
|
+
// serialize writes it — matching definition.serialize({ q: null }).
|
|
208
|
+
let forwarded: Record<string, unknown> = partial;
|
|
209
|
+
for (const key of Object.keys(partial)) {
|
|
210
|
+
const value = partial[key as keyof T];
|
|
211
|
+
if (value === undefined) {
|
|
212
|
+
if (forwarded === partial) forwarded = { ...partial };
|
|
213
|
+
forwarded[key] = null;
|
|
214
|
+
} else if (value === null) {
|
|
215
|
+
let encoded: string | null = null;
|
|
216
|
+
try {
|
|
217
|
+
encoded = codecs[key as keyof T]?.serialize(null as T[keyof T]) ?? null;
|
|
218
|
+
} catch {
|
|
219
|
+
// Codec can't serialize null — treat as a deletion.
|
|
220
|
+
}
|
|
221
|
+
if (encoded !== null) {
|
|
222
|
+
if (forwarded === partial) forwarded = { ...partial };
|
|
223
|
+
forwarded[key] = NULL_SENTINEL;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
135
227
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
136
|
-
void setValues(
|
|
228
|
+
void setValues(forwarded as any, nuqsSetOptions);
|
|
137
229
|
};
|
|
138
230
|
|
|
139
|
-
return [
|
|
231
|
+
return [normalized as T, setParams];
|
|
140
232
|
}
|
|
141
233
|
|
|
142
234
|
// ─── Definition binding ───────────────────────────────────────────
|
package/src/dev-tools/logs.ts
CHANGED
|
@@ -12,9 +12,86 @@
|
|
|
12
12
|
* Design docs: 18-build-system.md §"Dev Server", 02-rendering-pipeline.md
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import type { Plugin, ViteDevServer } from 'vite';
|
|
15
|
+
import type { Plugin, ViteDevServer, Logger } from 'vite';
|
|
16
16
|
import type { PluginContext } from '../plugin-context.js';
|
|
17
17
|
|
|
18
|
+
// ─── Original Console Snapshot ──────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Snapshot of the original console methods, captured at module load time
|
|
22
|
+
* (before any patching). Used as Vite's `customLogger` console so that
|
|
23
|
+
* forwardConsole output bypasses our patch entirely — eliminating the
|
|
24
|
+
* server→browser→server forwarding loop without string matching.
|
|
25
|
+
*/
|
|
26
|
+
export const originalConsole: Pick<Console, 'log' | 'warn' | 'error' | 'debug' | 'info'> = {
|
|
27
|
+
log: console.log.bind(console),
|
|
28
|
+
warn: console.warn.bind(console),
|
|
29
|
+
error: console.error.bind(console),
|
|
30
|
+
debug: console.debug.bind(console),
|
|
31
|
+
info: console.info.bind(console),
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Wrap a Vite Logger so its output methods route through `originalConsole`
|
|
36
|
+
* instead of the global (potentially patched) console. This prevents the
|
|
37
|
+
* server→browser→server forwarding loop even when the user supplies their
|
|
38
|
+
* own `customLogger` that internally calls console methods.
|
|
39
|
+
*/
|
|
40
|
+
export function wrapLoggerWithOriginalConsole(logger: Logger): Logger {
|
|
41
|
+
// Swap all patched console methods to originals during any logger call,
|
|
42
|
+
// since user loggers may call any combination of console methods internally.
|
|
43
|
+
function withOriginals<T>(fn: () => T): T {
|
|
44
|
+
const saved = {
|
|
45
|
+
log: console.log,
|
|
46
|
+
warn: console.warn,
|
|
47
|
+
error: console.error,
|
|
48
|
+
debug: console.debug,
|
|
49
|
+
info: console.info,
|
|
50
|
+
};
|
|
51
|
+
console.log = originalConsole.log;
|
|
52
|
+
console.warn = originalConsole.warn;
|
|
53
|
+
console.error = originalConsole.error;
|
|
54
|
+
console.debug = originalConsole.debug;
|
|
55
|
+
console.info = originalConsole.info;
|
|
56
|
+
try {
|
|
57
|
+
return fn();
|
|
58
|
+
} finally {
|
|
59
|
+
console.log = saved.log;
|
|
60
|
+
console.warn = saved.warn;
|
|
61
|
+
console.error = saved.error;
|
|
62
|
+
console.debug = saved.debug;
|
|
63
|
+
console.info = saved.info;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
get hasWarned() {
|
|
69
|
+
return logger.hasWarned;
|
|
70
|
+
},
|
|
71
|
+
set hasWarned(v) {
|
|
72
|
+
logger.hasWarned = v;
|
|
73
|
+
},
|
|
74
|
+
info(msg, opts) {
|
|
75
|
+
withOriginals(() => logger.info(msg, opts));
|
|
76
|
+
},
|
|
77
|
+
warn(msg, opts) {
|
|
78
|
+
withOriginals(() => logger.warn(msg, opts));
|
|
79
|
+
},
|
|
80
|
+
warnOnce(msg, opts) {
|
|
81
|
+
withOriginals(() => logger.warnOnce(msg, opts));
|
|
82
|
+
},
|
|
83
|
+
error(msg, opts) {
|
|
84
|
+
withOriginals(() => logger.error(msg, opts));
|
|
85
|
+
},
|
|
86
|
+
clearScreen(type) {
|
|
87
|
+
logger.clearScreen(type);
|
|
88
|
+
},
|
|
89
|
+
hasErrorLogged(error) {
|
|
90
|
+
return logger.hasErrorLogged(error);
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
18
95
|
// ─── Types ───────────────────────────────────────────────────────────────
|
|
19
96
|
|
|
20
97
|
/** Log levels that are patched and forwarded. */
|
|
@@ -215,50 +292,58 @@ export function isFrameworkInternalCaller(): boolean {
|
|
|
215
292
|
* 3. Sends via server.hot.send() — dropped if no clients connected
|
|
216
293
|
*/
|
|
217
294
|
function patchConsole(server: ViteDevServer, projectRoot: string): () => void {
|
|
218
|
-
const
|
|
295
|
+
const prevConsole = new Map<ServerLogLevel, (...args: unknown[]) => void>();
|
|
296
|
+
// Per-level guard so a console.warn inside a console.error handler isn't
|
|
297
|
+
// suppressed — only same-level synchronous re-entrancy is blocked.
|
|
298
|
+
const patchingLevel = new Set<ServerLogLevel>();
|
|
219
299
|
|
|
220
300
|
for (const level of LOG_LEVELS) {
|
|
221
|
-
|
|
301
|
+
prevConsole.set(level, console[level].bind(console));
|
|
222
302
|
|
|
223
303
|
console[level] = (...args: unknown[]) => {
|
|
224
|
-
//
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
// Break the server→browser→server console forwarding loop. Vite's
|
|
232
|
-
// forwardConsole re-logs browser console output on the server via
|
|
233
|
-
// `logger.error("[console.error] ...")`. Our patch would send it back
|
|
234
|
-
// to the browser, Vite's client patch forwards it again → exponential
|
|
235
|
-
// log spam. Detect the `[console.<level>]` prefix Vite adds and skip.
|
|
236
|
-
if (typeof args[0] === 'string' && args[0].includes('[console.')) return;
|
|
304
|
+
// Same-level re-entrancy guard: if prevConsole's wrapper calls back
|
|
305
|
+
// into console[level], just pass through without forwarding.
|
|
306
|
+
if (patchingLevel.has(level)) {
|
|
307
|
+
prevConsole.get(level)!(...args);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
237
310
|
|
|
238
|
-
|
|
311
|
+
patchingLevel.add(level);
|
|
239
312
|
try {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
313
|
+
// Call the previous console method first — guarantees terminal output
|
|
314
|
+
// even if serialization or forwarding fails downstream. If prevConsole
|
|
315
|
+
// throws, the error propagates (not swallowed) but patchingLevel is
|
|
316
|
+
// still cleaned up by the outer finally.
|
|
317
|
+
prevConsole.get(level)!(...args);
|
|
318
|
+
|
|
319
|
+
// Skip framework-internal logs (plugins/, adapters/) from browser forwarding.
|
|
320
|
+
if (isFrameworkInternalCaller()) return;
|
|
321
|
+
|
|
322
|
+
try {
|
|
323
|
+
const payload: ServerLogPayload = {
|
|
324
|
+
level,
|
|
325
|
+
args: args.map((arg) => serializeArg(arg)),
|
|
326
|
+
location: extractCallerLocation(projectRoot),
|
|
327
|
+
timestamp: Date.now(),
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
server.hot.send('timber:server-log', payload);
|
|
331
|
+
} catch (e) {
|
|
332
|
+
originalConsole.debug(
|
|
333
|
+
'[timber] server log forwarding failed:',
|
|
334
|
+
e instanceof Error ? e.message : e
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
} finally {
|
|
338
|
+
patchingLevel.delete(level);
|
|
254
339
|
}
|
|
255
340
|
};
|
|
256
341
|
}
|
|
257
342
|
|
|
258
|
-
// Return a cleanup function to restore
|
|
343
|
+
// Return a cleanup function to restore the previous console methods
|
|
259
344
|
return () => {
|
|
260
|
-
for (const [level,
|
|
261
|
-
console[level] =
|
|
345
|
+
for (const [level, prev] of prevConsole) {
|
|
346
|
+
console[level] = prev;
|
|
262
347
|
}
|
|
263
348
|
};
|
|
264
349
|
}
|
package/src/index.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import type { Plugin, PluginOption } from 'vite';
|
|
14
|
+
import { createLogger } from 'vite';
|
|
14
15
|
import { join, relative, resolve } from 'node:path';
|
|
15
16
|
import { createRequire } from 'node:module';
|
|
16
17
|
import react, { reactCompilerPreset } from '@vitejs/plugin-react';
|
|
@@ -23,7 +24,7 @@ import { timberShims } from './plugins/shims';
|
|
|
23
24
|
import { timberFonts } from './plugins/fonts';
|
|
24
25
|
import { timberStaticBuild } from './plugins/static-build';
|
|
25
26
|
import { timberBuildManifest } from './plugins/build-manifest';
|
|
26
|
-
import { timberDevLogs } from './dev-tools/logs';
|
|
27
|
+
import { timberDevLogs, originalConsole, wrapLoggerWithOriginalConsole } from './dev-tools/logs';
|
|
27
28
|
import { resolveForwardConsole } from './dev-tools/browser-logs';
|
|
28
29
|
import { timberReactProd } from './plugins/react-prod';
|
|
29
30
|
import { timberChunks } from './plugins/chunks';
|
|
@@ -438,10 +439,22 @@ export function timber(config?: TimberUserConfig): PluginOption[] {
|
|
|
438
439
|
// on the devBrowserLogs config (default: 'warn'). See TIM-1152.
|
|
439
440
|
serverConfig.forwardConsole = resolveForwardConsole(ctx.config.devBrowserLogs);
|
|
440
441
|
|
|
442
|
+
// Provide a customLogger whose output routes through the pre-patched
|
|
443
|
+
// console methods so Vite's forwardConsole output never re-enters our
|
|
444
|
+
// server→browser patch. If the user supplied their own customLogger,
|
|
445
|
+
// wrap it so its console calls also bypass patching.
|
|
446
|
+
const customLogger = userConfig.customLogger
|
|
447
|
+
? wrapLoggerWithOriginalConsole(userConfig.customLogger)
|
|
448
|
+
: createLogger(userConfig.logLevel ?? 'info', {
|
|
449
|
+
allowClearScreen: userConfig.clearScreen ?? true,
|
|
450
|
+
console: originalConsole as Console,
|
|
451
|
+
});
|
|
452
|
+
|
|
441
453
|
return {
|
|
442
454
|
build: { outDir: buildOutDir },
|
|
443
455
|
environments: envOutDirs,
|
|
444
456
|
optimizeDeps: { exclude: timberOptimizeDepsExclude },
|
|
457
|
+
customLogger,
|
|
445
458
|
server: serverConfig,
|
|
446
459
|
preview: previewConfig,
|
|
447
460
|
};
|
package/src/schema-bridge.ts
CHANGED
|
@@ -184,9 +184,10 @@ export function fromSchema<T>(schema: StandardSchemaV1<T>): Codec<T> {
|
|
|
184
184
|
return defaultResult.value;
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
-
// No default available —
|
|
188
|
-
//
|
|
189
|
-
// via
|
|
187
|
+
// No default available — the field is implicitly optional. Return
|
|
188
|
+
// undefined; defineSearchParams widens the field's inferred type to
|
|
189
|
+
// T | undefined via InferField so this doesn't lie.
|
|
190
|
+
// design/23-search-params.md §"Implicit Optionality"
|
|
190
191
|
return undefined as T;
|
|
191
192
|
},
|
|
192
193
|
|