@timber-js/app 0.2.0-alpha.80 → 0.2.0-alpha.81

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.
Files changed (39) hide show
  1. package/dist/_chunks/{stale-reload-C2plcNtG.js → stale-reload-BX5gL1r-.js} +1 -1
  2. package/dist/_chunks/{stale-reload-C2plcNtG.js.map → stale-reload-BX5gL1r-.js.map} +1 -1
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +2 -2
  5. package/dist/cli.js.map +1 -1
  6. package/dist/client/form.d.ts +2 -2
  7. package/dist/client/form.d.ts.map +1 -1
  8. package/dist/client/index.js.map +1 -1
  9. package/dist/client/internal.js +1 -1
  10. package/dist/config-types.d.ts +17 -0
  11. package/dist/config-types.d.ts.map +1 -1
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +40 -3
  14. package/dist/index.js.map +1 -1
  15. package/dist/plugin-context.d.ts +23 -0
  16. package/dist/plugin-context.d.ts.map +1 -1
  17. package/dist/search-params/index.js +62 -1
  18. package/dist/search-params/index.js.map +1 -0
  19. package/dist/segment-params/index.d.ts +0 -3
  20. package/dist/segment-params/index.d.ts.map +1 -1
  21. package/dist/segment-params/index.js +1 -3
  22. package/dist/server/action-client.d.ts +14 -6
  23. package/dist/server/action-client.d.ts.map +1 -1
  24. package/dist/server/index.d.ts +0 -1
  25. package/dist/server/index.d.ts.map +1 -1
  26. package/dist/server/index.js +1 -2
  27. package/dist/server/index.js.map +1 -1
  28. package/package.json +5 -1
  29. package/src/cli.ts +10 -5
  30. package/src/client/form.tsx +8 -6
  31. package/src/config-types.ts +17 -0
  32. package/src/index.ts +37 -0
  33. package/src/plugin-context.ts +40 -0
  34. package/src/plugins/adapter-build.ts +1 -1
  35. package/src/segment-params/index.ts +3 -23
  36. package/src/server/action-client.ts +26 -10
  37. package/src/server/index.ts +2 -3
  38. package/dist/_chunks/wrappers-_DTmImGt.js +0 -63
  39. package/dist/_chunks/wrappers-_DTmImGt.js.map +0 -1
package/src/index.ts CHANGED
@@ -43,6 +43,8 @@ import {
43
43
  mergeFileConfig,
44
44
  resolveAppDir,
45
45
  resolveClientJavascript,
46
+ resolveBuildDir,
47
+ DEFAULT_BUILD_DIR,
46
48
  } from './plugin-context.js';
47
49
 
48
50
  // ── Public API ────────────────────────────────────────────────────────────
@@ -247,6 +249,15 @@ export function timber(config?: TimberUserConfig): PluginOption[] {
247
249
  ctx.config.output ??= 'server';
248
250
  ctx.timer.end('config-load');
249
251
 
252
+ // ── Resolve build output directory ─────────────────────────
253
+ // Priority: timber.config.ts `buildDir` > Vite `build.outDir` > .timber/dist
254
+ // Set Vite's build.outDir so the RSC plugin and all environment
255
+ // builds write to the correct location.
256
+ const viteOutDir = userConfig.build?.outDir;
257
+ const timberBuildDir = ctx.config.buildDir;
258
+ const resolvedRoot = resolve(userConfig.root ?? process.cwd());
259
+ ctx.buildDir = resolveBuildDir(resolvedRoot, timberBuildDir, viteOutDir);
260
+
250
261
  // Warn if the Vite root differs from cwd and the re-loaded config
251
262
  // has reactCompiler but the early cwd-based load missed it. In this
252
263
  // edge case the user must move reactCompiler to inline config.
@@ -308,8 +319,28 @@ export function timber(config?: TimberUserConfig): PluginOption[] {
308
319
  }
309
320
  }
310
321
 
322
+ // Return the resolved buildDir as Vite's build.outDir so the RSC
323
+ // plugin and all environment builds write to the correct location.
324
+ // This is always set — either from timber config, Vite config, or default.
325
+ const buildOutDir = timberBuildDir
326
+ ? timberBuildDir
327
+ : viteOutDir && viteOutDir !== 'dist'
328
+ ? viteOutDir
329
+ : DEFAULT_BUILD_DIR;
330
+
331
+ // Set per-environment build.outDir so Vite's RSC plugin writes
332
+ // each environment to the correct subdirectory under our buildDir.
333
+ // Without this, the RSC plugin defaults to dist/<envName> instead
334
+ // of <buildOutDir>/<envName>.
335
+ const envOutDirs: Record<string, { build: { outDir: string } }> = {};
336
+ for (const envName of ['rsc', 'ssr', 'client']) {
337
+ envOutDirs[envName] = { build: { outDir: join(buildOutDir, envName) } };
338
+ }
339
+
311
340
  if (command === 'build') {
312
341
  return {
342
+ build: { outDir: buildOutDir },
343
+ environments: envOutDirs,
313
344
  oxc: {
314
345
  jsx: {
315
346
  development: false,
@@ -317,11 +348,17 @@ export function timber(config?: TimberUserConfig): PluginOption[] {
317
348
  },
318
349
  };
319
350
  }
351
+
352
+ // Dev mode: set outDir so dev-time references are consistent
353
+ return { build: { outDir: buildOutDir }, environments: envOutDirs };
320
354
  },
321
355
  configResolved(resolved) {
322
356
  ctx.root = resolved.root;
323
357
  ctx.appDir = resolveAppDir(resolved.root, ctx.config.appDir);
324
358
  ctx.dev = resolved.command === 'serve';
359
+ // Sync buildDir from Vite's resolved build.outDir. Vite keeps
360
+ // outDir as-is (may be relative), so resolve against root.
361
+ ctx.buildDir = resolve(resolved.root, resolved.build.outDir);
325
362
  // In production builds, swap to a no-op timer to avoid overhead
326
363
  if (!ctx.dev) {
327
364
  ctx.timer = createNoopTimer();
@@ -77,6 +77,16 @@ export interface PluginContext {
77
77
  timer: StartupTimer;
78
78
  /** Holding server that binds the port during dev startup (closed in timber-dev-server) */
79
79
  holdingServer?: HoldingServer | null;
80
+ /**
81
+ * Resolved absolute path to the build output directory.
82
+ *
83
+ * Defaults to `<root>/.timber/dist`. Can be overridden via
84
+ * `timber.config.ts` `buildDir` or Vite's `build.outDir`.
85
+ * Timber config takes precedence when both are set.
86
+ *
87
+ * Used by adapter-build and cli preview.
88
+ */
89
+ buildDir: string;
80
90
  }
81
91
 
82
92
  // ── App directory resolution ──────────────────────────────────────────────
@@ -126,9 +136,39 @@ export function createPluginContext(config?: TimberUserConfig, root?: string): P
126
136
  deploymentId: null,
127
137
  timer: createStartupTimer(),
128
138
  holdingServer: null,
139
+ buildDir: resolveBuildDir(projectRoot, resolvedConfig.buildDir),
129
140
  };
130
141
  }
131
142
 
143
+ // ── Build directory resolution ────────────────────────────────────────────
144
+
145
+ /** Default build output directory (relative to root). */
146
+ export const DEFAULT_BUILD_DIR = join('.timber', 'dist');
147
+
148
+ /**
149
+ * Resolve the build output directory.
150
+ *
151
+ * Priority:
152
+ * 1. Explicit `timberBuildDir` from timber.config.ts
153
+ * 2. Explicit `viteBuildOutDir` from Vite's build.outDir (if not the default 'dist')
154
+ * 3. Default: `.timber/dist`
155
+ *
156
+ * Returns an absolute path.
157
+ */
158
+ export function resolveBuildDir(
159
+ root: string,
160
+ timberBuildDir?: string,
161
+ viteBuildOutDir?: string
162
+ ): string {
163
+ if (timberBuildDir) {
164
+ return join(root, timberBuildDir);
165
+ }
166
+ if (viteBuildOutDir && viteBuildOutDir !== 'dist') {
167
+ return join(root, viteBuildOutDir);
168
+ }
169
+ return join(root, DEFAULT_BUILD_DIR);
170
+ }
171
+
132
172
  // ── Config file loading ───────────────────────────────────────────────────
133
173
 
134
174
  /**
@@ -35,7 +35,7 @@ export function timberAdapterBuild(ctx: PluginContext): Plugin {
35
35
  const adapter = ctx.config.adapter as TimberPlatformAdapter | undefined;
36
36
  if (!adapter || typeof adapter.buildOutput !== 'function') return;
37
37
 
38
- const buildDir = join(ctx.root, 'dist');
38
+ const buildDir = ctx.buildDir;
39
39
 
40
40
  // Serialize the build manifest as a JS module that sets the global.
41
41
  // The adapter writes this as _timber-manifest-init.mjs and imports it
@@ -1,29 +1,9 @@
1
- // @timber-js/app/segment-params — Typed route param coercion and search param definitions
1
+ // @timber-js/app/segment-params — Typed route param coercion
2
2
  //
3
- // This is the primary import path for both segmentParams and searchParams.
4
- // params.ts convention files import from here.
3
+ // This is the import path for defineSegmentParams and related types.
4
+ // For search params, import from @timber-js/app/search-params instead.
5
5
  //
6
6
  // See design/07-routing.md §"params.ts Convention File"
7
7
 
8
- // --- Segment params (route path param coercion) ---
9
8
  export type { ParamsDefinition, InferParamField, ParamField } from './define.js';
10
9
  export { defineSegmentParams } from './define.js';
11
-
12
- // --- Search params (re-exported from search-params for convenience) ---
13
- // This lets params.ts import both from a single path:
14
- // import { defineSegmentParams, defineSearchParams } from '@timber-js/app/segment-params'
15
- export { defineSearchParams } from '../search-params/define.js';
16
-
17
- // Codec bridges moved to @timber-js/app/codec
18
- // Import fromSchema / fromArraySchema from '@timber-js/app/codec' instead.
19
- export { withDefault, withUrlKey } from '../search-params/wrappers.js';
20
- // Codec is the canonical home for the Codec type — import from
21
- // @timber-js/app/codec. Re-export removed per TIM-721.
22
- export type {
23
- SearchParamCodec,
24
- SearchParamsDefinition,
25
- SetParams,
26
- SetParamsOptions,
27
- QueryStatesOptions,
28
- CodecMap,
29
- } from '../search-params/define.js';
@@ -138,13 +138,15 @@ export interface ActionBuilder<TCtx> {
138
138
  /** Declare the input schema. Validation errors are returned typed. */
139
139
  schema<TInput>(schema: ActionSchema<TInput>): ActionBuilderWithSchema<TCtx, TInput>;
140
140
  /** Define the action body without input validation. */
141
- action<TData>(fn: (ctx: ActionContext<TCtx, undefined>) => Promise<TData>): ActionFn<TData>;
141
+ action<TData>(
142
+ fn: (ctx: ActionContext<TCtx, undefined>) => Promise<TData>
143
+ ): ActionFn<undefined, TData>;
142
144
  }
143
145
 
144
146
  /** Builder after .schema() has been called. */
145
147
  export interface ActionBuilderWithSchema<TCtx, TInput> {
146
148
  /** Define the action body with validated input. */
147
- action<TData>(fn: (ctx: ActionContext<TCtx, TInput>) => Promise<TData>): ActionFn<TData>;
149
+ action<TData>(fn: (ctx: ActionContext<TCtx, TInput>) => Promise<TData>): ActionFn<TInput, TData>;
148
150
  }
149
151
 
150
152
  /**
@@ -159,11 +161,21 @@ export interface ActionBuilderWithSchema<TCtx, TInput> {
159
161
  * discards it. This lets validated actions be passed directly to forms
160
162
  * without casts.
161
163
  */
162
- export type ActionFn<TData> = {
164
+ /**
165
+ * Map schema output keys to `string | undefined` for form-facing APIs.
166
+ * HTML form values are always strings, and fields can be absent.
167
+ * Gives autocomplete for field names without lying about value types.
168
+ */
169
+ export type InputHint<T> =
170
+ T extends Record<string, unknown> ? { [K in keyof T]: string | undefined } : T;
171
+
172
+ export type ActionFn<TInput = unknown, TData = unknown> = {
163
173
  /** <form action={fn}> compatibility — React discards the return value. */
164
174
  (formData: FormData): void;
165
- /** Direct call: action(input) */
166
- (input?: unknown): Promise<ActionResult<TData>>;
175
+ /** Direct call: action(input) — optional when TInput is undefined (no-schema actions). */
176
+ (
177
+ ...args: undefined extends TInput ? [input?: TInput] : [input: TInput]
178
+ ): Promise<ActionResult<TData>>;
167
179
  /** React useActionState: action(prevState, formData) */
168
180
  (prevState: ActionResult<TData> | null, formData: FormData): Promise<ActionResult<TData>>;
169
181
  };
@@ -298,7 +310,7 @@ export function createActionClient<TCtx = Record<string, never>>(
298
310
  function buildAction<TInput, TData>(
299
311
  schema: ActionSchema<TInput> | undefined,
300
312
  fn: (ctx: ActionContext<TCtx, TInput>) => Promise<TData>
301
- ): ActionFn<TData> {
313
+ ): ActionFn<TInput, TData> {
302
314
  async function actionHandler(...args: unknown[]): Promise<ActionResult<TData>> {
303
315
  try {
304
316
  // Run middleware
@@ -389,18 +401,22 @@ export function createActionClient<TCtx = Record<string, never>>(
389
401
  }
390
402
  }
391
403
 
392
- return actionHandler as ActionFn<TData>;
404
+ return actionHandler as ActionFn<TInput, TData>;
393
405
  }
394
406
 
395
407
  return {
396
408
  schema<TInput>(schema: ActionSchema<TInput>) {
397
409
  return {
398
- action<TData>(fn: (ctx: ActionContext<TCtx, TInput>) => Promise<TData>): ActionFn<TData> {
410
+ action<TData>(
411
+ fn: (ctx: ActionContext<TCtx, TInput>) => Promise<TData>
412
+ ): ActionFn<TInput, TData> {
399
413
  return buildAction(schema, fn);
400
414
  },
401
415
  };
402
416
  },
403
- action<TData>(fn: (ctx: ActionContext<TCtx, undefined>) => Promise<TData>): ActionFn<TData> {
417
+ action<TData>(
418
+ fn: (ctx: ActionContext<TCtx, undefined>) => Promise<TData>
419
+ ): ActionFn<undefined, TData> {
404
420
  return buildAction(undefined, fn as (ctx: ActionContext<TCtx, unknown>) => Promise<TData>);
405
421
  },
406
422
  };
@@ -429,7 +445,7 @@ export function createActionClient<TCtx = Record<string, never>>(
429
445
  export function validated<TInput, TData>(
430
446
  schema: ActionSchema<TInput>,
431
447
  handler: (input: TInput) => Promise<TData>
432
- ): ActionFn<TData> {
448
+ ): ActionFn<TInput, TData> {
433
449
  return createActionClient()
434
450
  .schema(schema)
435
451
  .action(async ({ input }) => handler(input));
@@ -66,7 +66,6 @@ export { revalidatePath, revalidateTag } from './actions';
66
66
  // Design doc: design/17-logging.md §"trace_id is Always Set"
67
67
  export { getTraceId, getSpanId, withSpan, addSpanEvent } from './tracing';
68
68
 
69
- // Segment params — typed route param coercion for params.ts convention files.
70
- // Moved from @timber-js/app/segment-params to here per TIM-723.
71
- export { defineSegmentParams } from '../segment-params/define.js';
69
+ // Segment params types re-exported for convenience.
70
+ // defineSegmentParams itself lives at @timber-js/app/segment-params.
72
71
  export type { ParamsDefinition, InferParamField, ParamField } from '../segment-params/define.js';
@@ -1,63 +0,0 @@
1
- //#region src/search-params/wrappers.ts
2
- /**
3
- * Wrap a nullable codec with a default value. When the inner codec returns
4
- * null, the default is used instead. The output type becomes non-nullable.
5
- *
6
- * Works with any codec — nuqs parsers, custom codecs, fromSchema results.
7
- *
8
- * ```ts
9
- * import { parseAsInteger } from 'nuqs'
10
- * import { withDefault } from '@timber-js/app/search-params'
11
- *
12
- * const page = withDefault(parseAsInteger, 1)
13
- * // page.parse(undefined) → 1 (not null)
14
- * // page.parse('5') → 5
15
- * ```
16
- */
17
- function withDefault(codec, defaultValue) {
18
- return {
19
- parse(value) {
20
- const result = codec.parse(value);
21
- return result === null ? defaultValue : result;
22
- },
23
- serialize(value) {
24
- return codec.serialize(value);
25
- }
26
- };
27
- }
28
- /**
29
- * Attach a URL key alias to a codec. The alias determines what query
30
- * parameter key is used in the URL, while the TypeScript property name
31
- * stays descriptive.
32
- *
33
- * Aliases travel with codecs through object spread composition — when
34
- * you spread a bundle containing aliased codecs into defineSearchParams,
35
- * the aliases come along automatically.
36
- *
37
- * ```ts
38
- * import { parseAsString } from 'nuqs'
39
- * import { withUrlKey } from '@timber-js/app/search-params'
40
- *
41
- * export const searchable = {
42
- * q: withUrlKey(parseAsString, 'search'),
43
- * // ?search=shoes → { q: 'shoes' }
44
- * }
45
- * ```
46
- *
47
- * Composes with withDefault:
48
- * ```ts
49
- * import { parseAsInteger } from 'nuqs'
50
- * withUrlKey(withDefault(parseAsInteger, 1), 'p')
51
- * ```
52
- */
53
- function withUrlKey(codec, urlKey) {
54
- return {
55
- parse: codec.parse.bind(codec),
56
- serialize: codec.serialize.bind(codec),
57
- urlKey
58
- };
59
- }
60
- //#endregion
61
- export { withUrlKey as n, withDefault as t };
62
-
63
- //# sourceMappingURL=wrappers-_DTmImGt.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"wrappers-_DTmImGt.js","names":[],"sources":["../../src/search-params/wrappers.ts"],"sourcesContent":["/**\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":";;;;;;;;;;;;;;;;AA+BA,SAAgB,YACd,OACA,cACqB;AACrB,QAAO;EACL,MAAM,OAAyC;GAC7C,MAAM,SAAS,MAAM,MAAM,MAAM;AACjC,UAAO,WAAW,OAAO,eAAe;;EAE1C,UAAU,OAAyB;AACjC,UAAO,MAAM,UAAU,MAAM;;EAEhC;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCH,SAAgB,WACd,OACA,QAC+B;AAC/B,QAAO;EACL,OAAO,MAAM,MAAM,KAAK,MAAM;EAC9B,WAAW,MAAM,UAAU,KAAK,MAAM;EACtC;EACD"}