opshot 0.1.1 → 0.3.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/package.json CHANGED
@@ -1,72 +1,76 @@
1
1
  {
2
- "name": "opshot",
3
- "version": "0.1.1",
4
- "description": "Plain-object state for React: mutate it directly, re-render only the components that read what changed, and track every change operation",
5
- "type": "module",
6
- "exports": {
7
- ".": {
8
- "import": "./dist/index.js",
9
- "types": "./dist/index.d.ts"
10
- },
11
- "./react": {
12
- "import": "./dist/react.js",
13
- "types": "./dist/react.d.ts"
14
- }
15
- },
16
- "files": [
17
- "dist"
18
- ],
19
- "sideEffects": false,
20
- "publishConfig": {
21
- "access": "public"
22
- },
23
- "author": "Matt Cavender",
24
- "license": "ISC",
25
- "repository": {
26
- "type": "git",
27
- "url": "git+https://github.com/visionsofparadise/opshot.git"
28
- },
29
- "scripts": {
30
- "check": "concurrently \"eslint . --fix --cache --format ./agent-eslint.js\" \"tsc --noEmit --pretty false 2>&1 | node ./agent-tsc.js\"",
31
- "check:verbose": "concurrently \"eslint . --cache\" \"tsc --noEmit\"",
32
- "lint": "eslint . --fix --cache --format ./agent-eslint.js",
33
- "lint:verbose": "eslint .",
34
- "lint:fix": "eslint . --fix",
35
- "unit": "vitest run unit",
36
- "build": "tsup --config tsup.config.ts"
37
- },
38
- "peerDependencies": {
39
- "react": ">=18.0.0"
40
- },
41
- "peerDependenciesMeta": {
42
- "react": {
43
- "optional": true
44
- }
45
- },
46
- "dependencies": {
47
- "proxy-compare": "^3.0.1",
48
- "valtio": "2.3.2"
49
- },
50
- "devDependencies": {
51
- "@eslint/js": "^9.39.5",
52
- "@stylistic/eslint-plugin": "^5.10.0",
53
- "@testing-library/react": "^16.3.2",
54
- "@types/react": "^19.2.17",
55
- "concurrently": "^10.0.3",
56
- "eslint": "^9.39.5",
57
- "eslint-plugin-barrel-files": "^3.0.1",
58
- "eslint-plugin-check-file": "^3.3.1",
59
- "eslint-plugin-import-x": "^4.17.1",
60
- "eslint-plugin-react": "^7.37.5",
61
- "eslint-plugin-react-hooks": "^7.1.1",
62
- "fast-json-patch": "^3.1.1",
63
- "globals": "^17.7.0",
64
- "jsdom": "^29.1.1",
65
- "react": "^19.2.7",
66
- "react-dom": "^19.2.7",
67
- "tsup": "^8.5.1",
68
- "typescript": "^5.9.3",
69
- "typescript-eslint": "^8.64.0",
70
- "vitest": "^4.1.10"
71
- }
2
+ "name": "opshot",
3
+ "version": "0.3.1",
4
+ "description": "Plain-object state for React: mutate it directly, re-render only the components that read what changed, and track every change operation",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "import": "./dist/index.js",
9
+ "types": "./dist/index.d.ts"
10
+ }
11
+ },
12
+ "files": [
13
+ "dist"
14
+ ],
15
+ "sideEffects": false,
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "author": "Matt Cavender",
20
+ "license": "MIT",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/visionsofparadise/opshot.git"
24
+ },
25
+ "scripts": {
26
+ "check": "prettier --write . && eslint . --fix --cache && concurrently \"tsc --noEmit\" \"markdownlint-cli2 \\\"**/*.md\\\"\" \"textlint \\\"**/*.md\\\"\" \"knip --no-progress\" \"jscpd .\"",
27
+ "lint": "eslint . --fix --cache",
28
+ "lint:fix": "eslint . --fix",
29
+ "format": "prettier --write .",
30
+ "unit": "vitest run unit",
31
+ "integration": "vitest run integration",
32
+ "build": "tsup --config tsup.config.ts"
33
+ },
34
+ "peerDependencies": {
35
+ "react": ">=18.0.0"
36
+ },
37
+ "devDependencies": {
38
+ "@eslint/compat": "^1.3.2",
39
+ "@eslint/js": "^9.39.5",
40
+ "@stylistic/eslint-plugin": "^5.10.0",
41
+ "@testing-library/react": "^16.3.2",
42
+ "@types/react": "^19.2.17",
43
+ "concurrently": "^10.0.3",
44
+ "eslint": "^9.39.5",
45
+ "eslint-config-prettier": "^10.1.8",
46
+ "eslint-import-resolver-typescript": "^4.4.4",
47
+ "eslint-plugin-barrel-files": "^3.0.1",
48
+ "eslint-plugin-check-file": "^3.3.1",
49
+ "eslint-plugin-comment-rules": "^0.1.5",
50
+ "eslint-plugin-import-x": "^4.17.1",
51
+ "eslint-plugin-jsx-a11y": "^6.10.2",
52
+ "eslint-plugin-react": "^7.37.5",
53
+ "eslint-plugin-react-hooks": "^7.1.1",
54
+ "globals": "^17.7.0",
55
+ "jscpd": "^5.0.12",
56
+ "jsdom": "^29.1.1",
57
+ "knip": "^6.29.0",
58
+ "markdownlint-cli2": "^0.23.1",
59
+ "prettier": "^3.6.2",
60
+ "proxy-compare": "^3.0.1",
61
+ "react": "^19.2.7",
62
+ "react-dom": "^19.2.7",
63
+ "textlint": "^15.2.2",
64
+ "textlint-filter-rule-comments": "^1.2.2",
65
+ "textlint-rule-no-dead-link": "^6.2.1",
66
+ "tsup": "^8.5.1",
67
+ "typescript": "^5.9.3",
68
+ "typescript-eslint": "^8.64.0",
69
+ "valtio": "2.3.2",
70
+ "vitest": "^4.1.10"
71
+ },
72
+ "allowScripts": {
73
+ "esbuild@0.27.7": true,
74
+ "unrs-resolver@1.12.2": true
75
+ }
72
76
  }
package/dist/react.d.ts DELETED
@@ -1,9 +0,0 @@
1
- import { FC } from 'react';
2
- import { Group, Define, State } from './index.js';
3
- import 'valtio/vanilla';
4
-
5
- declare function resnapshot<P extends object>(component: FC<P>): FC<P>;
6
- declare const useCreateGroup: () => Group;
7
- declare const useCreateState: <T extends object>(define: Define<T>, group?: Group) => State<T>;
8
-
9
- export { resnapshot, useCreateGroup, useCreateState };
package/dist/react.js DELETED
@@ -1,267 +0,0 @@
1
- import { isChanged, createProxy } from 'proxy-compare';
2
- import { memo, useState, useMemo, useRef, useCallback, useSyncExternalStore, useLayoutEffect } from 'react';
3
- import { unstable_getInternalStates, snapshot, subscribe, ref, proxy } from 'valtio/vanilla';
4
-
5
- // src/react.tsx
6
- var { refSet } = unstable_getInternalStates();
7
- var isPlainArray = (value) => Array.isArray(value) && !refSet.has(value);
8
- var isPlainObject = (value) => {
9
- if (typeof value !== "object" || value === null || Array.isArray(value) || refSet.has(value)) return false;
10
- const prototype = Object.getPrototypeOf(value);
11
- return prototype === Object.prototype || prototype === null;
12
- };
13
- var isCloneable = (value) => isPlainObject(value) || isPlainArray(value);
14
- var cloneValue = (value) => {
15
- if (isPlainArray(value)) return value.map(cloneValue);
16
- if (isPlainObject(value)) {
17
- return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, cloneValue(child)]));
18
- }
19
- return value;
20
- };
21
- var toPointer = (path) => {
22
- if (path.length === 0) return "";
23
- return `/${path.map((segment) => String(segment).replaceAll("~", "~0").replaceAll("/", "~1")).join("/")}`;
24
- };
25
- var removing = (pointer) => ({ op: "remove", path: pointer });
26
- var carrying = (op, pointer, value) => {
27
- if (!isCloneable(value)) return { op, path: pointer, value };
28
- return {
29
- op,
30
- path: pointer,
31
- get value() {
32
- return cloneValue(value);
33
- }
34
- };
35
- };
36
- var addPair = (pointer, after) => ({ do: carrying("add", pointer, after), undo: removing(pointer) });
37
- var removePair = (pointer, before) => ({ do: removing(pointer), undo: carrying("add", pointer, before) });
38
- var replacePair = (pointer, before, after) => ({
39
- do: carrying("replace", pointer, after),
40
- undo: carrying("replace", pointer, before)
41
- });
42
- var diffValue = (before, after, path, ops) => {
43
- if (Object.is(before, after)) return;
44
- if (isPlainArray(before) && isPlainArray(after)) {
45
- if (before.length !== after.length) {
46
- ops.push(replacePair(toPointer(path), before, after));
47
- return;
48
- }
49
- for (let index = 0; index < after.length; index++) diffValue(before[index], after[index], [...path, index], ops);
50
- return;
51
- }
52
- if (isPlainObject(before) && isPlainObject(after)) {
53
- for (const key of /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])) {
54
- if (!Object.hasOwn(before, key)) ops.push(addPair(toPointer([...path, key]), after[key]));
55
- else if (!Object.hasOwn(after, key)) ops.push(removePair(toPointer([...path, key]), before[key]));
56
- else diffValue(before[key], after[key], [...path, key], ops);
57
- }
58
- return;
59
- }
60
- ops.push(replacePair(toPointer(path), before, after));
61
- };
62
- function diffSnapshots(before, after) {
63
- const ops = [];
64
- diffValue(before, after, [], ops);
65
- return ops;
66
- }
67
-
68
- // src/createState.ts
69
- var stateBrand = /* @__PURE__ */ Symbol.for("opshot.state");
70
- var hasOwn = (value, key) => Object.hasOwn(value, key);
71
- function createState(define) {
72
- return createGroupState(define);
73
- }
74
- function createGroupState(define, groupListeners) {
75
- const callback = typeof define === "function" ? define : () => define;
76
- const listeners = /* @__PURE__ */ new Set();
77
- const created = {};
78
- const requireProxy = () => {
79
- const { proxied } = created;
80
- if (!proxied) throw new Error("opshot: called during createState definition");
81
- return proxied;
82
- };
83
- const get = () => snapshot(requireProxy());
84
- const mutate = (callback2, options = {}) => {
85
- const proxied = requireProxy();
86
- if (handle.isMutating) throw new Error("opshot: nested mutate on the same state");
87
- handle.isMutating = true;
88
- const before = snapshot(proxied);
89
- try {
90
- callback2(proxied);
91
- } finally {
92
- handle.isMutating = false;
93
- }
94
- const after = snapshot(proxied);
95
- if (before === after) return;
96
- if (listeners.size === 0 && (groupListeners?.size ?? 0) === 0) return;
97
- const ops = diffSnapshots(before, after);
98
- if (ops.length === 0) return;
99
- for (const listener of [...groupListeners ?? []]) listener(after, ops, options);
100
- for (const listener of [...listeners]) listener(after, ops, options);
101
- };
102
- const subscribe = (listener) => {
103
- listeners.add(listener);
104
- return () => {
105
- listeners.delete(listener);
106
- };
107
- };
108
- const isSameState = (other) => isState(other) && other.op === handle;
109
- const unwrap = () => {
110
- const { op, ...rest } = get();
111
- return rest;
112
- };
113
- const literal = callback(mutate, get);
114
- if (Object.hasOwn(literal, "op")) throw new Error('opshot: "op" is a reserved key on a state');
115
- const base = Object.create(Reflect.getPrototypeOf(literal));
116
- Object.defineProperties(base, Object.getOwnPropertyDescriptors(literal));
117
- const handle = { proxy: base, isMutating: false, mutate, subscribe, isSameState, unwrap, [stateBrand]: true };
118
- Object.defineProperty(base, "op", { value: ref(handle), enumerable: true, writable: false, configurable: false });
119
- created.proxied = proxy(base);
120
- handle.proxy = created.proxied;
121
- return get();
122
- }
123
- function isState(value) {
124
- if (typeof value !== "object" || value === null || !hasOwn(value, "op")) return false;
125
- const handle = value.op;
126
- if (typeof handle !== "object" || handle === null || !hasOwn(handle, stateBrand)) return false;
127
- return handle[stateBrand] === true;
128
- }
129
-
130
- // src/createGroup.ts
131
- function createGroup() {
132
- const listeners = /* @__PURE__ */ new Set();
133
- return {
134
- createState(define) {
135
- return createGroupState(define, listeners);
136
- },
137
- subscribe(listener) {
138
- listeners.add(listener);
139
- return () => {
140
- listeners.delete(listener);
141
- };
142
- }
143
- };
144
- }
145
-
146
- // src/react.tsx
147
- function shouldTraverse(value) {
148
- if (value === null || typeof value !== "object") return false;
149
- if (Array.isArray(value)) return true;
150
- const prototype = Object.getPrototypeOf(value);
151
- return prototype === Object.prototype || prototype === null;
152
- }
153
- function findSnapshotPaths(value, path = [], paths = []) {
154
- if (isState(value)) {
155
- paths.push(path);
156
- return paths;
157
- }
158
- if (!shouldTraverse(value)) return paths;
159
- if (Array.isArray(value)) {
160
- value.forEach((item, index) => {
161
- findSnapshotPaths(item, [...path, index], paths);
162
- });
163
- } else {
164
- for (const [key, propertyValue] of Object.entries(value)) {
165
- if (key === "children") continue;
166
- findSnapshotPaths(propertyValue, [...path, key], paths);
167
- }
168
- }
169
- return paths;
170
- }
171
- function getAtPath(object, path) {
172
- let current = object;
173
- for (const segment of path) {
174
- if (current === null || current === void 0) return void 0;
175
- current = current[segment];
176
- }
177
- return current;
178
- }
179
- function setAtPath(object, path, value) {
180
- if (path.length === 0) return value;
181
- const head = path[0];
182
- if (head === void 0) throw new Error("setAtPath: non-empty path yielded no head segment");
183
- const tail = path.slice(1);
184
- const current = object[head];
185
- const updated = setAtPath(current, tail, value);
186
- if (Array.isArray(object)) {
187
- const clone = [...object];
188
- clone[head] = updated;
189
- return clone;
190
- }
191
- return { ...object, [head]: updated };
192
- }
193
- var targetCache = /* @__PURE__ */ new WeakMap();
194
- function useResnapshotAll(snapshots) {
195
- const lastRendered = useRef([]);
196
- const lastReturned = useRef([]);
197
- const nextProxies = snapshots.map((snap) => snap.op.proxy);
198
- const [proxies, setProxies] = useState(nextProxies);
199
- const isStale = proxies.length !== nextProxies.length || proxies.some((proxied, index) => proxied !== nextProxies[index]);
200
- if (isStale) setProxies(nextProxies);
201
- const trackings = useMemo(() => proxies.map(() => ({ affected: /* @__PURE__ */ new WeakMap(), proxyCache: /* @__PURE__ */ new WeakMap() })), [proxies]);
202
- const getSnapshot = useCallback(() => {
203
- const next = proxies.map((proxied) => snapshot(proxied));
204
- const last = lastReturned.current;
205
- if (last.length === next.length && last.every((snap, index) => snap === next[index])) return last;
206
- lastReturned.current = next;
207
- return next;
208
- }, [proxies]);
209
- const subscribe$1 = useCallback(
210
- (callback) => {
211
- const unsubscribes = proxies.map(
212
- (proxied, index) => subscribe(proxied, () => {
213
- const prev = lastRendered.current[index];
214
- const tracking = trackings[index];
215
- if (prev && tracking && prev !== snapshot(proxied)) {
216
- if (!tracking.affected.has(prev)) return;
217
- try {
218
- if (!isChanged(prev, snapshot(proxied), tracking.affected, /* @__PURE__ */ new WeakMap())) return;
219
- } catch {
220
- }
221
- }
222
- callback();
223
- })
224
- );
225
- return () => {
226
- for (const unsubscribe of unsubscribes) unsubscribe();
227
- };
228
- },
229
- [proxies, trackings]
230
- );
231
- const freshSnapshots = useSyncExternalStore(subscribe$1, getSnapshot);
232
- useLayoutEffect(() => {
233
- lastRendered.current = freshSnapshots;
234
- });
235
- const trackedSnapshots = useMemo(
236
- () => freshSnapshots.map((snap, index) => {
237
- const tracking = trackings[index];
238
- if (!tracking) return snap;
239
- return createProxy(snap, tracking.affected, tracking.proxyCache, targetCache);
240
- }),
241
- [freshSnapshots, trackings]
242
- );
243
- return isStale ? snapshots : trackedSnapshots;
244
- }
245
- function resnapshot(component) {
246
- const componentName = component.displayName ?? component.name;
247
- const Resnapshotted = (props) => {
248
- const snapshotPaths = useMemo(() => findSnapshotPaths(props), [props]);
249
- const staleSnapshots = useMemo(() => snapshotPaths.map((path) => getAtPath(props, path)).filter(isState), [props, snapshotPaths]);
250
- const freshSnapshots = useResnapshotAll(staleSnapshots);
251
- const freshProps = useMemo(() => {
252
- if (freshSnapshots === staleSnapshots) return props;
253
- return snapshotPaths.reduce((acc, path, index) => setAtPath(acc, path, freshSnapshots[index]), props);
254
- }, [props, snapshotPaths, staleSnapshots, freshSnapshots]);
255
- return component(freshProps);
256
- };
257
- Resnapshotted.displayName = `resnapshot(${componentName === "" ? "Anonymous" : componentName})`;
258
- return memo(Resnapshotted);
259
- }
260
- var useCreateGroup = () => useState(() => createGroup())[0];
261
- var useCreateState = (define, group) => {
262
- const created = useState(() => group ? group.createState(define) : createState(define))[0];
263
- const [fresh] = useResnapshotAll([created]);
264
- return fresh;
265
- };
266
-
267
- export { resnapshot, useCreateGroup, useCreateState };