@esportsplus/reactivity 0.37.0 → 0.37.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/bench/cellx.bench.ts +4 -4
- package/bench/dynamic.bench.ts +10 -9
- package/bench/kairo.bench.ts +28 -33
- package/bench/molbench.bench.ts +4 -4
- package/bench/reactive/array.bench.ts +163 -163
- package/bench/reactive/reactive-object.bench.ts +47 -45
- package/bench/system.bench.ts +263 -263
- package/build/compiler/array.js +68 -73
- package/build/compiler/bindings.d.ts +19 -0
- package/build/compiler/bindings.js +65 -0
- package/build/compiler/index.js +15 -6
- package/build/compiler/object.js +10 -6
- package/build/compiler/primitives.js +137 -138
- package/build/compiler/types.d.ts +7 -2
- package/build/reactive/array.js +19 -12
- package/build/system.js +1 -1
- package/package.json +3 -3
- package/pnpm-workspace.yaml +2 -4
- package/src/compiler/array.ts +86 -89
- package/src/compiler/bindings.ts +100 -0
- package/src/compiler/index.ts +22 -9
- package/src/compiler/object.ts +12 -7
- package/src/compiler/primitives.ts +190 -187
- package/src/compiler/types.ts +7 -2
- package/src/reactive/array.ts +27 -14
- package/src/system.ts +3 -1
- package/test/async-iterable.test.ts +42 -0
- package/test/compiler/compiler.test.ts +115 -45
- package/test/reactive/array.test.ts +54 -14
- package/vitest.config.ts +2 -2
|
@@ -5,6 +5,7 @@ import type { ReplacementIntent } from '@esportsplus/typescript/compiler';
|
|
|
5
5
|
import { NAMESPACE, TYPES } from '~/compiler/constants';
|
|
6
6
|
import type { Bindings } from '~/compiler/types';
|
|
7
7
|
import array from '~/compiler/array';
|
|
8
|
+
import scope from '~/compiler/bindings';
|
|
8
9
|
import object from '~/compiler/object';
|
|
9
10
|
import primitives from '~/compiler/primitives';
|
|
10
11
|
import pipeline from '~/compiler/index';
|
|
@@ -30,29 +31,39 @@ function isReactiveCall(node: ts.Node): node is ts.CallExpression {
|
|
|
30
31
|
return ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'reactive';
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
// Bindings resolve by symbol, so every transform needs a checker over the parsed source
|
|
35
|
+
function parse(code: string): { bindings: Bindings; sourceFile: ts.SourceFile } {
|
|
36
|
+
let { checker, sourceFile } = languageService.scratch(process.cwd() + '/test.ts', code);
|
|
37
|
+
|
|
38
|
+
return { bindings: scope.create(checker), sourceFile };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Full pipeline over a module importing reactive(): what a real consumer compiles
|
|
42
|
+
function transformModule(code: string): string {
|
|
43
|
+
code = `import { reactive } from '@esportsplus/reactivity';\n` + code;
|
|
44
|
+
|
|
45
|
+
let { checker, sourceFile } = languageService.scratch(process.cwd() + '/module.ts', code),
|
|
46
|
+
result = pipeline.transform({ checker, code, sourceFile } as never);
|
|
47
|
+
|
|
48
|
+
return applyIntents(code, sourceFile, result.replacements ?? []);
|
|
35
49
|
}
|
|
36
50
|
|
|
37
51
|
function transformPrimitives(code: string): { bindings: Bindings; output: string } {
|
|
38
|
-
let bindings
|
|
39
|
-
sourceFile = parse(code),
|
|
52
|
+
let { bindings, sourceFile } = parse(code),
|
|
40
53
|
{ replacements } = primitives(sourceFile, bindings, isReactiveCall);
|
|
41
54
|
|
|
42
55
|
return { bindings, output: applyIntents(code, sourceFile, replacements) };
|
|
43
56
|
}
|
|
44
57
|
|
|
45
|
-
function transformArray(code: string
|
|
46
|
-
let
|
|
47
|
-
|
|
48
|
-
intents = array(sourceFile, b, isReactiveCall);
|
|
58
|
+
function transformArray(code: string): { bindings: Bindings; output: string } {
|
|
59
|
+
let { bindings, sourceFile } = parse(code),
|
|
60
|
+
intents = array(sourceFile, bindings, isReactiveCall);
|
|
49
61
|
|
|
50
|
-
return { bindings
|
|
62
|
+
return { bindings, output: applyIntents(code, sourceFile, intents) };
|
|
51
63
|
}
|
|
52
64
|
|
|
53
65
|
function transformObject(code: string): { bindings: Bindings; output: string; prepend: string[] } {
|
|
54
|
-
let bindings
|
|
55
|
-
sourceFile = parse(code),
|
|
66
|
+
let { bindings, sourceFile } = parse(code),
|
|
56
67
|
result = object(sourceFile, bindings, isReactiveCall);
|
|
57
68
|
|
|
58
69
|
return {
|
|
@@ -62,6 +73,26 @@ function transformObject(code: string): { bindings: Bindings; output: string; pr
|
|
|
62
73
|
};
|
|
63
74
|
}
|
|
64
75
|
|
|
76
|
+
function typeOf(bindings: Bindings, name: string): number | undefined {
|
|
77
|
+
for (let [symbol, type] of bindings.symbols) {
|
|
78
|
+
if (symbol.name === name) {
|
|
79
|
+
return type;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function hasPath(bindings: Bindings, name: string, path: string): boolean {
|
|
87
|
+
for (let [symbol, paths] of bindings.paths) {
|
|
88
|
+
if (symbol.name === name && paths.has(path)) {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
65
96
|
|
|
66
97
|
describe('primitives transform', () => {
|
|
67
98
|
it('transforms reactive(0) to signal', () => {
|
|
@@ -128,13 +159,13 @@ describe('primitives transform', () => {
|
|
|
128
159
|
it('tracks bindings for signal type', () => {
|
|
129
160
|
let { bindings } = transformPrimitives('let x = reactive(0);');
|
|
130
161
|
|
|
131
|
-
expect(bindings
|
|
162
|
+
expect(typeOf(bindings, 'x')).toBe(TYPES.Signal);
|
|
132
163
|
});
|
|
133
164
|
|
|
134
165
|
it('tracks bindings for computed type', () => {
|
|
135
166
|
let { bindings } = transformPrimitives('let x = reactive(0); let d = reactive(() => x * 2);');
|
|
136
167
|
|
|
137
|
-
expect(bindings
|
|
168
|
+
expect(typeOf(bindings, 'd')).toBe(TYPES.Computed);
|
|
138
169
|
});
|
|
139
170
|
|
|
140
171
|
it('leaves a same-named plain variable in a sibling function untouched', () => {
|
|
@@ -222,7 +253,7 @@ describe('object transform', () => {
|
|
|
222
253
|
it('tracks nested array bindings', () => {
|
|
223
254
|
let { bindings } = transformObject('let obj = reactive({ items: [1, 2, 3] });');
|
|
224
255
|
|
|
225
|
-
expect(bindings
|
|
256
|
+
expect(hasPath(bindings, 'obj', 'items')).toBe(true);
|
|
226
257
|
});
|
|
227
258
|
});
|
|
228
259
|
|
|
@@ -242,49 +273,33 @@ describe('array transform', () => {
|
|
|
242
273
|
});
|
|
243
274
|
|
|
244
275
|
it('transforms arr.length read to arr.$length', () => {
|
|
245
|
-
let
|
|
246
|
-
|
|
247
|
-
bindings.set('arr', 0); // TYPES.Array = 0
|
|
248
|
-
let { output } = transformArray('let x = arr.length;', bindings);
|
|
276
|
+
let { output } = transformArray('let arr = reactive([1]); let x = arr.length;');
|
|
249
277
|
|
|
250
278
|
expect(output).toContain('arr.$length');
|
|
251
279
|
});
|
|
252
280
|
|
|
253
281
|
it('transforms arr.length = n to arr.$length = n', () => {
|
|
254
|
-
let
|
|
255
|
-
|
|
256
|
-
bindings.set('arr', 0);
|
|
257
|
-
let { output } = transformArray('arr.length = 5;', bindings);
|
|
282
|
+
let { output } = transformArray('let arr = reactive([1]); arr.length = 5;');
|
|
258
283
|
|
|
259
284
|
expect(output).toContain('arr.$length = 5');
|
|
260
285
|
});
|
|
261
286
|
|
|
262
287
|
it('transforms arr.length += n to arr.$length = arr.length + n', () => {
|
|
263
|
-
let
|
|
264
|
-
|
|
265
|
-
bindings.set('arr', 0);
|
|
266
|
-
let { output } = transformArray('arr.length += 3;', bindings);
|
|
288
|
+
let { output } = transformArray('let arr = reactive([1]); arr.length += 3;');
|
|
267
289
|
|
|
268
290
|
expect(output).toContain('arr.$length = arr.length + 3');
|
|
269
291
|
});
|
|
270
292
|
|
|
271
293
|
it('transforms every compound operator on arr.length with its own token', () => {
|
|
272
|
-
let bindings: Bindings = new Map();
|
|
273
|
-
|
|
274
|
-
bindings.set('arr', 0);
|
|
275
|
-
|
|
276
294
|
for (let op of ['<<', '>>', '>>>', '??', '||', '&&', '-', '*', '/', '%', '**', '&', '|', '^']) {
|
|
277
|
-
let { output } = transformArray(`arr.length ${op}= 2
|
|
295
|
+
let { output } = transformArray(`let arr = reactive([1]); arr.length ${op}= 2;`);
|
|
278
296
|
|
|
279
297
|
expect(output).toContain(`arr.$length = arr.length ${op} 2`);
|
|
280
298
|
}
|
|
281
299
|
});
|
|
282
300
|
|
|
283
301
|
it('transforms arr[i] = value to arr.$set(i, value)', () => {
|
|
284
|
-
let
|
|
285
|
-
|
|
286
|
-
bindings.set('arr', 0);
|
|
287
|
-
let { output } = transformArray('arr[0] = 42;', bindings);
|
|
302
|
+
let { output } = transformArray('let arr = reactive([1]); arr[0] = 42;');
|
|
288
303
|
|
|
289
304
|
expect(output).toContain('arr.$set(');
|
|
290
305
|
expect(output).toContain('42');
|
|
@@ -293,24 +308,19 @@ describe('array transform', () => {
|
|
|
293
308
|
it('tracks reactive array binding from reactive call', () => {
|
|
294
309
|
let { bindings } = transformArray('let arr = reactive([1, 2, 3]);');
|
|
295
310
|
|
|
296
|
-
expect(bindings
|
|
311
|
+
expect(typeOf(bindings, 'arr')).toBe(TYPES.Array);
|
|
297
312
|
});
|
|
298
313
|
|
|
299
314
|
it('tracks alias binding from reactive array', () => {
|
|
300
|
-
let bindings
|
|
301
|
-
|
|
302
|
-
bindings.set('a', 0);
|
|
303
|
-
transformArray('let b = a;', bindings);
|
|
315
|
+
let { bindings } = transformArray('let a = reactive([1]); let b = a;');
|
|
304
316
|
|
|
305
|
-
expect(bindings
|
|
317
|
+
expect(typeOf(bindings, 'b')).toBe(TYPES.Array);
|
|
306
318
|
});
|
|
307
319
|
|
|
308
320
|
it('tracks typed parameter as ReactiveArray', () => {
|
|
309
|
-
let bindings
|
|
310
|
-
|
|
311
|
-
transformArray('function fn(arr: ReactiveArray) { return arr; }', bindings);
|
|
321
|
+
let { bindings } = transformArray('function fn(arr: ReactiveArray) { return arr; }');
|
|
312
322
|
|
|
313
|
-
expect(bindings
|
|
323
|
+
expect(typeOf(bindings, 'arr')).toBe(TYPES.Array);
|
|
314
324
|
});
|
|
315
325
|
|
|
316
326
|
it('transforms empty array', () => {
|
|
@@ -321,6 +331,66 @@ describe('array transform', () => {
|
|
|
321
331
|
});
|
|
322
332
|
|
|
323
333
|
|
|
334
|
+
describe('binding resolution (full pipeline)', () => {
|
|
335
|
+
it('leaves a local array that shadows a reactive array untouched', () => {
|
|
336
|
+
let output = transformModule('let rows = reactive([] as number[]);\nfunction build(n: number) { let rows: number[] = []; rows[0] = n; return rows.length; }');
|
|
337
|
+
|
|
338
|
+
expect(output).toContain('let rows: number[] = []; rows[0] = n; return rows.length;');
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it('leaves a parameter that shadows a reactive array untouched', () => {
|
|
342
|
+
let output = transformModule('let rows = reactive([1]);\nconst f = (rows: number[]) => { rows[0] = 2; };');
|
|
343
|
+
|
|
344
|
+
expect(output).toContain('const f = (rows: number[]) => { rows[0] = 2; };');
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('leaves a local that shadows a signal untouched', () => {
|
|
348
|
+
let output = transformModule('let count = reactive(0);\nfunction f() { let count = 5; count++; return count; }');
|
|
349
|
+
|
|
350
|
+
expect(output).toContain('function f() { let count = 5; count++; return count; }');
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('leaves a parameter that shadows a signal untouched', () => {
|
|
354
|
+
let output = transformModule('let count = reactive(0);\nconst f = (count: number) => count + 1;');
|
|
355
|
+
|
|
356
|
+
expect(output).toContain('const f = (count: number) => count + 1;');
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('never rewrites property names, type members or destructured parameters', () => {
|
|
360
|
+
let output = transformModule('let count = reactive(0);\nlet o = { count: 1 };\nlet p = o.count;\nfunction f({ count }: { count: number }) { return count; }');
|
|
361
|
+
|
|
362
|
+
expect(output).toContain('let o = { count: 1 };');
|
|
363
|
+
expect(output).toContain('let p = o.count;');
|
|
364
|
+
expect(output).toContain('function f({ count }: { count: number }) { return count; }');
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it('expands a shorthand property into a read', () => {
|
|
368
|
+
let output = transformModule('let count = reactive(0);\nlet o = { count };');
|
|
369
|
+
|
|
370
|
+
expect(output).toContain(`let o = { count: ${NAMESPACE}.read(count) };`);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('rewrites a use that precedes the declaration in source order', () => {
|
|
374
|
+
let output = transformModule('function inc() { count++; }\nlet count = reactive(0);');
|
|
375
|
+
|
|
376
|
+
expect(output).toContain(`function inc() { ${NAMESPACE}.write(count, count.value + 1); }`);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it('exports the signal itself rather than splicing a read into the export list', () => {
|
|
380
|
+
let output = transformModule('let count = reactive(0);\nexport { count };');
|
|
381
|
+
|
|
382
|
+
expect(output).toContain('export { count };');
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it('transforms a namespace-qualified reactive() call', () => {
|
|
386
|
+
let output = transformModule("import * as r from '@esportsplus/reactivity';\nlet count = r.reactive(0);\nlet next = count + 1;");
|
|
387
|
+
|
|
388
|
+
expect(output).toContain(`let count = ${NAMESPACE}.signal(0);`);
|
|
389
|
+
expect(output).toContain(`let next = ${NAMESPACE}.read(count) + 1;`);
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
|
|
324
394
|
describe('index transform', () => {
|
|
325
395
|
it('exports patterns array', () => {
|
|
326
396
|
expect(pipeline.patterns).toEqual(['reactive(', 'reactive<']);
|
|
@@ -282,19 +282,21 @@ describe('ReactiveArray', () => {
|
|
|
282
282
|
expect(dispatched).toBe(false);
|
|
283
283
|
});
|
|
284
284
|
|
|
285
|
-
|
|
285
|
+
// An element's value says nothing about whether one was removed: listeners (e.g. a rendered
|
|
286
|
+
// list) must see every removal or they desync from the array
|
|
287
|
+
it('dispatches when popping an explicit undefined value', () => {
|
|
286
288
|
let arr = new ReactiveArray<number | undefined>([1, undefined]),
|
|
287
|
-
|
|
289
|
+
events: { item: number | undefined }[] = [];
|
|
288
290
|
|
|
289
|
-
arr.on('pop', () => {
|
|
291
|
+
arr.on('pop', (e) => { events.push(e); });
|
|
290
292
|
let item = arr.pop();
|
|
291
293
|
|
|
292
294
|
expect(item).toBe(undefined);
|
|
293
295
|
expect(arr.length).toBe(1);
|
|
294
|
-
expect(
|
|
296
|
+
expect(events).toEqual([{ item: undefined }]);
|
|
295
297
|
});
|
|
296
298
|
|
|
297
|
-
it('
|
|
299
|
+
it('updates reactive length when popping an explicit undefined value', async () => {
|
|
298
300
|
let arr = new ReactiveArray<number | undefined>([1, undefined]),
|
|
299
301
|
lengths: number[] = [];
|
|
300
302
|
|
|
@@ -307,7 +309,7 @@ describe('ReactiveArray', () => {
|
|
|
307
309
|
arr.pop();
|
|
308
310
|
await Promise.resolve();
|
|
309
311
|
|
|
310
|
-
expect(lengths).toEqual([2]);
|
|
312
|
+
expect(lengths).toEqual([2, 1]);
|
|
311
313
|
});
|
|
312
314
|
});
|
|
313
315
|
|
|
@@ -337,20 +339,20 @@ describe('ReactiveArray', () => {
|
|
|
337
339
|
expect(events).toEqual([{ item: 10 }]);
|
|
338
340
|
});
|
|
339
341
|
|
|
340
|
-
it('
|
|
342
|
+
it('dispatches when shifting an explicit undefined value', () => {
|
|
341
343
|
let arr = new ReactiveArray<number | undefined>([undefined, 1, 2]),
|
|
342
|
-
|
|
344
|
+
events: { item: number | undefined }[] = [];
|
|
343
345
|
|
|
344
|
-
arr.on('shift', () => {
|
|
346
|
+
arr.on('shift', (e) => { events.push(e); });
|
|
345
347
|
let item = arr.shift();
|
|
346
348
|
|
|
347
349
|
expect(item).toBe(undefined);
|
|
348
350
|
expect(arr.length).toBe(2);
|
|
349
351
|
expect(arr[0]).toBe(1);
|
|
350
|
-
expect(
|
|
352
|
+
expect(events).toEqual([{ item: undefined }]);
|
|
351
353
|
});
|
|
352
354
|
|
|
353
|
-
it('
|
|
355
|
+
it('updates reactive length when shifting an explicit undefined value', async () => {
|
|
354
356
|
let arr = new ReactiveArray<number | undefined>([undefined, 1, 2]),
|
|
355
357
|
lengths: number[] = [];
|
|
356
358
|
|
|
@@ -363,7 +365,17 @@ describe('ReactiveArray', () => {
|
|
|
363
365
|
arr.shift();
|
|
364
366
|
await Promise.resolve();
|
|
365
367
|
|
|
366
|
-
expect(lengths).toEqual([3]);
|
|
368
|
+
expect(lengths).toEqual([3, 2]);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it('does not dispatch when shifting an empty array', () => {
|
|
372
|
+
let arr = new ReactiveArray<number>(),
|
|
373
|
+
dispatched = false;
|
|
374
|
+
|
|
375
|
+
arr.on('shift', () => { dispatched = true; });
|
|
376
|
+
|
|
377
|
+
expect(arr.shift()).toBe(undefined);
|
|
378
|
+
expect(dispatched).toBe(false);
|
|
367
379
|
});
|
|
368
380
|
});
|
|
369
381
|
|
|
@@ -477,7 +489,7 @@ describe('ReactiveArray', () => {
|
|
|
477
489
|
expect(dispatched).toBe(false);
|
|
478
490
|
});
|
|
479
491
|
|
|
480
|
-
it('splice with negative start
|
|
492
|
+
it('splice with negative start dispatches the resolved start', () => {
|
|
481
493
|
let arr = new ReactiveArray([1, 2, 3, 4, 5]),
|
|
482
494
|
events: { start: number; deleteCount: number; items: number[] }[] = [];
|
|
483
495
|
|
|
@@ -487,7 +499,35 @@ describe('ReactiveArray', () => {
|
|
|
487
499
|
|
|
488
500
|
expect([...removed]).toEqual([4]);
|
|
489
501
|
expect([...arr]).toEqual([1, 2, 3, 5]);
|
|
490
|
-
expect(events).toEqual([{ start:
|
|
502
|
+
expect(events).toEqual([{ start: 3, deleteCount: 1, items: [] }]);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it('splice dispatches the count actually removed, not the requested count', () => {
|
|
506
|
+
let arr = new ReactiveArray([1, 2, 3]),
|
|
507
|
+
events: { start: number; deleteCount: number; items: number[] }[] = [];
|
|
508
|
+
|
|
509
|
+
arr.on('splice', (e) => { events.push(e); });
|
|
510
|
+
arr.splice(1, 100);
|
|
511
|
+
|
|
512
|
+
expect([...arr]).toEqual([1]);
|
|
513
|
+
expect(events).toEqual([{ start: 1, deleteCount: 2, items: [] }]);
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
it('splice clamps an out-of-range or fractional start the way Array.prototype.splice does', () => {
|
|
517
|
+
let arr = new ReactiveArray([1, 2, 3]),
|
|
518
|
+
events: { start: number; deleteCount: number; items: number[] }[] = [];
|
|
519
|
+
|
|
520
|
+
arr.on('splice', (e) => { events.push(e); });
|
|
521
|
+
arr.splice(100, 0, 4);
|
|
522
|
+
arr.splice(-100, 0, 0);
|
|
523
|
+
arr.splice(1.9, 0, 9);
|
|
524
|
+
|
|
525
|
+
expect([...arr]).toEqual([0, 9, 1, 2, 3, 4]);
|
|
526
|
+
expect(events).toEqual([
|
|
527
|
+
{ start: 3, deleteCount: 0, items: [4] },
|
|
528
|
+
{ start: 0, deleteCount: 0, items: [0] },
|
|
529
|
+
{ start: 1, deleteCount: 0, items: [9] }
|
|
530
|
+
]);
|
|
491
531
|
});
|
|
492
532
|
});
|
|
493
533
|
|
package/vitest.config.ts
CHANGED
|
@@ -5,8 +5,8 @@ import { resolve } from 'path';
|
|
|
5
5
|
export default defineConfig({
|
|
6
6
|
resolve: {
|
|
7
7
|
alias: {
|
|
8
|
-
'~': resolve(
|
|
9
|
-
'@esportsplus/reactivity': resolve(
|
|
8
|
+
'~': resolve(import.meta.dirname, 'src'),
|
|
9
|
+
'@esportsplus/reactivity': resolve(import.meta.dirname, 'src/index.ts')
|
|
10
10
|
}
|
|
11
11
|
},
|
|
12
12
|
test: {
|