svstate 0.0.1

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.
@@ -0,0 +1,2 @@
1
+ export { createSvState, type EffectContext, type Snapshot, type SnapshotFunction, type SvStateOptions, type Validator } from './state.svelte';
2
+ export { arrayValidator, dateValidator, numberValidator, stringValidator } from './validators';
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.stringValidator = exports.numberValidator = exports.dateValidator = exports.arrayValidator = exports.createSvState = void 0;
4
+ var state_svelte_1 = require("./state.svelte");
5
+ Object.defineProperty(exports, "createSvState", { enumerable: true, get: function () { return state_svelte_1.createSvState; } });
6
+ var validators_1 = require("./validators");
7
+ Object.defineProperty(exports, "arrayValidator", { enumerable: true, get: function () { return validators_1.arrayValidator; } });
8
+ Object.defineProperty(exports, "dateValidator", { enumerable: true, get: function () { return validators_1.dateValidator; } });
9
+ Object.defineProperty(exports, "numberValidator", { enumerable: true, get: function () { return validators_1.numberValidator; } });
10
+ Object.defineProperty(exports, "stringValidator", { enumerable: true, get: function () { return validators_1.stringValidator; } });
@@ -0,0 +1,2 @@
1
+ export type ProxyChanged<T extends object> = (target: T, property: string, currentValue: unknown, oldValue: unknown) => void;
2
+ export declare const ChangeProxy: <T extends object>(source: T, changed: ProxyChanged<T>) => T;
package/dist/proxy.js ADDED
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ChangeProxy = void 0;
4
+ const isProxiable = (value) => typeof value === 'object' &&
5
+ value !== null &&
6
+ !(value instanceof Date) &&
7
+ !(value instanceof Map) &&
8
+ !(value instanceof Set) &&
9
+ !(value instanceof WeakMap) &&
10
+ !(value instanceof WeakSet) &&
11
+ !(value instanceof RegExp) &&
12
+ !(value instanceof Error) &&
13
+ !(value instanceof Promise);
14
+ const ChangeProxy = (source, changed) => {
15
+ const createProxy = (target, parentPath) => new Proxy(target, {
16
+ get(object, property, receiver) {
17
+ const value = Reflect.get(object, property, receiver);
18
+ if (isProxiable(value)) {
19
+ const pathSegment = Number.isInteger(Number(property)) ? '' : String(property);
20
+ const childPath = pathSegment ? (parentPath ? `${parentPath}.${pathSegment}` : pathSegment) : parentPath;
21
+ return createProxy(value, childPath);
22
+ }
23
+ return value;
24
+ },
25
+ set(object, property, incomingValue, receiver) {
26
+ const oldValue = Reflect.get(object, property, receiver);
27
+ if (oldValue !== incomingValue) {
28
+ Reflect.set(object, property, incomingValue, receiver);
29
+ const pathSegment = Number.isInteger(Number(property)) ? '' : String(property);
30
+ const fullPath = pathSegment ? (parentPath ? `${parentPath}.${pathSegment}` : pathSegment) : parentPath;
31
+ changed(data, fullPath, incomingValue, oldValue);
32
+ }
33
+ return true;
34
+ }
35
+ });
36
+ const data = createProxy(source, '');
37
+ return data;
38
+ };
39
+ exports.ChangeProxy = ChangeProxy;
@@ -0,0 +1,45 @@
1
+ import { type Readable } from 'svelte/store';
2
+ export type Validator = {
3
+ [S in string]: string | Validator;
4
+ };
5
+ type Action<P extends object> = (parameters?: P) => Promise<void> | void;
6
+ export type Snapshot<T> = {
7
+ title: string;
8
+ data: T;
9
+ };
10
+ export type SnapshotFunction = (title: string, replace?: boolean) => void;
11
+ export type EffectContext<T> = {
12
+ snapshot: SnapshotFunction;
13
+ target: T;
14
+ property: string;
15
+ currentValue: unknown;
16
+ oldValue: unknown;
17
+ };
18
+ type Actuators<T extends Record<string, unknown>, V extends Validator, P extends object> = {
19
+ validator?: (source: T) => V;
20
+ effect?: (context: EffectContext<T>) => void;
21
+ action?: Action<P>;
22
+ actionCompleted?: (error?: unknown) => void | Promise<void>;
23
+ };
24
+ type StateResult<T, V> = {
25
+ errors: Readable<V | undefined>;
26
+ hasErrors: Readable<boolean>;
27
+ isDirty: Readable<boolean>;
28
+ actionInProgress: Readable<boolean>;
29
+ actionError: Readable<Error | undefined>;
30
+ snapshots: Readable<Snapshot<T>[]>;
31
+ };
32
+ export type SvStateOptions = {
33
+ resetDirtyOnAction: boolean;
34
+ debounceValidation: number;
35
+ allowConcurrentActions: boolean;
36
+ persistActionError: boolean;
37
+ };
38
+ export declare function createSvState<T extends Record<string, unknown>, V extends Validator, P extends object>(init: T, actuators?: Actuators<T, V, P>, options?: Partial<SvStateOptions>): {
39
+ data: T;
40
+ execute: (parameters?: P) => Promise<void>;
41
+ state: StateResult<T, V>;
42
+ rollback: (steps?: number) => void;
43
+ reset: () => void;
44
+ };
45
+ export {};
@@ -0,0 +1,130 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createSvState = createSvState;
4
+ const store_1 = require("svelte/store");
5
+ const proxy_1 = require("./proxy");
6
+ const checkHasErrors = (validator) => Object.values(validator).some((item) => (typeof item === 'string' ? !!item : checkHasErrors(item)));
7
+ const hasAnyErrors = ($errors) => !!$errors && checkHasErrors($errors);
8
+ const deepClone = (object) => {
9
+ if (object === null || typeof object !== 'object')
10
+ return object;
11
+ if (object instanceof Date)
12
+ return new Date(object);
13
+ if (Array.isArray(object))
14
+ return object.map((item) => deepClone(item));
15
+ const cloned = {};
16
+ for (const key in object)
17
+ if (Object.prototype.hasOwnProperty.call(object, key))
18
+ cloned[key] = deepClone(object[key]);
19
+ return cloned;
20
+ };
21
+ const defaultOptions = {
22
+ resetDirtyOnAction: true,
23
+ debounceValidation: 0,
24
+ allowConcurrentActions: false,
25
+ persistActionError: false
26
+ };
27
+ function createSvState(init, actuators, options) {
28
+ const usedOptions = { ...defaultOptions, ...options };
29
+ const { validator, effect } = actuators ?? {};
30
+ const errors = (0, store_1.writable)();
31
+ const hasErrors = (0, store_1.derived)(errors, hasAnyErrors);
32
+ const isDirty = (0, store_1.writable)(false);
33
+ const actionInProgress = (0, store_1.writable)(false);
34
+ const actionError = (0, store_1.writable)();
35
+ const snapshots = (0, store_1.writable)([{ title: 'Initial', data: deepClone(init) }]);
36
+ const stateObject = $state(init);
37
+ const createSnapshot = (title, replace = true) => {
38
+ const currentSnapshots = (0, store_1.get)(snapshots);
39
+ const createdSnapshot = { title, data: deepClone(stateObject) };
40
+ const lastSnapshot = currentSnapshots.at(-1);
41
+ if (replace && lastSnapshot && lastSnapshot.title === title)
42
+ snapshots.set([...currentSnapshots.slice(0, -1), createdSnapshot]);
43
+ else
44
+ snapshots.set([...currentSnapshots, createdSnapshot]);
45
+ };
46
+ let validationScheduled = false;
47
+ let validationTimeout;
48
+ const scheduleValidation = () => {
49
+ if (!validator)
50
+ return;
51
+ if (usedOptions.debounceValidation > 0) {
52
+ clearTimeout(validationTimeout);
53
+ validationTimeout = setTimeout(() => {
54
+ errors.set(validator(data));
55
+ }, usedOptions.debounceValidation);
56
+ }
57
+ else {
58
+ if (validationScheduled)
59
+ return;
60
+ validationScheduled = true;
61
+ queueMicrotask(() => {
62
+ errors.set(validator(data));
63
+ validationScheduled = false;
64
+ });
65
+ }
66
+ };
67
+ const data = (0, proxy_1.ChangeProxy)(stateObject, (target, property, currentValue, oldValue) => {
68
+ if (!usedOptions.persistActionError)
69
+ actionError.set(undefined);
70
+ isDirty.set(true);
71
+ const effectResult = effect?.({ snapshot: createSnapshot, target, property, currentValue, oldValue });
72
+ if (effectResult instanceof Promise)
73
+ throw new Error('svstate: effect callback must be synchronous. Use action for async operations.');
74
+ scheduleValidation();
75
+ });
76
+ if (validator)
77
+ errors.set(validator(data));
78
+ const execute = async (parameters) => {
79
+ if (!usedOptions.allowConcurrentActions && (0, store_1.get)(actionInProgress))
80
+ return;
81
+ actionError.set(undefined);
82
+ actionInProgress.set(true);
83
+ try {
84
+ await actuators?.action?.(parameters);
85
+ if (usedOptions.resetDirtyOnAction)
86
+ isDirty.set(false);
87
+ snapshots.set([{ title: 'Initial', data: deepClone(stateObject) }]);
88
+ await actuators?.actionCompleted?.();
89
+ }
90
+ catch (caughtError) {
91
+ await actuators?.actionCompleted?.(caughtError);
92
+ actionError.set(caughtError instanceof Error ? caughtError : undefined);
93
+ }
94
+ finally {
95
+ actionInProgress.set(false);
96
+ }
97
+ };
98
+ const rollback = (steps = 1) => {
99
+ const currentSnapshots = (0, store_1.get)(snapshots);
100
+ if (currentSnapshots.length <= 1)
101
+ return;
102
+ const targetIndex = Math.max(0, currentSnapshots.length - 1 - steps);
103
+ const targetSnapshot = currentSnapshots[targetIndex];
104
+ if (!targetSnapshot)
105
+ return;
106
+ Object.assign(stateObject, deepClone(targetSnapshot.data));
107
+ snapshots.set(currentSnapshots.slice(0, targetIndex + 1));
108
+ if (validator)
109
+ errors.set(validator(data));
110
+ };
111
+ const reset = () => {
112
+ const currentSnapshots = (0, store_1.get)(snapshots);
113
+ const initialSnapshot = currentSnapshots[0];
114
+ if (!initialSnapshot)
115
+ return;
116
+ Object.assign(stateObject, deepClone(initialSnapshot.data));
117
+ snapshots.set([initialSnapshot]);
118
+ if (validator)
119
+ errors.set(validator(data));
120
+ };
121
+ const state = {
122
+ errors,
123
+ hasErrors,
124
+ isDirty,
125
+ actionInProgress,
126
+ actionError,
127
+ snapshots
128
+ };
129
+ return { data, execute, state, rollback, reset };
130
+ }
@@ -0,0 +1,60 @@
1
+ type BaseOption = 'trim' | 'normalize';
2
+ export declare function stringValidator(input: string, ...prepares: (BaseOption | 'upper')[]): StringValidatorBuilder;
3
+ export declare function stringValidator(input: string, ...prepares: (BaseOption | 'lower')[]): StringValidatorBuilder;
4
+ export declare function stringValidator(input: string, ...prepares: BaseOption[]): StringValidatorBuilder;
5
+ type StringValidatorBuilder = {
6
+ required(): StringValidatorBuilder;
7
+ noSpace(): StringValidatorBuilder;
8
+ minLength(length: number): StringValidatorBuilder;
9
+ maxLength(length: number): StringValidatorBuilder;
10
+ uppercase(): StringValidatorBuilder;
11
+ lowercase(): StringValidatorBuilder;
12
+ startsWith(prefix: string | string[]): StringValidatorBuilder;
13
+ regexp(regexp: RegExp, message?: string): StringValidatorBuilder;
14
+ inArray(values: string[] | Record<string, unknown>): StringValidatorBuilder;
15
+ email(): StringValidatorBuilder;
16
+ website(prefix?: 'required' | 'forbidden' | 'optional'): StringValidatorBuilder;
17
+ endsWith(suffix: string | string[]): StringValidatorBuilder;
18
+ contains(substring: string): StringValidatorBuilder;
19
+ alphanumeric(): StringValidatorBuilder;
20
+ numeric(): StringValidatorBuilder;
21
+ getError(): string;
22
+ };
23
+ export declare function numberValidator(input: number): NumberValidatorBuilder;
24
+ type NumberValidatorBuilder = {
25
+ required(): NumberValidatorBuilder;
26
+ min(n: number): NumberValidatorBuilder;
27
+ max(n: number): NumberValidatorBuilder;
28
+ between(min: number, max: number): NumberValidatorBuilder;
29
+ integer(): NumberValidatorBuilder;
30
+ positive(): NumberValidatorBuilder;
31
+ negative(): NumberValidatorBuilder;
32
+ nonNegative(): NumberValidatorBuilder;
33
+ multipleOf(n: number): NumberValidatorBuilder;
34
+ decimal(places: number): NumberValidatorBuilder;
35
+ percentage(): NumberValidatorBuilder;
36
+ getError(): string;
37
+ };
38
+ export declare function arrayValidator<T>(input: T[]): ArrayValidatorBuilder;
39
+ type ArrayValidatorBuilder = {
40
+ required(): ArrayValidatorBuilder;
41
+ minLength(n: number): ArrayValidatorBuilder;
42
+ maxLength(n: number): ArrayValidatorBuilder;
43
+ unique(): ArrayValidatorBuilder;
44
+ getError(): string;
45
+ };
46
+ export declare function dateValidator(input: Date | string | number): DateValidatorBuilder;
47
+ type DateValidatorBuilder = {
48
+ required(): DateValidatorBuilder;
49
+ before(target: Date | string | number): DateValidatorBuilder;
50
+ after(target: Date | string | number): DateValidatorBuilder;
51
+ between(start: Date | string | number, end: Date | string | number): DateValidatorBuilder;
52
+ past(): DateValidatorBuilder;
53
+ future(): DateValidatorBuilder;
54
+ weekday(): DateValidatorBuilder;
55
+ weekend(): DateValidatorBuilder;
56
+ minAge(years: number): DateValidatorBuilder;
57
+ maxAge(years: number): DateValidatorBuilder;
58
+ getError(): string;
59
+ };
60
+ export {};
@@ -0,0 +1,318 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.stringValidator = stringValidator;
4
+ exports.numberValidator = numberValidator;
5
+ exports.arrayValidator = arrayValidator;
6
+ exports.dateValidator = dateValidator;
7
+ const prepareOps = {
8
+ trim: (s) => s.trim(),
9
+ normalize: (s) => s.replaceAll(/\s{2,}/g, ' '),
10
+ upper: (s) => s.toLocaleUpperCase(),
11
+ lower: (s) => s.toLocaleLowerCase()
12
+ };
13
+ function stringValidator(input, ...prepares) {
14
+ let error = '';
15
+ const setError = (message) => {
16
+ if (!error)
17
+ error = message;
18
+ };
19
+ const processedInput = prepares.reduce((s, op) => prepareOps[op](s), input);
20
+ const builder = {
21
+ required() {
22
+ if (!error && !processedInput)
23
+ setError('Required');
24
+ return builder;
25
+ },
26
+ noSpace() {
27
+ if (!error && processedInput.includes(' '))
28
+ setError('No space allowed');
29
+ return builder;
30
+ },
31
+ minLength(length) {
32
+ if (!error && processedInput.length < length)
33
+ setError(`Min length ${length}`);
34
+ return builder;
35
+ },
36
+ maxLength(length) {
37
+ if (!error && processedInput.length > length)
38
+ setError(`Max length ${length}`);
39
+ return builder;
40
+ },
41
+ uppercase() {
42
+ if (!error && processedInput !== processedInput.toLocaleUpperCase())
43
+ setError('Uppercase only');
44
+ return builder;
45
+ },
46
+ lowercase() {
47
+ if (!error && processedInput !== processedInput.toLocaleLowerCase())
48
+ setError('Lowercase only');
49
+ return builder;
50
+ },
51
+ startsWith(prefix) {
52
+ if (error)
53
+ return builder;
54
+ const prefixes = Array.isArray(prefix) ? prefix : [prefix];
55
+ if (processedInput && !prefixes.some((p) => processedInput.startsWith(p)))
56
+ setError(`Must start with ${prefixes.join(', ')}`);
57
+ return builder;
58
+ },
59
+ regexp(regexp, message) {
60
+ if (!error && processedInput && !regexp.test(processedInput))
61
+ setError(message ?? 'Not allowed chars');
62
+ return builder;
63
+ },
64
+ inArray(values) {
65
+ if (error)
66
+ return builder;
67
+ const allowed = Array.isArray(values) ? values : Object.keys(values);
68
+ if (processedInput && !allowed.includes(processedInput))
69
+ setError(`Must be one of: ${allowed.join(', ')}`);
70
+ return builder;
71
+ },
72
+ email() {
73
+ if (!error && processedInput && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(processedInput))
74
+ setError('Invalid email format');
75
+ return builder;
76
+ },
77
+ website(prefix = 'optional') {
78
+ if (error || !processedInput || prefix === 'optional')
79
+ return builder;
80
+ const hasPrefix = /^https?:\/\//.test(processedInput);
81
+ if (prefix === 'required' && !hasPrefix)
82
+ setError('Must start with http:// or https://');
83
+ else if (prefix === 'forbidden' && hasPrefix)
84
+ setError('Must not start with http:// or https://');
85
+ return builder;
86
+ },
87
+ endsWith(suffix) {
88
+ if (error || !processedInput)
89
+ return builder;
90
+ const suffixes = Array.isArray(suffix) ? suffix : [suffix];
91
+ if (!suffixes.some((s) => processedInput.endsWith(s)))
92
+ setError(`Must end with ${suffixes.join(', ')}`);
93
+ return builder;
94
+ },
95
+ contains(substring) {
96
+ if (!error && processedInput && !processedInput.includes(substring))
97
+ setError(`Must contain "${substring}"`);
98
+ return builder;
99
+ },
100
+ alphanumeric() {
101
+ if (!error && processedInput && !/^[\dA-Za-z]+$/.test(processedInput))
102
+ setError('Only letters and numbers allowed');
103
+ return builder;
104
+ },
105
+ numeric() {
106
+ if (!error && processedInput && !/^\d+$/.test(processedInput))
107
+ setError('Only numbers allowed');
108
+ return builder;
109
+ },
110
+ getError() {
111
+ return error;
112
+ }
113
+ };
114
+ return builder;
115
+ }
116
+ function numberValidator(input) {
117
+ let error = '';
118
+ const setError = (message) => {
119
+ if (!error)
120
+ error = message;
121
+ };
122
+ const builder = {
123
+ required() {
124
+ if (!error && Number.isNaN(input))
125
+ setError('Required');
126
+ return builder;
127
+ },
128
+ min(n) {
129
+ if (!error && input < n)
130
+ setError(`Minimum ${n}`);
131
+ return builder;
132
+ },
133
+ max(n) {
134
+ if (!error && input > n)
135
+ setError(`Maximum ${n}`);
136
+ return builder;
137
+ },
138
+ between(min, max) {
139
+ if (!error && (input < min || input > max))
140
+ setError(`Must be between ${min} and ${max}`);
141
+ return builder;
142
+ },
143
+ integer() {
144
+ if (!error && !Number.isInteger(input))
145
+ setError('Must be an integer');
146
+ return builder;
147
+ },
148
+ positive() {
149
+ if (!error && input <= 0)
150
+ setError('Must be positive');
151
+ return builder;
152
+ },
153
+ negative() {
154
+ if (!error && input >= 0)
155
+ setError('Must be negative');
156
+ return builder;
157
+ },
158
+ nonNegative() {
159
+ if (!error && input < 0)
160
+ setError('Must be non-negative');
161
+ return builder;
162
+ },
163
+ multipleOf(n) {
164
+ if (!error && input % n !== 0)
165
+ setError(`Must be a multiple of ${n}`);
166
+ return builder;
167
+ },
168
+ decimal(places) {
169
+ if (error || Number.isNaN(input))
170
+ return builder;
171
+ const parts = String(input).split('.');
172
+ const actualPlaces = parts[1]?.length ?? 0;
173
+ if (actualPlaces > places)
174
+ setError(`Maximum ${places} decimal places`);
175
+ return builder;
176
+ },
177
+ percentage() {
178
+ if (!error && (input < 0 || input > 100))
179
+ setError('Must be between 0 and 100');
180
+ return builder;
181
+ },
182
+ getError() {
183
+ return error;
184
+ }
185
+ };
186
+ return builder;
187
+ }
188
+ function arrayValidator(input) {
189
+ let error = '';
190
+ const setError = (message) => {
191
+ if (!error)
192
+ error = message;
193
+ };
194
+ const builder = {
195
+ required() {
196
+ if (!error && input.length === 0)
197
+ setError('Required');
198
+ return builder;
199
+ },
200
+ minLength(n) {
201
+ if (!error && input.length < n)
202
+ setError(`Minimum ${n} items`);
203
+ return builder;
204
+ },
205
+ maxLength(n) {
206
+ if (!error && input.length > n)
207
+ setError(`Maximum ${n} items`);
208
+ return builder;
209
+ },
210
+ unique() {
211
+ if (error)
212
+ return builder;
213
+ const seen = new Set();
214
+ for (const item of input) {
215
+ const key = typeof item === 'object' ? JSON.stringify(item) : String(item);
216
+ if (seen.has(key)) {
217
+ setError('Items must be unique');
218
+ break;
219
+ }
220
+ seen.add(key);
221
+ }
222
+ return builder;
223
+ },
224
+ getError() {
225
+ return error;
226
+ }
227
+ };
228
+ return builder;
229
+ }
230
+ function dateValidator(input) {
231
+ let error = '';
232
+ const setError = (message) => {
233
+ if (!error)
234
+ error = message;
235
+ };
236
+ const date = input instanceof Date ? input : new Date(input);
237
+ const isValid = !Number.isNaN(date.getTime());
238
+ const builder = {
239
+ required() {
240
+ if (!error && !isValid)
241
+ setError('Required');
242
+ return builder;
243
+ },
244
+ before(target) {
245
+ if (!error && isValid) {
246
+ const targetDate = target instanceof Date ? target : new Date(target);
247
+ if (date >= targetDate)
248
+ setError(`Must be before ${targetDate.toISOString()}`);
249
+ }
250
+ return builder;
251
+ },
252
+ after(target) {
253
+ if (!error && isValid) {
254
+ const targetDate = target instanceof Date ? target : new Date(target);
255
+ if (date <= targetDate)
256
+ setError(`Must be after ${targetDate.toISOString()}`);
257
+ }
258
+ return builder;
259
+ },
260
+ between(start, end) {
261
+ if (!error && isValid) {
262
+ const startDate = start instanceof Date ? start : new Date(start);
263
+ const endDate = end instanceof Date ? end : new Date(end);
264
+ if (date < startDate || date > endDate)
265
+ setError(`Must be between ${startDate.toISOString()} and ${endDate.toISOString()}`);
266
+ }
267
+ return builder;
268
+ },
269
+ past() {
270
+ if (!error && isValid && date >= new Date())
271
+ setError('Must be in the past');
272
+ return builder;
273
+ },
274
+ future() {
275
+ if (!error && isValid && date <= new Date())
276
+ setError('Must be in the future');
277
+ return builder;
278
+ },
279
+ weekday() {
280
+ if (!error && isValid) {
281
+ const day = date.getDay();
282
+ if (day === 0 || day === 6)
283
+ setError('Must be a weekday');
284
+ }
285
+ return builder;
286
+ },
287
+ weekend() {
288
+ if (!error && isValid) {
289
+ const day = date.getDay();
290
+ if (day !== 0 && day !== 6)
291
+ setError('Must be a weekend');
292
+ }
293
+ return builder;
294
+ },
295
+ minAge(years) {
296
+ if (!error && isValid) {
297
+ const minDate = new Date();
298
+ minDate.setFullYear(minDate.getFullYear() - years);
299
+ if (date > minDate)
300
+ setError(`Must be at least ${years} years ago`);
301
+ }
302
+ return builder;
303
+ },
304
+ maxAge(years) {
305
+ if (!error && isValid) {
306
+ const maxDate = new Date();
307
+ maxDate.setFullYear(maxDate.getFullYear() - years);
308
+ if (date < maxDate)
309
+ setError(`Must be at most ${years} years ago`);
310
+ }
311
+ return builder;
312
+ },
313
+ getError() {
314
+ return error;
315
+ }
316
+ };
317
+ return builder;
318
+ }
package/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "svstate",
3
+ "version": "0.0.1",
4
+ "description": "Supercharged $state() for Svelte 5: deep reactive proxy with validation, cross-field rules, computed & side-effects",
5
+ "author": "BCsabaEngine",
6
+ "license": "ISC",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "default": "./dist/index.js"
13
+ }
14
+ },
15
+ "engines": {
16
+ "node": ">=20",
17
+ "npm": ">=9"
18
+ },
19
+ "files": [
20
+ "dist/**/*.js",
21
+ "dist/**/*.d.ts"
22
+ ],
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/BCsabaEngine/svstate.git"
26
+ },
27
+ "bugs": {
28
+ "url": "https://github.com/BCsabaEngine/svstate/issues"
29
+ },
30
+ "homepage": "https://github.com/BCsabaEngine/svstate",
31
+ "scripts": {
32
+ "demo": "cd demo && ./node_modules/.bin/vite dev --host --open",
33
+ "demo:build": "cd demo && ./node_modules/.bin/vite build",
34
+ "test": "vitest run --pass-with-no-tests",
35
+ "test:coverage": "vitest run --coverage",
36
+ "clean": "tsc --build --clean",
37
+ "build": "tsc --build --clean && tsc --build --force",
38
+ "format:check": "prettier --check .",
39
+ "format:fix": "prettier --write . | grep -v 'unchanged' | sed G",
40
+ "lint:check": "eslint .",
41
+ "lint:fix": "eslint --fix .",
42
+ "fix": "node --run format:fix && node --run lint:fix && node --run format:fix",
43
+ "all": "node --run fix && node --run build && node --run test && node --run demo:build",
44
+ "npm:reinstall": "rm -rf ./node_modules && rm -f ./package-lock.json && npm i && npm i"
45
+ },
46
+ "keywords": [
47
+ "svelte",
48
+ "svelte5",
49
+ "state",
50
+ "validation",
51
+ "runes",
52
+ "proxy"
53
+ ],
54
+ "devDependencies": {
55
+ "@sveltejs/vite-plugin-svelte": "^6.2.4",
56
+ "@types/node": "^25.0.9",
57
+ "@typescript-eslint/eslint-plugin": "^8.53.0",
58
+ "@typescript-eslint/parser": "^8.53.0",
59
+ "@vitest/coverage-v8": "^4.0.17",
60
+ "eslint": "^9.39.2",
61
+ "eslint-config-prettier": "^10.1.8",
62
+ "eslint-plugin-simple-import-sort": "^12.1.1",
63
+ "eslint-plugin-unicorn": "^62.0.0",
64
+ "nodemon": "^3.1.11",
65
+ "prettier": "^3.8.0",
66
+ "svelte": "^5.46.4",
67
+ "ts-node": "^10.9.2",
68
+ "tsx": "^4.21.0",
69
+ "typescript": "^5.9.3",
70
+ "vitest": "^4.0.17"
71
+ },
72
+ "peerDependencies": {
73
+ "svelte": "^5.0.0"
74
+ }
75
+ }