@esportsplus/reactivity 0.39.2 → 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 +12 -6
- 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/constants.d.ts +2 -1
- package/build/constants.js +2 -1
- package/build/index.d.ts +1 -0
- package/build/reactive/array.d.ts +40 -1
- package/build/reactive/array.js +82 -16
- package/build/reactive/index.d.ts +1 -1
- package/build/reactive/object.d.ts +3 -3
- package/build/reactive/object.js +1 -1
- package/build/system.d.ts +3 -2
- package/build/system.js +10 -5
- 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/constants.ts +4 -1
- package/src/index.ts +1 -0
- package/src/reactive/array.ts +133 -22
- package/src/reactive/index.ts +3 -3
- package/src/reactive/object.ts +4 -4
- package/src/system.ts +20 -5
- 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 +100 -1
- package/test/reactive/array.test.ts +189 -8
- 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/README.md
CHANGED
|
@@ -222,7 +222,8 @@ let user = new ReactiveObject_1();
|
|
|
222
222
|
| `reactive({...})` | Creates a reactive object with signals and computeds |
|
|
223
223
|
| `reactive([...])` | Creates a reactive array |
|
|
224
224
|
| `effect(fn, apply?)` | Runs a function that re-executes when dependencies change. Returns a dispose function. Optional `apply(value, prev)` runs untracked after each run with `fn`'s return value |
|
|
225
|
-
| `root(fn)` | Creates an untracked scope. If `fn` accepts an argument, a dispose function is provided |
|
|
225
|
+
| `root(fn, owner?)` | Creates an untracked scope. If `fn` accepts an argument, a dispose function is provided. With `owner`, the scope belongs to that node instead of whatever is running: `root(() => …, owner)` runs `fn` owned by `owner` (to re-enter it after an `await`), `root((dispose) => …, owner)` creates a child root of `owner`. Reads stay untracked. Throws on a disposed owner |
|
|
226
|
+
| `owner()` | The node that owns what is created now: the running effect/computed, else the running root's scope, else `null`. Its `.owner` chain reaches every enclosing owner |
|
|
226
227
|
| `onCleanup(fn)` | Registers a cleanup function for the current effect/computed |
|
|
227
228
|
|
|
228
229
|
### Low-Level Functions
|
|
@@ -301,6 +302,7 @@ All standard array methods (`push`, `pop`, `shift`, `unshift`, `splice`, `sort`,
|
|
|
301
302
|
|
|
302
303
|
| Event | Payload | Description |
|
|
303
304
|
|-------|---------|-------------|
|
|
305
|
+
| `batch` | `ArrayOp<T>[]` | Every mutation of a task, in order, each as `{ op, ...payload }` of its event; delivered once inside the reactivity pass that follows (`flush()` settles it), outside any owner. A `clear` drops the mutations before it |
|
|
304
306
|
| `clear` | `undefined` | Array was cleared |
|
|
305
307
|
| `concat` | `{ items: T[] }` | Items were concatenated |
|
|
306
308
|
| `pop` | `{ item: T }` | Item was popped |
|
|
@@ -330,13 +332,17 @@ All standard array methods (`push`, `pop`, `shift`, `unshift`, `splice`, `sort`,
|
|
|
330
332
|
|
|
331
333
|
```bash
|
|
332
334
|
pnpm install
|
|
333
|
-
pnpm build
|
|
334
|
-
pnpm
|
|
335
|
-
pnpm
|
|
335
|
+
pnpm build # tsc -p tsconfig.build.json
|
|
336
|
+
pnpm typecheck # tsc --noEmit over src, test and bench
|
|
337
|
+
pnpm lint # oxlint, type-aware, warnings denied
|
|
338
|
+
pnpm test # vitest run
|
|
339
|
+
pnpm bench # vitest bench --run (add --reporter=verbose to print results when piped)
|
|
340
|
+
pnpm verify # build, typecheck, lint and test
|
|
336
341
|
```
|
|
337
342
|
|
|
338
|
-
`agent:test` (`
|
|
339
|
-
CI-facing aliases of the same
|
|
343
|
+
`agent:test` (`pnpm typecheck && vitest run`) and `agent:bench` (`vitest bench --run`) are the
|
|
344
|
+
CI-facing aliases of the same test/bench commands. `test/compiler/output.test.ts` pins whole
|
|
345
|
+
compiled modules; a compiler change that alters them must update the snapshot deliberately.
|
|
340
346
|
|
|
341
347
|
## Changes in 0.35.0
|
|
342
348
|
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { afterAll, test } from 'vitest';
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { coordinator, languageService } from '@esportsplus/typescript/compiler';
|
|
6
|
+
|
|
7
|
+
import pipeline from '~/compiler/index';
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
// A small app: a state module, a central barrel, consumers reading through it, and modules the
|
|
11
|
+
// compiler must leave alone (no import at all, or imports that never reach reactive)
|
|
12
|
+
const FILES: Record<string, string> = {
|
|
13
|
+
'app.ts': [
|
|
14
|
+
"export { reactive as make } from '@esportsplus/reactivity';",
|
|
15
|
+
"export * from './state';"
|
|
16
|
+
].join('\n'),
|
|
17
|
+
'consumer.ts': [
|
|
18
|
+
"import { count, list, make, store } from './app';",
|
|
19
|
+
"import * as app from './app';",
|
|
20
|
+
'export let local = make(0);',
|
|
21
|
+
'export let a = () => count + app.count + local;',
|
|
22
|
+
'export let b = () => list.length + store.items.length;',
|
|
23
|
+
'export let c = () => { list[0] = count; local += count; };'
|
|
24
|
+
].join('\n'),
|
|
25
|
+
'helpers.ts': [
|
|
26
|
+
"import { join } from 'node:path';",
|
|
27
|
+
'export let path = (a: string, b: string) => join(a, b);',
|
|
28
|
+
'export let sum = (values: number[]) => values.reduce((total, value) => total + value, 0);'
|
|
29
|
+
].join('\n'),
|
|
30
|
+
'plain.ts': [
|
|
31
|
+
'export let clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));',
|
|
32
|
+
'export let range = (n: number) => Array.from({ length: n }, (_, i) => i);'
|
|
33
|
+
].join('\n'),
|
|
34
|
+
'runtime.ts': [
|
|
35
|
+
"import { reactive } from '@esportsplus/reactivity';",
|
|
36
|
+
'let count = reactive(0);',
|
|
37
|
+
'let doubled = reactive(() => count * 2);',
|
|
38
|
+
'let list = reactive([1, 2, 3]);',
|
|
39
|
+
'let store = reactive({ items: [1], n: 1 });',
|
|
40
|
+
'export let run = (): number => {',
|
|
41
|
+
' let total = 0;',
|
|
42
|
+
' for (let i = 0; i < 100; i++) {',
|
|
43
|
+
' count++;',
|
|
44
|
+
' list[0] = i;',
|
|
45
|
+
' store.n = i;',
|
|
46
|
+
' total += doubled + list.length + store.n;',
|
|
47
|
+
' }',
|
|
48
|
+
' return total;',
|
|
49
|
+
'};'
|
|
50
|
+
].join('\n'),
|
|
51
|
+
'state.ts': [
|
|
52
|
+
"import { reactive } from '@esportsplus/reactivity';",
|
|
53
|
+
'export let count = reactive(0);',
|
|
54
|
+
'export let list = reactive([1, 2]);',
|
|
55
|
+
'export let store = reactive({ items: [1], n: 1 });'
|
|
56
|
+
].join('\n')
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const NAMES = Object.keys(FILES);
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
let directory = mkdtempSync(join(process.cwd(), '.fixture-bench-')).replace(/\\/g, '/'),
|
|
63
|
+
config = directory + '/tsconfig.json';
|
|
64
|
+
|
|
65
|
+
for (let i = 0, n = NAMES.length; i < n; i++) {
|
|
66
|
+
writeFileSync(join(directory, NAMES[i]), FILES[NAMES[i]]);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
writeFileSync(config, JSON.stringify({
|
|
70
|
+
compilerOptions: { module: 'esnext', moduleResolution: 'bundler', strict: true, target: 'esnext', types: [] },
|
|
71
|
+
files: NAMES.map(name => './' + name)
|
|
72
|
+
}));
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
// Each pass hands the coordinator a checker it has not seen, so its memo starts empty and every
|
|
76
|
+
// query a transform needs reaches tsgo, as when a build transforms each module once
|
|
77
|
+
function transform(name: string): string {
|
|
78
|
+
let id = directory + '/' + name,
|
|
79
|
+
{ checker, program } = languageService.update(config, id, FILES[name]);
|
|
80
|
+
|
|
81
|
+
return coordinator.transform([pipeline], FILES[name], program.getSourceFile(id)!, { checker: new Proxy(checker, {}), program }, directory, new Map()).code;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
afterAll(() => {
|
|
86
|
+
languageService.dispose();
|
|
87
|
+
rmSync(directory, { force: true, recursive: true });
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
test('compiler transform', async ({ bench }) => {
|
|
92
|
+
await bench.compare(
|
|
93
|
+
bench('every module of the app', () => {
|
|
94
|
+
for (let i = 0, n = NAMES.length; i < n; i++) {
|
|
95
|
+
transform(NAMES[i]);
|
|
96
|
+
}
|
|
97
|
+
}),
|
|
98
|
+
bench('modules without reactive (no import, unrelated import)', () => {
|
|
99
|
+
transform('helpers.ts');
|
|
100
|
+
transform('plain.ts');
|
|
101
|
+
})
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('compiled output', async ({ bench }) => {
|
|
106
|
+
let file = directory + '/runtime.compiled.ts';
|
|
107
|
+
|
|
108
|
+
writeFileSync(file, transform('runtime.ts'));
|
|
109
|
+
|
|
110
|
+
let { run } = await import(pathToFileURL(file).href) as { run: () => number };
|
|
111
|
+
|
|
112
|
+
await bench('signal, computed, array and object reads and writes', () => {
|
|
113
|
+
run();
|
|
114
|
+
}).run();
|
|
115
|
+
});
|
package/bench/kairo.bench.ts
CHANGED
|
@@ -157,7 +157,7 @@ function diamond() {
|
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
function mux() {
|
|
160
|
-
let heads =
|
|
160
|
+
let heads = Array.from({ length: 100 }, () => framework.signal(0));
|
|
161
161
|
|
|
162
162
|
let muxed = framework.computed(() => Object.fromEntries(heads.map((h) => h.read()).entries()));
|
|
163
163
|
|
|
@@ -231,6 +231,7 @@ test('ReactiveArray reactive length', async ({ bench }) => {
|
|
|
231
231
|
let arr = new ReactiveArray([1, 2, 3]);
|
|
232
232
|
|
|
233
233
|
let stop = effect(() => {
|
|
234
|
+
// oxlint-disable-next-line no-unused-expressions -- the tracked read is what is measured
|
|
234
235
|
arr.$length;
|
|
235
236
|
});
|
|
236
237
|
|
|
@@ -16,8 +16,8 @@ test('ReactiveObject creation', async ({ bench }) => {
|
|
|
16
16
|
}),
|
|
17
17
|
bench('create with async computed property', () => {
|
|
18
18
|
new ReactiveObject({
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
async: () => Promise.resolve(0),
|
|
20
|
+
value: 1
|
|
21
21
|
});
|
|
22
22
|
})
|
|
23
23
|
);
|
|
@@ -29,6 +29,7 @@ test('ReactiveObject read/write', async ({ bench }) => {
|
|
|
29
29
|
bench('read property (signal-backed)', () => {
|
|
30
30
|
let obj = new ReactiveObject({ a: 1, b: 2, c: 3, d: 4, e: 5 });
|
|
31
31
|
|
|
32
|
+
// oxlint-disable-next-line no-unused-expressions -- the getter read is what is measured
|
|
32
33
|
(obj as any).a;
|
|
33
34
|
}),
|
|
34
35
|
bench('write property (signal-backed)', () => {
|
package/build/compiler/array.js
CHANGED
|
@@ -32,10 +32,10 @@ function declare(ctx, node) {
|
|
|
32
32
|
}
|
|
33
33
|
let typeParam = elementType ? `<${elementType}>` : '';
|
|
34
34
|
ctx.replacements.push({
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
generate: (_sourceFile, text) => expression.elements.length > 0
|
|
36
|
+
? ` new ${NAMESPACE}.ReactiveArray${typeParam}(${text(expression)})`
|
|
37
|
+
: ` new ${NAMESPACE}.ReactiveArray${typeParam}()`,
|
|
38
|
+
node
|
|
39
39
|
});
|
|
40
40
|
}
|
|
41
41
|
}
|
|
@@ -73,10 +73,10 @@ function length(access, replacements) {
|
|
|
73
73
|
if (parent && ts.isBinaryExpression(parent) && parent.left === access && (parent.operatorToken.kind === ts.SyntaxKind.EqualsToken || COMPOUND_OPERATORS.has(parent.operatorToken.kind))) {
|
|
74
74
|
let op = COMPOUND_OPERATORS.get(parent.operatorToken.kind);
|
|
75
75
|
replacements.push({
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
76
|
+
generate: (_sourceFile, text) => op === undefined
|
|
77
|
+
? `${text(expr)}.$length = ${text(parent.right)}`
|
|
78
|
+
: `${text(expr)}.$length = ${text(expr)}.length ${op} ${text(parent.right)}`,
|
|
79
|
+
node: parent
|
|
80
80
|
});
|
|
81
81
|
}
|
|
82
82
|
else if (parent &&
|
|
@@ -84,19 +84,19 @@ function length(access, replacements) {
|
|
|
84
84
|
(parent.operator === ts.SyntaxKind.PlusPlusToken || parent.operator === ts.SyntaxKind.MinusMinusToken)) {
|
|
85
85
|
let op = parent.operator === ts.SyntaxKind.PlusPlusToken ? '+' : '-';
|
|
86
86
|
replacements.push({
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
generate: (_sourceFile, text) => `${text(expr)}.$length = ${text(expr)}.length ${op} 1`,
|
|
88
|
+
node: parent
|
|
89
89
|
});
|
|
90
90
|
}
|
|
91
91
|
else {
|
|
92
92
|
replacements.push({
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
generate: (_sourceFile, text) => `${text(expr)}.$length`,
|
|
94
|
+
node: access
|
|
95
95
|
});
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
98
|
function write(access, ctx) {
|
|
99
|
-
let parent = access.parent
|
|
99
|
+
let parent = access.parent;
|
|
100
100
|
if (ts.isDeleteExpression(parent)) {
|
|
101
101
|
ctx.failures.push({ message: 'an element of a reactive array cannot be deleted; use splice()', node: parent });
|
|
102
102
|
return;
|
|
@@ -104,19 +104,22 @@ function write(access, ctx) {
|
|
|
104
104
|
if (ts.isBinaryExpression(parent)) {
|
|
105
105
|
let op = COMPOUND_OPERATORS.get(parent.operatorToken.kind), right = parent.right;
|
|
106
106
|
ctx.replacements.push({
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
107
|
+
generate: (_sourceFile, text) => op === undefined
|
|
108
|
+
? `${text(access.expression)}.$set(${text(access.argumentExpression)}, ${text(right)})`
|
|
109
|
+
: `((_a, _k) => _a.$set(_k, _a[_k] ${op} (${text(right)})))(${text(access.expression)}, ${text(access.argumentExpression)})`,
|
|
110
|
+
node: parent
|
|
111
111
|
});
|
|
112
112
|
return;
|
|
113
113
|
}
|
|
114
114
|
let unary = parent, op = unary.operator === ts.SyntaxKind.PlusPlusToken ? '+' : '-';
|
|
115
115
|
ctx.replacements.push({
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
116
|
+
generate: (_sourceFile, text) => {
|
|
117
|
+
let array = text(access.expression), index = text(access.argumentExpression);
|
|
118
|
+
return ts.isPrefixUnaryExpression(unary)
|
|
119
|
+
? `((_a, _k) => (_a.$set(_k, _a[_k] ${op} 1), _a[_k]))(${array}, ${index})`
|
|
120
|
+
: `((_a, _k, _v) => (_a.$set(_k, _v ${op} 1), _v))(${array}, ${index}, ${array}[${index}])`;
|
|
121
|
+
},
|
|
122
|
+
node: unary
|
|
120
123
|
});
|
|
121
124
|
}
|
|
122
125
|
export default (sourceFile, bindings, isReactiveCall) => {
|
|
@@ -147,10 +150,10 @@ export default (sourceFile, bindings, isReactiveCall) => {
|
|
|
147
150
|
}
|
|
148
151
|
let name = assignment.left.text, elements = scope.unwrap(assignment.right);
|
|
149
152
|
result.replacements.push({
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
153
|
+
generate: (_sourceFile, text) => elements.elements.length > 0
|
|
154
|
+
? `${name}.splice(0, ${name}.length, ...${text(elements)})`
|
|
155
|
+
: `${name}.splice(0, ${name}.length)`,
|
|
156
|
+
node: assignment
|
|
154
157
|
});
|
|
155
158
|
}
|
|
156
159
|
return result;
|
|
@@ -4,12 +4,13 @@ import { TYPES } from './constants.js';
|
|
|
4
4
|
import type { Bindings } from './types.js';
|
|
5
5
|
declare const arrays: (bindings: Bindings, nodes: ts.Node[]) => Set<ts.Node>;
|
|
6
6
|
declare const classify: (bindings: Bindings, call: ts.CallExpression) => TYPES | null;
|
|
7
|
-
declare const create: (
|
|
7
|
+
declare const create: (queries: ts.Checker, program: ts.Program, sourceFile: ts.SourceFile) => Bindings;
|
|
8
8
|
declare const denotes: (bindings: Bindings, expression: ts.Expression) => boolean;
|
|
9
9
|
declare const holds: (bindings: Bindings, found: Origin | null) => boolean;
|
|
10
10
|
declare const isReactiveCall: (bindings: Bindings, node: ts.Node) => node is ts.CallExpression;
|
|
11
11
|
declare const kind: (bindings: Bindings, found: Origin | null) => TYPES | null;
|
|
12
12
|
declare const origin: (bindings: Bindings, node: ts.Node) => Origin | null;
|
|
13
|
+
declare const prefetch: (bindings: Bindings, calls: ts.CallExpression[]) => void;
|
|
13
14
|
declare const unwrap: (expression: ts.Expression) => ts.Expression;
|
|
14
15
|
declare const _default: {
|
|
15
16
|
arrays: typeof arrays;
|
|
@@ -20,6 +21,7 @@ declare const _default: {
|
|
|
20
21
|
isReactiveCall: typeof isReactiveCall;
|
|
21
22
|
kind: typeof kind;
|
|
22
23
|
origin: typeof origin;
|
|
24
|
+
prefetch: typeof prefetch;
|
|
23
25
|
unwrap: typeof unwrap;
|
|
24
26
|
};
|
|
25
27
|
export default _default;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ts } from '@esportsplus/typescript';
|
|
2
|
-
import { references } from '@esportsplus/typescript/compiler';
|
|
2
|
+
import { checker, references } from '@esportsplus/typescript/compiler';
|
|
3
3
|
import { ENTRYPOINT, PACKAGE_NAME, TYPES } from './constants.js';
|
|
4
4
|
const ARRAY_BRAND = 'once';
|
|
5
5
|
const REACTIVE_ARRAY = 'ReactiveArray';
|
|
@@ -121,13 +121,16 @@ const classify = (bindings, call) => {
|
|
|
121
121
|
let type = bindings.checker.getTypeAtLocation(arg);
|
|
122
122
|
return type ? kindOf(bindings.checker, type) : null;
|
|
123
123
|
};
|
|
124
|
-
const create = (
|
|
125
|
-
checker,
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
124
|
+
const create = (queries, program, sourceFile) => {
|
|
125
|
+
let memoized = checker.cached(queries, program);
|
|
126
|
+
return {
|
|
127
|
+
checker: memoized,
|
|
128
|
+
entries: new Set(references.exported(memoized, program, PACKAGE_NAME, ENTRYPOINT).map(references.key)),
|
|
129
|
+
origins: references.origins(memoized, program, sourceFile),
|
|
130
|
+
program,
|
|
131
|
+
sourceFile
|
|
132
|
+
};
|
|
133
|
+
};
|
|
131
134
|
const denotes = (bindings, expression) => {
|
|
132
135
|
return references.denotes(bindings.checker, bindings.program, expression, bindings.entries);
|
|
133
136
|
};
|
|
@@ -179,7 +182,27 @@ const kind = (bindings, found) => {
|
|
|
179
182
|
const origin = (bindings, node) => {
|
|
180
183
|
return bindings.origins.get(node) ?? references.origin(bindings.checker, bindings.program, node);
|
|
181
184
|
};
|
|
185
|
+
const prefetch = (bindings, calls) => {
|
|
186
|
+
let nodes = [];
|
|
187
|
+
for (let i = 0, n = calls.length; i < n; i++) {
|
|
188
|
+
let arg = calls[i].arguments[0];
|
|
189
|
+
if (!arg || calls[i].arguments.length !== 1 || ts.isSpreadElement(arg)) {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
let value = unwrap(arg);
|
|
193
|
+
if (ts.isArrayLiteralExpression(value) ||
|
|
194
|
+
ts.isArrowFunction(value) ||
|
|
195
|
+
ts.isFunctionExpression(value) ||
|
|
196
|
+
ts.isObjectLiteralExpression(value)) {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
nodes.push(arg);
|
|
200
|
+
}
|
|
201
|
+
if (nodes.length > 0) {
|
|
202
|
+
bindings.checker.getTypeAtLocation(nodes);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
182
205
|
const unwrap = (expression) => {
|
|
183
206
|
return references.unwrap(expression);
|
|
184
207
|
};
|
|
185
|
-
export default { arrays, classify, create, denotes, holds, isReactiveCall, kind, origin, unwrap };
|
|
208
|
+
export default { arrays, classify, create, denotes, holds, isReactiveCall, kind, origin, prefetch, unwrap };
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { ImportIntent, ReplacementIntent, TransformContext } from '@esportsplus/typescript/compiler';
|
|
2
2
|
declare const _default: {
|
|
3
|
+
name: string;
|
|
3
4
|
transform: (ctx: TransformContext) => {
|
|
4
|
-
dependencies: string[];
|
|
5
|
-
} | {
|
|
6
5
|
dependencies?: undefined;
|
|
6
|
+
} | {
|
|
7
|
+
dependencies: string[];
|
|
7
8
|
} | {
|
|
8
9
|
imports: ImportIntent[];
|
|
9
10
|
prepend: string[];
|
package/build/compiler/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import array from './array.js';
|
|
|
4
4
|
import object from './object.js';
|
|
5
5
|
import primitives from './primitives.js';
|
|
6
6
|
import scope from './bindings.js';
|
|
7
|
+
const REGEX_IMPORT = /\bimport\b|\brequire\s*\(/;
|
|
7
8
|
function failed(sourceFile, failures) {
|
|
8
9
|
let lines = [];
|
|
9
10
|
for (let i = 0, n = failures.length; i < n; i++) {
|
|
@@ -42,27 +43,23 @@ function isLowered(site) {
|
|
|
42
43
|
function siteOf(identifier) {
|
|
43
44
|
let parent = identifier.parent;
|
|
44
45
|
if ((ts.isPropertyAccessExpression(parent) && parent.name === identifier) ||
|
|
45
|
-
(ts.isElementAccessExpression(parent) && parent.argumentExpression === identifier)) {
|
|
46
|
+
(ts.isElementAccessExpression(parent) && parent.argumentExpression === identifier && (ts.isStringLiteral(identifier) || ts.isNoSubstitutionTemplateLiteral(identifier)))) {
|
|
46
47
|
return parent;
|
|
47
48
|
}
|
|
48
49
|
return identifier;
|
|
49
50
|
}
|
|
50
51
|
export default {
|
|
52
|
+
name: PACKAGE_NAME,
|
|
51
53
|
transform: (ctx) => {
|
|
52
|
-
|
|
54
|
+
if (!REGEX_IMPORT.test(ctx.code)) {
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
57
|
+
let { checker, program, sourceFile } = ctx, bindings = scope.create(checker, program, sourceFile), calls = new Set(), dependencies = new Set(), entries = new Set(), failures = [], self = sourceFile.fileName.toLowerCase();
|
|
53
58
|
for (let [identifier, found] of bindings.origins) {
|
|
54
|
-
|
|
55
|
-
if (!entry && scope.kind(bindings, found) === null) {
|
|
56
|
-
continue;
|
|
57
|
-
}
|
|
58
|
-
for (let file of [...found.through, found.declaration.path]) {
|
|
59
|
-
if (file.toLowerCase() !== self) {
|
|
60
|
-
dependencies.add(file);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
if (!entry) {
|
|
59
|
+
if (!scope.holds(bindings, found)) {
|
|
64
60
|
continue;
|
|
65
61
|
}
|
|
62
|
+
entries.add(identifier);
|
|
66
63
|
let site = siteOf(identifier), parent = site.parent;
|
|
67
64
|
while (parent && ts.isParenthesizedExpression(parent)) {
|
|
68
65
|
parent = parent.parent;
|
|
@@ -77,6 +74,17 @@ export default {
|
|
|
77
74
|
});
|
|
78
75
|
}
|
|
79
76
|
}
|
|
77
|
+
scope.prefetch(bindings, [...calls]);
|
|
78
|
+
for (let [identifier, found] of bindings.origins) {
|
|
79
|
+
if (!entries.has(identifier) && scope.kind(bindings, found) === null) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
for (let file of [...found.through, found.declaration.path]) {
|
|
83
|
+
if (file.toLowerCase() !== self) {
|
|
84
|
+
dependencies.add(file);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
80
88
|
let isReactiveCall = (node) => calls.has(node), intents = {
|
|
81
89
|
imports: [],
|
|
82
90
|
prepend: [],
|
|
@@ -111,7 +119,7 @@ export default {
|
|
|
111
119
|
continue;
|
|
112
120
|
}
|
|
113
121
|
intents.replacements.push({
|
|
114
|
-
generate: () => `${NAMESPACE}.reactive(${call.arguments.map(
|
|
122
|
+
generate: (_sourceFile, text) => `${NAMESPACE}.reactive(${call.arguments.map(text).join(', ')})`,
|
|
115
123
|
node: call
|
|
116
124
|
});
|
|
117
125
|
}
|
package/build/compiler/object.js
CHANGED
|
@@ -2,7 +2,6 @@ import { ts } from '@esportsplus/typescript';
|
|
|
2
2
|
import { code, uid } from '@esportsplus/typescript/compiler';
|
|
3
3
|
import { NAMESPACE, TYPES } from './constants.js';
|
|
4
4
|
function analyzeProperty(prop, bindings) {
|
|
5
|
-
let sourceFile = bindings.sourceFile;
|
|
6
5
|
if (!ts.isPropertyAssignment(prop)) {
|
|
7
6
|
return null;
|
|
8
7
|
}
|
|
@@ -13,7 +12,7 @@ function analyzeProperty(prop, bindings) {
|
|
|
13
12
|
else {
|
|
14
13
|
return null;
|
|
15
14
|
}
|
|
16
|
-
let unwrapped = prop.initializer, value = unwrapped
|
|
15
|
+
let unwrapped = prop.initializer, value = unwrapped;
|
|
17
16
|
while (ts.isAsExpression(unwrapped) || ts.isTypeAssertion(unwrapped) || ts.isParenthesizedExpression(unwrapped)) {
|
|
18
17
|
unwrapped = unwrapped.expression;
|
|
19
18
|
}
|
|
@@ -23,11 +22,11 @@ function analyzeProperty(prop, bindings) {
|
|
|
23
22
|
(ts.isTypeReferenceNode(type) &&
|
|
24
23
|
ts.isIdentifier(type.typeName) &&
|
|
25
24
|
type.typeName.text === 'Array')) {
|
|
26
|
-
return { isStatic: false, key, type: TYPES.Array,
|
|
25
|
+
return { isStatic: false, key, type: TYPES.Array, value };
|
|
27
26
|
}
|
|
28
27
|
}
|
|
29
28
|
if (ts.isArrowFunction(unwrapped) || ts.isFunctionExpression(unwrapped)) {
|
|
30
|
-
return { isStatic: false, key, type: TYPES.Computed,
|
|
29
|
+
return { isStatic: false, key, type: TYPES.Computed, value };
|
|
31
30
|
}
|
|
32
31
|
if (ts.isArrayLiteralExpression(unwrapped)) {
|
|
33
32
|
let elements = unwrapped.elements, isStatic = value === unwrapped;
|
|
@@ -36,23 +35,23 @@ function analyzeProperty(prop, bindings) {
|
|
|
36
35
|
isStatic = false;
|
|
37
36
|
}
|
|
38
37
|
}
|
|
39
|
-
return { isStatic, key, type: TYPES.Array,
|
|
38
|
+
return { isStatic, key, type: TYPES.Array, value };
|
|
40
39
|
}
|
|
41
40
|
let type = isStaticValue(unwrapped) ? undefined : bindings.checker.getTypeAtLocation(value);
|
|
42
41
|
if (type && (bindings.checker.isArrayType(type) || bindings.checker.isTupleType(type))) {
|
|
43
|
-
return { isStatic: false, key, type: TYPES.Array,
|
|
42
|
+
return { isStatic: false, key, type: TYPES.Array, value };
|
|
44
43
|
}
|
|
45
44
|
return {
|
|
46
45
|
isStatic: isStaticValue(value),
|
|
47
46
|
key,
|
|
48
47
|
type: TYPES.Signal,
|
|
49
|
-
|
|
48
|
+
value
|
|
50
49
|
};
|
|
51
50
|
}
|
|
52
|
-
function buildClassCode(classname, properties, typehint) {
|
|
51
|
+
function buildClassCode(classname, properties, sourceFile, typehint) {
|
|
53
52
|
let accessors = [], body = [], constraint = [], fields = [], generics = [], parameters = [], setters = 0;
|
|
54
53
|
for (let i = 0, n = properties.length; i < n; i++) {
|
|
55
|
-
let { isStatic, key, type,
|
|
54
|
+
let { isStatic, key, type, value: initializer } = properties[i], generic = typehint ? `T['${key}']` : `T${parameters.length}`, parameter = `_p${parameters.length}`;
|
|
56
55
|
if (typehint && type === TYPES.Signal) {
|
|
57
56
|
constraint.push(`'${key}'?: unknown`);
|
|
58
57
|
isStatic = false;
|
|
@@ -68,7 +67,7 @@ function buildClassCode(classname, properties, typehint) {
|
|
|
68
67
|
${NAMESPACE}.write(this.#${key}, ${value});
|
|
69
68
|
}
|
|
70
69
|
`);
|
|
71
|
-
fields.push(`#${key} = this[${NAMESPACE}.SIGNAL](${
|
|
70
|
+
fields.push(`#${key} = this[${NAMESPACE}.SIGNAL](${initializer.getText(sourceFile)});`);
|
|
72
71
|
}
|
|
73
72
|
else {
|
|
74
73
|
accessors.push(`
|
|
@@ -186,12 +185,12 @@ export default (sourceFile, bindings, isReactiveCall) => {
|
|
|
186
185
|
let prepend = [], replacements = [];
|
|
187
186
|
for (let i = 0, n = ctx.calls.length; i < n; i++) {
|
|
188
187
|
let call = ctx.calls[i], typehint = call.typehint;
|
|
189
|
-
prepend.push(buildClassCode(call.classname, call.properties, typehint));
|
|
188
|
+
prepend.push(buildClassCode(call.classname, call.properties, sourceFile, typehint));
|
|
190
189
|
replacements.push({
|
|
191
|
-
generate: () => {
|
|
190
|
+
generate: (_sourceFile, text) => {
|
|
192
191
|
let args = call.properties
|
|
193
192
|
.filter(({ isStatic, type }) => typehint || !isStatic || type !== TYPES.Signal)
|
|
194
|
-
.map(p => p.
|
|
193
|
+
.map(p => text(p.value))
|
|
195
194
|
.join(', ');
|
|
196
195
|
return typehint
|
|
197
196
|
? ` new ${call.classname}<${typehint}>(${args})`
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
declare const _default: ({ root }?: {
|
|
2
2
|
root?: string;
|
|
3
3
|
}) => {
|
|
4
|
+
buildStart: () => void;
|
|
4
5
|
closeBundle: () => void;
|
|
5
6
|
closeWatcher: () => void;
|
|
6
7
|
configResolved: (config: unknown) => void;
|
|
@@ -20,10 +21,18 @@ declare const _default: ({ root }?: {
|
|
|
20
21
|
renderChunk: (code: string, chunk: {
|
|
21
22
|
fileName: string;
|
|
22
23
|
}) => null;
|
|
23
|
-
transform:
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
transform: {
|
|
25
|
+
filter: {
|
|
26
|
+
id: {
|
|
27
|
+
exclude: RegExp[];
|
|
28
|
+
include: RegExp;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
handler: (code: string, id: string) => {
|
|
32
|
+
code: string;
|
|
33
|
+
map: import("@esportsplus/typescript/compiler").SourceMapV3;
|
|
34
|
+
} | null;
|
|
35
|
+
};
|
|
27
36
|
watchChange: (id: string) => void;
|
|
28
37
|
};
|
|
29
38
|
export default _default;
|
|
@@ -86,7 +86,7 @@ function isPatternTarget(node) {
|
|
|
86
86
|
function readNode(identifier) {
|
|
87
87
|
let parent = identifier.parent;
|
|
88
88
|
if ((ts.isPropertyAccessExpression(parent) && parent.name === identifier) ||
|
|
89
|
-
(ts.isElementAccessExpression(parent) && parent.argumentExpression === identifier)) {
|
|
89
|
+
(ts.isElementAccessExpression(parent) && parent.argumentExpression === identifier && (ts.isStringLiteral(identifier) || ts.isNoSubstitutionTemplateLiteral(identifier)))) {
|
|
90
90
|
return parent;
|
|
91
91
|
}
|
|
92
92
|
return identifier;
|
|
@@ -103,10 +103,10 @@ function reference(ctx, node, kind) {
|
|
|
103
103
|
ctx.failures.push({ message: 'a reactive binding cannot be assigned through a destructuring pattern; assign it directly', node });
|
|
104
104
|
return;
|
|
105
105
|
}
|
|
106
|
-
let
|
|
106
|
+
let binding = node.getText(ctx.bindings.sourceFile);
|
|
107
107
|
if (ts.isShorthandPropertyAssignment(parent)) {
|
|
108
108
|
ctx.replacements.push({
|
|
109
|
-
generate: () => `${
|
|
109
|
+
generate: () => `${binding}: ${NAMESPACE}.read(${binding})`,
|
|
110
110
|
node: parent
|
|
111
111
|
});
|
|
112
112
|
return;
|
|
@@ -129,7 +129,7 @@ function reference(ctx, node, kind) {
|
|
|
129
129
|
}
|
|
130
130
|
if (!writeCtx) {
|
|
131
131
|
ctx.replacements.push({
|
|
132
|
-
generate: () => `${NAMESPACE}.read(${
|
|
132
|
+
generate: () => `${NAMESPACE}.read(${binding})`,
|
|
133
133
|
node
|
|
134
134
|
});
|
|
135
135
|
return;
|
|
@@ -141,14 +141,14 @@ function reference(ctx, node, kind) {
|
|
|
141
141
|
if (writeCtx === 'simple' && ts.isBinaryExpression(parent)) {
|
|
142
142
|
let right = parent.right;
|
|
143
143
|
ctx.replacements.push({
|
|
144
|
-
generate: (
|
|
144
|
+
generate: (_sourceFile, text) => `${NAMESPACE}.write(${binding}, ${text(right)})`,
|
|
145
145
|
node: parent
|
|
146
146
|
});
|
|
147
147
|
}
|
|
148
148
|
else if (writeCtx === 'compound' && ts.isBinaryExpression(parent)) {
|
|
149
149
|
let op = COMPOUND_OPERATORS.get(parent.operatorToken.kind) ?? '+', right = parent.right;
|
|
150
150
|
ctx.replacements.push({
|
|
151
|
-
generate: (
|
|
151
|
+
generate: (_sourceFile, text) => `${NAMESPACE}.write(${binding}, ${binding}.value ${op} ${text(right)})`,
|
|
152
152
|
node: parent
|
|
153
153
|
});
|
|
154
154
|
}
|
|
@@ -156,20 +156,20 @@ function reference(ctx, node, kind) {
|
|
|
156
156
|
let unary = parent, delta = unary.operator === ts.SyntaxKind.PlusPlusToken ? '+ 1' : '- 1';
|
|
157
157
|
if (ts.isExpressionStatement(unary.parent)) {
|
|
158
158
|
ctx.replacements.push({
|
|
159
|
-
generate: () => `${NAMESPACE}.write(${
|
|
159
|
+
generate: () => `${NAMESPACE}.write(${binding}, ${binding}.value ${delta})`,
|
|
160
160
|
node: unary
|
|
161
161
|
});
|
|
162
162
|
}
|
|
163
163
|
else if (ts.isPrefixUnaryExpression(unary)) {
|
|
164
164
|
ctx.replacements.push({
|
|
165
|
-
generate: () => `(${NAMESPACE}.write(${
|
|
165
|
+
generate: () => `(${NAMESPACE}.write(${binding}, ${binding}.value ${delta}), ${binding}.value)`,
|
|
166
166
|
node: unary
|
|
167
167
|
});
|
|
168
168
|
}
|
|
169
169
|
else {
|
|
170
170
|
let tmp = `_t${ctx.tmpCounter++}`;
|
|
171
171
|
ctx.replacements.push({
|
|
172
|
-
generate: () => `((${tmp}) => (${NAMESPACE}.write(${
|
|
172
|
+
generate: () => `((${tmp}) => (${NAMESPACE}.write(${binding}, ${tmp} ${delta}), ${tmp}))(${binding}.value)`,
|
|
173
173
|
node: unary
|
|
174
174
|
});
|
|
175
175
|
}
|