@fumari/stf 1.0.0 → 1.0.2

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
@@ -2,60 +2,9 @@ import { t as FieldKey } from "./types-Do_aTV5I.js";
2
2
  import { ReactNode } from "react";
3
3
  import * as react_jsx_runtime0 from "react/jsx-runtime";
4
4
 
5
- //#region src/lib/stf.d.ts
6
- interface Stf {
7
- dataEngine: DataEngine;
8
- }
9
- declare function StfProvider({
10
- value,
11
- children
12
- }: {
13
- value: Stf;
14
- children: ReactNode;
15
- }): react_jsx_runtime0.JSX.Element;
16
- declare function useStf(options: {
17
- /**
18
- * Note: the passed object will be modified in place, use `structuredClone()` to keep the original object unchanged.
19
- */
20
- defaultValues?: DefaultValue<Record<string, unknown>>;
21
- }): Stf;
22
- declare function useDataEngine(instance?: Stf | DataEngine): DataEngine;
23
- interface ArrayItemInfo {
24
- field: FieldKey;
25
- index: number;
26
- }
27
- declare function useArray(field: FieldKey, options?: {
28
- defaultValue?: DefaultValue<unknown[]>;
29
- }): {
30
- items: ArrayItemInfo[];
31
- insertItem(itemValue?: unknown): void;
32
- removeItem(index: number): void;
33
- };
34
- type PropertyItemInfo<T> = {
35
- kind: 'fixed' | 'fallback';
36
- field: FieldKey;
37
- key: string;
38
- info: T;
39
- } | {
40
- kind: 'pattern';
41
- field: FieldKey;
42
- key: string;
43
- pattern: string;
44
- info: T;
45
- };
46
- declare function useObject<T>(field: FieldKey, options: {
47
- defaultValue?: DefaultValue<object>;
48
- properties: Record<string, T>;
49
- patternProperties?: Record<string, T>;
50
- fallback?: T;
51
- }): {
52
- properties: PropertyItemInfo<T>[];
53
- onAppend(name: string, value?: unknown): void;
54
- onDelete(name: string): unknown;
55
- };
56
- //#endregion
57
5
  //#region src/lib/data-engine.d.ts
58
6
  type DefaultValue<T = unknown> = T | (() => T);
7
+ declare function getDefaultValue<T>(defaultValue: DefaultValue<T>): T;
59
8
  interface DataEngineListener {
60
9
  /**
61
10
  * when specified, only call the listener for events affecting the specified field.
@@ -92,10 +41,16 @@ interface OnInitContext {
92
41
  /** custom data */
93
42
  custom?: Record<string, unknown>;
94
43
  }
44
+ interface NamespaceConfig {
45
+ reset?: (ctx: {
46
+ engine: DataEngine;
47
+ }) => void;
48
+ }
95
49
  declare class DataEngine {
96
50
  private data;
97
- /** namespace -> engine */
98
- private namespaces;
51
+ readonly namespaces: Map<string, {
52
+ engine: DataEngine;
53
+ } & NamespaceConfig>;
99
54
  private readonly listeners;
100
55
  constructor(defaultValues?: DefaultValue<NonNullable<object>>);
101
56
  listen(listener: DataEngineListener): void;
@@ -103,11 +58,11 @@ declare class DataEngine {
103
58
  getData(): object;
104
59
  /**
105
60
  * init a field
106
- * @param key the key of field
61
+ * @param field the key of field
107
62
  * @param defaultValue the initial value, the field is also created for `undefined`
108
63
  * @returns the value of initialized field, or the current value of field if already initialized
109
64
  */
110
- init(key: FieldKey, defaultValue?: DefaultValue, ctx?: OnInitContext): unknown;
65
+ init(field: FieldKey, defaultValue?: DefaultValue, ctx?: OnInitContext): unknown;
111
66
  delete(key: FieldKey, ctx?: OnDeleteContext): unknown | undefined;
112
67
  get(key: FieldKey): unknown;
113
68
  /**
@@ -118,9 +73,63 @@ declare class DataEngine {
118
73
  /**
119
74
  * create an isolated data engine
120
75
  */
121
- namespace(namespace: string, initialValue?: DefaultValue<NonNullable<object>>): DataEngine;
122
- reset(data: Record<string, unknown>): void;
76
+ namespace(namespace: string, initialValue?: DefaultValue<NonNullable<object>>, config?: NamespaceConfig): DataEngine;
77
+ reset(data: NonNullable<object>): void;
123
78
  }
79
+ //#endregion
80
+ //#region src/lib/stf.d.ts
81
+ interface Stf {
82
+ dataEngine: DataEngine;
83
+ }
84
+ declare function StfProvider({
85
+ value,
86
+ children
87
+ }: {
88
+ value: Stf;
89
+ children: ReactNode;
90
+ }): react_jsx_runtime0.JSX.Element;
91
+ declare function useStf(options: {
92
+ /**
93
+ * Note: the passed object will be modified in place, use `structuredClone()` to keep the original object unchanged.
94
+ */
95
+ defaultValues?: DefaultValue<Record<string, unknown>>;
96
+ }): Stf;
97
+ declare function useDataEngine(instance?: Stf | DataEngine): DataEngine;
98
+ interface ArrayItemInfo {
99
+ field: FieldKey;
100
+ index: number;
101
+ }
102
+ declare function useArray(field: FieldKey, options?: {
103
+ defaultValue?: DefaultValue<unknown[]>;
104
+ }): {
105
+ items: ArrayItemInfo[];
106
+ insertItem(itemValue?: unknown): void;
107
+ removeItem(index: number): void;
108
+ };
109
+ type PropertyItemInfo<T> = {
110
+ kind: 'fixed' | 'fallback';
111
+ field: FieldKey;
112
+ key: string;
113
+ info: T;
114
+ } | {
115
+ kind: 'pattern';
116
+ field: FieldKey;
117
+ key: string;
118
+ pattern: string;
119
+ info: T;
120
+ };
121
+ declare function useObject<T>(field: FieldKey, options: {
122
+ defaultValue?: DefaultValue<object>; /** ignore fixed properties unless defined */
123
+ lazy?: boolean;
124
+ properties: Record<string, T>;
125
+ patternProperties?: Record<string, T>;
126
+ fallback?: T;
127
+ }): {
128
+ properties: PropertyItemInfo<T>[];
129
+ _objectKeys: string[];
130
+ onAppend(name: string, value?: unknown): void;
131
+ onDelete(name: string): unknown;
132
+ };
124
133
  declare function useFieldValue<V = unknown>(key: FieldKey, options?: {
125
134
  stf?: Stf | DataEngine;
126
135
  defaultValue?: DefaultValue;
@@ -135,5 +144,10 @@ declare function useFieldValue<V = unknown>(key: FieldKey, options?: {
135
144
  declare function useListener(listener: DataEngineListener & {
136
145
  stf?: Stf | DataEngine;
137
146
  }): void;
147
+ declare function useNamespace(options: {
148
+ namespace: string;
149
+ initial?: DefaultValue<NonNullable<object>>;
150
+ stf?: Stf | DataEngine;
151
+ }): DataEngine;
138
152
  //#endregion
139
- export { ArrayItemInfo, DataEngine, DataEngineListener, DefaultValue, FieldKey, OnDeleteContext, OnInitContext, OnUpdateContext, PropertyItemInfo, Stf, StfProvider, useArray, useDataEngine, useFieldValue, useListener, useObject, useStf };
153
+ export { ArrayItemInfo, DataEngine, DataEngineListener, DefaultValue, FieldKey, NamespaceConfig, OnDeleteContext, OnInitContext, OnUpdateContext, PropertyItemInfo, Stf, StfProvider, getDefaultValue, useArray, useDataEngine, useFieldValue, useListener, useNamespace, useObject, useStf };
package/dist/index.js CHANGED
@@ -1,118 +1,9 @@
1
1
  'use client';
2
2
 
3
- import { deepEqual, fieldKeyStartsWith, objectGet, objectSet, stringifyFieldKey } from "./lib/utils.js";
3
+ import { deepEqual, fieldKeyStartsWith, isPlainObject, objectGet, objectSet, stringifyFieldKey } from "./lib/utils.js";
4
4
  import { createContext, use, useEffect, useMemo, useRef, useState } from "react";
5
5
  import { jsx } from "react/jsx-runtime";
6
6
 
7
- //#region src/lib/stf.tsx
8
- const Context = createContext(null);
9
- function StfProvider({ value, children }) {
10
- return /* @__PURE__ */ jsx(Context, {
11
- value,
12
- children
13
- });
14
- }
15
- function useStf(options) {
16
- const { defaultValues } = options;
17
- const dataEngine = useMemo(() => new DataEngine(defaultValues), []);
18
- return useMemo(() => ({ dataEngine }), [dataEngine]);
19
- }
20
- function useDataEngine(instance) {
21
- if (instance instanceof DataEngine) return instance;
22
- if (instance) return instance.dataEngine;
23
- return use(Context).dataEngine;
24
- }
25
- function useArray(field, options = {}) {
26
- const engine = useDataEngine();
27
- const [items] = useFieldValue(field, {
28
- defaultValue: options.defaultValue,
29
- compute(value) {
30
- const items = [];
31
- if (Array.isArray(value)) for (let i = 0; i < value.length; i++) items.push({
32
- field: [...field, i],
33
- index: i
34
- });
35
- return items;
36
- },
37
- isChanged(prev, next) {
38
- return prev.length !== next.length;
39
- }
40
- });
41
- return {
42
- items,
43
- insertItem(itemValue) {
44
- const value = engine.get(field);
45
- engine.update(field, Array.isArray(value) ? [...value, itemValue] : [itemValue]);
46
- },
47
- removeItem(index) {
48
- engine.delete([...field, index]);
49
- }
50
- };
51
- }
52
- function useObject(field, options) {
53
- const engine = useDataEngine();
54
- const [objectKeys] = useFieldValue(field, {
55
- defaultValue: options.defaultValue,
56
- compute(currentValue) {
57
- return currentValue ? Object.keys(currentValue) : [];
58
- },
59
- isChanged(prev, next) {
60
- return !deepEqual(prev, next);
61
- }
62
- });
63
- return {
64
- properties: useMemo(() => {
65
- const properties = [];
66
- const unknownKeys = new Set(objectKeys);
67
- for (const [key, prop] of Object.entries(options.properties)) {
68
- unknownKeys.delete(key);
69
- properties.push({
70
- kind: "fixed",
71
- field: [...field, key],
72
- key,
73
- info: prop
74
- });
75
- }
76
- for (const [pattern, prop] of Object.entries(options.patternProperties ?? {})) {
77
- const regex = RegExp(pattern);
78
- for (const key of unknownKeys) {
79
- if (!key.match(regex)) continue;
80
- unknownKeys.delete(key);
81
- properties.push({
82
- kind: "pattern",
83
- info: prop,
84
- key,
85
- pattern,
86
- field: [...field, key]
87
- });
88
- }
89
- }
90
- if (options.fallback) for (const key of unknownKeys) properties.push({
91
- kind: "fallback",
92
- field: [...field, key],
93
- key,
94
- info: options.fallback
95
- });
96
- return properties;
97
- }, [
98
- field,
99
- objectKeys,
100
- options.fallback,
101
- options.patternProperties,
102
- options.properties
103
- ]),
104
- onAppend(name, value) {
105
- name = name.trim();
106
- if (name.length === 0) return;
107
- engine.init([...field, name], value);
108
- },
109
- onDelete(name) {
110
- return engine.delete([...field, name]);
111
- }
112
- };
113
- }
114
-
115
- //#endregion
116
7
  //#region src/lib/data-engine.ts
117
8
  function getDefaultValue(defaultValue) {
118
9
  return typeof defaultValue === "function" ? defaultValue() : defaultValue;
@@ -178,34 +69,46 @@ var DataEngine = class DataEngine {
178
69
  }
179
70
  /**
180
71
  * init a field
181
- * @param key the key of field
72
+ * @param field the key of field
182
73
  * @param defaultValue the initial value, the field is also created for `undefined`
183
74
  * @returns the value of initialized field, or the current value of field if already initialized
184
75
  */
185
- init(key, defaultValue, ctx = {}) {
186
- if (key.length === 0) return this.data;
187
- let cur = this.data;
188
- const currentKey = [];
76
+ init(field, defaultValue, ctx = {}) {
77
+ if (field.length === 0) return this.data;
78
+ const parentKey = [];
189
79
  const parentUpdateCtx = {
190
80
  swallow: true,
191
81
  ...ctx
192
82
  };
193
- for (let i = 0; i < key.length; i++) {
194
- const propKey = key[i];
195
- const propValue = cur[propKey];
196
- if (i === key.length - 1) {
197
- if (propValue !== void 0) return propValue;
198
- cur[propKey] = getDefaultValue(defaultValue);
199
- this.listeners.onUpdate(currentKey, parentUpdateCtx);
200
- this.listeners.onInit(key, ctx);
201
- return cur[propKey];
202
- } else if (typeof propValue === "object" && propValue !== null) cur = propValue;
203
- else {
204
- if (propValue !== void 0) console.warn(`the original value of field ${currentKey.join(".")} is overidden, this might be unexpected.`);
205
- cur = cur[propKey] = {};
206
- this.listeners.onUpdate(currentKey, parentUpdateCtx);
83
+ let parent = this.data;
84
+ const fieldsToInit = [];
85
+ let initStart = null;
86
+ for (let i = 0; i < field.length; i++) {
87
+ const key = field[i];
88
+ const value = parent[key];
89
+ if (i === field.length - 1) {
90
+ if (value !== void 0) return value;
91
+ const out = parent[key] = getDefaultValue(defaultValue);
92
+ fieldsToInit.push(field);
93
+ for (const initField of fieldsToInit) this.listeners.onInit(initField, ctx);
94
+ this.listeners.onUpdate(initStart ?? parentKey, parentUpdateCtx);
95
+ return out;
96
+ } else if (isPlainObject(value) || Array.isArray(value)) {
97
+ parent = value;
98
+ parentKey.push(key);
99
+ } else {
100
+ const nextKey = field[i + 1];
101
+ parent = parent[key] = typeof nextKey === "number" ? new Array(nextKey + 1) : {};
102
+ if (value === void 0) {
103
+ initStart ??= [...parentKey];
104
+ parentKey.push(key);
105
+ fieldsToInit.push([...parentKey]);
106
+ } else {
107
+ parentKey.push(key);
108
+ this.listeners.onUpdate(parentKey, parentUpdateCtx);
109
+ console.warn(`the original value of field ${parentKey.join(".")} is overidden, this might be unexpected.`);
110
+ }
207
111
  }
208
- currentKey.push(propKey);
209
112
  }
210
113
  }
211
114
  delete(key, ctx = {}) {
@@ -213,16 +116,18 @@ var DataEngine = class DataEngine {
213
116
  const parentKey = key.slice(0, -1);
214
117
  const prop = key[key.length - 1];
215
118
  const parent = this.get(parentKey);
216
- if (Array.isArray(parent) && typeof prop === "number") {
119
+ if (typeof prop === "number" && Array.isArray(parent)) {
120
+ if (parent.length === 0) return;
121
+ const isLast = prop === parent.length - 1;
217
122
  const deleted = parent.splice(prop, 1);
218
123
  if (deleted.length === 0) return;
219
124
  this.listeners.onDelete(key, ctx);
220
125
  this.listeners.onUpdate(parentKey, {
221
- swallow: false,
126
+ swallow: isLast,
222
127
  ...ctx
223
128
  });
224
129
  return deleted[0];
225
- } else if (typeof parent === "object" && parent !== null) {
130
+ } else if (isPlainObject(parent) && prop in parent) {
226
131
  const temp = parent[prop];
227
132
  delete parent[prop];
228
133
  this.listeners.onDelete(key, ctx);
@@ -255,34 +160,156 @@ var DataEngine = class DataEngine {
255
160
  /**
256
161
  * create an isolated data engine
257
162
  */
258
- namespace(namespace, initialValue) {
163
+ namespace(namespace, initialValue, config) {
259
164
  let child = this.namespaces.get(namespace);
260
165
  if (!child) {
261
- child = new DataEngine(initialValue);
166
+ child = {
167
+ engine: new DataEngine(initialValue),
168
+ ...config
169
+ };
262
170
  this.namespaces.set(namespace, child);
263
- }
264
- return child;
171
+ } else Object.assign(child, config);
172
+ return child.engine;
265
173
  }
266
174
  reset(data) {
267
- this.namespaces.clear();
268
175
  this.update([], data);
176
+ for (const { engine, reset } of this.namespaces.values()) reset?.({ engine });
269
177
  }
270
178
  };
179
+
180
+ //#endregion
181
+ //#region src/lib/stf.tsx
182
+ const Context = createContext(null);
183
+ function StfProvider({ value, children }) {
184
+ return /* @__PURE__ */ jsx(Context, {
185
+ value,
186
+ children
187
+ });
188
+ }
189
+ function useStf(options) {
190
+ const optionsRef = useRef(options);
191
+ optionsRef.current = options;
192
+ return useMemo(() => ({ dataEngine: new DataEngine(optionsRef.current.defaultValues) }), []);
193
+ }
194
+ function useDataEngine(instance) {
195
+ if (instance instanceof DataEngine) return instance;
196
+ if (instance) return instance.dataEngine;
197
+ return use(Context).dataEngine;
198
+ }
199
+ function useArray(field, options = {}) {
200
+ const engine = useDataEngine();
201
+ const { defaultValue } = options;
202
+ const [items] = useFieldValue(field, {
203
+ defaultValue,
204
+ compute(value) {
205
+ const items = [];
206
+ if (!Array.isArray(value)) return items;
207
+ for (let i = 0; i < value.length; i++) items.push({
208
+ field: [...field, i],
209
+ index: i
210
+ });
211
+ return items;
212
+ },
213
+ isChanged(prev, next) {
214
+ return prev.length !== next.length;
215
+ }
216
+ });
217
+ return {
218
+ items,
219
+ insertItem(itemValue) {
220
+ const value = engine.get(field);
221
+ const idx = Array.isArray(value) ? value.length : 0;
222
+ engine.init([...field, idx], itemValue);
223
+ },
224
+ removeItem(index) {
225
+ engine.delete([...field, index]);
226
+ }
227
+ };
228
+ }
229
+ function useObject(field, options) {
230
+ const { properties: definedProps, patternProperties: definedPatternProps = {}, defaultValue, fallback, lazy } = options;
231
+ const engine = useDataEngine();
232
+ const [objectKeys] = useFieldValue(field, {
233
+ defaultValue,
234
+ compute(currentValue) {
235
+ return isPlainObject(currentValue) ? Object.keys(currentValue) : [];
236
+ },
237
+ isChanged(prev, next) {
238
+ return !deepEqual(prev, next);
239
+ }
240
+ });
241
+ return {
242
+ properties: useMemo(() => {
243
+ const properties = [];
244
+ const unknownKeys = new Set(objectKeys);
245
+ for (const [key, prop] of Object.entries(definedProps)) {
246
+ if (lazy && !unknownKeys.has(key)) continue;
247
+ unknownKeys.delete(key);
248
+ properties.push({
249
+ kind: "fixed",
250
+ field: [...field, key],
251
+ key,
252
+ info: prop
253
+ });
254
+ }
255
+ for (const [pattern, prop] of Object.entries(definedPatternProps)) {
256
+ const regex = RegExp(pattern);
257
+ for (const key of unknownKeys) {
258
+ if (!key.match(regex)) continue;
259
+ unknownKeys.delete(key);
260
+ properties.push({
261
+ kind: "pattern",
262
+ info: prop,
263
+ key,
264
+ pattern,
265
+ field: [...field, key]
266
+ });
267
+ }
268
+ }
269
+ if (fallback) for (const key of unknownKeys) properties.push({
270
+ kind: "fallback",
271
+ field: [...field, key],
272
+ key,
273
+ info: fallback
274
+ });
275
+ return properties;
276
+ }, [
277
+ definedPatternProps,
278
+ definedProps,
279
+ fallback,
280
+ field,
281
+ lazy,
282
+ objectKeys
283
+ ]),
284
+ _objectKeys: objectKeys,
285
+ onAppend(name, value) {
286
+ name = name.trim();
287
+ if (name.length === 0) return;
288
+ engine.init([...field, name], value);
289
+ },
290
+ onDelete(name) {
291
+ return engine.delete([...field, name]);
292
+ }
293
+ };
294
+ }
271
295
  function useFieldValue(key, options = {}) {
272
- const engine = useDataEngine(options.stf);
273
- const { compute = (v) => v, defaultValue, isChanged = (a, b) => a !== b } = options;
274
- const [value, setValue] = useState(() => compute(engine.init(key, defaultValue)));
296
+ const { stf, compute = (v) => v, defaultValue, isChanged = (a, b) => a !== b } = options;
297
+ const engine = useDataEngine(stf);
298
+ const [value, setValue] = useState(() => compute(defaultValue === void 0 ? engine.get(key) : engine.init(key, defaultValue)));
299
+ const prevEngineRef = useRef(engine);
300
+ if (prevEngineRef.current !== engine) {
301
+ setValue(compute(defaultValue === void 0 ? engine.get(key) : engine.init(key, defaultValue)));
302
+ prevEngineRef.current = engine;
303
+ }
304
+ function onUpdate() {
305
+ const computed = compute(engine.get(key));
306
+ setValue((prev) => isChanged(prev, computed) ? computed : prev);
307
+ }
275
308
  useListener({
276
309
  field: key,
277
- stf: options.stf,
278
- onInit() {
279
- const computed = compute(engine.get(key));
280
- setValue((prev) => isChanged(prev, computed) ? computed : prev);
281
- },
282
- onUpdate() {
283
- const computed = compute(engine.get(key));
284
- setValue((prev) => isChanged(prev, computed) ? computed : prev);
285
- },
310
+ stf,
311
+ onInit: onUpdate,
312
+ onUpdate,
286
313
  onDelete() {
287
314
  const computed = compute(void 0);
288
315
  setValue((prev) => isChanged(prev, computed) ? computed : prev);
@@ -313,6 +340,12 @@ function useListener(listener) {
313
340
  };
314
341
  }, [engine, listener.field]);
315
342
  }
343
+ function useNamespace(options) {
344
+ const { namespace, stf, initial } = options;
345
+ return useDataEngine(stf).namespace(namespace, initial, { reset({ engine }) {
346
+ engine.update([], getDefaultValue(initial));
347
+ } });
348
+ }
316
349
 
317
350
  //#endregion
318
- export { DataEngine, StfProvider, useArray, useDataEngine, useFieldValue, useListener, useObject, useStf };
351
+ export { DataEngine, StfProvider, getDefaultValue, useArray, useDataEngine, useFieldValue, useListener, useNamespace, useObject, useStf };
@@ -7,7 +7,7 @@ declare function objectGet(obj: unknown, key: (string | number)[]): unknown | un
7
7
  *
8
8
  * @returns updated value, throw error if parent object doesn't exist
9
9
  */
10
- declare function objectSet(obj: unknown, key: FieldKey, value: unknown): unknown;
10
+ declare function objectSet(obj: unknown, field: FieldKey, value: unknown): unknown;
11
11
  /**
12
12
  * doesn't handle recursive objects
13
13
  */
@@ -17,5 +17,6 @@ declare function stringifyFieldKey(fieldKey: FieldKey): string;
17
17
  * @returns if `a` starts with `b`.
18
18
  */
19
19
  declare function fieldKeyStartsWith(a: string, b: string): boolean;
20
+ declare function isPlainObject(value: unknown): value is Record<string, unknown>;
20
21
  //#endregion
21
- export { deepEqual, fieldKeyStartsWith, objectGet, objectSet, stringifyFieldKey };
22
+ export { deepEqual, fieldKeyStartsWith, isPlainObject, objectGet, objectSet, stringifyFieldKey };
package/dist/lib/utils.js CHANGED
@@ -1,10 +1,9 @@
1
1
  //#region src/lib/utils.ts
2
2
  function objectGet(obj, key) {
3
3
  let cur = obj;
4
- for (const prop of key) {
5
- if (typeof cur !== "object" || cur === null || !(prop in cur)) return;
6
- cur = cur[prop];
7
- }
4
+ for (const prop of key) if (isPlainObject(cur) && prop in cur) cur = cur[prop];
5
+ else if (typeof prop === "number" && Array.isArray(cur)) cur = cur[prop];
6
+ else return;
8
7
  return cur;
9
8
  }
10
9
  /**
@@ -12,11 +11,13 @@ function objectGet(obj, key) {
12
11
  *
13
12
  * @returns updated value, throw error if parent object doesn't exist
14
13
  */
15
- function objectSet(obj, key, value) {
16
- if (key.length === 0) return value;
17
- const parent = objectGet(obj, key.slice(0, -1));
18
- if (typeof parent !== "object" || parent === null) throw new Error("missing parent object");
19
- parent[key[key.length - 1]] = value;
14
+ function objectSet(obj, field, value) {
15
+ if (field.length === 0) return value;
16
+ const parent = objectGet(obj, field.slice(0, -1));
17
+ const key = field[field.length - 1];
18
+ if (isPlainObject(parent)) parent[key] = value;
19
+ else if (typeof key === "number" && Array.isArray(parent)) parent[key] = value;
20
+ else throw new Error("missing parent object");
20
21
  return obj;
21
22
  }
22
23
  /**
@@ -31,13 +32,14 @@ function deepEqual(a, b) {
31
32
  return a.every((item, index) => deepEqual(item, b[index]));
32
33
  }
33
34
  if (Array.isArray(a) || Array.isArray(b)) return false;
35
+ if (!isPlainObject(a) || !isPlainObject(b)) return false;
34
36
  const keysA = Object.keys(a);
35
37
  const keysB = Object.keys(b);
36
38
  if (keysA.length !== keysB.length) return false;
37
- return keysA.every((key) => Object.prototype.hasOwnProperty.call(b, key) && deepEqual(a[key], b[key]));
39
+ return keysA.every((key) => key in b && deepEqual(a[key], b[key]));
38
40
  }
39
41
  function stringifyFieldKey(fieldKey) {
40
- return fieldKey.map((v) => `${typeof v}:${v}`).join(".");
42
+ return fieldKey.map((v) => typeof v === "string" ? `_${v}` : `n${v}`).join(".");
41
43
  }
42
44
  /**
43
45
  * @returns if `a` starts with `b`.
@@ -45,6 +47,11 @@ function stringifyFieldKey(fieldKey) {
45
47
  function fieldKeyStartsWith(a, b) {
46
48
  return b.length === 0 || a === b || a.startsWith(b + ".");
47
49
  }
50
+ function isPlainObject(value) {
51
+ if (typeof value !== "object" || value === null) return false;
52
+ const prototype = Object.getPrototypeOf(value);
53
+ return prototype === null || prototype === Object.prototype || Object.getPrototypeOf(prototype) === null;
54
+ }
48
55
 
49
56
  //#endregion
50
- export { deepEqual, fieldKeyStartsWith, objectGet, objectSet, stringifyFieldKey };
57
+ export { deepEqual, fieldKeyStartsWith, isPlainObject, objectGet, objectSet, stringifyFieldKey };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fumari/stf",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Schema to Form.",
5
5
  "keywords": [],
6
6
  "license": "MIT",