@formwright/core 0.1.0

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Formwright contributors
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.
@@ -0,0 +1,162 @@
1
+ // src/reactive.ts
2
+ var activeObserver = null;
3
+ var batchDepth = 0;
4
+ var pendingEffects = /* @__PURE__ */ new Set();
5
+ var flushing = false;
6
+ function link(source) {
7
+ const obs = activeObserver;
8
+ if (obs === null) return;
9
+ if (!source.observers.has(obs)) {
10
+ source.observers.add(obs);
11
+ obs.sources.add(source);
12
+ }
13
+ }
14
+ function clearSources(obs) {
15
+ for (const src of obs.sources) src.observers.delete(obs);
16
+ obs.sources.clear();
17
+ }
18
+ function flush() {
19
+ if (flushing) return;
20
+ flushing = true;
21
+ try {
22
+ while (pendingEffects.size > 0) {
23
+ const next = pendingEffects.values().next().value;
24
+ pendingEffects.delete(next);
25
+ next.run();
26
+ }
27
+ } finally {
28
+ flushing = false;
29
+ }
30
+ }
31
+ var SignalNode = class {
32
+ constructor(value) {
33
+ this.value = value;
34
+ }
35
+ value;
36
+ observers = /* @__PURE__ */ new Set();
37
+ get() {
38
+ link(this);
39
+ return this.value;
40
+ }
41
+ peek() {
42
+ return this.value;
43
+ }
44
+ set(next) {
45
+ if (Object.is(next, this.value)) return;
46
+ this.value = next;
47
+ for (const obs of [...this.observers]) obs.notify();
48
+ if (batchDepth === 0) flush();
49
+ }
50
+ update(fn) {
51
+ this.set(fn(this.value));
52
+ }
53
+ };
54
+ var ComputedNode = class {
55
+ constructor(fn) {
56
+ this.fn = fn;
57
+ }
58
+ fn;
59
+ observers = /* @__PURE__ */ new Set();
60
+ sources = /* @__PURE__ */ new Set();
61
+ value;
62
+ dirty = true;
63
+ notify() {
64
+ if (this.dirty) return;
65
+ this.dirty = true;
66
+ for (const obs of [...this.observers]) obs.notify();
67
+ }
68
+ get() {
69
+ link(this);
70
+ if (this.dirty) this.recompute();
71
+ return this.value;
72
+ }
73
+ peek() {
74
+ if (this.dirty) this.recompute();
75
+ return this.value;
76
+ }
77
+ recompute() {
78
+ clearSources(this);
79
+ const prev = activeObserver;
80
+ activeObserver = this;
81
+ try {
82
+ this.value = this.fn();
83
+ this.dirty = false;
84
+ } finally {
85
+ activeObserver = prev;
86
+ }
87
+ }
88
+ };
89
+ var EffectNode = class {
90
+ constructor(fn) {
91
+ this.fn = fn;
92
+ this.run();
93
+ }
94
+ fn;
95
+ sources = /* @__PURE__ */ new Set();
96
+ cleanup = void 0;
97
+ disposed = false;
98
+ notify() {
99
+ if (this.disposed) return;
100
+ pendingEffects.add(this);
101
+ }
102
+ run() {
103
+ if (this.disposed) return;
104
+ this.runCleanup();
105
+ clearSources(this);
106
+ const prev = activeObserver;
107
+ activeObserver = this;
108
+ try {
109
+ this.cleanup = this.fn();
110
+ } finally {
111
+ activeObserver = prev;
112
+ }
113
+ }
114
+ runCleanup() {
115
+ if (typeof this.cleanup === "function") {
116
+ this.cleanup();
117
+ this.cleanup = void 0;
118
+ }
119
+ }
120
+ dispose() {
121
+ if (this.disposed) return;
122
+ this.disposed = true;
123
+ this.runCleanup();
124
+ clearSources(this);
125
+ pendingEffects.delete(this);
126
+ }
127
+ };
128
+ function signal(initial) {
129
+ return new SignalNode(initial);
130
+ }
131
+ function computed(fn) {
132
+ return new ComputedNode(fn);
133
+ }
134
+ function effect(fn) {
135
+ const node = new EffectNode(fn);
136
+ return () => node.dispose();
137
+ }
138
+ function untrack(fn) {
139
+ const prev = activeObserver;
140
+ activeObserver = null;
141
+ try {
142
+ return fn();
143
+ } finally {
144
+ activeObserver = prev;
145
+ }
146
+ }
147
+ function batch(fn) {
148
+ batchDepth++;
149
+ try {
150
+ return fn();
151
+ } finally {
152
+ batchDepth--;
153
+ if (batchDepth === 0) flush();
154
+ }
155
+ }
156
+ function isTracking() {
157
+ return activeObserver !== null;
158
+ }
159
+
160
+ export { batch, computed, effect, isTracking, signal, untrack };
161
+ //# sourceMappingURL=chunk-EZUHEI5F.js.map
162
+ //# sourceMappingURL=chunk-EZUHEI5F.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/reactive.ts"],"names":[],"mappings":";AA2CA,IAAI,cAAA,GAAkC,IAAA;AACtC,IAAI,UAAA,GAAa,CAAA;AACjB,IAAM,cAAA,uBAAqB,GAAA,EAAgB;AAC3C,IAAI,QAAA,GAAW,KAAA;AAEf,SAAS,KAAK,MAAA,EAAsB;AAClC,EAAA,MAAM,GAAA,GAAM,cAAA;AACZ,EAAA,IAAI,QAAQ,IAAA,EAAM;AAClB,EAAA,IAAI,CAAC,MAAA,CAAO,SAAA,CAAU,GAAA,CAAI,GAAG,CAAA,EAAG;AAC9B,IAAA,MAAA,CAAO,SAAA,CAAU,IAAI,GAAG,CAAA;AACxB,IAAA,GAAA,CAAI,OAAA,CAAQ,IAAI,MAAM,CAAA;AAAA,EACxB;AACF;AAEA,SAAS,aAAa,GAAA,EAAqB;AACzC,EAAA,KAAA,MAAW,OAAO,GAAA,CAAI,OAAA,EAAS,GAAA,CAAI,SAAA,CAAU,OAAO,GAAG,CAAA;AACvD,EAAA,GAAA,CAAI,QAAQ,KAAA,EAAM;AACpB;AAEA,SAAS,KAAA,GAAc;AACrB,EAAA,IAAI,QAAA,EAAU;AACd,EAAA,QAAA,GAAW,IAAA;AACX,EAAA,IAAI;AAEF,IAAA,OAAO,cAAA,CAAe,OAAO,CAAA,EAAG;AAC9B,MAAA,MAAM,IAAA,GAAO,cAAA,CAAe,MAAA,EAAO,CAAE,MAAK,CAAE,KAAA;AAC5C,MAAA,cAAA,CAAe,OAAO,IAAI,CAAA;AAC1B,MAAA,IAAA,CAAK,GAAA,EAAI;AAAA,IACX;AAAA,EACF,CAAA,SAAE;AACA,IAAA,QAAA,GAAW,KAAA;AAAA,EACb;AACF;AAEA,IAAM,aAAN,MAAsC;AAAA,EAEpC,YAAoB,KAAA,EAAU;AAAV,IAAA,IAAA,CAAA,KAAA,GAAA,KAAA;AAAA,EAAW;AAAA,EAAX,KAAA;AAAA,EADX,SAAA,uBAAgB,GAAA,EAAc;AAAA,EAGvC,GAAA,GAAS;AACP,IAAA,IAAA,CAAK,IAAI,CAAA;AACT,IAAA,OAAO,IAAA,CAAK,KAAA;AAAA,EACd;AAAA,EAEA,IAAA,GAAU;AACR,IAAA,OAAO,IAAA,CAAK,KAAA;AAAA,EACd;AAAA,EAEA,IAAI,IAAA,EAAe;AACjB,IAAA,IAAI,MAAA,CAAO,EAAA,CAAG,IAAA,EAAM,IAAA,CAAK,KAAK,CAAA,EAAG;AACjC,IAAA,IAAA,CAAK,KAAA,GAAQ,IAAA;AAEb,IAAA,KAAA,MAAW,OAAO,CAAC,GAAG,KAAK,SAAS,CAAA,MAAO,MAAA,EAAO;AAClD,IAAA,IAAI,UAAA,KAAe,GAAG,KAAA,EAAM;AAAA,EAC9B;AAAA,EAEA,OAAO,EAAA,EAA0B;AAC/B,IAAA,IAAA,CAAK,GAAA,CAAI,EAAA,CAAG,IAAA,CAAK,KAAK,CAAC,CAAA;AAAA,EACzB;AACF,CAAA;AAEA,IAAM,eAAN,MAAkD;AAAA,EAMhD,YAA6B,EAAA,EAAa;AAAb,IAAA,IAAA,CAAA,EAAA,GAAA,EAAA;AAAA,EAAc;AAAA,EAAd,EAAA;AAAA,EALpB,SAAA,uBAAgB,GAAA,EAAc;AAAA,EAC9B,OAAA,uBAAc,GAAA,EAAY;AAAA,EAC3B,KAAA;AAAA,EACA,KAAA,GAAQ,IAAA;AAAA,EAIhB,MAAA,GAAe;AACb,IAAA,IAAI,KAAK,KAAA,EAAO;AAChB,IAAA,IAAA,CAAK,KAAA,GAAQ,IAAA;AACb,IAAA,KAAA,MAAW,OAAO,CAAC,GAAG,KAAK,SAAS,CAAA,MAAO,MAAA,EAAO;AAAA,EACpD;AAAA,EAEA,GAAA,GAAS;AACP,IAAA,IAAA,CAAK,IAAI,CAAA;AACT,IAAA,IAAI,IAAA,CAAK,KAAA,EAAO,IAAA,CAAK,SAAA,EAAU;AAC/B,IAAA,OAAO,IAAA,CAAK,KAAA;AAAA,EACd;AAAA,EAEA,IAAA,GAAU;AACR,IAAA,IAAI,IAAA,CAAK,KAAA,EAAO,IAAA,CAAK,SAAA,EAAU;AAC/B,IAAA,OAAO,IAAA,CAAK,KAAA;AAAA,EACd;AAAA,EAEQ,SAAA,GAAkB;AACxB,IAAA,YAAA,CAAa,IAAI,CAAA;AACjB,IAAA,MAAM,IAAA,GAAO,cAAA;AACb,IAAA,cAAA,GAAiB,IAAA;AACjB,IAAA,IAAI;AACF,MAAA,IAAA,CAAK,KAAA,GAAQ,KAAK,EAAA,EAAG;AACrB,MAAA,IAAA,CAAK,KAAA,GAAQ,KAAA;AAAA,IACf,CAAA,SAAE;AACA,MAAA,cAAA,GAAiB,IAAA;AAAA,IACnB;AAAA,EACF;AACF,CAAA;AAEA,IAAM,aAAN,MAAqC;AAAA,EAKnC,YAA6B,EAAA,EAA+B;AAA/B,IAAA,IAAA,CAAA,EAAA,GAAA,EAAA;AAC3B,IAAA,IAAA,CAAK,GAAA,EAAI;AAAA,EACX;AAAA,EAF6B,EAAA;AAAA,EAJpB,OAAA,uBAAc,GAAA,EAAY;AAAA,EAC3B,OAAA,GAA+B,MAAA;AAAA,EAC/B,QAAA,GAAW,KAAA;AAAA,EAMnB,MAAA,GAAe;AACb,IAAA,IAAI,KAAK,QAAA,EAAU;AACnB,IAAA,cAAA,CAAe,IAAI,IAAI,CAAA;AAAA,EACzB;AAAA,EAEA,GAAA,GAAY;AACV,IAAA,IAAI,KAAK,QAAA,EAAU;AACnB,IAAA,IAAA,CAAK,UAAA,EAAW;AAChB,IAAA,YAAA,CAAa,IAAI,CAAA;AACjB,IAAA,MAAM,IAAA,GAAO,cAAA;AACb,IAAA,cAAA,GAAiB,IAAA;AACjB,IAAA,IAAI;AACF,MAAA,IAAA,CAAK,OAAA,GAAU,KAAK,EAAA,EAAG;AAAA,IACzB,CAAA,SAAE;AACA,MAAA,cAAA,GAAiB,IAAA;AAAA,IACnB;AAAA,EACF;AAAA,EAEQ,UAAA,GAAmB;AACzB,IAAA,IAAI,OAAO,IAAA,CAAK,OAAA,KAAY,UAAA,EAAY;AACtC,MAAA,IAAA,CAAK,OAAA,EAAQ;AACb,MAAA,IAAA,CAAK,OAAA,GAAU,MAAA;AAAA,IACjB;AAAA,EACF;AAAA,EAEA,OAAA,GAAgB;AACd,IAAA,IAAI,KAAK,QAAA,EAAU;AACnB,IAAA,IAAA,CAAK,QAAA,GAAW,IAAA;AAChB,IAAA,IAAA,CAAK,UAAA,EAAW;AAChB,IAAA,YAAA,CAAa,IAAI,CAAA;AACjB,IAAA,cAAA,CAAe,OAAO,IAAI,CAAA;AAAA,EAC5B;AACF,CAAA;AAGO,SAAS,OAAU,OAAA,EAA4B;AACpD,EAAA,OAAO,IAAI,WAAW,OAAO,CAAA;AAC/B;AAGO,SAAS,SAAY,EAAA,EAA4B;AACtD,EAAA,OAAO,IAAI,aAAa,EAAE,CAAA;AAC5B;AAOO,SAAS,OAAO,EAAA,EAAwC;AAC7D,EAAA,MAAM,IAAA,GAAO,IAAI,UAAA,CAAW,EAAE,CAAA;AAC9B,EAAA,OAAO,MAAM,KAAK,OAAA,EAAQ;AAC5B;AAGO,SAAS,QAAW,EAAA,EAAgB;AACzC,EAAA,MAAM,IAAA,GAAO,cAAA;AACb,EAAA,cAAA,GAAiB,IAAA;AACjB,EAAA,IAAI;AACF,IAAA,OAAO,EAAA,EAAG;AAAA,EACZ,CAAA,SAAE;AACA,IAAA,cAAA,GAAiB,IAAA;AAAA,EACnB;AACF;AAGO,SAAS,MAAS,EAAA,EAAgB;AACvC,EAAA,UAAA,EAAA;AACA,EAAA,IAAI;AACF,IAAA,OAAO,EAAA,EAAG;AAAA,EACZ,CAAA,SAAE;AACA,IAAA,UAAA,EAAA;AACA,IAAA,IAAI,UAAA,KAAe,GAAG,KAAA,EAAM;AAAA,EAC9B;AACF;AAGO,SAAS,UAAA,GAAsB;AACpC,EAAA,OAAO,cAAA,KAAmB,IAAA;AAC5B","file":"chunk-EZUHEI5F.js","sourcesContent":["/**\n * Fine-grained reactivity — the engine behind Formwright's surgical DOM updates.\n *\n * This is a small, correct, **zero-dependency** push-pull implementation:\n * - {@link signal} holds a value and the set of observers that read it.\n * - {@link computed} is lazy and cached: it recomputes only when read *after* a\n * dependency changed.\n * - {@link effect} runs immediately and re-runs when any signal/computed it read\n * changes — this is what binds a single value to a single DOM node.\n *\n * Writing a signal marks the dependency graph dirty and synchronously flushes the\n * affected effects (so DOM updates are immediate and deterministic). Only effects\n * that actually read the changed value re-run — there is no diffing and no virtual\n * DOM. The public surface here is intentionally framework-agnostic and swappable\n * (e.g. for `alien-signals`) without touching the rest of the library.\n */\n\nexport interface ReadSignal<T> {\n /** Read the value and subscribe the current effect/computed to changes. */\n get(): T;\n /** Read the value without subscribing. */\n peek(): T;\n}\n\nexport interface WriteSignal<T> extends ReadSignal<T> {\n set(value: T): void;\n update(fn: (prev: T) => T): void;\n}\n\nexport type Dispose = () => void;\n\ninterface Observer {\n /** Sources this observer currently depends on. */\n readonly sources: Set<Source>;\n /** Called when a dependency changed; the observer reacts (recompute or queue). */\n notify(): void;\n}\n\ninterface Source {\n /** Observers currently subscribed to this source. */\n readonly observers: Set<Observer>;\n}\n\nlet activeObserver: Observer | null = null;\nlet batchDepth = 0;\nconst pendingEffects = new Set<EffectNode>();\nlet flushing = false;\n\nfunction link(source: Source): void {\n const obs = activeObserver;\n if (obs === null) return;\n if (!source.observers.has(obs)) {\n source.observers.add(obs);\n obs.sources.add(source);\n }\n}\n\nfunction clearSources(obs: Observer): void {\n for (const src of obs.sources) src.observers.delete(obs);\n obs.sources.clear();\n}\n\nfunction flush(): void {\n if (flushing) return;\n flushing = true;\n try {\n // Re-check each iteration: running an effect may queue more effects.\n while (pendingEffects.size > 0) {\n const next = pendingEffects.values().next().value as EffectNode;\n pendingEffects.delete(next);\n next.run();\n }\n } finally {\n flushing = false;\n }\n}\n\nclass SignalNode<T> implements Source {\n readonly observers = new Set<Observer>();\n constructor(private value: T) {}\n\n get(): T {\n link(this);\n return this.value;\n }\n\n peek(): T {\n return this.value;\n }\n\n set(next: T): void {\n if (Object.is(next, this.value)) return;\n this.value = next;\n // Snapshot observers: notify() may mutate downstream sets, not ours.\n for (const obs of [...this.observers]) obs.notify();\n if (batchDepth === 0) flush();\n }\n\n update(fn: (prev: T) => T): void {\n this.set(fn(this.value));\n }\n}\n\nclass ComputedNode<T> implements Source, Observer {\n readonly observers = new Set<Observer>();\n readonly sources = new Set<Source>();\n private value!: T;\n private dirty = true;\n\n constructor(private readonly fn: () => T) {}\n\n notify(): void {\n if (this.dirty) return; // already invalidated; observers already notified\n this.dirty = true;\n for (const obs of [...this.observers]) obs.notify();\n }\n\n get(): T {\n link(this);\n if (this.dirty) this.recompute();\n return this.value;\n }\n\n peek(): T {\n if (this.dirty) this.recompute();\n return this.value;\n }\n\n private recompute(): void {\n clearSources(this);\n const prev = activeObserver;\n activeObserver = this;\n try {\n this.value = this.fn();\n this.dirty = false;\n } finally {\n activeObserver = prev;\n }\n }\n}\n\nclass EffectNode implements Observer {\n readonly sources = new Set<Source>();\n private cleanup: (() => void) | void = undefined;\n private disposed = false;\n\n constructor(private readonly fn: () => void | (() => void)) {\n this.run();\n }\n\n notify(): void {\n if (this.disposed) return;\n pendingEffects.add(this);\n }\n\n run(): void {\n if (this.disposed) return;\n this.runCleanup();\n clearSources(this);\n const prev = activeObserver;\n activeObserver = this;\n try {\n this.cleanup = this.fn();\n } finally {\n activeObserver = prev;\n }\n }\n\n private runCleanup(): void {\n if (typeof this.cleanup === \"function\") {\n this.cleanup();\n this.cleanup = undefined;\n }\n }\n\n dispose(): void {\n if (this.disposed) return;\n this.disposed = true;\n this.runCleanup();\n clearSources(this);\n pendingEffects.delete(this);\n }\n}\n\n/** Create a writable reactive value. */\nexport function signal<T>(initial: T): WriteSignal<T> {\n return new SignalNode(initial);\n}\n\n/** Create a lazily-evaluated, cached derived value. */\nexport function computed<T>(fn: () => T): ReadSignal<T> {\n return new ComputedNode(fn);\n}\n\n/**\n * Run `fn` immediately and again whenever a signal/computed it read changes.\n * `fn` may return a cleanup function, run before each re-run and on dispose.\n * Returns a {@link Dispose} to stop the effect.\n */\nexport function effect(fn: () => void | (() => void)): Dispose {\n const node = new EffectNode(fn);\n return () => node.dispose();\n}\n\n/** Read reactive values inside `fn` without subscribing the current observer. */\nexport function untrack<T>(fn: () => T): T {\n const prev = activeObserver;\n activeObserver = null;\n try {\n return fn();\n } finally {\n activeObserver = prev;\n }\n}\n\n/** Batch multiple writes so effects flush once, after `fn` returns. */\nexport function batch<T>(fn: () => T): T {\n batchDepth++;\n try {\n return fn();\n } finally {\n batchDepth--;\n if (batchDepth === 0) flush();\n }\n}\n\n/** True when called inside an effect/computed tracking context. */\nexport function isTracking(): boolean {\n return activeObserver !== null;\n}\n"]}