@esportsplus/reactivity 0.22.3 → 0.23.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/build/index.d.ts +1 -1
- package/build/index.js +1 -1
- package/build/reactive/array.d.ts +3 -0
- package/build/reactive/array.js +32 -2
- package/build/reactive/index.d.ts +17 -14
- package/build/reactive/index.js +7 -25
- package/build/system.js +1 -1
- package/build/transformer/detector.d.ts +2 -0
- package/build/transformer/detector.js +38 -0
- package/build/transformer/index.d.ts +10 -0
- package/build/transformer/index.js +55 -0
- package/build/transformer/plugins/esbuild.d.ts +5 -0
- package/build/transformer/plugins/esbuild.js +31 -0
- package/build/transformer/plugins/tsc.d.ts +3 -0
- package/build/transformer/plugins/tsc.js +4 -0
- package/build/transformer/plugins/vite.d.ts +5 -0
- package/build/transformer/plugins/vite.js +28 -0
- package/build/transformer/transforms/auto-dispose.d.ts +3 -0
- package/build/transformer/transforms/auto-dispose.js +119 -0
- package/build/transformer/transforms/reactive-array.d.ts +4 -0
- package/build/transformer/transforms/reactive-array.js +93 -0
- package/build/transformer/transforms/reactive-object.d.ts +4 -0
- package/build/transformer/transforms/reactive-object.js +164 -0
- package/build/transformer/transforms/reactive-primitives.d.ts +4 -0
- package/build/transformer/transforms/reactive-primitives.js +335 -0
- package/build/transformer/transforms/utilities.d.ts +8 -0
- package/build/transformer/transforms/utilities.js +73 -0
- package/build/types.d.ts +14 -4
- package/package.json +30 -3
- package/readme.md +276 -2
- package/src/constants.ts +1 -1
- package/src/index.ts +1 -1
- package/src/reactive/array.ts +49 -2
- package/src/reactive/index.ts +33 -57
- package/src/system.ts +14 -5
- package/src/transformer/detector.ts +65 -0
- package/src/transformer/index.ts +78 -0
- package/src/transformer/plugins/esbuild.ts +47 -0
- package/src/transformer/plugins/tsc.ts +8 -0
- package/src/transformer/plugins/vite.ts +39 -0
- package/src/transformer/transforms/auto-dispose.ts +191 -0
- package/src/transformer/transforms/reactive-array.ts +143 -0
- package/src/transformer/transforms/reactive-object.ts +253 -0
- package/src/transformer/transforms/reactive-primitives.ts +461 -0
- package/src/transformer/transforms/utilities.ts +119 -0
- package/src/types.ts +24 -5
- package/test/arrays.ts +146 -0
- package/test/effects.ts +168 -0
- package/test/index.ts +8 -0
- package/test/nested.ts +201 -0
- package/test/objects.ts +106 -0
- package/test/primitives.ts +171 -0
- package/test/vite.config.ts +40 -0
- package/build/reactive/object.d.ts +0 -7
- package/build/reactive/object.js +0 -79
- package/src/reactive/object.ts +0 -116
package/readme.md
CHANGED
|
@@ -1,2 +1,276 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
# @esportsplus/reactivity
|
|
2
|
+
|
|
3
|
+
A fine-grained reactivity system with compile-time transformations. Write reactive code with natural JavaScript syntax while the compiler generates optimized signal-based code.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @esportsplus/reactivity
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Core Concepts
|
|
12
|
+
|
|
13
|
+
The library provides a `reactive()` function that acts as a compile-time macro. At build time, transformer plugins convert `reactive()` calls into optimized signal/computed primitives.
|
|
14
|
+
|
|
15
|
+
### Reactive Primitives
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { reactive, effect } from '@esportsplus/reactivity';
|
|
19
|
+
|
|
20
|
+
// Signals - reactive values
|
|
21
|
+
let count = reactive(0);
|
|
22
|
+
let name = reactive('John');
|
|
23
|
+
|
|
24
|
+
// Read values naturally
|
|
25
|
+
console.log(count); // 0
|
|
26
|
+
console.log(name); // 'John'
|
|
27
|
+
|
|
28
|
+
// Write with simple assignment
|
|
29
|
+
count = 10;
|
|
30
|
+
name = 'Jane';
|
|
31
|
+
|
|
32
|
+
// Compound assignments work
|
|
33
|
+
count += 5;
|
|
34
|
+
count++;
|
|
35
|
+
|
|
36
|
+
// Computed values - derived from other reactive values
|
|
37
|
+
let doubled = reactive(() => count * 2);
|
|
38
|
+
console.log(doubled); // 30
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Reactive Objects
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
import { reactive } from '@esportsplus/reactivity';
|
|
45
|
+
|
|
46
|
+
let user = reactive({
|
|
47
|
+
age: 25,
|
|
48
|
+
name: 'John',
|
|
49
|
+
// Computed properties are arrow functions
|
|
50
|
+
canVote: () => user.age >= 18
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
console.log(user.name); // 'John'
|
|
54
|
+
console.log(user.canVote); // true
|
|
55
|
+
|
|
56
|
+
user.age = 17;
|
|
57
|
+
console.log(user.canVote); // false
|
|
58
|
+
|
|
59
|
+
// Cleanup resources
|
|
60
|
+
user.dispose();
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Reactive Arrays
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
import { reactive } from '@esportsplus/reactivity';
|
|
67
|
+
|
|
68
|
+
let state = reactive({
|
|
69
|
+
items: [1, 2, 3],
|
|
70
|
+
total: () => state.items.reduce((a, b) => a + b, 0)
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
console.log(state.total); // 6
|
|
74
|
+
|
|
75
|
+
state.items.push(4, 5);
|
|
76
|
+
console.log(state.total); // 15
|
|
77
|
+
|
|
78
|
+
// Listen to array events
|
|
79
|
+
state.items.on('push', ({ items }) => {
|
|
80
|
+
console.log('Added:', items);
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Effects
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
import { effect, reactive } from '@esportsplus/reactivity';
|
|
88
|
+
|
|
89
|
+
let count = reactive(0);
|
|
90
|
+
|
|
91
|
+
let cleanup = effect(() => {
|
|
92
|
+
console.log('Count is:', count);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
count = 1; // logs: Count is: 1
|
|
96
|
+
count = 2; // logs: Count is: 2
|
|
97
|
+
|
|
98
|
+
cleanup(); // stops the effect
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Transformer Plugins
|
|
102
|
+
|
|
103
|
+
The library requires a build-time transformer to convert `reactive()` calls into optimized code. Three plugins are available:
|
|
104
|
+
|
|
105
|
+
### Vite Plugin
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
// vite.config.ts
|
|
109
|
+
import { defineConfig } from 'vite';
|
|
110
|
+
import { plugin as reactivity } from '@esportsplus/reactivity/plugins/vite';
|
|
111
|
+
|
|
112
|
+
export default defineConfig({
|
|
113
|
+
plugins: [
|
|
114
|
+
reactivity()
|
|
115
|
+
]
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### esbuild Plugin
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
import esbuild from 'esbuild';
|
|
123
|
+
import reactivity from '@esportsplus/reactivity/plugins/esbuild';
|
|
124
|
+
|
|
125
|
+
await esbuild.build({
|
|
126
|
+
entryPoints: ['src/index.ts'],
|
|
127
|
+
bundle: true,
|
|
128
|
+
outfile: 'dist/index.js',
|
|
129
|
+
plugins: [
|
|
130
|
+
reactivity()
|
|
131
|
+
]
|
|
132
|
+
});
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### TypeScript Custom Transformer
|
|
136
|
+
|
|
137
|
+
For direct TypeScript compilation using `ttsc` or `ts-patch`:
|
|
138
|
+
|
|
139
|
+
```json
|
|
140
|
+
// tsconfig.json
|
|
141
|
+
{
|
|
142
|
+
"compilerOptions": {
|
|
143
|
+
"plugins": [
|
|
144
|
+
{ "transform": "@esportsplus/reactivity/plugins/tsc" }
|
|
145
|
+
]
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Plugin Options
|
|
151
|
+
|
|
152
|
+
All plugins accept the same options:
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
interface TransformOptions {
|
|
156
|
+
// Inject automatic disposal tracking (experimental)
|
|
157
|
+
autoDispose?: boolean;
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Example with options:
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
// vite.config.ts
|
|
165
|
+
import { plugin as reactivity } from '@esportsplus/reactivity/plugins/vite';
|
|
166
|
+
|
|
167
|
+
export default defineConfig({
|
|
168
|
+
plugins: [
|
|
169
|
+
reactivity({ autoDispose: true })
|
|
170
|
+
]
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## How It Works
|
|
175
|
+
|
|
176
|
+
The transformer converts your code at compile time:
|
|
177
|
+
|
|
178
|
+
**Input:**
|
|
179
|
+
```typescript
|
|
180
|
+
let count = reactive(0);
|
|
181
|
+
let doubled = reactive(() => count * 2);
|
|
182
|
+
|
|
183
|
+
count = 5;
|
|
184
|
+
console.log(doubled);
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
**Output:**
|
|
188
|
+
```typescript
|
|
189
|
+
import { computed, read, set, signal } from '@esportsplus/reactivity';
|
|
190
|
+
|
|
191
|
+
let count = signal(0);
|
|
192
|
+
let doubled = computed(() => read(count) * 2);
|
|
193
|
+
|
|
194
|
+
set(count, 5);
|
|
195
|
+
console.log(read(doubled));
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Reactive objects are transformed into classes:
|
|
199
|
+
|
|
200
|
+
**Input:**
|
|
201
|
+
```typescript
|
|
202
|
+
let user = reactive({
|
|
203
|
+
name: 'John',
|
|
204
|
+
greeting: () => `Hello, ${user.name}`
|
|
205
|
+
});
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**Output:**
|
|
209
|
+
```typescript
|
|
210
|
+
class ReactiveObject_1 {
|
|
211
|
+
#name = signal('John');
|
|
212
|
+
#greeting = null;
|
|
213
|
+
|
|
214
|
+
get name() { return read(this.#name); }
|
|
215
|
+
set name(v) { set(this.#name, v); }
|
|
216
|
+
get greeting() { return read(this.#greeting ??= computed(() => `Hello, ${this.name}`)); }
|
|
217
|
+
|
|
218
|
+
dispose() {
|
|
219
|
+
if (this.#greeting) dispose(this.#greeting);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
let user = new ReactiveObject_1();
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## API Reference
|
|
227
|
+
|
|
228
|
+
### Core Functions
|
|
229
|
+
|
|
230
|
+
| Function | Description |
|
|
231
|
+
|----------|-------------|
|
|
232
|
+
| `reactive(value)` | Creates a signal from a primitive value |
|
|
233
|
+
| `reactive(() => expr)` | Creates a computed value |
|
|
234
|
+
| `reactive({...})` | Creates a reactive object with signals and computeds |
|
|
235
|
+
| `reactive([...])` | Creates a reactive array |
|
|
236
|
+
| `effect(fn)` | Runs a function that re-executes when dependencies change |
|
|
237
|
+
| `root(fn)` | Creates an untracked scope for effects |
|
|
238
|
+
| `onCleanup(fn)` | Registers a cleanup function for the current effect |
|
|
239
|
+
|
|
240
|
+
### Low-Level Functions
|
|
241
|
+
|
|
242
|
+
These are typically only used by the transformer output:
|
|
243
|
+
|
|
244
|
+
| Function | Description |
|
|
245
|
+
|----------|-------------|
|
|
246
|
+
| `signal(value)` | Creates a raw signal |
|
|
247
|
+
| `computed(fn)` | Creates a raw computed |
|
|
248
|
+
| `read(node)` | Reads a signal or computed value |
|
|
249
|
+
| `set(signal, value)` | Sets a signal value |
|
|
250
|
+
| `dispose(computed)` | Disposes a computed and its dependencies |
|
|
251
|
+
|
|
252
|
+
### Type Guards
|
|
253
|
+
|
|
254
|
+
| Function | Description |
|
|
255
|
+
|----------|-------------|
|
|
256
|
+
| `isSignal(value)` | Checks if value is a Signal |
|
|
257
|
+
| `isComputed(value)` | Checks if value is a Computed |
|
|
258
|
+
|
|
259
|
+
## ReactiveArray Events
|
|
260
|
+
|
|
261
|
+
| Event | Payload | Description |
|
|
262
|
+
|-------|---------|-------------|
|
|
263
|
+
| `clear` | `undefined` | Array was cleared |
|
|
264
|
+
| `concat` | `{ items: T[] }` | Items were concatenated |
|
|
265
|
+
| `pop` | `{ item: T }` | Item was popped |
|
|
266
|
+
| `push` | `{ items: T[] }` | Items were pushed |
|
|
267
|
+
| `reverse` | `undefined` | Array was reversed |
|
|
268
|
+
| `set` | `{ index, item }` | Item was set at index |
|
|
269
|
+
| `shift` | `{ item: T }` | Item was shifted |
|
|
270
|
+
| `sort` | `{ order: number[] }` | Array was sorted |
|
|
271
|
+
| `splice` | `{ start, deleteCount, items }` | Array was spliced |
|
|
272
|
+
| `unshift` | `{ items: T[] }` | Items were unshifted |
|
|
273
|
+
|
|
274
|
+
## License
|
|
275
|
+
|
|
276
|
+
MIT
|
package/src/constants.ts
CHANGED
package/src/index.ts
CHANGED
package/src/reactive/array.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isArray } from '@esportsplus/utilities';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { read, set, signal } from '~/system';
|
|
3
|
+
import { REACTIVE_ARRAY, REACTIVE_OBJECT } from '~/constants';
|
|
4
|
+
import type { Signal } from '~/types';
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
type Events<T> = {
|
|
@@ -43,17 +44,46 @@ type Listener<V> = {
|
|
|
43
44
|
type Listeners = Record<string, (Listener<any> | null)[]>;
|
|
44
45
|
|
|
45
46
|
|
|
47
|
+
function isReactiveObject(value: unknown): value is { dispose(): void } {
|
|
48
|
+
return value !== null && typeof value === 'object' && (value as any)[REACTIVE_OBJECT] === true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
46
52
|
class ReactiveArray<T> extends Array<T> {
|
|
53
|
+
private _length: Signal<number>;
|
|
54
|
+
|
|
47
55
|
listeners: Listeners = {};
|
|
48
56
|
|
|
49
57
|
|
|
50
58
|
constructor(...items: T[]) {
|
|
51
59
|
super(...items);
|
|
60
|
+
this._length = signal(items.length);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
$length() {
|
|
65
|
+
return read(this._length);
|
|
52
66
|
}
|
|
53
67
|
|
|
68
|
+
$set(i: number, value: T) {
|
|
69
|
+
let prev = this[i];
|
|
70
|
+
|
|
71
|
+
if (prev === value) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
this[i] = value;
|
|
76
|
+
|
|
77
|
+
if (i >= super.length) {
|
|
78
|
+
set(this._length, i + 1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
this.dispatch('set', { index: i, item: value });
|
|
82
|
+
}
|
|
54
83
|
|
|
55
84
|
clear() {
|
|
56
85
|
this.dispose();
|
|
86
|
+
set(this._length, 0);
|
|
57
87
|
this.dispatch('clear');
|
|
58
88
|
}
|
|
59
89
|
|
|
@@ -78,6 +108,7 @@ class ReactiveArray<T> extends Array<T> {
|
|
|
78
108
|
}
|
|
79
109
|
|
|
80
110
|
if (added.length) {
|
|
111
|
+
set(this._length, super.length);
|
|
81
112
|
this.dispatch('concat', { items: added });
|
|
82
113
|
}
|
|
83
114
|
|
|
@@ -125,6 +156,8 @@ class ReactiveArray<T> extends Array<T> {
|
|
|
125
156
|
item.dispose();
|
|
126
157
|
}
|
|
127
158
|
}
|
|
159
|
+
|
|
160
|
+
set(this._length, 0);
|
|
128
161
|
}
|
|
129
162
|
|
|
130
163
|
on<K extends keyof Events<T>>(event: K, listener: Listener<Events<T>[K]>) {
|
|
@@ -164,9 +197,12 @@ class ReactiveArray<T> extends Array<T> {
|
|
|
164
197
|
let item = super.pop();
|
|
165
198
|
|
|
166
199
|
if (item !== undefined) {
|
|
200
|
+
set(this._length, super.length);
|
|
201
|
+
|
|
167
202
|
if (isReactiveObject(item)) {
|
|
168
203
|
item.dispose();
|
|
169
204
|
}
|
|
205
|
+
|
|
170
206
|
this.dispatch('pop', { item });
|
|
171
207
|
}
|
|
172
208
|
|
|
@@ -174,8 +210,13 @@ class ReactiveArray<T> extends Array<T> {
|
|
|
174
210
|
}
|
|
175
211
|
|
|
176
212
|
push(...items: T[]) {
|
|
213
|
+
if (!items.length) {
|
|
214
|
+
return super.length;
|
|
215
|
+
}
|
|
216
|
+
|
|
177
217
|
let length = super.push(...items);
|
|
178
218
|
|
|
219
|
+
set(this._length, length);
|
|
179
220
|
this.dispatch('push', { items });
|
|
180
221
|
|
|
181
222
|
return length;
|
|
@@ -192,9 +233,12 @@ class ReactiveArray<T> extends Array<T> {
|
|
|
192
233
|
let item = super.shift();
|
|
193
234
|
|
|
194
235
|
if (item !== undefined) {
|
|
236
|
+
set(this._length, super.length);
|
|
237
|
+
|
|
195
238
|
if (isReactiveObject(item)) {
|
|
196
239
|
item.dispose();
|
|
197
240
|
}
|
|
241
|
+
|
|
198
242
|
this.dispatch('shift', { item });
|
|
199
243
|
}
|
|
200
244
|
|
|
@@ -250,6 +294,8 @@ class ReactiveArray<T> extends Array<T> {
|
|
|
250
294
|
let removed = super.splice(start, deleteCount, ...items);
|
|
251
295
|
|
|
252
296
|
if (items.length > 0 || removed.length > 0) {
|
|
297
|
+
set(this._length, super.length);
|
|
298
|
+
|
|
253
299
|
for (let i = 0, n = removed.length; i < n; i++) {
|
|
254
300
|
let item = removed[i];
|
|
255
301
|
|
|
@@ -267,6 +313,7 @@ class ReactiveArray<T> extends Array<T> {
|
|
|
267
313
|
unshift(...items: T[]) {
|
|
268
314
|
let length = super.unshift(...items);
|
|
269
315
|
|
|
316
|
+
set(this._length, length);
|
|
270
317
|
this.dispatch('unshift', { items });
|
|
271
318
|
|
|
272
319
|
return length;
|
package/src/reactive/index.ts
CHANGED
|
@@ -1,58 +1,34 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { onCleanup, root } from '~/system';
|
|
1
|
+
import { REACTIVE_OBJECT } from '~/constants';
|
|
3
2
|
import { ReactiveArray } from './array';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
type
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
response = new ReactiveObject(input) as any as API<T>;
|
|
37
|
-
}
|
|
38
|
-
else if (isArray(input)) {
|
|
39
|
-
response = new ReactiveArray(...input) as API<T>;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (response) {
|
|
43
|
-
if (root.disposables) {
|
|
44
|
-
dispose = true;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return response;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
throw new Error(`@esportsplus/reactivity: 'reactive' received invalid input - ${JSON.stringify(input)}`);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
if (dispose) {
|
|
54
|
-
onCleanup(() => value.dispose());
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return value;
|
|
58
|
-
};
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
// Branded type to prevent assignment to computed values
|
|
6
|
+
declare const READONLY: unique symbol;
|
|
7
|
+
|
|
8
|
+
type ReactiveObject<T extends Record<PropertyKey, unknown>> = T & {
|
|
9
|
+
[REACTIVE_OBJECT]: true;
|
|
10
|
+
dispose(): void;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type ReactiveObjectGuard<T> = T extends { dispose: any } ? { never: '[ dispose ] is a reserved key' } : T;
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
// Function input → branded return type (prevents assignment)
|
|
17
|
+
function reactive<T extends () => unknown>(_input: T): ReturnType<T> & { readonly [READONLY]: true };
|
|
18
|
+
// Object literal → existing ReactiveObject behavior
|
|
19
|
+
function reactive<T extends Record<PropertyKey, any>>(_input: ReactiveObjectGuard<T>): ReactiveObject<T>;
|
|
20
|
+
// Array literal → existing ReactiveArray behavior
|
|
21
|
+
function reactive<T>(_input: T[]): ReactiveArray<T>;
|
|
22
|
+
// Everything else → passthrough type (allows assignment)
|
|
23
|
+
function reactive<T>(_input: T): T;
|
|
24
|
+
function reactive(_input: unknown): unknown {
|
|
25
|
+
throw new Error(
|
|
26
|
+
'@esportsplus/reactivity: reactive() called at runtime. ' +
|
|
27
|
+
'Ensure vite-plugin-reactivity-compile is configured.'
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
export default reactive;
|
|
33
|
+
export { reactive, ReactiveArray };
|
|
34
|
+
export type { ReactiveObject };
|
package/src/system.ts
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
|
-
import { isObject } from '@esportsplus/utilities';
|
|
2
1
|
import {
|
|
3
|
-
COMPUTED,
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
COMPUTED,
|
|
3
|
+
SIGNAL,
|
|
4
|
+
STABILIZER_IDLE,
|
|
5
|
+
STABILIZER_RESCHEDULE,
|
|
6
|
+
STABILIZER_RUNNING,
|
|
7
|
+
STABILIZER_SCHEDULED,
|
|
8
|
+
STATE_CHECK,
|
|
9
|
+
STATE_DIRTY,
|
|
10
|
+
STATE_IN_HEAP,
|
|
11
|
+
STATE_NONE,
|
|
12
|
+
STATE_NOTIFY_MASK,
|
|
13
|
+
STATE_RECOMPUTING
|
|
6
14
|
} from './constants';
|
|
7
15
|
import { Computed, Link, Signal } from './types';
|
|
16
|
+
import { isObject } from '@esportsplus/utilities';
|
|
8
17
|
|
|
9
18
|
|
|
10
19
|
let depth = 0,
|
|
@@ -558,4 +567,4 @@ export {
|
|
|
558
567
|
read, root,
|
|
559
568
|
set, signal
|
|
560
569
|
};
|
|
561
|
-
export type { Computed, Signal };
|
|
570
|
+
export type { Computed, Signal };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { mightNeedTransform as checkTransform } from '@esportsplus/typescript/transformer';
|
|
2
|
+
import ts from 'typescript';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
interface DetectContext {
|
|
6
|
+
hasImport: boolean;
|
|
7
|
+
hasUsage: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
const REACTIVE_REGEX = /\breactive\b/;
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
function visit(ctx: DetectContext, node: ts.Node): void {
|
|
15
|
+
if (ctx.hasImport && ctx.hasUsage) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (
|
|
20
|
+
ts.isImportDeclaration(node) &&
|
|
21
|
+
node.importClause?.namedBindings &&
|
|
22
|
+
ts.isNamedImports(node.importClause.namedBindings)
|
|
23
|
+
) {
|
|
24
|
+
let elements = node.importClause.namedBindings.elements;
|
|
25
|
+
|
|
26
|
+
for (let i = 0, n = elements.length; i < n; i++) {
|
|
27
|
+
let el = elements[i],
|
|
28
|
+
name = el.propertyName?.text ?? el.name.text;
|
|
29
|
+
|
|
30
|
+
if (name === 'reactive') {
|
|
31
|
+
ctx.hasImport = true;
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (
|
|
38
|
+
ts.isCallExpression(node) &&
|
|
39
|
+
ts.isIdentifier(node.expression) &&
|
|
40
|
+
node.expression.text === 'reactive'
|
|
41
|
+
) {
|
|
42
|
+
ctx.hasUsage = true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
ts.forEachChild(node, n => visit(ctx, n));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
const mightNeedTransform = (code: string): boolean => {
|
|
50
|
+
if (!checkTransform(code, { regex: REACTIVE_REGEX })) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let ctx: DetectContext = {
|
|
55
|
+
hasImport: false,
|
|
56
|
+
hasUsage: false
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
visit(ctx, ts.createSourceFile('detect.ts', code, ts.ScriptTarget.Latest, false));
|
|
60
|
+
|
|
61
|
+
return ctx.hasImport && ctx.hasUsage;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
export { mightNeedTransform };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { Bindings, TransformOptions, TransformResult } from '~/types';
|
|
2
|
+
import { injectAutoDispose } from './transforms/auto-dispose';
|
|
3
|
+
import { mightNeedTransform } from './detector';
|
|
4
|
+
import { transformReactiveArrays } from './transforms/reactive-array';
|
|
5
|
+
import { transformReactiveObjects } from './transforms/reactive-object';
|
|
6
|
+
import { transformReactivePrimitives } from './transforms/reactive-primitives';
|
|
7
|
+
import ts from 'typescript';
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
const createTransformer = (options?: TransformOptions): ts.TransformerFactory<ts.SourceFile> => {
|
|
11
|
+
return () => {
|
|
12
|
+
return (sourceFile: ts.SourceFile): ts.SourceFile => {
|
|
13
|
+
let result = transform(sourceFile, options);
|
|
14
|
+
|
|
15
|
+
return result.transformed ? result.sourceFile : sourceFile;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const transform = (sourceFile: ts.SourceFile, options?: TransformOptions): TransformResult => {
|
|
21
|
+
let bindings: Bindings = new Map(),
|
|
22
|
+
code = sourceFile.getFullText(),
|
|
23
|
+
current = sourceFile,
|
|
24
|
+
original = code,
|
|
25
|
+
result: string;
|
|
26
|
+
|
|
27
|
+
if (!mightNeedTransform(code)) {
|
|
28
|
+
return { code, sourceFile, transformed: false };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Run all transforms, only re-parse between transforms if code changed
|
|
32
|
+
result = transformReactiveObjects(current, bindings);
|
|
33
|
+
|
|
34
|
+
if (result !== code) {
|
|
35
|
+
current = ts.createSourceFile(sourceFile.fileName, result, sourceFile.languageVersion, true);
|
|
36
|
+
code = result;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
result = transformReactiveArrays(current, bindings);
|
|
40
|
+
|
|
41
|
+
if (result !== code) {
|
|
42
|
+
current = ts.createSourceFile(sourceFile.fileName, result, sourceFile.languageVersion, true);
|
|
43
|
+
code = result;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
result = transformReactivePrimitives(current, bindings);
|
|
47
|
+
|
|
48
|
+
if (result !== code) {
|
|
49
|
+
current = ts.createSourceFile(sourceFile.fileName, result, sourceFile.languageVersion, true);
|
|
50
|
+
code = result;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (options?.autoDispose) {
|
|
54
|
+
result = injectAutoDispose(current);
|
|
55
|
+
|
|
56
|
+
if (result !== code) {
|
|
57
|
+
current = ts.createSourceFile(sourceFile.fileName, result, sourceFile.languageVersion, true);
|
|
58
|
+
code = result;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (code === original) {
|
|
63
|
+
return { code, sourceFile, transformed: false };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
code,
|
|
68
|
+
sourceFile: current,
|
|
69
|
+
transformed: true
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
export { createTransformer, mightNeedTransform, transform };
|
|
75
|
+
export { injectAutoDispose } from './transforms/auto-dispose';
|
|
76
|
+
export { transformReactiveArrays } from './transforms/reactive-array';
|
|
77
|
+
export { transformReactiveObjects } from './transforms/reactive-object';
|
|
78
|
+
export { transformReactivePrimitives } from './transforms/reactive-primitives';
|