@formisch/svelte 0.7.5 → 0.7.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.
@@ -1,13 +1,13 @@
1
1
  import * as v from "valibot";
2
2
  import { untrack } from "svelte";
3
3
 
4
- //#region src/types/schema.d.ts
4
+ //#region src/types/schema/schema.d.ts
5
5
  /**
6
6
  * Schema type.
7
7
  */
8
8
  type Schema = v.GenericSchema | v.GenericSchemaAsync;
9
9
  //#endregion
10
- //#region src/types/signal.d.ts
10
+ //#region src/types/signal/signal.d.ts
11
11
  /**
12
12
  * Signal interface.
13
13
  */
@@ -30,7 +30,7 @@ interface Untrack {
30
30
  <T>(fn: () => T): T;
31
31
  }
32
32
  //#endregion
33
- //#region src/types/field.d.ts
33
+ //#region src/types/field/field.d.ts
34
34
  /**
35
35
  * Field element type.
36
36
  */
@@ -196,7 +196,7 @@ type InternalFieldStore = InternalArrayStore | InternalObjectStore | InternalVal
196
196
  */
197
197
  declare const INTERNAL: "~internal";
198
198
  //#endregion
199
- //#region src/types/utils.d.ts
199
+ //#region src/types/utils/utils.d.ts
200
200
  /**
201
201
  * Checks if a type is `any`.
202
202
  */
@@ -222,7 +222,7 @@ type DeepPartial<TValue> = TValue extends Record<PropertyKey, unknown> | readonl
222
222
  */
223
223
  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;
224
224
  //#endregion
225
- //#region src/types/form.d.ts
225
+ //#region src/types/form/form.d.ts
226
226
  /**
227
227
  * Validation mode type.
228
228
  */
@@ -305,7 +305,7 @@ type SubmitHandler<TSchema extends Schema> = (output: v.InferOutput<TSchema>) =>
305
305
  */
306
306
  type SubmitEventHandler<TSchema extends Schema> = (output: v.InferOutput<TSchema>, event: SubmitEvent) => MaybePromise<unknown>;
307
307
  //#endregion
308
- //#region src/types/path.d.ts
308
+ //#region src/types/path/path.d.ts
309
309
  /**
310
310
  * Path key type.
311
311
  */
@@ -319,42 +319,88 @@ type Path = readonly PathKey[];
319
319
  */
320
320
  type RequiredPath = readonly [PathKey, ...Path];
321
321
  /**
322
- * Extracts the exact keys of a tuple, array or object.
322
+ * Extracts the exact keys of a tuple, array or object. Tuples return their
323
+ * literal numeric indices, dynamic arrays return `number`, objects return
324
+ * their `keyof` keys, and any other input returns `never`.
323
325
  */
324
- 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;
326
+ 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;
325
327
  /**
326
- * Merges array and object unions into a single object.
328
+ * Returns the flat object of all indexable properties of `TValue`. For object
329
+ * unions, properties from every member are merged so that any single property
330
+ * is accessible. For primitives and other non-indexable types, the result is
331
+ * `{}`.
327
332
  *
328
- * Hint: This is necessary to make any property accessible. By default,
329
- * properties that do not exist in all union options are not accessible
330
- * and result in "any" when accessed.
333
+ * Hint: This is necessary to make properties accessible across union members.
334
+ * By default, properties that do not exist in all union options are not
335
+ * accessible and result in "any" when accessed.
331
336
  */
332
- type MergeUnion<TValue> = { [TKey in KeyOf<TValue>]: TValue extends Record<TKey, infer TItem> ? TItem : never };
337
+ type PropertiesOf<TValue> = { [TKey in ExactKeysOf<TValue>]: TValue extends Record<TKey, infer TItem> ? TItem : never };
333
338
  /**
334
339
  * Lazily evaluates only the first valid path segment based on the given value.
335
340
  */
336
- 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;
341
+ 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;
337
342
  /**
338
343
  * Returns the path if valid, otherwise the first possible valid path based on
339
344
  * the given value.
340
345
  */
341
346
  type ValidPath<TValue, TPath extends RequiredPath> = TPath extends LazyPath<Required<TValue>, TPath> ? TPath : LazyPath<Required<TValue>, TPath>;
342
347
  /**
348
+ * Detects whether the consuming project is configured with
349
+ * `exactOptionalPropertyTypes: true`.
350
+ *
351
+ * Hint: If `false` the built-in `Required<T>` strips `| undefined` from
352
+ * optional properties, so `Required<{ key?: undefined }>['key']` collapses
353
+ * to `never` — under strict mode the same expression yields `undefined`.
354
+ */
355
+ type IsExactOptionalProps = Required<{
356
+ key?: undefined;
357
+ }>["key"] extends never ? false : true;
358
+ /**
359
+ * Like the built-in `Required<T>`, but preserves `| undefined` in two
360
+ * places where `Required<T>` strips it:
361
+ *
362
+ * 1. Optional property values under `exactOptionalPropertyTypes: false`
363
+ * — without this, input typings for `v.optional`/`v.nullish` schemas
364
+ * narrow incorrectly (issue #15).
365
+ * 2. Array/tuple element types — e.g. `(string | undefined)[]` stays
366
+ * `(string | undefined)[]` instead of becoming `string[]`. Arrays
367
+ * fall through unchanged because they only have a numeric index
368
+ * signature and don't structurally extend `Record<PropertyKey,
369
+ * unknown>` (which requires string keys).
370
+ */
371
+ type ExactRequired<TValue> = TValue extends Record<PropertyKey, unknown> ? IsExactOptionalProps extends true ? Required<TValue> : { [TKey in keyof Required<TValue>]: TValue[TKey] } : TValue;
372
+ /**
343
373
  * Extracts the value type at the given path.
344
374
  */
345
- 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;
375
+ 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;
346
376
  /**
347
- * Checks if a value is an array or contains one.
377
+ * Checks whether a value is an array or contains one anywhere in its shape.
378
+ *
379
+ * Hint: The inner conditionals (`TValue extends readonly unknown[]` and
380
+ * `TValue extends Record<PropertyKey, unknown>`) distribute over union members,
381
+ * so the inner expression returns the union of each member's result (e.g.
382
+ * `true | false` when some members contain arrays and others don't).
383
+ * Downstream code uses `IsOrHasArray<T> extends true`, but
384
+ * `boolean extends true` is `false` — so we collapse the result via
385
+ * `true extends ...`, which is `true` whenever at least one union member
386
+ * contributed `true`.
348
387
  */
349
- 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;
388
+ 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;
350
389
  /**
351
390
  * Extracts the exact keys of a tuple, array or object that contain arrays.
352
391
  */
353
- 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;
392
+ 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;
393
+ /**
394
+ * Returns the flat object of indexable properties of `TValue` whose values
395
+ * are or contain arrays. Mirrors `PropertiesOf` but keyed by
396
+ * `ExactKeysOfArrayPath` so the lookup is provably valid for array-path
397
+ * navigation in `LazyArrayPath`.
398
+ */
399
+ type PropertiesOfArrayPath<TValue> = { [TKey in ExactKeysOfArrayPath<TValue>]: TValue extends Record<TKey, infer TItem> ? TItem : never };
354
400
  /**
355
401
  * Lazily evaluates only the first valid array path segment based on the given value.
356
402
  */
357
- 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;
403
+ 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;
358
404
  /**
359
405
  * Returns the path if valid, otherwise the first possible valid array path
360
406
  * based on the given value.
@@ -107,7 +107,7 @@ function initializeFieldStore(internalFieldStore, schema, initialInput, path, nu
107
107
  if (internalFieldStore.kind === "object") {
108
108
  internalFieldStore.children ??= {};
109
109
  for (const key in schema.entries) {
110
- internalFieldStore.children[key] = {};
110
+ internalFieldStore.children[key] ??= {};
111
111
  path.push(key);
112
112
  initializeFieldStore(internalFieldStore.children[key], schema.entries[key], initialInput?.[key], path);
113
113
  path.pop();
@@ -118,6 +118,7 @@ function initializeFieldStore(internalFieldStore, schema, initialInput, path, nu
118
118
  internalFieldStore.input = /* @__PURE__ */ createSignal(objectInput);
119
119
  }
120
120
  } else {
121
+ if (internalFieldStore.kind && internalFieldStore.kind !== "value") throw new Error(`Store initialized as "${internalFieldStore.kind}" cannot be reinitialized as "value"`);
121
122
  internalFieldStore.kind = "value";
122
123
  if (internalFieldStore.kind === "value") {
123
124
  internalFieldStore.initialInput = /* @__PURE__ */ createSignal(initialInput);
@@ -198,7 +198,7 @@ function reset(form, config) {
198
198
  untrack(() => {
199
199
  const internalFormStore = form[INTERNAL];
200
200
  const internalFieldStore = config?.path ? getFieldStore(internalFormStore, config.path) : internalFormStore;
201
- if (config?.initialInput) setInitialFieldInput(internalFieldStore, config.initialInput);
201
+ if (config && "initialInput" in config) setInitialFieldInput(internalFieldStore, config.initialInput);
202
202
  walkFieldStore(internalFieldStore, (internalFieldStore$1) => {
203
203
  internalFieldStore$1.elements = internalFieldStore$1.initialElements;
204
204
  if (!config?.keepErrors) internalFieldStore$1.errors.value = null;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@formisch/svelte",
3
- "description": "The modular and type-safe form library for Svelte",
4
- "version": "0.7.5",
3
+ "description": "The lightweight, schema-first, and fully type-safe form library for Svelte",
4
+ "version": "0.7.6",
5
5
  "license": "MIT",
6
6
  "author": "Fabian Hiller",
7
7
  "homepage": "https://formisch.dev",
@@ -30,7 +30,9 @@
30
30
  "files": [
31
31
  "dist",
32
32
  "!dist/**/*.test.*",
33
- "!dist/**/*.spec.*"
33
+ "!dist/**/*.test-d.*",
34
+ "!dist/**/*.spec.*",
35
+ "!dist/vitest"
34
36
  ],
35
37
  "publishConfig": {
36
38
  "access": "public"
@@ -39,6 +41,7 @@
39
41
  "prepare": "svelte-kit sync || echo ''",
40
42
  "build": "svelte-package --input src && node ./scripts/rewrite-imports.js",
41
43
  "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
44
+ "test": "vitest run --typecheck",
42
45
  "lint": "eslint \"src/**/*.ts*\" && tsc --noEmit",
43
46
  "lint.fix": "eslint \"src/**/*.ts*\" --fix",
44
47
  "format": "prettier --write ./src",
@@ -53,15 +56,20 @@
53
56
  "@sveltejs/kit": "^2.37.1",
54
57
  "@sveltejs/package": "^2.5.0",
55
58
  "@sveltejs/vite-plugin-svelte": "^6.2.0",
59
+ "@testing-library/jest-dom": "^6.6.0",
60
+ "@testing-library/svelte": "^5.2.4",
61
+ "@vitest/coverage-v8": "^3.2.4",
56
62
  "eslint": "^9.35.0",
57
63
  "eslint-plugin-svelte": "^3.12.2",
58
64
  "globals": "^16.4.0",
65
+ "jsdom": "^26.1.0",
59
66
  "publint": "^0.3.12",
60
67
  "svelte": "^5.38.8",
61
68
  "svelte-check": "^4.3.1",
62
69
  "typescript": "^5.9.2",
63
70
  "valibot": "^1.2.0",
64
- "vite": "^7.1.5"
71
+ "vite": "^7.1.5",
72
+ "vitest": "^3.2.4"
65
73
  },
66
74
  "peerDependencies": {
67
75
  "svelte": "^5.29.0",