@cplieger/reactive 2.1.0 → 2.1.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/NOTICE ADDED
@@ -0,0 +1,3 @@
1
+ reactive
2
+ Copyright 2026 cplieger
3
+ https://github.com/cplieger/reactive
package/README.md CHANGED
@@ -1,11 +1,6 @@
1
1
  # reactive
2
2
 
3
- [![npm](https://img.shields.io/npm/v/@cplieger/reactive)](https://www.npmjs.com/package/@cplieger/reactive)
4
- [![JSR](https://jsr.io/badges/@cplieger/reactive)](https://jsr.io/@cplieger/reactive)
5
- [![Test coverage](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/cplieger/reactive/badges/coverage.json)](https://github.com/cplieger/reactive/actions/workflows/coverage.yml)
6
- [![Mutation (TS)](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/cplieger/reactive/badges/mutation-ts.json)](https://github.com/cplieger/reactive/issues?q=label%3Astryker-tracker)
7
- [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/13218/badge)](https://www.bestpractices.dev/projects/13218)
8
- [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/cplieger/reactive/badge)](https://scorecard.dev/viewer/?uri=github.com/cplieger/reactive)
3
+ [![npm](https://img.shields.io/npm/v/@cplieger/reactive)](https://www.npmjs.com/package/@cplieger/reactive) [![JSR](https://jsr.io/badges/@cplieger/reactive)](https://jsr.io/@cplieger/reactive) [![Mutation (TS)](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/cplieger/reactive/badges/mutation-ts.json)](https://github.com/cplieger/reactive/issues?q=label%3Astryker-tracker)
9
4
 
10
5
  > Signals + collections + DOM-reconciliation micro-framework for TypeScript
11
6
 
@@ -23,6 +18,8 @@ A standalone reactive app-skeleton:
23
18
 
24
19
  The store, `SignalMap`, and `Collection` are thin facades over a single signal engine: one reactive core, not several. Zero dependencies beyond the DOM API.
25
20
 
21
+ > **What needs a DOM:** the render half, `el`, `bindList`, `reconcile`, `patch` and `trackHandler`. The engine and the state tiers do not: `signal`, `effect`, `computed`, `batch`, `untracked`, `createStore`, `SignalMap`, `createCollection` and `createBus`. Nothing touches the DOM at import time, so importing the package under Node, Deno, Bun or Workers succeeds and only the DOM functions throw when called.
22
+
26
23
  Mirrors semantics from @preact/signals-core and solid-js reactivity.
27
24
 
28
25
  ## Flush model
@@ -202,3 +199,5 @@ This project was built with AI-assisted tooling using [Claude](https://claude.co
202
199
  ## License
203
200
 
204
201
  Apache-2.0. See [LICENSE](LICENSE).
202
+
203
+ Third-party attributions are in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
@@ -0,0 +1,8 @@
1
+ # Third-party notices
2
+
3
+ No third-party code is included in this repository. The signal engine is a re-derivation of an upstream design, not a copy of its source, so no license text is reproduced here.
4
+
5
+ Two designs are followed, each named at the line of ours that follows it:
6
+
7
+ - The dependency graph and the scheduler in `src/signal.ts` follow [@preact/signals-core](https://github.com/preactjs/signals) (MIT): the doubly-linked source/target edge nodes (`DepNode`, `SourceNode`, `TargetNode` at `src/signal.ts:75-100`), the pull-based glitch-free `refreshComputed` (`src/signal.ts:327`), the eager drain in `endBatch` (`src/signal.ts:397`), and the untracked `subscribe` callback (`src/signal.ts:711`, upstream `preactjs/signals#188`). The pinned baseline is `@preact/signals-core@1.14.4`; the header of `src/signal.ts` records every deliberate delta from it and the upstream machinery this engine does not carry, and `CONTRIBUTING.md` records the drift-audit procedure.
8
+ - The explicit dependency helper `on()` (`src/signal.ts:734`) follows [solid-js](https://github.com/solidjs/solid) (MIT), whose `on()` has the same deps-plus-body shape; `untracked()` (`src/signal.ts:666`) is Solid's `untrack()` under the name Preact gives it.
package/jsr.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/reactive",
3
- "version": "2.1.0",
3
+ "version": "2.1.2",
4
4
  "license": "Apache-2.0",
5
5
  "exports": {
6
6
  ".": "./src/index.ts"
@@ -8,7 +8,9 @@
8
8
  "publish": {
9
9
  "include": [
10
10
  "LICENSE",
11
+ "NOTICE",
11
12
  "README.md",
13
+ "THIRD_PARTY_NOTICES.md",
12
14
  "src/**/*.ts"
13
15
  ],
14
16
  "exclude": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/reactive",
3
- "version": "2.1.0",
3
+ "version": "2.1.2",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "exports": {
@@ -9,7 +9,11 @@
9
9
  "scripts": {
10
10
  "typecheck": "tsc -p tsconfig.json",
11
11
  "typecheck:tests": "tsc -p tsconfig.tests.json",
12
- "test": "vitest --run"
12
+ "test": "vitest --run",
13
+ "lint:eslint": "eslint .",
14
+ "lint:prettier": "prettier --check .",
15
+ "lint:knip": "knip",
16
+ "lint:fix": "eslint . --fix && prettier --write ."
13
17
  },
14
18
  "devDependencies": {
15
19
  "@eslint/js": "10.0.1",
@@ -17,16 +21,17 @@
17
21
  "@stryker-mutator/typescript-checker": "10.0.0",
18
22
  "@stryker-mutator/vitest-runner": "10.0.0",
19
23
  "@typescript/native": "npm:typescript@7.0.2",
20
- "@vitest/browser": "4.1.11",
21
- "@vitest/browser-playwright": "4.1.11",
22
- "@vitest/coverage-v8": "4.1.11",
23
- "eslint": "10.9.1",
24
- "fast-check": "4.9.0",
25
- "playwright": "1.62.1",
26
- "prettier": "3.9.6",
24
+ "@vitest/browser": "5.0.1",
25
+ "@vitest/browser-playwright": "5.0.1",
26
+ "@vitest/coverage-v8": "5.0.1",
27
+ "eslint": "10.10.0",
28
+ "fast-check": "4.10.1",
29
+ "knip": "6.36.0",
30
+ "playwright": "1.63.0",
31
+ "prettier": "3.9.7",
27
32
  "typescript": "npm:@typescript/typescript6@6.0.2",
28
- "typescript-eslint": "8.69.0",
29
- "vitest": "4.1.11"
33
+ "typescript-eslint": "8.70.0",
34
+ "vitest": "5.0.1"
30
35
  },
31
36
  "overrides": {
32
37
  "qs": "^6.15.2"
@@ -61,6 +66,9 @@
61
66
  "files": [
62
67
  "src/",
63
68
  "jsr.json",
64
- "!src/**/*.test.ts"
69
+ "!src/**/*.test.ts",
70
+ "LICENSE",
71
+ "NOTICE",
72
+ "THIRD_PARTY_NOTICES.md"
65
73
  ]
66
74
  }
package/src/reconcile.ts CHANGED
@@ -30,10 +30,31 @@ export function reconcile<T>(
30
30
  }
31
31
  }
32
32
 
33
- let target: Node | null = null;
34
- for (let i = items.length - 1; i >= 0; i--) {
35
- const item = items[i] as T;
33
+ // DEPARTING ELEMENTS GO FIRST, before anything is placed. The walk below runs
34
+ // backwards and skips an element already sitting before the last one it placed; a
35
+ // departing element still in the tree makes every PREDECESSOR's `nextSibling` point at
36
+ // a node about to vanish, so the guard sees a mismatch and re-seats a row nothing
37
+ // changed — which restarts its animations and drops `:hover` and focus. So `onRemove`
38
+ // fires before the survivors are placed, holding its own intact element; only the
39
+ // siblings' positions are not yet final. `key` is still called exactly once per item,
40
+ // and the pairs carry it into the walk so neither loop indexes an array.
41
+ const pairs: [T, string][] = [];
42
+ const wanted = new Set<string>();
43
+ for (const item of items) {
36
44
  const k = spec.key(item);
45
+ pairs.push([item, k]);
46
+ wanted.add(k);
47
+ }
48
+ for (const [k, el] of existing) {
49
+ if (!wanted.has(k)) {
50
+ spec.onRemove?.(el, k);
51
+ el.remove();
52
+ existing.delete(k);
53
+ }
54
+ }
55
+
56
+ let target: Node | null = null;
57
+ for (const [item, k] of pairs.reverse()) {
37
58
  let el = existing.get(k);
38
59
  if (el === undefined) {
39
60
  el = spec.mount(item);
@@ -52,9 +73,4 @@ export function reconcile<T>(
52
73
  }
53
74
  target = el;
54
75
  }
55
-
56
- for (const [k, el] of existing) {
57
- spec.onRemove?.(el, k);
58
- el.remove();
59
- }
60
76
  }
package/src/signal.ts CHANGED
@@ -746,19 +746,27 @@ export function on<T, U>(
746
746
  fn: (value: T, prev: T | undefined, prevResult: U | undefined) => U,
747
747
  options?: { defer?: false },
748
748
  ): () => U;
749
- // Single accessor, deferred: first call returns undefined.
749
+ /** Single accessor, deferred: the first call returns undefined. A `defer`
750
+ * typed as `boolean` selects this overload even when the value is `false`,
751
+ * because the type cannot rule the deferred call out; pass a literal
752
+ * `false` (or omit `options`) to keep the non-undefined return. */
750
753
  export function on<T, U>(
751
754
  deps: () => T,
752
755
  fn: (value: T, prev: T | undefined, prevResult: U | undefined) => U,
753
756
  options: { defer: boolean },
754
757
  ): () => U | undefined;
755
- // Array of accessors, non-deferred (or explicit `{ defer: false }`): never undefined.
758
+ /** Array of accessors, non-deferred (or explicit `{ defer: false }`): never
759
+ * undefined. `value` and `prev` are the accessors' results in declaration
760
+ * order, typed `unknown[]` — a heterogeneous list is not correlated
761
+ * positionally, so narrow each element before use. */
756
762
  export function on<U>(
757
763
  deps: (() => unknown)[],
758
764
  fn: (value: unknown[], prev: unknown[] | undefined, prevResult: U | undefined) => U,
759
765
  options?: { defer?: false },
760
766
  ): () => U;
761
- // Array of accessors, deferred: first call returns undefined.
767
+ /** Array of accessors, deferred: the first call returns undefined, having read
768
+ * every accessor to seed `prev`. Same `boolean`-widening caveat as the single
769
+ * deferred overload. */
762
770
  export function on<U>(
763
771
  deps: (() => unknown)[],
764
772
  fn: (value: unknown[], prev: unknown[] | undefined, prevResult: U | undefined) => U,