@esportsplus/reactivity 0.40.0 → 0.41.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/README.md +9 -5
- package/bench/compiler.bench.ts +115 -0
- package/bench/kairo.bench.ts +1 -1
- package/bench/reactive/array.bench.ts +1 -0
- package/bench/reactive/reactive-object.bench.ts +3 -2
- package/build/compiler/array.js +28 -25
- package/build/compiler/bindings.d.ts +3 -1
- package/build/compiler/bindings.js +32 -9
- package/build/compiler/index.d.ts +3 -2
- package/build/compiler/index.js +21 -13
- package/build/compiler/object.js +12 -13
- package/build/compiler/plugins/vite.d.ts +13 -4
- package/build/compiler/primitives.js +9 -9
- package/build/reactive/index.d.ts +1 -1
- package/build/reactive/object.d.ts +3 -3
- package/build/reactive/object.js +1 -1
- package/oxlintrc.json +3 -0
- package/package.json +15 -7
- package/pnpm-workspace.yaml +4 -7
- package/src/compiler/array.ts +30 -27
- package/src/compiler/bindings.ts +49 -10
- package/src/compiler/index.ts +37 -20
- package/src/compiler/object.ts +14 -16
- package/src/compiler/primitives.ts +13 -10
- package/src/reactive/array.ts +5 -3
- package/src/reactive/index.ts +3 -3
- package/src/reactive/object.ts +4 -4
- package/src/system.ts +2 -0
- package/test/compiler/__snapshots__/output.test.ts.snap +157 -0
- package/test/compiler/compiler.test.ts +51 -9
- package/test/compiler/composition.test.ts +110 -0
- package/test/compiler/coverage.test.ts +22 -22
- package/test/compiler/origins.test.ts +156 -0
- package/test/compiler/output.test.ts +116 -0
- package/test/ownership.test.ts +1 -1
- package/test/reactive/array.test.ts +8 -7
- package/test/reactive/reactive.test.ts +4 -4
- package/test/system.test.ts +3 -4
- package/test/untrack.test.ts +1 -1
- package/tsconfig.build.json +3 -0
- package/tsconfig.json +2 -2
- package/vitest.config.ts +2 -2
- package/bench/tsconfig.json +0 -16
- package/test/tsconfig.json +0 -16
package/src/system.ts
CHANGED
|
@@ -21,6 +21,7 @@ let asyncFactory = new WeakMap<Computed<unknown>, Computed<unknown>>(),
|
|
|
21
21
|
depth = 0,
|
|
22
22
|
disposeHead: Walk | null = null,
|
|
23
23
|
draining = false,
|
|
24
|
+
// oxlint-disable-next-line unicorn/no-new-array -- preallocated heap; slots are filled by height
|
|
24
25
|
heap: (Computed<unknown> | undefined)[] = new Array(64),
|
|
25
26
|
heap_i = 0,
|
|
26
27
|
heap_n = 0,
|
|
@@ -276,6 +277,7 @@ function link<T>(dep: Signal<T> | Computed<T>, sub: Computed<T>) {
|
|
|
276
277
|
pooled.nextSub = null,
|
|
277
278
|
pooled.version = version,
|
|
278
279
|
pooled)
|
|
280
|
+
// oxlint-disable-next-line sort-keys -- creation order fixes the hidden class every link shares
|
|
279
281
|
: {
|
|
280
282
|
dep,
|
|
281
283
|
sub,
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`compiled output > app.ts 1`] = `null`;
|
|
4
|
+
|
|
5
|
+
exports[`compiled output > consumer.ts 1`] = `
|
|
6
|
+
"import * as reactivity_u0 from '@esportsplus/reactivity';
|
|
7
|
+
import { count as renamed, later, make, total } from './app';
|
|
8
|
+
import * as app from './app';
|
|
9
|
+
export let a = reactivity_u0.signal(0);
|
|
10
|
+
export let b = () => reactivity_u0.read(renamed) + reactivity_u0.read(total) + reactivity_u0.read(later);
|
|
11
|
+
export let c = () => reactivity_u0.read(app.count) + app.list.$length;
|
|
12
|
+
export let d = () => reactivity_u0.read(app['count']);
|
|
13
|
+
export let e = () => { app.list.$set(0, 1); };"
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
exports[`compiled output > forms.ts 1`] = `
|
|
17
|
+
"import * as reactivity_u0 from '@esportsplus/reactivity';
|
|
18
|
+
|
|
19
|
+
class ReactiveObject_1x8s4en0<T0 extends unknown[], T1 extends reactivity_u0.Computed<ReturnType<T1>>['fn'], T2 extends unknown[]> extends reactivity_u0.ReactiveObject<any> {
|
|
20
|
+
#a = this[reactivity_u0.SIGNAL](1);
|
|
21
|
+
#b = this[reactivity_u0.SIGNAL]('x');
|
|
22
|
+
#items;
|
|
23
|
+
#sum;
|
|
24
|
+
#neg = this[reactivity_u0.SIGNAL](-1);
|
|
25
|
+
#source;
|
|
26
|
+
#flag = this[reactivity_u0.SIGNAL](true);
|
|
27
|
+
|
|
28
|
+
constructor(_p0: T0, _p1: T1, _p2: T2) {
|
|
29
|
+
super(null);
|
|
30
|
+
this.#items = this[reactivity_u0.REACTIVE_ARRAY](_p0);
|
|
31
|
+
this.#sum = this[reactivity_u0.COMPUTED](_p1);
|
|
32
|
+
this.#source = this[reactivity_u0.REACTIVE_ARRAY](_p2);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
get a() {
|
|
37
|
+
return reactivity_u0.read(this.#a);
|
|
38
|
+
}
|
|
39
|
+
set a(_v0) {
|
|
40
|
+
reactivity_u0.write(this.#a, _v0);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
get b() {
|
|
45
|
+
return reactivity_u0.read(this.#b);
|
|
46
|
+
}
|
|
47
|
+
set b(_v1) {
|
|
48
|
+
reactivity_u0.write(this.#b, _v1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
get items() {
|
|
53
|
+
return this.#items;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
get sum() {
|
|
58
|
+
return reactivity_u0.read(this.#sum);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
get neg() {
|
|
63
|
+
return reactivity_u0.read(this.#neg);
|
|
64
|
+
}
|
|
65
|
+
set neg(_v2) {
|
|
66
|
+
reactivity_u0.write(this.#neg, _v2);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
get source() {
|
|
71
|
+
return this.#source;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
get flag() {
|
|
76
|
+
return reactivity_u0.read(this.#flag);
|
|
77
|
+
}
|
|
78
|
+
set flag(_v3) {
|
|
79
|
+
reactivity_u0.write(this.#flag, _v3);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class ReactiveObject_1x8s4en1<T extends { 'a'?: unknown, 'items'?: unknown[] }> extends reactivity_u0.ReactiveObject<any> {
|
|
86
|
+
#a;
|
|
87
|
+
#items;
|
|
88
|
+
|
|
89
|
+
constructor(_p0: T['a'], _p1: T['items']) {
|
|
90
|
+
super(null);
|
|
91
|
+
this.#a = this[reactivity_u0.SIGNAL](_p0);
|
|
92
|
+
this.#items = this[reactivity_u0.REACTIVE_ARRAY](_p1);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
get a() {
|
|
97
|
+
return reactivity_u0.read(this.#a) as T['a'];
|
|
98
|
+
}
|
|
99
|
+
set a(_v0) {
|
|
100
|
+
reactivity_u0.write(this.#a, _v0);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
get items() {
|
|
105
|
+
return this.#items;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
declare function total(): number;
|
|
112
|
+
declare let list: number[];
|
|
113
|
+
declare let n: number;
|
|
114
|
+
let count = reactivity_u0.signal(0);
|
|
115
|
+
let doubled = reactivity_u0.computed(() => reactivity_u0.read(count) * 2);
|
|
116
|
+
let typed = reactivity_u0.signal(n);
|
|
117
|
+
let fn = reactivity_u0.computed(total);
|
|
118
|
+
let runtime = reactivity_u0.reactive(list);
|
|
119
|
+
let obj = new ReactiveObject_1x8s4en0([1, 2], () => 3, list);
|
|
120
|
+
let shorthand = reactivity_u0.reactive({ list });
|
|
121
|
+
let hinted = new ReactiveObject_1x8s4en1<{ a: number; items: number[] }>(1, []);
|
|
122
|
+
let spread = reactivity_u0.reactive({ ...{ a: 1 }, b: 2 });
|
|
123
|
+
let method = reactivity_u0.reactive({ m() { return 1; } });
|
|
124
|
+
let arr = new reactivity_u0.ReactiveArray([1, 2, 3]);
|
|
125
|
+
let empty = new reactivity_u0.ReactiveArray<string>();
|
|
126
|
+
let declared: number[] = new reactivity_u0.ReactiveArray<number>([4]);
|
|
127
|
+
reactivity_u0.write(count, 5);
|
|
128
|
+
reactivity_u0.write(count, count.value + 2);
|
|
129
|
+
reactivity_u0.write(count, count.value || 3);
|
|
130
|
+
reactivity_u0.write(count, count.value + 1);
|
|
131
|
+
reactivity_u0.write(count, count.value - 1);
|
|
132
|
+
let p = ((_t0) => (reactivity_u0.write(count, _t0 + 1), _t0))(count.value);
|
|
133
|
+
let q = (reactivity_u0.write(count, count.value + 1), count.value);
|
|
134
|
+
let o = { count: reactivity_u0.read(count) };
|
|
135
|
+
let t = typeof reactivity_u0.read(count);
|
|
136
|
+
arr.$set(0, 4);
|
|
137
|
+
((_a, _k) => _a.$set(_k, _a[_k] + (2)))(arr, 1);
|
|
138
|
+
((_a, _k, _v) => (_a.$set(_k, _v + 1), _v))(arr, 2, arr[2]);
|
|
139
|
+
let r = ((_a, _k) => (_a.$set(_k, _a[_k] + 1), _a[_k]))(arr, 0);
|
|
140
|
+
let size = arr.$length;
|
|
141
|
+
arr.$length = 1;
|
|
142
|
+
arr.$length = arr.length + 1;
|
|
143
|
+
arr.$length = arr.length + 1;
|
|
144
|
+
arr.splice(0, arr.length, ...[5, 6]);
|
|
145
|
+
arr.splice(0, arr.length);
|
|
146
|
+
export { count, doubled, typed, fn, runtime, obj, shorthand, hinted, spread, method, empty, declared, p, q, o, t, r, size };
|
|
147
|
+
export default reactivity_u0.signal(0);"
|
|
148
|
+
`;
|
|
149
|
+
|
|
150
|
+
exports[`compiled output > state.ts 1`] = `
|
|
151
|
+
"import * as reactivity_u0 from '@esportsplus/reactivity';
|
|
152
|
+
export let count = reactivity_u0.signal(0);
|
|
153
|
+
export let list = new reactivity_u0.ReactiveArray([1, 2]);
|
|
154
|
+
export let later: number;
|
|
155
|
+
later = reactivity_u0.signal(1);
|
|
156
|
+
export default reactivity_u0.computed(() => 2);"
|
|
157
|
+
`;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import { ts } from '@esportsplus/typescript';
|
|
3
|
-
import { languageService } from '@esportsplus/typescript/compiler';
|
|
3
|
+
import { coordinator, languageService } from '@esportsplus/typescript/compiler';
|
|
4
4
|
import type { ReplacementIntent } from '@esportsplus/typescript/compiler';
|
|
5
|
-
import { NAMESPACE, TYPES } from '~/compiler/constants';
|
|
5
|
+
import { NAMESPACE, PACKAGE_NAME, TYPES } from '~/compiler/constants';
|
|
6
6
|
import type { Bindings } from '~/compiler/types';
|
|
7
7
|
import array from '~/compiler/array';
|
|
8
8
|
import scope from '~/compiler/bindings';
|
|
@@ -21,7 +21,7 @@ function applyIntents(code: string, sourceFile: ts.SourceFile, intents: Replacem
|
|
|
21
21
|
end = intent.node.getEnd(),
|
|
22
22
|
start = intent.node.getStart(sourceFile);
|
|
23
23
|
|
|
24
|
-
code = code.slice(0, start) + intent.generate(sourceFile) + code.slice(end);
|
|
24
|
+
code = code.slice(0, start) + intent.generate(sourceFile, node => node.getText(sourceFile)) + code.slice(end);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
return code;
|
|
@@ -40,16 +40,15 @@ function parse(code: string): { bindings: Bindings; code: string; isReactiveCall
|
|
|
40
40
|
return { bindings, code, isReactiveCall: (node): node is ts.CallExpression => scope.isReactiveCall(bindings, node), sourceFile };
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
// Full pipeline over a module importing reactive(): what a real consumer compiles
|
|
43
|
+
// Full pipeline over a module importing reactive(), composed by the coordinator: what a real consumer compiles
|
|
44
44
|
function transformModule(code: string): string {
|
|
45
45
|
return transformSource(`import { reactive } from '@esportsplus/reactivity';\n` + code);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
function transformSource(code: string): string {
|
|
49
|
-
let { checker, program, sourceFile } = languageService.scratch(process.cwd() + '/module.ts', code)
|
|
50
|
-
result = pipeline.transform({ checker, code, program, sourceFile } as never);
|
|
49
|
+
let { checker, program, sourceFile } = languageService.scratch(process.cwd() + '/module.ts', code);
|
|
51
50
|
|
|
52
|
-
return
|
|
51
|
+
return coordinator.transform([pipeline], code, sourceFile, { checker, program }, process.cwd(), new Map()).code;
|
|
53
52
|
}
|
|
54
53
|
|
|
55
54
|
function transformPrimitives(source: string): { bindings: Bindings; failures: string[]; output: string } {
|
|
@@ -509,15 +508,58 @@ describe('binding resolution (full pipeline)', () => {
|
|
|
509
508
|
|
|
510
509
|
|
|
511
510
|
describe('index transform', () => {
|
|
512
|
-
// A module can reach reactive() or read a binding under any name, so no
|
|
511
|
+
// A module can reach reactive() or read a binding under any name, so no name prefilter may skip it
|
|
513
512
|
it('has no text patterns', () => {
|
|
514
513
|
expect('patterns' in pipeline).toBe(false);
|
|
515
514
|
});
|
|
516
515
|
|
|
516
|
+
it('names itself in coordinator errors', () => {
|
|
517
|
+
expect(pipeline.name).toBe(PACKAGE_NAME);
|
|
518
|
+
});
|
|
519
|
+
|
|
517
520
|
it('has transform function', () => {
|
|
518
521
|
expect(typeof pipeline.transform).toBe('function');
|
|
519
522
|
});
|
|
520
|
-
|
|
523
|
+
|
|
524
|
+
it('skips a module without an import before resolving anything', () => {
|
|
525
|
+
let code = 'let reactive = (n: number) => n;\nexport let x = reactive(1);',
|
|
526
|
+
{ program, sourceFile } = languageService.scratch(process.cwd() + '/plain.ts', code),
|
|
527
|
+
checker = new Proxy({}, {
|
|
528
|
+
get: () => {
|
|
529
|
+
throw new Error('checker queried');
|
|
530
|
+
}
|
|
531
|
+
}) as ts.Checker;
|
|
532
|
+
|
|
533
|
+
expect(pipeline.transform({ checker, code, program, shared: new Map(), sourceFile })).toEqual({});
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
it('classifies every reactive() argument from one batched type request', () => {
|
|
537
|
+
let code = "import { reactive } from '@esportsplus/reactivity';\ndeclare let n: number;\ndeclare function total(): number;\nlet a = reactive(n);\nlet b = reactive(total);\nlet c = a + b + a + b;",
|
|
538
|
+
{ checker, program, sourceFile } = languageService.scratch(process.cwd() + '/batched.ts', code),
|
|
539
|
+
single = 0,
|
|
540
|
+
counted = new Proxy(checker, {
|
|
541
|
+
get(target, key) {
|
|
542
|
+
let value = Reflect.get(target, key);
|
|
543
|
+
|
|
544
|
+
if (key !== 'getTypeAtLocation' || typeof value !== 'function') {
|
|
545
|
+
return value;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return (node: unknown) => {
|
|
549
|
+
if (!Array.isArray(node)) {
|
|
550
|
+
single++;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
return value.call(target, node);
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
}),
|
|
557
|
+
output = coordinator.transform([pipeline], code, sourceFile, { checker: counted, program }, process.cwd(), new Map()).code;
|
|
558
|
+
|
|
559
|
+
expect(output).toContain(`let a = ${NAMESPACE}.signal(n);`);
|
|
560
|
+
expect(output).toContain(`let b = ${NAMESPACE}.computed(total);`);
|
|
561
|
+
expect(single).toBe(0);
|
|
562
|
+
});});
|
|
521
563
|
|
|
522
564
|
|
|
523
565
|
describe('plugins', () => {
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { ts } from '@esportsplus/typescript';
|
|
3
|
+
import { coordinator, languageService } from '@esportsplus/typescript/compiler';
|
|
4
|
+
import type { Plugin, ReplacementIntent } from '@esportsplus/typescript/compiler';
|
|
5
|
+
import { NAMESPACE } from '~/compiler/constants';
|
|
6
|
+
|
|
7
|
+
import pipeline from '~/compiler/index';
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
// Stands in for template: rewrites every `tag` tagged template, which reactivity's own rewrites may contain
|
|
11
|
+
const FOREIGN: Plugin = {
|
|
12
|
+
name: 'foreign',
|
|
13
|
+
transform: ({ sourceFile }) => {
|
|
14
|
+
let replacements: ReplacementIntent[] = [],
|
|
15
|
+
visit = (node: ts.Node): void => {
|
|
16
|
+
if (ts.isTaggedTemplateExpression(node) && ts.isIdentifier(node.tag) && node.tag.text === 'tag') {
|
|
17
|
+
replacements.push({ generate: () => 'TAGGED', node });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
node.forEachChild(visit);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
visit(sourceFile);
|
|
24
|
+
|
|
25
|
+
return { replacements };
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const PRELUDE = "import { reactive } from '@esportsplus/reactivity';\n";
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
function compile(code: string, plugins: Plugin[] = [pipeline]): string {
|
|
33
|
+
code = PRELUDE + code;
|
|
34
|
+
|
|
35
|
+
let { checker, program, sourceFile } = languageService.scratch(process.cwd() + '/composed.ts', code);
|
|
36
|
+
|
|
37
|
+
return coordinator.transform(plugins, code, sourceFile, { checker, program }, process.cwd(), new Map()).code;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
describe('nested rewrites', () => {
|
|
42
|
+
let output = compile([
|
|
43
|
+
'let x = reactive(0);',
|
|
44
|
+
'let y = reactive(1);',
|
|
45
|
+
'x = y;',
|
|
46
|
+
'x += y;',
|
|
47
|
+
'let arr = reactive([y]);',
|
|
48
|
+
'arr[0] = y;',
|
|
49
|
+
'arr[1] += y;',
|
|
50
|
+
'arr[y] = 2;',
|
|
51
|
+
'let at = arr[y];',
|
|
52
|
+
'arr.length = y;',
|
|
53
|
+
'arr = [y];',
|
|
54
|
+
'let o = reactive({ a: y, items: [y] });',
|
|
55
|
+
'let s = reactive({ ...{ b: 1 }, c: y });',
|
|
56
|
+
'let e = { y: { y } };'
|
|
57
|
+
].join('\n'));
|
|
58
|
+
|
|
59
|
+
it('reads a signal on the right of a write to another', () => {
|
|
60
|
+
expect(output).toContain(`${NAMESPACE}.write(x, ${NAMESPACE}.read(y));`);
|
|
61
|
+
expect(output).toContain(`${NAMESPACE}.write(x, x.value + ${NAMESPACE}.read(y));`);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('reads a signal inside array lowerings', () => {
|
|
65
|
+
expect(output).toContain(`let arr = new ${NAMESPACE}.ReactiveArray([${NAMESPACE}.read(y)]);`);
|
|
66
|
+
expect(output).toContain(`arr.$set(0, ${NAMESPACE}.read(y));`);
|
|
67
|
+
expect(output).toContain(`((_a, _k) => _a.$set(_k, _a[_k] + (${NAMESPACE}.read(y))))(arr, 1);`);
|
|
68
|
+
expect(output).toContain(`arr.$set(${NAMESPACE}.read(y), 2);`);
|
|
69
|
+
expect(output).toContain(`let at = arr[${NAMESPACE}.read(y)];`);
|
|
70
|
+
expect(output).toContain(`arr.$length = ${NAMESPACE}.read(y);`);
|
|
71
|
+
expect(output).toContain(`arr.splice(0, arr.length, ...[${NAMESPACE}.read(y)]);`);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('reads a signal inside object lowerings', () => {
|
|
75
|
+
expect(output).toMatch(new RegExp(`let o = +new ReactiveObject_\\w+\\(${NAMESPACE}\\.read\\(y\\), \\[${NAMESPACE}\\.read\\(y\\)\\]\\);`));
|
|
76
|
+
expect(output).toContain(`let s = ${NAMESPACE}.reactive({ ...{ b: 1 }, c: ${NAMESPACE}.read(y) });`);
|
|
77
|
+
expect(output).toContain(`let e = { y: { y: ${NAMESPACE}.read(y) } };`);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
describe('composition with another plugin', () => {
|
|
83
|
+
let code = [
|
|
84
|
+
'declare function tag(strings: TemplateStringsArray): number;',
|
|
85
|
+
'let count = reactive(tag`a`);',
|
|
86
|
+
'count = tag`b`;',
|
|
87
|
+
'count += tag`c`;',
|
|
88
|
+
'let o = reactive({ n: tag`d` });',
|
|
89
|
+
'let s = reactive({ ...{}, n: tag`e` });',
|
|
90
|
+
'let arr = reactive([tag`f`]);',
|
|
91
|
+
'arr[0] = tag`g`;',
|
|
92
|
+
'arr = [tag`h`];'
|
|
93
|
+
].join('\n');
|
|
94
|
+
|
|
95
|
+
for (let [order, plugins] of [['before', [FOREIGN, pipeline]], ['after', [pipeline, FOREIGN]]] as const) {
|
|
96
|
+
it(`keeps the other plugin's replacements inside every lowering when it runs ${order}`, () => {
|
|
97
|
+
let output = compile(code, [...plugins]);
|
|
98
|
+
|
|
99
|
+
expect(output).not.toContain('tag`');
|
|
100
|
+
expect(output).toContain(`let count = ${NAMESPACE}.signal(TAGGED);`);
|
|
101
|
+
expect(output).toContain(`${NAMESPACE}.write(count, TAGGED);`);
|
|
102
|
+
expect(output).toContain(`${NAMESPACE}.write(count, count.value + TAGGED);`);
|
|
103
|
+
expect(output).toMatch(/let o = +new ReactiveObject_\w+\(TAGGED\);/);
|
|
104
|
+
expect(output).toContain(`let s = ${NAMESPACE}.reactive({ ...{}, n: TAGGED });`);
|
|
105
|
+
expect(output).toContain(`let arr = new ${NAMESPACE}.ReactiveArray([TAGGED]);`);
|
|
106
|
+
expect(output).toContain('arr.$set(0, TAGGED);');
|
|
107
|
+
expect(output).toContain('arr.splice(0, arr.length, ...[TAGGED]);');
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
});
|
|
@@ -7,18 +7,24 @@ import vite from '~/compiler/plugins/vite';
|
|
|
7
7
|
|
|
8
8
|
// Every route by which a module reaches `reactive`, or a binding it created, must compile
|
|
9
9
|
const FILES: Record<string, string> = {
|
|
10
|
-
'state.ts': [
|
|
11
|
-
"import { reactive } from '@esportsplus/reactivity';",
|
|
12
|
-
'export let count = reactive(0);',
|
|
13
|
-
'export let list = reactive([1, 2]);',
|
|
14
|
-
'export let store = reactive({ n: 1, items: [1] });',
|
|
15
|
-
'export default reactive(0);'
|
|
16
|
-
].join('\n'),
|
|
17
10
|
'barrel.ts': [
|
|
18
11
|
"export { reactive as make } from '@esportsplus/reactivity';",
|
|
19
12
|
"export * as R from '@esportsplus/reactivity';",
|
|
20
13
|
"export * from './state';"
|
|
21
14
|
].join('\n'),
|
|
15
|
+
'entries.ts': [
|
|
16
|
+
"import { make, R } from './barrel';",
|
|
17
|
+
"import * as B from './barrel';",
|
|
18
|
+
'export let a = make(0);',
|
|
19
|
+
'export let b = R.reactive(0);',
|
|
20
|
+
'export let c = B.R.reactive(0);',
|
|
21
|
+
"export let d = R['reactive'](0);",
|
|
22
|
+
'const alias = make;',
|
|
23
|
+
'export let e = alias(0);',
|
|
24
|
+
'const { reactive: destructured } = R;',
|
|
25
|
+
'export let f = destructured(0);',
|
|
26
|
+
'export let g = () => a + b + c + d + e + f;'
|
|
27
|
+
].join('\n'),
|
|
22
28
|
'reads.ts': [
|
|
23
29
|
"import { count, list, store } from './state';",
|
|
24
30
|
"import * as S from './state';",
|
|
@@ -33,18 +39,12 @@ const FILES: Record<string, string> = {
|
|
|
33
39
|
'export let g = () => store.items.length;',
|
|
34
40
|
"export let h = () => S['count'] + 1;"
|
|
35
41
|
].join('\n'),
|
|
36
|
-
'
|
|
37
|
-
"import {
|
|
38
|
-
|
|
39
|
-
'export let
|
|
40
|
-
'export let
|
|
41
|
-
'export
|
|
42
|
-
"export let d = R['reactive'](0);",
|
|
43
|
-
'const alias = make;',
|
|
44
|
-
'export let e = alias(0);',
|
|
45
|
-
'const { reactive: destructured } = R;',
|
|
46
|
-
'export let f = destructured(0);',
|
|
47
|
-
'export let g = () => a + b + c + d + e + f;'
|
|
42
|
+
'state.ts': [
|
|
43
|
+
"import { reactive } from '@esportsplus/reactivity';",
|
|
44
|
+
'export let count = reactive(0);',
|
|
45
|
+
'export let list = reactive([1, 2]);',
|
|
46
|
+
'export let store = reactive({ n: 1, items: [1] });',
|
|
47
|
+
'export default reactive(0);'
|
|
48
48
|
].join('\n')
|
|
49
49
|
};
|
|
50
50
|
|
|
@@ -73,7 +73,7 @@ function project(files: Record<string, string>) {
|
|
|
73
73
|
return {
|
|
74
74
|
directory,
|
|
75
75
|
plugin,
|
|
76
|
-
transform: (name: string) => plugin.transform(files[name], directory + '/' + name)
|
|
76
|
+
transform: (name: string) => plugin.transform.handler(files[name], directory + '/' + name)
|
|
77
77
|
};
|
|
78
78
|
}
|
|
79
79
|
|
|
@@ -133,8 +133,8 @@ describe('compiler coverage', () => {
|
|
|
133
133
|
|
|
134
134
|
it('fails the build when a signal is destructured out of a module namespace', () => {
|
|
135
135
|
let files = {
|
|
136
|
-
'
|
|
137
|
-
'
|
|
136
|
+
'copy.ts': "import * as S from './state';\nconst { count } = S;\nexport let n = count;",
|
|
137
|
+
'state.ts': "import { reactive } from '@esportsplus/reactivity';\nexport let count = reactive(0);"
|
|
138
138
|
},
|
|
139
139
|
{ transform } = project(files);
|
|
140
140
|
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { NAMESPACE } from '~/compiler/constants';
|
|
5
|
+
|
|
6
|
+
import vite from '~/compiler/plugins/vite';
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
// `reactive`, and the bindings it creates, reached under any name: resolution may prune what it
|
|
10
|
+
// sends to the checker, never by name
|
|
11
|
+
const FILES: Record<string, string> = {
|
|
12
|
+
'app.ts': [
|
|
13
|
+
"export { reactive as signalOf } from '@esportsplus/reactivity';",
|
|
14
|
+
"export { default as fallback } from './state';",
|
|
15
|
+
"export * from './mid';"
|
|
16
|
+
].join('\n'),
|
|
17
|
+
'consumer.ts': [
|
|
18
|
+
"import { deepCount, fallback, held, later, signalOf } from './app';",
|
|
19
|
+
"import * as app from './app';",
|
|
20
|
+
'const { signalOf: unpacked } = app;',
|
|
21
|
+
'export let a = signalOf(0);',
|
|
22
|
+
'export let b = app.signalOf(0);',
|
|
23
|
+
"export let c = app['signalOf'](0);",
|
|
24
|
+
'export let d = unpacked(0);',
|
|
25
|
+
'export let e = held(0);',
|
|
26
|
+
'export let f = () => fallback + deepCount + later;',
|
|
27
|
+
'export let g = () => app.later + app.deepCount;'
|
|
28
|
+
].join('\n'),
|
|
29
|
+
'deep.ts': [
|
|
30
|
+
"export * from '@esportsplus/reactivity';",
|
|
31
|
+
"export { count as deepCount } from './state';"
|
|
32
|
+
].join('\n'),
|
|
33
|
+
'held.ts': [
|
|
34
|
+
"import { reactive } from '@esportsplus/reactivity';",
|
|
35
|
+
'export const held = reactive;'
|
|
36
|
+
].join('\n'),
|
|
37
|
+
'local.ts': [
|
|
38
|
+
"import { reactive as x } from '@esportsplus/reactivity';",
|
|
39
|
+
'export function before() { return late; }',
|
|
40
|
+
'let late: number;',
|
|
41
|
+
'late = x(0);',
|
|
42
|
+
'export let after = late + 1;',
|
|
43
|
+
'export let count = x(1);',
|
|
44
|
+
'export let next = count + 1;',
|
|
45
|
+
'export function shadow(x: (n: number) => number) { return x(1); }'
|
|
46
|
+
].join('\n'),
|
|
47
|
+
'mid.ts': [
|
|
48
|
+
"export * from './deep';",
|
|
49
|
+
"export { held } from './held';",
|
|
50
|
+
"export { later } from './state';"
|
|
51
|
+
].join('\n'),
|
|
52
|
+
'star.ts': [
|
|
53
|
+
"import { reactive } from './mid';",
|
|
54
|
+
'export let s = reactive(0);',
|
|
55
|
+
'export let t = s + 1;'
|
|
56
|
+
].join('\n'),
|
|
57
|
+
'state.ts': [
|
|
58
|
+
"import { reactive } from '@esportsplus/reactivity';",
|
|
59
|
+
'export let count = reactive(0);',
|
|
60
|
+
'export let later: number;',
|
|
61
|
+
'later = reactive(1);',
|
|
62
|
+
'export default reactive(2);'
|
|
63
|
+
].join('\n')
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
let fixtures: string[] = [];
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
function compile(name: string, files: Record<string, string> = FILES): string {
|
|
71
|
+
let directory = mkdtempSync(join(process.cwd(), '.fixture-origins-')).replace(/\\/g, '/');
|
|
72
|
+
|
|
73
|
+
fixtures.push(directory);
|
|
74
|
+
|
|
75
|
+
for (let [file, code] of Object.entries(files)) {
|
|
76
|
+
writeFileSync(join(directory, file), code);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
writeFileSync(join(directory, 'tsconfig.json'), JSON.stringify({
|
|
80
|
+
compilerOptions: { module: 'esnext', moduleResolution: 'bundler', strict: true, target: 'esnext', types: [] },
|
|
81
|
+
files: Object.keys(files).map(file => './' + file)
|
|
82
|
+
}));
|
|
83
|
+
|
|
84
|
+
let plugin = vite({ root: directory });
|
|
85
|
+
|
|
86
|
+
plugin.configResolved({ command: 'build', root: directory });
|
|
87
|
+
|
|
88
|
+
return plugin.transform.handler(files[name], directory + '/' + name)?.code ?? files[name];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
afterEach(() => {
|
|
93
|
+
for (let i = 0, n = fixtures.length; i < n; i++) {
|
|
94
|
+
rmSync(fixtures[i], { force: true, recursive: true });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
fixtures = [];
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
describe('origins reached under any name', () => {
|
|
102
|
+
it('compiles reactive() imported under a local alias, and leaves a parameter shadowing it', () => {
|
|
103
|
+
let output = compile('local.ts');
|
|
104
|
+
|
|
105
|
+
expect(output).toContain(`export let count = ${NAMESPACE}.signal(1);`);
|
|
106
|
+
expect(output).toContain(`export let next = ${NAMESPACE}.read(count) + 1;`);
|
|
107
|
+
expect(output).toContain('export function shadow(x: (n: number) => number) { return x(1); }');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('reads a let assigned with reactive() later, before and after the assignment', () => {
|
|
111
|
+
let output = compile('local.ts');
|
|
112
|
+
|
|
113
|
+
expect(output).toContain(`export function before() { return ${NAMESPACE}.read(late); }`);
|
|
114
|
+
expect(output).toContain(`late = ${NAMESPACE}.signal(0);`);
|
|
115
|
+
expect(output).toContain(`export let after = ${NAMESPACE}.read(late) + 1;`);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('reads a let assigned with reactive() in another module', () => {
|
|
119
|
+
let output = compile('consumer.ts');
|
|
120
|
+
|
|
121
|
+
expect(output).toContain(`${NAMESPACE}.read(later)`);
|
|
122
|
+
expect(output).toContain(`${NAMESPACE}.read(app.later)`);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('compiles reactive() re-exported renamed through a central module, by name and through its namespace', () => {
|
|
126
|
+
let output = compile('consumer.ts');
|
|
127
|
+
|
|
128
|
+
for (let name of ['a', 'b', 'c', 'd']) {
|
|
129
|
+
expect(output).toContain(`export let ${name} = ${NAMESPACE}.signal(0);`);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('compiles reactive() held by an exported const', () => {
|
|
134
|
+
expect(compile('consumer.ts')).toContain(`export let e = ${NAMESPACE}.signal(0);`);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('reads a default export re-exported renamed, and bindings through export * two deep', () => {
|
|
138
|
+
let output = compile('consumer.ts');
|
|
139
|
+
|
|
140
|
+
expect(output).toContain(`export let f = () => ${NAMESPACE}.read(fallback) + ${NAMESPACE}.read(deepCount) + ${NAMESPACE}.read(later);`);
|
|
141
|
+
expect(output).toContain(`export let g = () => ${NAMESPACE}.read(app.later) + ${NAMESPACE}.read(app.deepCount);`);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('compiles reactive() reached through export * two deep', () => {
|
|
145
|
+
let output = compile('star.ts');
|
|
146
|
+
|
|
147
|
+
expect(output).toContain(`export let s = ${NAMESPACE}.signal(0);`);
|
|
148
|
+
expect(output).toContain(`export let t = ${NAMESPACE}.read(s) + 1;`);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('leaves a module that imports nothing untouched', () => {
|
|
152
|
+
let code = 'let reactive = (n: number) => n;\nexport let x = reactive(1);';
|
|
153
|
+
|
|
154
|
+
expect(compile('plain.ts', { ...FILES, 'plain.ts': code })).toBe(code);
|
|
155
|
+
});
|
|
156
|
+
});
|