@fumari/stf 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.
- package/LICENSE +21 -0
- package/dist/index.d.ts +129 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +299 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/utils.d.ts +23 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +3 -0
- package/dist/types-PXiOOMNs.d.ts +5 -0
- package/dist/types-PXiOOMNs.d.ts.map +1 -0
- package/dist/utils-W82BoH4I.js +53 -0
- package/dist/utils-W82BoH4I.js.map +1 -0
- package/package.json +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Fuma
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { t as FieldKey } from "./types-PXiOOMNs.js";
|
|
2
|
+
import { ReactNode } from "react";
|
|
3
|
+
import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
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(stf?: Stf): 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
|
+
//#region src/lib/data-engine.d.ts
|
|
58
|
+
type DefaultValue<T = unknown> = T | (() => T);
|
|
59
|
+
interface DataEngineListener {
|
|
60
|
+
/**
|
|
61
|
+
* when specified, only call the listener for events affecting the specified field.
|
|
62
|
+
*/
|
|
63
|
+
field?: FieldKey;
|
|
64
|
+
/**
|
|
65
|
+
* when field value is changed
|
|
66
|
+
*/
|
|
67
|
+
onUpdate?: (key: FieldKey, ctx: OnUpdateContext) => void;
|
|
68
|
+
/**
|
|
69
|
+
* when `init(field)` is called
|
|
70
|
+
*/
|
|
71
|
+
onInit?: (key: FieldKey) => void;
|
|
72
|
+
/**
|
|
73
|
+
* when `delete(field)` is called
|
|
74
|
+
*/
|
|
75
|
+
onDelete?: (key: FieldKey) => void;
|
|
76
|
+
}
|
|
77
|
+
interface OnUpdateContext {
|
|
78
|
+
/**
|
|
79
|
+
* An update is swallow if the change doesn't affect the values of children fields.
|
|
80
|
+
*/
|
|
81
|
+
swallow: boolean;
|
|
82
|
+
}
|
|
83
|
+
declare class DataEngine {
|
|
84
|
+
private data;
|
|
85
|
+
private attachedDataMap;
|
|
86
|
+
private readonly listeners;
|
|
87
|
+
constructor(defaultValues?: DefaultValue<NonNullable<object>>);
|
|
88
|
+
listen(listener: DataEngineListener): void;
|
|
89
|
+
unlisten(listener: DataEngineListener): void;
|
|
90
|
+
getData(): object;
|
|
91
|
+
/**
|
|
92
|
+
* init a field
|
|
93
|
+
* @param key the key of field
|
|
94
|
+
* @param defaultValue the initial value, the field is also created for `undefined`
|
|
95
|
+
* @returns the value of initialized field, or the current value of field if already initialized
|
|
96
|
+
*/
|
|
97
|
+
init(key: FieldKey, defaultValue?: DefaultValue): unknown;
|
|
98
|
+
delete(key: FieldKey): unknown | undefined;
|
|
99
|
+
get(key: FieldKey): unknown;
|
|
100
|
+
/**
|
|
101
|
+
* update the value of field if it exists
|
|
102
|
+
* @returns if the field is updated
|
|
103
|
+
*/
|
|
104
|
+
update(key: FieldKey, value: unknown): boolean;
|
|
105
|
+
attachedData<T>(namespace: string): {
|
|
106
|
+
get: (field: FieldKey) => T | undefined;
|
|
107
|
+
set: (field: FieldKey, value: T) => void;
|
|
108
|
+
delete: (field?: FieldKey) => void;
|
|
109
|
+
};
|
|
110
|
+
reset(data: Record<string, unknown>): void;
|
|
111
|
+
}
|
|
112
|
+
declare function useFieldValue<V = unknown>(key: FieldKey, options?: {
|
|
113
|
+
stf?: Stf;
|
|
114
|
+
defaultValue?: DefaultValue;
|
|
115
|
+
/**
|
|
116
|
+
* compute value from the actual field value.
|
|
117
|
+
*
|
|
118
|
+
* to re-compute on in-place updates (may happen on objects, arrays), you should clone the object here.
|
|
119
|
+
*/
|
|
120
|
+
compute?: (currentValue: unknown) => V;
|
|
121
|
+
/** determine whether the value/computed value is changed */
|
|
122
|
+
isChanged?: (prev: V, next: V) => boolean;
|
|
123
|
+
}): readonly [V, (newValue: unknown) => boolean];
|
|
124
|
+
declare function useListener(listener: DataEngineListener & {
|
|
125
|
+
stf?: Stf;
|
|
126
|
+
}): void;
|
|
127
|
+
//#endregion
|
|
128
|
+
export { ArrayItemInfo, DataEngine, DataEngineListener, DefaultValue, FieldKey, PropertyItemInfo, Stf, StfProvider, useArray, useDataEngine, useFieldValue, useListener, useObject, useStf };
|
|
129
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/lib/stf.tsx","../src/lib/data-engine.ts"],"sourcesContent":[],"mappings":";;;;;UAOiB,GAAA;cACH;AADd;AAIgB,iBAAA,WAAA,CAAW;EAAA,KAAA;EAAA;CAAA,EAAA;EAAG,KAAA,EAA4B,GAA5B;EAAO,QAAA,EAAoC,SAApC;CAAqB,CAAA,EAA0B,kBAAA,CAAA,GAAA,CAAA,OAA1B;AAAe,iBAIzD,MAAA,CAJyD,OAAA,EAAA;EAAW;;AAIpF;EAI+B,aAAA,CAAA,EAAb,YAAa,CAAA,MAAA,CAAA,MAAA,EAAA,OAAA,CAAA,CAAA;CAAb,CAAA,EACd,GADc;AACd,iBAgBY,aAAA,CAhBZ,GAAA,CAAA,EAgBgC,GAhBhC,CAAA,EAgBmC,UAhBnC;AAAG,UAqBU,aAAA,CArBV;EAgBS,KAAA,EAMP,QANO;EAKC,KAAA,EAAA,MAAA;AAKjB;AACS,iBADO,QAAA,CACP,KAAA,EAAA,QAAA,EAAA,QAAA,EAAA;EACmB,YAAA,CAAA,EAAA,YAAA,CAAA,OAAA,EAAA,CAAA;;;EAkChB,UAAA,CAAA,SAAgB,CAAA,EAAA,OAAA,CAAA,EAAA,IAAA;EAGf,UAAA,CAAA,KAAA,EAAA,MAAA,CAAA,EAAA,IAAA;CAED;AAIC,KATD,gBASC,CAAA,CAAA,CAAA,GAAA;EAGD,IAAA,EAAA,OAAA,GAAA,UAAA;EAAC,KAAA,EATA,QASA;EAGG,GAAA,EAAA,MAAA;EACP,IAAA,EAXG,CAWH;CAEU,GAAA;EACY,IAAA,EAAA,SAAA;EAAf,KAAA,EAVH,QAUG;EACuB,GAAA,EAAA,MAAA;EAAf,OAAA,EAAA,MAAA;EACT,IAAA,EATH,CASG;;iBANC,oBACP;iBAEU;cACH,eAAe;sBACP,eAAe;aACxB;AClGf,CAAA,CAAA,EAAY;EAMK,UAAA,kBAAkB,EAAA,CAAA,EAAA;EAIzB,QAAA,CAAA,IAAA,EAAA,MAAA,EAAA,KAAA,CAAA,EAAA,OAAA,CAAA,EAAA,IAAA;EAIS,QAAA,CAAA,IAAA,EAAA,MAAA,CAAA,EAAA,OAAA;CAAe;;;KAdtB,4BAA4B,WAAW;UAMlC,kBAAA;;ADJjB;AAIA;EAA8B,KAAA,CAAA,ECIpB,QDJoB;EAAO;;;EAA+C,QAAA,CAAA,EAAA,CAAA,GAAA,ECQjE,QDRiE,EAAA,GAAA,ECQlD,eDRkD,EAAA,GAAA,IAAA;EAAA;AAIpF;;EAIkB,MAAA,CAAA,EAAA,CAAA,GAAA,ECID,QDJC,EAAA,GAAA,IAAA;EACd;;AAgBJ;EAKiB,QAAA,CAAA,EAAA,CAAA,GAAA,ECdE,QDcW,EAAA,GACrB,IAAA;AAIT;UChBU,eAAA,CDiBD;EACmB;;;EAkChB,OAAA,EAAA,OAAA;;AAKA,cCCC,UAAA,CDDD;EAIC,QAAA,IAAA;EAGD,QAAA,eAAA;EAAC,iBAAA,SAAA;EAGG,WAAA,CAAS,aAAA,CAAA,ECJI,YDIJ,CCJiB,WDIjB,CAAA,MAAA,CAAA,CAAA;EAChB,MAAA,CAAA,QAAA,ECDU,kBDCV,CAAA,EAAA,IAAA;EAEU,QAAA,CAAA,QAAA,ECCE,kBDDF,CAAA,EAAA,IAAA;EACY,OAAA,CAAA,CAAA,EAAA,MAAA;EAAf;;;;;;YCcJ,yBAAyB;cA+BvB;WAoBH;;AAjKX;AAMA;;EAQmB,MAAA,CAAA,GAAA,EA2JL,QA3JK,EAAA,KAAA,EAAA,OAAA,CAAA,EAAA,OAAA;EAAe,YAAA,CAAA,CAAA,CAAA,CAAA,SAAA,EAAA,MAAA,CAAA,EAAA;IAIjB,GAAA,EAAA,CAAA,KAAA,EAmKE,QAnKF,EAAA,GAmKa,CAnKb,GAAA,SAAA;IAIE,GAAA,EAAA,CAAA,KAAA,EAoKA,QApKA,EAAA,KAAA,EAoKiB,CApKjB,EAAA,GAAA,IAAA;IAAQ,MAAA,EAAA,CAAA,KAAA,CAAA,EAuKJ,QAvKI,EAAA,GAAA,IAAA;EAGjB,CAAA;EA0DG,KAAA,CAAA,IAAA,EAsHC,MAtHS,CAAA,MAAA,EAAA,OAAA,CAAA,CAAA,EAAA,IAAA;;AAKM,iBAuHb,aAvHa,CAAA,IAAA,OAAA,CAAA,CAAA,GAAA,EAwHtB,QAxHsB,EAAA,OAkGO,CAlGP,EAAA;EAIV,GAAA,CAAA,EAsHT,GAtHS;EAIE,YAAA,CAAA,EAmHF,YAnHE;EAcT;;;;;EAuEO,OAAA,CAAA,EAAA,CAAA,YAAA,EAAA,OAAA,EAAA,GAqCsB,CArCtB;EAAW;EAKX,SAAA,CAAA,EAAA,CAAA,IAAA,EAkCI,CAlCJ,EAAA,IAAA,EAkCa,CAlCb,EAAA,GAAA,OAAA;CAAiB,CAAA,EAAA,SAAA,CAmC5B,CAnC4B,EAAA,CAAA,QAAA,EAAA,OAAA,EAAA,GAAA,OAAA,CAAA;AAGb,iBAqDP,WAAA,CArDO,QAAA,EAqDe,kBArDf,GAAA;EAYT,GAAA,CAAA,EAyCqD,GAzCrD;CAAM,CAAA,EAAA,IAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { a as stringifyFieldKey, i as objectSet, n as deepEqual, r as objectGet } from "./utils-W82BoH4I.js";
|
|
4
|
+
import { createContext, use, useEffect, useMemo, useRef, useState } from "react";
|
|
5
|
+
import { jsx } from "react/jsx-runtime";
|
|
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(stf) {
|
|
21
|
+
if (stf) return stf.dataEngine;
|
|
22
|
+
return use(Context).dataEngine;
|
|
23
|
+
}
|
|
24
|
+
function useArray(field, options = {}) {
|
|
25
|
+
const engine = useDataEngine();
|
|
26
|
+
const [items] = useFieldValue(field, {
|
|
27
|
+
defaultValue: options.defaultValue,
|
|
28
|
+
compute(value) {
|
|
29
|
+
const items$1 = [];
|
|
30
|
+
if (Array.isArray(value)) for (let i = 0; i < value.length; i++) items$1.push({
|
|
31
|
+
field: [...field, i],
|
|
32
|
+
index: i
|
|
33
|
+
});
|
|
34
|
+
return items$1;
|
|
35
|
+
},
|
|
36
|
+
isChanged(prev, next) {
|
|
37
|
+
return prev.length !== next.length;
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
return {
|
|
41
|
+
items,
|
|
42
|
+
insertItem(itemValue) {
|
|
43
|
+
const value = engine.get(field);
|
|
44
|
+
engine.update(field, Array.isArray(value) ? [...value, itemValue] : [itemValue]);
|
|
45
|
+
},
|
|
46
|
+
removeItem(index) {
|
|
47
|
+
engine.delete([...field, index]);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function useObject(field, options) {
|
|
52
|
+
const engine = useDataEngine();
|
|
53
|
+
const [objectKeys] = useFieldValue(field, {
|
|
54
|
+
defaultValue: options.defaultValue,
|
|
55
|
+
compute(currentValue) {
|
|
56
|
+
return currentValue ? Object.keys(currentValue) : [];
|
|
57
|
+
},
|
|
58
|
+
isChanged(prev, next) {
|
|
59
|
+
return !deepEqual(prev, next);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
return {
|
|
63
|
+
properties: useMemo(() => {
|
|
64
|
+
const properties = [];
|
|
65
|
+
const unknownKeys = new Set(objectKeys);
|
|
66
|
+
for (const [key, prop] of Object.entries(options.properties)) {
|
|
67
|
+
unknownKeys.delete(key);
|
|
68
|
+
properties.push({
|
|
69
|
+
kind: "fixed",
|
|
70
|
+
field: [...field, key],
|
|
71
|
+
key,
|
|
72
|
+
info: prop
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
for (const [pattern, prop] of Object.entries(options.patternProperties ?? {})) {
|
|
76
|
+
const regex = RegExp(pattern);
|
|
77
|
+
for (const key of unknownKeys) {
|
|
78
|
+
if (!key.match(regex)) continue;
|
|
79
|
+
unknownKeys.delete(key);
|
|
80
|
+
properties.push({
|
|
81
|
+
kind: "pattern",
|
|
82
|
+
info: prop,
|
|
83
|
+
key,
|
|
84
|
+
pattern,
|
|
85
|
+
field: [...field, key]
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (options.fallback) for (const key of unknownKeys) properties.push({
|
|
90
|
+
kind: "fallback",
|
|
91
|
+
field: [...field, key],
|
|
92
|
+
key,
|
|
93
|
+
info: options.fallback
|
|
94
|
+
});
|
|
95
|
+
return properties;
|
|
96
|
+
}, [
|
|
97
|
+
field,
|
|
98
|
+
objectKeys,
|
|
99
|
+
options.fallback,
|
|
100
|
+
options.patternProperties,
|
|
101
|
+
options.properties
|
|
102
|
+
]),
|
|
103
|
+
onAppend(name, value) {
|
|
104
|
+
name = name.trim();
|
|
105
|
+
if (name.length === 0) return;
|
|
106
|
+
engine.init([...field, name], value);
|
|
107
|
+
},
|
|
108
|
+
onDelete(name) {
|
|
109
|
+
return engine.delete([...field, name]);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
//#endregion
|
|
115
|
+
//#region src/lib/data-engine.ts
|
|
116
|
+
function getDefaultValue(defaultValue) {
|
|
117
|
+
return typeof defaultValue === "function" ? defaultValue() : defaultValue;
|
|
118
|
+
}
|
|
119
|
+
var ListenerManager = class {
|
|
120
|
+
constructor() {
|
|
121
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
122
|
+
this.indexed = /* @__PURE__ */ new Map();
|
|
123
|
+
}
|
|
124
|
+
add(listener) {
|
|
125
|
+
if (!listener.field) {
|
|
126
|
+
this.listeners.add(listener);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const key = stringifyFieldKey(listener.field);
|
|
130
|
+
const set = this.indexed.get(key) ?? /* @__PURE__ */ new Set();
|
|
131
|
+
set.add(listener);
|
|
132
|
+
this.indexed.set(key, set);
|
|
133
|
+
}
|
|
134
|
+
remove(listener) {
|
|
135
|
+
if (!listener.field) this.listeners.delete(listener);
|
|
136
|
+
else this.indexed.get(stringifyFieldKey(listener.field))?.delete(listener);
|
|
137
|
+
}
|
|
138
|
+
onUpdate(field, ctx) {
|
|
139
|
+
for (const v of this.listeners) v.onUpdate?.(field, ctx);
|
|
140
|
+
const updatedKey = stringifyFieldKey(field);
|
|
141
|
+
if (ctx.swallow) {
|
|
142
|
+
const set = this.indexed.get(updatedKey);
|
|
143
|
+
if (set) for (const v of set) v.onUpdate?.(field, ctx);
|
|
144
|
+
} else for (const [k, listeners] of this.indexed.entries()) {
|
|
145
|
+
if (k !== updatedKey && !k.startsWith(updatedKey + ".")) continue;
|
|
146
|
+
for (const v of listeners) v.onUpdate?.(field, ctx);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
onInit(field) {
|
|
150
|
+
for (const v of this.listeners) v.onInit?.(field);
|
|
151
|
+
const set = this.indexed.get(stringifyFieldKey(field));
|
|
152
|
+
if (set) for (const v of set) v.onInit?.(field);
|
|
153
|
+
}
|
|
154
|
+
onDelete(field) {
|
|
155
|
+
for (const v of this.listeners) v.onDelete?.(field);
|
|
156
|
+
const set = this.indexed.get(stringifyFieldKey(field));
|
|
157
|
+
if (set) for (const v of set) v.onDelete?.(field);
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
var DataEngine = class {
|
|
161
|
+
constructor(defaultValues = {}) {
|
|
162
|
+
this.attachedDataMap = /* @__PURE__ */ new Map();
|
|
163
|
+
this.listeners = new ListenerManager();
|
|
164
|
+
this.data = getDefaultValue(defaultValues);
|
|
165
|
+
}
|
|
166
|
+
listen(listener) {
|
|
167
|
+
this.listeners.add(listener);
|
|
168
|
+
}
|
|
169
|
+
unlisten(listener) {
|
|
170
|
+
this.listeners.remove(listener);
|
|
171
|
+
}
|
|
172
|
+
getData() {
|
|
173
|
+
return this.data;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* init a field
|
|
177
|
+
* @param key the key of field
|
|
178
|
+
* @param defaultValue the initial value, the field is also created for `undefined`
|
|
179
|
+
* @returns the value of initialized field, or the current value of field if already initialized
|
|
180
|
+
*/
|
|
181
|
+
init(key, defaultValue) {
|
|
182
|
+
if (key.length === 0) return this.data;
|
|
183
|
+
let cur = this.data;
|
|
184
|
+
const currentKey = [];
|
|
185
|
+
for (let i = 0; i < key.length; i++) {
|
|
186
|
+
const propKey = key[i];
|
|
187
|
+
const propValue = cur[propKey];
|
|
188
|
+
if (i === key.length - 1) {
|
|
189
|
+
if (propValue !== void 0) return propValue;
|
|
190
|
+
cur[propKey] = getDefaultValue(defaultValue);
|
|
191
|
+
this.listeners.onUpdate(currentKey, { swallow: true });
|
|
192
|
+
this.listeners.onInit(key);
|
|
193
|
+
return cur[propKey];
|
|
194
|
+
} else if (typeof propValue === "object" && propValue !== null) cur = propValue;
|
|
195
|
+
else {
|
|
196
|
+
if (propValue !== void 0) console.warn(`the original value of field ${currentKey.join(".")} is overidden, this might be unexpected.`);
|
|
197
|
+
cur = cur[propKey] = {};
|
|
198
|
+
this.listeners.onUpdate(currentKey, { swallow: true });
|
|
199
|
+
}
|
|
200
|
+
currentKey.push(propKey);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
delete(key) {
|
|
204
|
+
if (key.length === 0) return;
|
|
205
|
+
const parentKey = key.slice(0, -1);
|
|
206
|
+
const prop = key[key.length - 1];
|
|
207
|
+
const parent = this.get(parentKey);
|
|
208
|
+
if (Array.isArray(parent) && typeof prop === "number") {
|
|
209
|
+
const [deleted] = parent.splice(prop, 1);
|
|
210
|
+
this.listeners.onUpdate(parentKey, { swallow: false });
|
|
211
|
+
this.listeners.onDelete(key);
|
|
212
|
+
return deleted;
|
|
213
|
+
} else if (typeof parent === "object" && parent !== null) {
|
|
214
|
+
const temp = parent[prop];
|
|
215
|
+
delete parent[prop];
|
|
216
|
+
this.listeners.onUpdate(parentKey, { swallow: true });
|
|
217
|
+
this.listeners.onDelete(key);
|
|
218
|
+
return temp;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
get(key) {
|
|
222
|
+
return objectGet(this.data, key);
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* update the value of field if it exists
|
|
226
|
+
* @returns if the field is updated
|
|
227
|
+
*/
|
|
228
|
+
update(key, value) {
|
|
229
|
+
try {
|
|
230
|
+
this.data = objectSet(this.data, key, value);
|
|
231
|
+
this.listeners.onUpdate(key, { swallow: false });
|
|
232
|
+
return true;
|
|
233
|
+
} catch {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
attachedData(namespace) {
|
|
238
|
+
return {
|
|
239
|
+
get: (field) => {
|
|
240
|
+
return this.attachedDataMap.get(`${namespace}:${stringifyFieldKey(field)}`);
|
|
241
|
+
},
|
|
242
|
+
set: (field, value) => {
|
|
243
|
+
this.attachedDataMap.set(`${namespace}:${stringifyFieldKey(field)}`, value);
|
|
244
|
+
},
|
|
245
|
+
delete: (field) => {
|
|
246
|
+
if (field) this.attachedDataMap.delete(`${namespace}:${stringifyFieldKey(field)}`);
|
|
247
|
+
else for (const key of this.attachedDataMap.keys()) if (key.startsWith(`${namespace}:`)) this.attachedDataMap.delete(key);
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
reset(data) {
|
|
252
|
+
this.update([], data);
|
|
253
|
+
this.attachedDataMap.clear();
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
function useFieldValue(key, options = {}) {
|
|
257
|
+
const engine = useDataEngine(options.stf);
|
|
258
|
+
const { compute = (v) => v, defaultValue, isChanged = (a, b) => a !== b } = options;
|
|
259
|
+
const [value, setValue] = useState(() => compute(engine.init(key, defaultValue)));
|
|
260
|
+
useListener({
|
|
261
|
+
field: key,
|
|
262
|
+
onUpdate() {
|
|
263
|
+
const computed = compute(engine.get(key));
|
|
264
|
+
if (isChanged(value, computed)) setValue(computed);
|
|
265
|
+
},
|
|
266
|
+
onDelete() {
|
|
267
|
+
const computed = compute(void 0);
|
|
268
|
+
if (isChanged(value, computed)) setValue(computed);
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
return [value, (newValue) => engine.update(key, newValue)];
|
|
272
|
+
}
|
|
273
|
+
function useListener(listener) {
|
|
274
|
+
const engine = useDataEngine(listener.stf);
|
|
275
|
+
const listenerRef = useRef(listener);
|
|
276
|
+
listenerRef.current = listener;
|
|
277
|
+
useEffect(() => {
|
|
278
|
+
const internal = {
|
|
279
|
+
field: listener.field,
|
|
280
|
+
onDelete(...args) {
|
|
281
|
+
return listenerRef.current.onDelete?.(...args);
|
|
282
|
+
},
|
|
283
|
+
onInit(...args) {
|
|
284
|
+
return listenerRef.current.onInit?.(...args);
|
|
285
|
+
},
|
|
286
|
+
onUpdate(...args) {
|
|
287
|
+
return listenerRef.current.onUpdate?.(...args);
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
engine.listen(internal);
|
|
291
|
+
return () => {
|
|
292
|
+
engine.unlisten(internal);
|
|
293
|
+
};
|
|
294
|
+
}, [engine, listener.field]);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
//#endregion
|
|
298
|
+
export { DataEngine, StfProvider, useArray, useDataEngine, useFieldValue, useListener, useObject, useStf };
|
|
299
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["items"],"sources":["../src/lib/stf.tsx","../src/lib/data-engine.ts"],"sourcesContent":["import { createContext, ReactNode, use, useMemo } from 'react';\nimport { DataEngine, DefaultValue, useFieldValue } from './data-engine';\nimport { FieldKey } from './types';\nimport { deepEqual } from './utils';\n\nconst Context = createContext<Stf | null>(null);\n\nexport interface Stf {\n dataEngine: DataEngine;\n}\n\nexport function StfProvider({ value, children }: { value: Stf; children: ReactNode }) {\n return <Context value={value}>{children}</Context>;\n}\n\nexport function useStf(options: {\n /**\n * Note: the passed object will be modified in place, use `structuredClone()` to keep the original object unchanged.\n */\n defaultValues?: DefaultValue<Record<string, unknown>>;\n}): Stf {\n const { defaultValues } = options;\n\n const dataEngine = useMemo(\n () => new DataEngine(defaultValues),\n // eslint-disable-next-line react-hooks/exhaustive-deps -- assume unchanged\n [],\n );\n return useMemo(\n () => ({\n dataEngine,\n }),\n [dataEngine],\n );\n}\n\nexport function useDataEngine(stf?: Stf) {\n if (stf) return stf.dataEngine;\n return use(Context)!.dataEngine;\n}\n\nexport interface ArrayItemInfo {\n field: FieldKey;\n index: number;\n}\n\nexport function useArray(\n field: FieldKey,\n options: { defaultValue?: DefaultValue<unknown[]> } = {},\n) {\n const engine = useDataEngine();\n const [items] = useFieldValue(field, {\n defaultValue: options.defaultValue,\n compute(value) {\n const items: ArrayItemInfo[] = [];\n if (Array.isArray(value)) {\n for (let i = 0; i < value.length; i++) {\n items.push({\n field: [...field, i],\n index: i,\n });\n }\n }\n return items;\n },\n isChanged(prev, next) {\n return prev.length !== next.length;\n },\n });\n\n return {\n items,\n insertItem(itemValue?: unknown) {\n const value = engine.get(field);\n\n engine.update(field, Array.isArray(value) ? [...value, itemValue] : [itemValue]);\n },\n removeItem(index: number) {\n engine.delete([...field, index]);\n },\n };\n}\nexport type PropertyItemInfo<T> =\n | {\n kind: 'fixed' | 'fallback';\n field: FieldKey;\n key: string;\n info: T;\n }\n | {\n kind: 'pattern';\n field: FieldKey;\n key: string;\n pattern: string;\n info: T;\n };\n\nexport function useObject<T>(\n field: FieldKey,\n options: {\n defaultValue?: DefaultValue<object>;\n properties: Record<string, T>;\n patternProperties?: Record<string, T>;\n fallback?: T;\n },\n) {\n const engine = useDataEngine();\n const [objectKeys] = useFieldValue(field, {\n defaultValue: options.defaultValue,\n compute(currentValue) {\n return currentValue ? Object.keys(currentValue) : [];\n },\n isChanged(prev, next) {\n return !deepEqual(prev, next);\n },\n });\n\n const properties = useMemo(() => {\n const properties: PropertyItemInfo<T>[] = [];\n const unknownKeys = new Set(objectKeys);\n for (const [key, prop] of Object.entries(options.properties)) {\n unknownKeys.delete(key);\n properties.push({\n kind: 'fixed',\n field: [...field, key],\n key,\n info: prop,\n });\n }\n\n for (const [pattern, prop] of Object.entries(options.patternProperties ?? {})) {\n const regex = RegExp(pattern);\n\n for (const key of unknownKeys) {\n if (!key.match(regex)) continue;\n unknownKeys.delete(key);\n properties.push({\n kind: 'pattern',\n info: prop,\n key,\n pattern,\n field: [...field, key],\n });\n }\n }\n\n if (options.fallback) {\n for (const key of unknownKeys) {\n properties.push({\n kind: 'fallback',\n field: [...field, key],\n key,\n info: options.fallback,\n });\n }\n }\n\n return properties;\n }, [field, objectKeys, options.fallback, options.patternProperties, options.properties]);\n\n return {\n properties,\n onAppend(name: string, value?: unknown) {\n name = name.trim();\n if (name.length === 0) return;\n\n engine.init([...field, name], value);\n },\n onDelete(name: string) {\n return engine.delete([...field, name]);\n },\n };\n}\n","import { useEffect, useRef, useState } from 'react';\nimport { objectGet, objectSet, stringifyFieldKey } from './utils';\nimport type { FieldKey } from './types';\nimport { Stf, useDataEngine } from './stf';\n\nexport type DefaultValue<T = unknown> = T | (() => T);\n\nfunction getDefaultValue<T>(defaultValue: DefaultValue<T>): T {\n return typeof defaultValue === 'function' ? (defaultValue as () => T)() : defaultValue;\n}\n\nexport interface DataEngineListener {\n /**\n * when specified, only call the listener for events affecting the specified field.\n */\n field?: FieldKey;\n /**\n * when field value is changed\n */\n onUpdate?: (key: FieldKey, ctx: OnUpdateContext) => void;\n /**\n * when `init(field)` is called\n */\n onInit?: (key: FieldKey) => void;\n /**\n * when `delete(field)` is called\n */\n onDelete?: (key: FieldKey) => void;\n}\n\ninterface OnUpdateContext {\n /**\n * An update is swallow if the change doesn't affect the values of children fields.\n */\n swallow: boolean;\n}\n\nclass ListenerManager {\n private readonly listeners = new Set<DataEngineListener>();\n private readonly indexed = new Map<string, Set<DataEngineListener>>();\n\n add(listener: DataEngineListener) {\n if (!listener.field) {\n this.listeners.add(listener);\n return;\n }\n const key = stringifyFieldKey(listener.field);\n const set = this.indexed.get(key) ?? new Set();\n set.add(listener);\n this.indexed.set(key, set);\n }\n\n remove(listener: DataEngineListener) {\n if (!listener.field) {\n this.listeners.delete(listener);\n } else {\n this.indexed.get(stringifyFieldKey(listener.field))?.delete(listener);\n }\n }\n\n onUpdate(field: FieldKey, ctx: OnUpdateContext) {\n for (const v of this.listeners) v.onUpdate?.(field, ctx);\n const updatedKey = stringifyFieldKey(field);\n\n if (ctx.swallow) {\n const set = this.indexed.get(updatedKey);\n if (set) for (const v of set) v.onUpdate?.(field, ctx);\n } else {\n for (const [k, listeners] of this.indexed.entries()) {\n if (k !== updatedKey && !k.startsWith(updatedKey + '.')) continue;\n for (const v of listeners) v.onUpdate?.(field, ctx);\n }\n }\n }\n\n onInit(field: FieldKey) {\n for (const v of this.listeners) v.onInit?.(field);\n const set = this.indexed.get(stringifyFieldKey(field));\n if (set) for (const v of set) v.onInit?.(field);\n }\n\n onDelete(field: FieldKey) {\n for (const v of this.listeners) v.onDelete?.(field);\n const set = this.indexed.get(stringifyFieldKey(field));\n if (set) for (const v of set) v.onDelete?.(field);\n }\n}\n\nexport class DataEngine {\n private data: NonNullable<object>;\n private attachedDataMap = new Map<string, unknown>();\n private readonly listeners = new ListenerManager();\n\n constructor(defaultValues: DefaultValue<NonNullable<object>> = {}) {\n this.data = getDefaultValue(defaultValues);\n }\n\n listen(listener: DataEngineListener) {\n this.listeners.add(listener);\n }\n\n unlisten(listener: DataEngineListener) {\n this.listeners.remove(listener);\n }\n\n getData() {\n return this.data;\n }\n\n /**\n * init a field\n * @param key the key of field\n * @param defaultValue the initial value, the field is also created for `undefined`\n * @returns the value of initialized field, or the current value of field if already initialized\n */\n init(key: FieldKey, defaultValue?: DefaultValue): unknown {\n if (key.length === 0) return this.data;\n let cur = this.data as Record<string, unknown>;\n const currentKey: FieldKey = [];\n\n for (let i = 0; i < key.length; i++) {\n const propKey = key[i];\n const propValue = cur[propKey];\n\n if (i === key.length - 1) {\n if (propValue !== undefined) return propValue;\n cur[propKey] = getDefaultValue(defaultValue);\n\n this.listeners.onUpdate(currentKey, { swallow: true });\n this.listeners.onInit(key);\n return cur[propKey];\n } else if (typeof propValue === 'object' && propValue !== null) {\n cur = propValue as Record<string, unknown>;\n } else {\n if (propValue !== undefined)\n console.warn(\n `the original value of field ${currentKey.join('.')} is overidden, this might be unexpected.`,\n );\n\n cur = cur[propKey] = {};\n this.listeners.onUpdate(currentKey, { swallow: true });\n }\n currentKey.push(propKey);\n }\n }\n\n delete(key: FieldKey): unknown | undefined {\n if (key.length === 0) return;\n const parentKey = key.slice(0, -1);\n const prop = key[key.length - 1];\n const parent = this.get(parentKey);\n\n if (Array.isArray(parent) && typeof prop === 'number') {\n const [deleted] = parent.splice(prop, 1);\n this.listeners.onUpdate(parentKey, { swallow: false });\n this.listeners.onDelete(key);\n return deleted;\n } else if (typeof parent === 'object' && parent !== null) {\n const temp = (parent as Record<string, unknown>)[prop];\n delete parent[prop as never];\n this.listeners.onUpdate(parentKey, { swallow: true });\n this.listeners.onDelete(key);\n return temp;\n }\n }\n\n get(key: FieldKey) {\n return objectGet(this.data, key);\n }\n\n /**\n * update the value of field if it exists\n * @returns if the field is updated\n */\n update(key: FieldKey, value: unknown): boolean {\n try {\n this.data = objectSet(this.data, key, value) as NonNullable<object>;\n this.listeners.onUpdate(key, { swallow: false });\n return true;\n } catch {\n return false;\n }\n }\n\n attachedData<T>(namespace: string) {\n return {\n get: (field: FieldKey): T | undefined => {\n return this.attachedDataMap.get(`${namespace}:${stringifyFieldKey(field)}`) as\n | T\n | undefined;\n },\n set: (field: FieldKey, value: T) => {\n this.attachedDataMap.set(`${namespace}:${stringifyFieldKey(field)}`, value);\n },\n delete: (field?: FieldKey) => {\n if (field) {\n this.attachedDataMap.delete(`${namespace}:${stringifyFieldKey(field)}`);\n } else {\n for (const key of this.attachedDataMap.keys()) {\n if (key.startsWith(`${namespace}:`)) this.attachedDataMap.delete(key);\n }\n }\n },\n };\n }\n\n reset(data: Record<string, unknown>) {\n this.update([], data);\n this.attachedDataMap.clear();\n }\n}\n\nexport function useFieldValue<V = unknown>(\n key: FieldKey,\n options: {\n stf?: Stf;\n defaultValue?: DefaultValue;\n\n /**\n * compute value from the actual field value.\n *\n * to re-compute on in-place updates (may happen on objects, arrays), you should clone the object here.\n */\n compute?: (currentValue: unknown) => V;\n /** determine whether the value/computed value is changed */\n isChanged?: (prev: V, next: V) => boolean;\n } = {},\n) {\n const engine = useDataEngine(options.stf);\n const { compute = (v) => v as V, defaultValue, isChanged = (a, b) => a !== b } = options;\n const [value, setValue] = useState<V>(() => compute(engine.init(key, defaultValue)));\n\n useListener({\n field: key,\n onUpdate() {\n const computed = compute(engine.get(key));\n if (isChanged(value, computed)) setValue(computed);\n },\n onDelete() {\n const computed = compute(undefined);\n if (isChanged(value, computed)) setValue(computed);\n },\n });\n\n return [value, (newValue: unknown) => engine.update(key, newValue)] as const;\n}\n\nexport function useListener(listener: DataEngineListener & { stf?: Stf }) {\n const engine = useDataEngine(listener.stf);\n const listenerRef = useRef(listener);\n listenerRef.current = listener;\n\n useEffect(() => {\n const internal: DataEngineListener = {\n field: listener.field,\n onDelete(...args) {\n return listenerRef.current.onDelete?.(...args);\n },\n onInit(...args) {\n return listenerRef.current.onInit?.(...args);\n },\n onUpdate(...args) {\n return listenerRef.current.onUpdate?.(...args);\n },\n };\n\n engine.listen(internal);\n return () => {\n engine.unlisten(internal);\n };\n }, [engine, listener.field]);\n}\n"],"mappings":";;;;;;;AAKA,MAAM,UAAU,cAA0B,KAAK;AAM/C,SAAgB,YAAY,EAAE,OAAO,YAAiD;AACpF,QAAO,oBAAC;EAAe;EAAQ;GAAmB;;AAGpD,SAAgB,OAAO,SAKf;CACN,MAAM,EAAE,kBAAkB;CAE1B,MAAM,aAAa,cACX,IAAI,WAAW,cAAc,EAEnC,EAAE,CACH;AACD,QAAO,eACE,EACL,YACD,GACD,CAAC,WAAW,CACb;;AAGH,SAAgB,cAAc,KAAW;AACvC,KAAI,IAAK,QAAO,IAAI;AACpB,QAAO,IAAI,QAAQ,CAAE;;AAQvB,SAAgB,SACd,OACA,UAAsD,EAAE,EACxD;CACA,MAAM,SAAS,eAAe;CAC9B,MAAM,CAAC,SAAS,cAAc,OAAO;EACnC,cAAc,QAAQ;EACtB,QAAQ,OAAO;GACb,MAAMA,UAAyB,EAAE;AACjC,OAAI,MAAM,QAAQ,MAAM,CACtB,MAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,IAChC,SAAM,KAAK;IACT,OAAO,CAAC,GAAG,OAAO,EAAE;IACpB,OAAO;IACR,CAAC;AAGN,UAAOA;;EAET,UAAU,MAAM,MAAM;AACpB,UAAO,KAAK,WAAW,KAAK;;EAE/B,CAAC;AAEF,QAAO;EACL;EACA,WAAW,WAAqB;GAC9B,MAAM,QAAQ,OAAO,IAAI,MAAM;AAE/B,UAAO,OAAO,OAAO,MAAM,QAAQ,MAAM,GAAG,CAAC,GAAG,OAAO,UAAU,GAAG,CAAC,UAAU,CAAC;;EAElF,WAAW,OAAe;AACxB,UAAO,OAAO,CAAC,GAAG,OAAO,MAAM,CAAC;;EAEnC;;AAiBH,SAAgB,UACd,OACA,SAMA;CACA,MAAM,SAAS,eAAe;CAC9B,MAAM,CAAC,cAAc,cAAc,OAAO;EACxC,cAAc,QAAQ;EACtB,QAAQ,cAAc;AACpB,UAAO,eAAe,OAAO,KAAK,aAAa,GAAG,EAAE;;EAEtD,UAAU,MAAM,MAAM;AACpB,UAAO,CAAC,UAAU,MAAM,KAAK;;EAEhC,CAAC;AA6CF,QAAO;EACL,YA5CiB,cAAc;GAC/B,MAAM,aAAoC,EAAE;GAC5C,MAAM,cAAc,IAAI,IAAI,WAAW;AACvC,QAAK,MAAM,CAAC,KAAK,SAAS,OAAO,QAAQ,QAAQ,WAAW,EAAE;AAC5D,gBAAY,OAAO,IAAI;AACvB,eAAW,KAAK;KACd,MAAM;KACN,OAAO,CAAC,GAAG,OAAO,IAAI;KACtB;KACA,MAAM;KACP,CAAC;;AAGJ,QAAK,MAAM,CAAC,SAAS,SAAS,OAAO,QAAQ,QAAQ,qBAAqB,EAAE,CAAC,EAAE;IAC7E,MAAM,QAAQ,OAAO,QAAQ;AAE7B,SAAK,MAAM,OAAO,aAAa;AAC7B,SAAI,CAAC,IAAI,MAAM,MAAM,CAAE;AACvB,iBAAY,OAAO,IAAI;AACvB,gBAAW,KAAK;MACd,MAAM;MACN,MAAM;MACN;MACA;MACA,OAAO,CAAC,GAAG,OAAO,IAAI;MACvB,CAAC;;;AAIN,OAAI,QAAQ,SACV,MAAK,MAAM,OAAO,YAChB,YAAW,KAAK;IACd,MAAM;IACN,OAAO,CAAC,GAAG,OAAO,IAAI;IACtB;IACA,MAAM,QAAQ;IACf,CAAC;AAIN,UAAO;KACN;GAAC;GAAO;GAAY,QAAQ;GAAU,QAAQ;GAAmB,QAAQ;GAAW,CAAC;EAItF,SAAS,MAAc,OAAiB;AACtC,UAAO,KAAK,MAAM;AAClB,OAAI,KAAK,WAAW,EAAG;AAEvB,UAAO,KAAK,CAAC,GAAG,OAAO,KAAK,EAAE,MAAM;;EAEtC,SAAS,MAAc;AACrB,UAAO,OAAO,OAAO,CAAC,GAAG,OAAO,KAAK,CAAC;;EAEzC;;;;;ACpKH,SAAS,gBAAmB,cAAkC;AAC5D,QAAO,OAAO,iBAAiB,aAAc,cAA0B,GAAG;;AA6B5E,IAAM,kBAAN,MAAsB;;mCACS,IAAI,KAAyB;iCAC/B,IAAI,KAAsC;;CAErE,IAAI,UAA8B;AAChC,MAAI,CAAC,SAAS,OAAO;AACnB,QAAK,UAAU,IAAI,SAAS;AAC5B;;EAEF,MAAM,MAAM,kBAAkB,SAAS,MAAM;EAC7C,MAAM,MAAM,KAAK,QAAQ,IAAI,IAAI,oBAAI,IAAI,KAAK;AAC9C,MAAI,IAAI,SAAS;AACjB,OAAK,QAAQ,IAAI,KAAK,IAAI;;CAG5B,OAAO,UAA8B;AACnC,MAAI,CAAC,SAAS,MACZ,MAAK,UAAU,OAAO,SAAS;MAE/B,MAAK,QAAQ,IAAI,kBAAkB,SAAS,MAAM,CAAC,EAAE,OAAO,SAAS;;CAIzE,SAAS,OAAiB,KAAsB;AAC9C,OAAK,MAAM,KAAK,KAAK,UAAW,GAAE,WAAW,OAAO,IAAI;EACxD,MAAM,aAAa,kBAAkB,MAAM;AAE3C,MAAI,IAAI,SAAS;GACf,MAAM,MAAM,KAAK,QAAQ,IAAI,WAAW;AACxC,OAAI,IAAK,MAAK,MAAM,KAAK,IAAK,GAAE,WAAW,OAAO,IAAI;QAEtD,MAAK,MAAM,CAAC,GAAG,cAAc,KAAK,QAAQ,SAAS,EAAE;AACnD,OAAI,MAAM,cAAc,CAAC,EAAE,WAAW,aAAa,IAAI,CAAE;AACzD,QAAK,MAAM,KAAK,UAAW,GAAE,WAAW,OAAO,IAAI;;;CAKzD,OAAO,OAAiB;AACtB,OAAK,MAAM,KAAK,KAAK,UAAW,GAAE,SAAS,MAAM;EACjD,MAAM,MAAM,KAAK,QAAQ,IAAI,kBAAkB,MAAM,CAAC;AACtD,MAAI,IAAK,MAAK,MAAM,KAAK,IAAK,GAAE,SAAS,MAAM;;CAGjD,SAAS,OAAiB;AACxB,OAAK,MAAM,KAAK,KAAK,UAAW,GAAE,WAAW,MAAM;EACnD,MAAM,MAAM,KAAK,QAAQ,IAAI,kBAAkB,MAAM,CAAC;AACtD,MAAI,IAAK,MAAK,MAAM,KAAK,IAAK,GAAE,WAAW,MAAM;;;AAIrD,IAAa,aAAb,MAAwB;CAKtB,YAAY,gBAAmD,EAAE,EAAE;yCAHzC,IAAI,KAAsB;mBACvB,IAAI,iBAAiB;AAGhD,OAAK,OAAO,gBAAgB,cAAc;;CAG5C,OAAO,UAA8B;AACnC,OAAK,UAAU,IAAI,SAAS;;CAG9B,SAAS,UAA8B;AACrC,OAAK,UAAU,OAAO,SAAS;;CAGjC,UAAU;AACR,SAAO,KAAK;;;;;;;;CASd,KAAK,KAAe,cAAsC;AACxD,MAAI,IAAI,WAAW,EAAG,QAAO,KAAK;EAClC,IAAI,MAAM,KAAK;EACf,MAAM,aAAuB,EAAE;AAE/B,OAAK,IAAI,IAAI,GAAG,IAAI,IAAI,QAAQ,KAAK;GACnC,MAAM,UAAU,IAAI;GACpB,MAAM,YAAY,IAAI;AAEtB,OAAI,MAAM,IAAI,SAAS,GAAG;AACxB,QAAI,cAAc,OAAW,QAAO;AACpC,QAAI,WAAW,gBAAgB,aAAa;AAE5C,SAAK,UAAU,SAAS,YAAY,EAAE,SAAS,MAAM,CAAC;AACtD,SAAK,UAAU,OAAO,IAAI;AAC1B,WAAO,IAAI;cACF,OAAO,cAAc,YAAY,cAAc,KACxD,OAAM;QACD;AACL,QAAI,cAAc,OAChB,SAAQ,KACN,+BAA+B,WAAW,KAAK,IAAI,CAAC,0CACrD;AAEH,UAAM,IAAI,WAAW,EAAE;AACvB,SAAK,UAAU,SAAS,YAAY,EAAE,SAAS,MAAM,CAAC;;AAExD,cAAW,KAAK,QAAQ;;;CAI5B,OAAO,KAAoC;AACzC,MAAI,IAAI,WAAW,EAAG;EACtB,MAAM,YAAY,IAAI,MAAM,GAAG,GAAG;EAClC,MAAM,OAAO,IAAI,IAAI,SAAS;EAC9B,MAAM,SAAS,KAAK,IAAI,UAAU;AAElC,MAAI,MAAM,QAAQ,OAAO,IAAI,OAAO,SAAS,UAAU;GACrD,MAAM,CAAC,WAAW,OAAO,OAAO,MAAM,EAAE;AACxC,QAAK,UAAU,SAAS,WAAW,EAAE,SAAS,OAAO,CAAC;AACtD,QAAK,UAAU,SAAS,IAAI;AAC5B,UAAO;aACE,OAAO,WAAW,YAAY,WAAW,MAAM;GACxD,MAAM,OAAQ,OAAmC;AACjD,UAAO,OAAO;AACd,QAAK,UAAU,SAAS,WAAW,EAAE,SAAS,MAAM,CAAC;AACrD,QAAK,UAAU,SAAS,IAAI;AAC5B,UAAO;;;CAIX,IAAI,KAAe;AACjB,SAAO,UAAU,KAAK,MAAM,IAAI;;;;;;CAOlC,OAAO,KAAe,OAAyB;AAC7C,MAAI;AACF,QAAK,OAAO,UAAU,KAAK,MAAM,KAAK,MAAM;AAC5C,QAAK,UAAU,SAAS,KAAK,EAAE,SAAS,OAAO,CAAC;AAChD,UAAO;UACD;AACN,UAAO;;;CAIX,aAAgB,WAAmB;AACjC,SAAO;GACL,MAAM,UAAmC;AACvC,WAAO,KAAK,gBAAgB,IAAI,GAAG,UAAU,GAAG,kBAAkB,MAAM,GAAG;;GAI7E,MAAM,OAAiB,UAAa;AAClC,SAAK,gBAAgB,IAAI,GAAG,UAAU,GAAG,kBAAkB,MAAM,IAAI,MAAM;;GAE7E,SAAS,UAAqB;AAC5B,QAAI,MACF,MAAK,gBAAgB,OAAO,GAAG,UAAU,GAAG,kBAAkB,MAAM,GAAG;QAEvE,MAAK,MAAM,OAAO,KAAK,gBAAgB,MAAM,CAC3C,KAAI,IAAI,WAAW,GAAG,UAAU,GAAG,CAAE,MAAK,gBAAgB,OAAO,IAAI;;GAI5E;;CAGH,MAAM,MAA+B;AACnC,OAAK,OAAO,EAAE,EAAE,KAAK;AACrB,OAAK,gBAAgB,OAAO;;;AAIhC,SAAgB,cACd,KACA,UAYI,EAAE,EACN;CACA,MAAM,SAAS,cAAc,QAAQ,IAAI;CACzC,MAAM,EAAE,WAAW,MAAM,GAAQ,cAAc,aAAa,GAAG,MAAM,MAAM,MAAM;CACjF,MAAM,CAAC,OAAO,YAAY,eAAkB,QAAQ,OAAO,KAAK,KAAK,aAAa,CAAC,CAAC;AAEpF,aAAY;EACV,OAAO;EACP,WAAW;GACT,MAAM,WAAW,QAAQ,OAAO,IAAI,IAAI,CAAC;AACzC,OAAI,UAAU,OAAO,SAAS,CAAE,UAAS,SAAS;;EAEpD,WAAW;GACT,MAAM,WAAW,QAAQ,OAAU;AACnC,OAAI,UAAU,OAAO,SAAS,CAAE,UAAS,SAAS;;EAErD,CAAC;AAEF,QAAO,CAAC,QAAQ,aAAsB,OAAO,OAAO,KAAK,SAAS,CAAC;;AAGrE,SAAgB,YAAY,UAA8C;CACxE,MAAM,SAAS,cAAc,SAAS,IAAI;CAC1C,MAAM,cAAc,OAAO,SAAS;AACpC,aAAY,UAAU;AAEtB,iBAAgB;EACd,MAAM,WAA+B;GACnC,OAAO,SAAS;GAChB,SAAS,GAAG,MAAM;AAChB,WAAO,YAAY,QAAQ,WAAW,GAAG,KAAK;;GAEhD,OAAO,GAAG,MAAM;AACd,WAAO,YAAY,QAAQ,SAAS,GAAG,KAAK;;GAE9C,SAAS,GAAG,MAAM;AAChB,WAAO,YAAY,QAAQ,WAAW,GAAG,KAAK;;GAEjD;AAED,SAAO,OAAO,SAAS;AACvB,eAAa;AACX,UAAO,SAAS,SAAS;;IAE1B,CAAC,QAAQ,SAAS,MAAM,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { t as FieldKey } from "../types-PXiOOMNs.js";
|
|
2
|
+
|
|
3
|
+
//#region src/lib/utils.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* test if array a starts with array b, only compare values via `===`.
|
|
7
|
+
*/
|
|
8
|
+
declare function arrayStartsWith(a: unknown[], b: unknown[]): boolean;
|
|
9
|
+
declare function objectGet(obj: unknown, key: (string | number)[]): unknown | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* set the value of field if it exists (in place)
|
|
12
|
+
*
|
|
13
|
+
* @returns updated value, throw error if parent object doesn't exist
|
|
14
|
+
*/
|
|
15
|
+
declare function objectSet(obj: unknown, key: FieldKey, value: unknown): unknown;
|
|
16
|
+
/**
|
|
17
|
+
* doesn't handle recursive objects
|
|
18
|
+
*/
|
|
19
|
+
declare function deepEqual(a: unknown, b: unknown): boolean;
|
|
20
|
+
declare function stringifyFieldKey(fieldKey: FieldKey): string;
|
|
21
|
+
//#endregion
|
|
22
|
+
export { arrayStartsWith, deepEqual, objectGet, objectSet, stringifyFieldKey };
|
|
23
|
+
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","names":[],"sources":["../../src/lib/utils.ts"],"sourcesContent":[],"mappings":";;;;;;AAKA;AAUgB,iBAVA,eAAA,CAUS,CAAA,EAAA,OAAA,EAAA,EAAA,CAAA,EAAA,OAAA,EAAA,CAAA,EAAA,OAAA;AAiBT,iBAjBA,SAAA,CAiB6B,GAAQ,EAAA,OAAA,EAAA,GAAA,EAAA,CAAA,MAAA,GAAA,MAAA,CAAA,EAAA,CAAA,EAAA,OAAA,GAAA,SAAA;AAcrD;AAsCA;;;;iBApDgB,SAAA,oBAA6B;;;;iBAc7B,SAAA;iBAsCA,iBAAA,WAA4B"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types-PXiOOMNs.d.ts","names":[],"sources":["../src/lib/types.ts"],"sourcesContent":[],"mappings":";KAAY,QAAA"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
//#region src/lib/utils.ts
|
|
2
|
+
/**
|
|
3
|
+
* test if array a starts with array b, only compare values via `===`.
|
|
4
|
+
*/
|
|
5
|
+
function arrayStartsWith(a, b) {
|
|
6
|
+
if (b.length > a.length) return false;
|
|
7
|
+
for (let i = 0; i < b.length; i++) if (a[i] !== b[i]) return false;
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
function objectGet(obj, key) {
|
|
11
|
+
let cur = obj;
|
|
12
|
+
for (const prop of key) {
|
|
13
|
+
if (typeof cur !== "object" || cur === null || !(prop in cur)) return;
|
|
14
|
+
cur = cur[prop];
|
|
15
|
+
}
|
|
16
|
+
return cur;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* set the value of field if it exists (in place)
|
|
20
|
+
*
|
|
21
|
+
* @returns updated value, throw error if parent object doesn't exist
|
|
22
|
+
*/
|
|
23
|
+
function objectSet(obj, key, value) {
|
|
24
|
+
if (key.length === 0) return value;
|
|
25
|
+
const parent = objectGet(obj, key.slice(0, -1));
|
|
26
|
+
if (typeof parent !== "object" || parent === null) throw new Error("missing parent object");
|
|
27
|
+
parent[key[key.length - 1]] = value;
|
|
28
|
+
return obj;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* doesn't handle recursive objects
|
|
32
|
+
*/
|
|
33
|
+
function deepEqual(a, b) {
|
|
34
|
+
if (a === b) return true;
|
|
35
|
+
if (a == null || b == null) return false;
|
|
36
|
+
if (typeof a !== "object" || typeof b !== "object") return false;
|
|
37
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
38
|
+
if (a.length !== b.length) return false;
|
|
39
|
+
return a.every((item, index) => deepEqual(item, b[index]));
|
|
40
|
+
}
|
|
41
|
+
if (Array.isArray(a) || Array.isArray(b)) return false;
|
|
42
|
+
const keysA = Object.keys(a);
|
|
43
|
+
const keysB = Object.keys(b);
|
|
44
|
+
if (keysA.length !== keysB.length) return false;
|
|
45
|
+
return keysA.every((key) => Object.prototype.hasOwnProperty.call(b, key) && deepEqual(a[key], b[key]));
|
|
46
|
+
}
|
|
47
|
+
function stringifyFieldKey(fieldKey) {
|
|
48
|
+
return fieldKey.map((v) => `${typeof v}:${v}`).join(".");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
//#endregion
|
|
52
|
+
export { stringifyFieldKey as a, objectSet as i, deepEqual as n, objectGet as r, arrayStartsWith as t };
|
|
53
|
+
//# sourceMappingURL=utils-W82BoH4I.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils-W82BoH4I.js","names":[],"sources":["../src/lib/utils.ts"],"sourcesContent":["import { FieldKey } from './types';\n\n/**\n * test if array a starts with array b, only compare values via `===`.\n */\nexport function arrayStartsWith(a: unknown[], b: unknown[]): boolean {\n if (b.length > a.length) return false;\n\n for (let i = 0; i < b.length; i++) {\n if (a[i] !== b[i]) return false;\n }\n\n return true;\n}\n\nexport function objectGet(obj: unknown, key: (string | number)[]): unknown | undefined {\n let cur = obj;\n\n for (const prop of key) {\n if (typeof cur !== 'object' || cur === null || !(prop in cur)) return;\n\n cur = cur[prop as keyof typeof cur];\n }\n\n return cur;\n}\n\n/**\n * set the value of field if it exists (in place)\n *\n * @returns updated value, throw error if parent object doesn't exist\n */\nexport function objectSet(obj: unknown, key: FieldKey, value: unknown): unknown {\n if (key.length === 0) {\n return value;\n }\n\n const parent = objectGet(obj, key.slice(0, -1));\n if (typeof parent !== 'object' || parent === null) throw new Error('missing parent object');\n (parent as Record<string, unknown>)[key[key.length - 1]] = value;\n return obj;\n}\n\n/**\n * doesn't handle recursive objects\n */\nexport function deepEqual(a: unknown, b: unknown): boolean {\n if (a === b) {\n return true;\n }\n\n if (a == null || b == null) {\n return false;\n }\n\n if (typeof a !== 'object' || typeof b !== 'object') {\n return false;\n }\n\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) {\n return false;\n }\n return a.every((item, index) => deepEqual(item, b[index]));\n }\n\n if (Array.isArray(a) || Array.isArray(b)) {\n return false;\n }\n\n const keysA = Object.keys(a as object);\n const keysB = Object.keys(b as object);\n\n if (keysA.length !== keysB.length) {\n return false;\n }\n\n return keysA.every(\n (key) =>\n Object.prototype.hasOwnProperty.call(b, key) &&\n deepEqual((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key]),\n );\n}\n\nexport function stringifyFieldKey(fieldKey: FieldKey) {\n return fieldKey.map((v) => `${typeof v}:${v}`).join('.');\n}\n"],"mappings":";;;;AAKA,SAAgB,gBAAgB,GAAc,GAAuB;AACnE,KAAI,EAAE,SAAS,EAAE,OAAQ,QAAO;AAEhC,MAAK,IAAI,IAAI,GAAG,IAAI,EAAE,QAAQ,IAC5B,KAAI,EAAE,OAAO,EAAE,GAAI,QAAO;AAG5B,QAAO;;AAGT,SAAgB,UAAU,KAAc,KAA+C;CACrF,IAAI,MAAM;AAEV,MAAK,MAAM,QAAQ,KAAK;AACtB,MAAI,OAAO,QAAQ,YAAY,QAAQ,QAAQ,EAAE,QAAQ,KAAM;AAE/D,QAAM,IAAI;;AAGZ,QAAO;;;;;;;AAQT,SAAgB,UAAU,KAAc,KAAe,OAAyB;AAC9E,KAAI,IAAI,WAAW,EACjB,QAAO;CAGT,MAAM,SAAS,UAAU,KAAK,IAAI,MAAM,GAAG,GAAG,CAAC;AAC/C,KAAI,OAAO,WAAW,YAAY,WAAW,KAAM,OAAM,IAAI,MAAM,wBAAwB;AAC3F,CAAC,OAAmC,IAAI,IAAI,SAAS,MAAM;AAC3D,QAAO;;;;;AAMT,SAAgB,UAAU,GAAY,GAAqB;AACzD,KAAI,MAAM,EACR,QAAO;AAGT,KAAI,KAAK,QAAQ,KAAK,KACpB,QAAO;AAGT,KAAI,OAAO,MAAM,YAAY,OAAO,MAAM,SACxC,QAAO;AAGT,KAAI,MAAM,QAAQ,EAAE,IAAI,MAAM,QAAQ,EAAE,EAAE;AACxC,MAAI,EAAE,WAAW,EAAE,OACjB,QAAO;AAET,SAAO,EAAE,OAAO,MAAM,UAAU,UAAU,MAAM,EAAE,OAAO,CAAC;;AAG5D,KAAI,MAAM,QAAQ,EAAE,IAAI,MAAM,QAAQ,EAAE,CACtC,QAAO;CAGT,MAAM,QAAQ,OAAO,KAAK,EAAY;CACtC,MAAM,QAAQ,OAAO,KAAK,EAAY;AAEtC,KAAI,MAAM,WAAW,MAAM,OACzB,QAAO;AAGT,QAAO,MAAM,OACV,QACC,OAAO,UAAU,eAAe,KAAK,GAAG,IAAI,IAC5C,UAAW,EAA8B,MAAO,EAA8B,KAAK,CACtF;;AAGH,SAAgB,kBAAkB,UAAoB;AACpD,QAAO,SAAS,KAAK,MAAM,GAAG,OAAO,EAAE,GAAG,IAAI,CAAC,KAAK,IAAI"}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fumari/stf",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Schema to Form.",
|
|
5
|
+
"keywords": [],
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Fuma Nama",
|
|
8
|
+
"repository": "github:fuma-nama/fumadocs",
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"type": "module",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": "./dist/index.js",
|
|
15
|
+
"./lib/utils": "./dist/lib/utils.js",
|
|
16
|
+
"./package.json": "./package.json"
|
|
17
|
+
},
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "25.0.5",
|
|
23
|
+
"@types/react": "^19.2.8",
|
|
24
|
+
"tsdown": "^0.19.0",
|
|
25
|
+
"eslint-config-custom": "0.0.0",
|
|
26
|
+
"tsconfig": "0.0.0"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"@types/react": "*",
|
|
30
|
+
"react": "^19.2.0",
|
|
31
|
+
"react-dom": "^19.2.0"
|
|
32
|
+
},
|
|
33
|
+
"peerDependenciesMeta": {
|
|
34
|
+
"@types/react": {
|
|
35
|
+
"optional": true
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsdown",
|
|
40
|
+
"clean": "rimraf dist",
|
|
41
|
+
"dev": "tsdown --watch",
|
|
42
|
+
"lint": "eslint .",
|
|
43
|
+
"types:check": "tsc --noEmit"
|
|
44
|
+
}
|
|
45
|
+
}
|