@formisch/preact 0.9.5 → 0.9.6

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/index.d.ts CHANGED
@@ -4,20 +4,20 @@ import { JSX } from "preact";
4
4
 
5
5
  //#region ../../packages/core/dist/index.preact.d.ts
6
6
 
7
- //#region src/types/schema.d.ts
7
+ //#region src/types/schema/schema.d.ts
8
8
  /**
9
9
  * Schema type.
10
10
  */
11
11
  type Schema = v.GenericSchema | v.GenericSchemaAsync;
12
12
  //#endregion
13
- //#region src/types/signal.d.ts
13
+ //#region src/types/signal/signal.d.ts
14
14
 
15
15
  /**
16
16
  * Batch interface.
17
17
  */
18
18
 
19
19
  //#endregion
20
- //#region src/types/field.d.ts
20
+ //#region src/types/field/field.d.ts
21
21
  /**
22
22
  * Field element type.
23
23
  */
@@ -183,7 +183,7 @@ type InternalFieldStore = InternalArrayStore | InternalObjectStore | InternalVal
183
183
  */
184
184
  declare const INTERNAL: "~internal";
185
185
  //#endregion
186
- //#region src/types/utils.d.ts
186
+ //#region src/types/utils/utils.d.ts
187
187
  /**
188
188
  * Checks if a type is `any`.
189
189
  */
@@ -209,7 +209,7 @@ type DeepPartial<TValue> = TValue extends Record<PropertyKey, unknown> | readonl
209
209
  */
210
210
  type PartialValues<TValue> = TValue extends readonly (infer TItem)[] ? number extends TValue["length"] ? (TItem extends Record<PropertyKey, unknown> | readonly unknown[] ? { [TKey in keyof TItem]: PartialValues<TItem[TKey]> } : TItem)[] : { [TKey in keyof TValue]: PartialValues<TValue[TKey]> } : TValue extends Record<PropertyKey, unknown> ? { [TKey in keyof TValue]: PartialValues<TValue[TKey]> } : TValue | undefined;
211
211
  //#endregion
212
- //#region src/types/form.d.ts
212
+ //#region src/types/form/form.d.ts
213
213
  /**
214
214
  * Validation mode type.
215
215
  */
@@ -292,7 +292,7 @@ type SubmitHandler<TSchema extends Schema> = (output: v.InferOutput<TSchema>) =>
292
292
  */
293
293
  type SubmitEventHandler<TSchema extends Schema> = (output: v.InferOutput<TSchema>, event: SubmitEvent) => MaybePromise<unknown>;
294
294
  //#endregion
295
- //#region src/types/path.d.ts
295
+ //#region src/types/path/path.d.ts
296
296
  /**
297
297
  * Path key type.
298
298
  */
@@ -306,42 +306,88 @@ type Path = readonly PathKey[];
306
306
  */
307
307
  type RequiredPath = readonly [PathKey, ...Path];
308
308
  /**
309
- * Extracts the exact keys of a tuple, array or object.
309
+ * Extracts the exact keys of a tuple, array or object. Tuples return their
310
+ * literal numeric indices, dynamic arrays return `number`, objects return
311
+ * their `keyof` keys, and any other input returns `never`.
310
312
  */
311
- type KeyOf<TValue> = IsAny<TValue> extends true ? never : TValue extends readonly unknown[] ? number extends TValue["length"] ? number : { [TKey in keyof TValue]: TKey extends `${infer TIndex extends number}` ? TIndex : never }[number] : TValue extends Record<string, unknown> ? keyof TValue & PathKey : never;
313
+ type ExactKeysOf<TValue> = IsAny<TValue> extends true ? never : TValue extends readonly unknown[] ? number extends TValue["length"] ? number : { [TKey in keyof TValue]: TKey extends `${infer TIndex extends number}` ? TIndex : never }[number] : TValue extends Record<PropertyKey, unknown> ? keyof TValue & PathKey : never;
312
314
  /**
313
- * Merges array and object unions into a single object.
315
+ * Returns the flat object of all indexable properties of `TValue`. For object
316
+ * unions, properties from every member are merged so that any single property
317
+ * is accessible. For primitives and other non-indexable types, the result is
318
+ * `{}`.
314
319
  *
315
- * Hint: This is necessary to make any property accessible. By default,
316
- * properties that do not exist in all union options are not accessible
317
- * and result in "any" when accessed.
320
+ * Hint: This is necessary to make properties accessible across union members.
321
+ * By default, properties that do not exist in all union options are not
322
+ * accessible and result in "any" when accessed.
318
323
  */
319
- type MergeUnion<TValue> = { [TKey in KeyOf<TValue>]: TValue extends Record<TKey, infer TItem> ? TItem : never };
324
+ type PropertiesOf<TValue> = { [TKey in ExactKeysOf<TValue>]: TValue extends Record<TKey, infer TItem> ? TItem : never };
320
325
  /**
321
326
  * Lazily evaluates only the first valid path segment based on the given value.
322
327
  */
323
- type LazyPath<TValue, TPathToCheck extends Path, TValidPath extends Path = readonly []> = TPathToCheck extends readonly [] ? TValidPath : TPathToCheck extends readonly [infer TFirstKey extends KeyOf<TValue>, ...infer TPathRest extends Path] ? LazyPath<Required<MergeUnion<TValue>[TFirstKey]>, TPathRest, readonly [...TValidPath, TFirstKey]> : IsNever<KeyOf<TValue>> extends false ? readonly [...TValidPath, KeyOf<TValue>] : TValidPath;
328
+ type LazyPath<TValue, TPathToCheck extends Path, TValidPath extends Path = readonly []> = TPathToCheck extends readonly [] ? TValidPath : TPathToCheck extends readonly [infer TFirstKey extends ExactKeysOf<TValue>, ...infer TPathRest extends Path] ? LazyPath<Required<PropertiesOf<TValue>[TFirstKey]>, TPathRest, readonly [...TValidPath, TFirstKey]> : IsNever<ExactKeysOf<TValue>> extends false ? readonly [...TValidPath, ExactKeysOf<TValue>] : TValidPath;
324
329
  /**
325
330
  * Returns the path if valid, otherwise the first possible valid path based on
326
331
  * the given value.
327
332
  */
328
333
  type ValidPath<TValue, TPath extends RequiredPath> = TPath extends LazyPath<Required<TValue>, TPath> ? TPath : LazyPath<Required<TValue>, TPath>;
329
334
  /**
335
+ * Detects whether the consuming project is configured with
336
+ * `exactOptionalPropertyTypes: true`.
337
+ *
338
+ * Hint: If `false` the built-in `Required<T>` strips `| undefined` from
339
+ * optional properties, so `Required<{ key?: undefined }>['key']` collapses
340
+ * to `never` — under strict mode the same expression yields `undefined`.
341
+ */
342
+ type IsExactOptionalProps = Required<{
343
+ key?: undefined;
344
+ }>["key"] extends never ? false : true;
345
+ /**
346
+ * Like the built-in `Required<T>`, but preserves `| undefined` in two
347
+ * places where `Required<T>` strips it:
348
+ *
349
+ * 1. Optional property values under `exactOptionalPropertyTypes: false`
350
+ * — without this, input typings for `v.optional`/`v.nullish` schemas
351
+ * narrow incorrectly (issue #15).
352
+ * 2. Array/tuple element types — e.g. `(string | undefined)[]` stays
353
+ * `(string | undefined)[]` instead of becoming `string[]`. Arrays
354
+ * fall through unchanged because they only have a numeric index
355
+ * signature and don't structurally extend `Record<PropertyKey,
356
+ * unknown>` (which requires string keys).
357
+ */
358
+ type ExactRequired<TValue> = TValue extends Record<PropertyKey, unknown> ? IsExactOptionalProps extends true ? Required<TValue> : { [TKey in keyof Required<TValue>]: TValue[TKey] } : TValue;
359
+ /**
330
360
  * Extracts the value type at the given path.
331
361
  */
332
- type PathValue<TValue, TPath extends Path> = TPath extends readonly [infer TKey, ...infer TRest extends Path] ? TKey extends KeyOf<Required<TValue>> ? PathValue<MergeUnion<Required<TValue>>[TKey], TRest> : unknown : TValue;
362
+ type PathValue<TValue, TPath extends Path> = TPath extends readonly [infer TKey, ...infer TRest extends Path] ? TKey extends ExactKeysOf<ExactRequired<TValue>> ? PathValue<PropertiesOf<ExactRequired<TValue>>[TKey], TRest> : unknown : TValue;
333
363
  /**
334
- * Checks if a value is an array or contains one.
364
+ * Checks whether a value is an array or contains one anywhere in its shape.
365
+ *
366
+ * Hint: The inner conditionals (`TValue extends readonly unknown[]` and
367
+ * `TValue extends Record<PropertyKey, unknown>`) distribute over union members,
368
+ * so the inner expression returns the union of each member's result (e.g.
369
+ * `true | false` when some members contain arrays and others don't).
370
+ * Downstream code uses `IsOrHasArray<T> extends true`, but
371
+ * `boolean extends true` is `false` — so we collapse the result via
372
+ * `true extends ...`, which is `true` whenever at least one union member
373
+ * contributed `true`.
335
374
  */
336
- type IsOrHasArray<TValue> = IsAny<TValue> extends true ? false : TValue extends readonly unknown[] ? true : TValue extends Record<string, unknown> ? true extends { [TKey in keyof TValue]: IsOrHasArray<TValue[TKey]> }[keyof TValue] ? true : false : false;
375
+ type IsOrHasArray<TValue> = true extends (IsAny<TValue> extends true ? false : TValue extends readonly unknown[] ? true : TValue extends Record<PropertyKey, unknown> ? { [TKey in keyof TValue]: IsOrHasArray<TValue[TKey]> }[keyof TValue] : false) ? true : false;
337
376
  /**
338
377
  * Extracts the exact keys of a tuple, array or object that contain arrays.
339
378
  */
340
- type KeyOfArrayPath<TValue> = IsAny<TValue> extends true ? never : TValue extends readonly (infer TItem)[] ? number extends TValue["length"] ? IsOrHasArray<TItem> extends true ? number : never : { [TKey in keyof TValue]: TKey extends `${infer TIndex extends number}` ? IsOrHasArray<NonNullable<TValue[TKey]>> extends true ? TIndex : never : never }[number] : TValue extends Record<string, unknown> ? { [TKey in keyof TValue]: IsOrHasArray<NonNullable<TValue[TKey]>> extends true ? TKey : never }[keyof TValue] & PathKey : never;
379
+ type ExactKeysOfArrayPath<TValue> = IsAny<TValue> extends true ? never : TValue extends readonly (infer TItem)[] ? number extends TValue["length"] ? IsOrHasArray<TItem> extends true ? number : never : { [TKey in keyof TValue]: TKey extends `${infer TIndex extends number}` ? IsOrHasArray<NonNullable<TValue[TKey]>> extends true ? TIndex : never : never }[number] : TValue extends Record<PropertyKey, unknown> ? { [TKey in keyof TValue]: IsOrHasArray<NonNullable<TValue[TKey]>> extends true ? TKey : never }[keyof TValue] & PathKey : never;
380
+ /**
381
+ * Returns the flat object of indexable properties of `TValue` whose values
382
+ * are or contain arrays. Mirrors `PropertiesOf` but keyed by
383
+ * `ExactKeysOfArrayPath` so the lookup is provably valid for array-path
384
+ * navigation in `LazyArrayPath`.
385
+ */
386
+ type PropertiesOfArrayPath<TValue> = { [TKey in ExactKeysOfArrayPath<TValue>]: TValue extends Record<TKey, infer TItem> ? TItem : never };
341
387
  /**
342
388
  * Lazily evaluates only the first valid array path segment based on the given value.
343
389
  */
344
- type LazyArrayPath<TValue, TPathToCheck extends Path, TValidPath extends Path = readonly []> = TPathToCheck extends readonly [] ? TValue extends readonly unknown[] ? TValidPath : readonly [...TValidPath, KeyOfArrayPath<TValue>] : TPathToCheck extends readonly [infer TFirstKey extends KeyOfArrayPath<TValue>, ...infer TPathRest extends Path] ? LazyArrayPath<Required<MergeUnion<TValue>[TFirstKey]>, TPathRest, readonly [...TValidPath, TFirstKey]> : IsNever<KeyOfArrayPath<TValue>> extends false ? readonly [...TValidPath, KeyOfArrayPath<TValue>] : never;
390
+ type LazyArrayPath<TValue, TPathToCheck extends Path, TValidPath extends Path = readonly []> = TPathToCheck extends readonly [] ? TValue extends readonly unknown[] ? TValidPath : readonly [...TValidPath, ExactKeysOfArrayPath<TValue>] : TPathToCheck extends readonly [infer TFirstKey extends ExactKeysOfArrayPath<TValue>, ...infer TPathRest extends Path] ? LazyArrayPath<Required<PropertiesOfArrayPath<TValue>[TFirstKey]>, TPathRest, readonly [...TValidPath, TFirstKey]> : IsNever<ExactKeysOfArrayPath<TValue>> extends false ? readonly [...TValidPath, ExactKeysOfArrayPath<TValue>] : never;
345
391
  /**
346
392
  * Returns the path if valid, otherwise the first possible valid array path
347
393
  * based on the given value.
package/dist/index.js CHANGED
@@ -76,7 +76,7 @@ function initializeFieldStore(internalFieldStore, schema, initialInput, path, nu
76
76
  if (internalFieldStore.kind === "object") {
77
77
  internalFieldStore.children ??= {};
78
78
  for (const key in schema.entries) {
79
- internalFieldStore.children[key] = {};
79
+ internalFieldStore.children[key] ??= {};
80
80
  path.push(key);
81
81
  initializeFieldStore(internalFieldStore.children[key], schema.entries[key], initialInput?.[key], path);
82
82
  path.pop();
@@ -87,6 +87,7 @@ function initializeFieldStore(internalFieldStore, schema, initialInput, path, nu
87
87
  internalFieldStore.input = createSignal(objectInput);
88
88
  }
89
89
  } else {
90
+ if (internalFieldStore.kind && internalFieldStore.kind !== "value") throw new Error(`Store initialized as "${internalFieldStore.kind}" cannot be reinitialized as "value"`);
90
91
  internalFieldStore.kind = "value";
91
92
  if (internalFieldStore.kind === "value") {
92
93
  internalFieldStore.initialInput = createSignal(initialInput);
@@ -704,7 +705,7 @@ function reset(form, config) {
704
705
  untrack(() => {
705
706
  const internalFormStore = form[INTERNAL];
706
707
  const internalFieldStore = config?.path ? getFieldStore(internalFormStore, config.path) : internalFormStore;
707
- if (config?.initialInput) setInitialFieldInput(internalFieldStore, config.initialInput);
708
+ if (config && "initialInput" in config) setInitialFieldInput(internalFieldStore, config.initialInput);
708
709
  walkFieldStore(internalFieldStore, (internalFieldStore$1) => {
709
710
  internalFieldStore$1.elements = internalFieldStore$1.initialElements;
710
711
  if (!config?.keepErrors) internalFieldStore$1.errors.value = null;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@formisch/preact",
3
- "description": "The modular and type-safe form library for Preact",
4
- "version": "0.9.5",
3
+ "description": "The lightweight, schema-first, and fully type-safe form library for Preact",
4
+ "version": "0.9.6",
5
5
  "license": "MIT",
6
6
  "author": "Fabian Hiller",
7
7
  "homepage": "https://formisch.dev",
@@ -34,6 +34,7 @@
34
34
  },
35
35
  "scripts": {
36
36
  "build": "tsdown",
37
+ "test": "vitest run --typecheck",
37
38
  "lint": "eslint \"src/**/*.ts*\" && tsc --noEmit",
38
39
  "lint.fix": "eslint \"src/**/*.ts*\" --fix",
39
40
  "format": "prettier --write ./src",
@@ -45,13 +46,18 @@
45
46
  "@formisch/methods": "workspace:*",
46
47
  "@preact/preset-vite": "^2.9.3",
47
48
  "@preact/signals": "^2.2.1",
49
+ "@testing-library/jest-dom": "^6.6.0",
50
+ "@testing-library/preact": "^3.2.4",
51
+ "@vitest/coverage-v8": "^3.2.4",
48
52
  "eslint": "^9.31.0",
49
53
  "eslint-config-preact": "^2.0.0",
54
+ "jsdom": "^26.1.0",
50
55
  "preact": "^10.25.3",
51
56
  "tsdown": "^0.16.8",
52
57
  "typescript": "^5.8.3",
53
58
  "valibot": "^1.2.0",
54
- "vite": "^6.0.4"
59
+ "vite": "^6.0.4",
60
+ "vitest": "^3.2.4"
55
61
  },
56
62
  "peerDependencies": {
57
63
  "@preact/signals": "^2.0.0",