@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
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { COMPUTED, REACTIVE_ARRAY, SIGNAL } from '../constants.js';
|
|
2
|
-
import { Computed } from '../types.js';
|
|
3
2
|
import { ReactiveArray } from './array.js';
|
|
3
|
+
import type { Computed } from '../types.js';
|
|
4
4
|
declare class ReactiveObject<T extends Record<PropertyKey, unknown>> {
|
|
5
5
|
protected disposers: VoidFunction[] | null;
|
|
6
6
|
constructor(data: T | null);
|
|
7
|
-
protected [COMPUTED]<T extends Computed<ReturnType<T>>['fn']>(value: T): import("../
|
|
7
|
+
protected [COMPUTED]<T extends Computed<ReturnType<T>>['fn']>(value: T): import("../index.js").ComputedResult<ReturnType<T>>;
|
|
8
8
|
protected [REACTIVE_ARRAY]<U>(value: U[]): ReactiveArray<U>;
|
|
9
|
-
protected [SIGNAL]<T>(value: T): import("../
|
|
9
|
+
protected [SIGNAL]<T>(value: T): import("../index.js").Signal<T>;
|
|
10
10
|
dispose(): void;
|
|
11
11
|
}
|
|
12
12
|
declare const isReactiveObject: (value: unknown) => value is ReactiveObject<Record<PropertyKey, unknown>>;
|
package/build/reactive/object.js
CHANGED
package/oxlintrc.json
ADDED
package/package.json
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"author": "ICJR",
|
|
3
3
|
"dependencies": {
|
|
4
|
+
"@esportsplus/typescript": "^0.39.0",
|
|
4
5
|
"@esportsplus/utilities": "^0.28.6"
|
|
5
6
|
},
|
|
6
7
|
"devDependencies": {
|
|
7
|
-
"@
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"
|
|
8
|
+
"@types/node": "^26.6.6",
|
|
9
|
+
"oxlint": "^1.90.0",
|
|
10
|
+
"oxlint-tsgolint": "^7.0.2005",
|
|
11
|
+
"vite": "^8.3.5",
|
|
12
|
+
"vitest": "^5.1.0"
|
|
13
|
+
},
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=26"
|
|
11
16
|
},
|
|
12
17
|
"exports": {
|
|
13
18
|
".": {
|
|
@@ -36,13 +41,16 @@
|
|
|
36
41
|
},
|
|
37
42
|
"type": "module",
|
|
38
43
|
"types": "build/index.d.ts",
|
|
39
|
-
"version": "0.
|
|
44
|
+
"version": "0.41.0",
|
|
40
45
|
"scripts": {
|
|
41
46
|
"agent:bench": "vitest bench --run",
|
|
42
|
-
"agent:test": "
|
|
47
|
+
"agent:test": "pnpm typecheck && vitest run",
|
|
43
48
|
"bench": "vitest bench --run",
|
|
44
|
-
"build": "tsc",
|
|
49
|
+
"build": "tsc -p tsconfig.build.json",
|
|
50
|
+
"lint": "oxlint -c oxlintrc.json --type-aware --deny-warnings",
|
|
45
51
|
"test": "vitest run",
|
|
52
|
+
"typecheck": "tsc --noEmit",
|
|
53
|
+
"verify": "pnpm build && pnpm typecheck && pnpm lint && pnpm test",
|
|
46
54
|
"-": "-"
|
|
47
55
|
}
|
|
48
56
|
}
|
package/pnpm-workspace.yaml
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
allowBuilds:
|
|
2
2
|
esbuild: true
|
|
3
3
|
minimumReleaseAgeExclude:
|
|
4
|
-
- '@esportsplus
|
|
5
|
-
- '@
|
|
6
|
-
-
|
|
7
|
-
-
|
|
8
|
-
- '@vitest/spy@5.0.0'
|
|
9
|
-
- vitest@5.0.0
|
|
10
|
-
- '@esportsplus/cli-passthrough@0.0.18'
|
|
4
|
+
- '@esportsplus/*'
|
|
5
|
+
- '@vitest/mocker@5.1.0'
|
|
6
|
+
- '@vitest/spy@5.1.0'
|
|
7
|
+
- vitest@5.1.0
|
package/src/compiler/array.ts
CHANGED
|
@@ -68,10 +68,10 @@ function declare(ctx: { isReactiveCall: IsReactiveCall; replacements: Replacemen
|
|
|
68
68
|
let typeParam = elementType ? `<${elementType}>` : '';
|
|
69
69
|
|
|
70
70
|
ctx.replacements.push({
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
71
|
+
generate: (_sourceFile, text) => expression.elements.length > 0
|
|
72
|
+
? ` new ${NAMESPACE}.ReactiveArray${typeParam}(${text(expression)})`
|
|
73
|
+
: ` new ${NAMESPACE}.ReactiveArray${typeParam}()`,
|
|
74
|
+
node
|
|
75
75
|
});
|
|
76
76
|
}
|
|
77
77
|
}
|
|
@@ -126,10 +126,10 @@ function length(access: ts.PropertyAccessExpression, replacements: ReplacementIn
|
|
|
126
126
|
let op = COMPOUND_OPERATORS.get(parent.operatorToken.kind);
|
|
127
127
|
|
|
128
128
|
replacements.push({
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
129
|
+
generate: (_sourceFile, text) => op === undefined
|
|
130
|
+
? `${text(expr)}.$length = ${text(parent.right)}`
|
|
131
|
+
: `${text(expr)}.$length = ${text(expr)}.length ${op} ${text(parent.right)}`,
|
|
132
|
+
node: parent
|
|
133
133
|
});
|
|
134
134
|
}
|
|
135
135
|
// arr.length++ / arr.length-- / ++arr.length / --arr.length
|
|
@@ -141,24 +141,22 @@ function length(access: ts.PropertyAccessExpression, replacements: ReplacementIn
|
|
|
141
141
|
let op = parent.operator === ts.SyntaxKind.PlusPlusToken ? '+' : '-';
|
|
142
142
|
|
|
143
143
|
replacements.push({
|
|
144
|
-
|
|
145
|
-
|
|
144
|
+
generate: (_sourceFile, text) => `${text(expr)}.$length = ${text(expr)}.length ${op} 1`,
|
|
145
|
+
node: parent
|
|
146
146
|
});
|
|
147
147
|
}
|
|
148
148
|
// Read-only: arr.length -> arr.$length
|
|
149
149
|
else {
|
|
150
150
|
replacements.push({
|
|
151
|
-
|
|
152
|
-
|
|
151
|
+
generate: (_sourceFile, text) => `${text(expr)}.$length`,
|
|
152
|
+
node: access
|
|
153
153
|
});
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
// An element write must notify, so it goes through $set; operands are evaluated once
|
|
158
158
|
function write(access: ts.ElementAccessExpression, ctx: ArrayTransformResult): void {
|
|
159
|
-
let parent = access.parent
|
|
160
|
-
array = (sf: ts.SourceFile) => access.expression.getText(sf),
|
|
161
|
-
index = (sf: ts.SourceFile) => access.argumentExpression.getText(sf);
|
|
159
|
+
let parent = access.parent!;
|
|
162
160
|
|
|
163
161
|
if (ts.isDeleteExpression(parent)) {
|
|
164
162
|
ctx.failures.push({ message: 'an element of a reactive array cannot be deleted; use splice()', node: parent });
|
|
@@ -170,10 +168,10 @@ function write(access: ts.ElementAccessExpression, ctx: ArrayTransformResult): v
|
|
|
170
168
|
right = parent.right;
|
|
171
169
|
|
|
172
170
|
ctx.replacements.push({
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
171
|
+
generate: (_sourceFile, text) => op === undefined
|
|
172
|
+
? `${text(access.expression)}.$set(${text(access.argumentExpression)}, ${text(right)})`
|
|
173
|
+
: `((_a, _k) => _a.$set(_k, _a[_k] ${op} (${text(right)})))(${text(access.expression)}, ${text(access.argumentExpression)})`,
|
|
174
|
+
node: parent
|
|
177
175
|
});
|
|
178
176
|
|
|
179
177
|
return;
|
|
@@ -183,10 +181,15 @@ function write(access: ts.ElementAccessExpression, ctx: ArrayTransformResult): v
|
|
|
183
181
|
op = unary.operator === ts.SyntaxKind.PlusPlusToken ? '+' : '-';
|
|
184
182
|
|
|
185
183
|
ctx.replacements.push({
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
184
|
+
generate: (_sourceFile, text) => {
|
|
185
|
+
let array = text(access.expression),
|
|
186
|
+
index = text(access.argumentExpression);
|
|
187
|
+
|
|
188
|
+
return ts.isPrefixUnaryExpression(unary)
|
|
189
|
+
? `((_a, _k) => (_a.$set(_k, _a[_k] ${op} 1), _a[_k]))(${array}, ${index})`
|
|
190
|
+
: `((_a, _k, _v) => (_a.$set(_k, _v ${op} 1), _v))(${array}, ${index}, ${array}[${index}])`;
|
|
191
|
+
},
|
|
192
|
+
node: unary
|
|
190
193
|
});
|
|
191
194
|
}
|
|
192
195
|
|
|
@@ -234,10 +237,10 @@ export default (sourceFile: ts.SourceFile, bindings: Bindings, isReactiveCall: I
|
|
|
234
237
|
elements = scope.unwrap(assignment.right) as ts.ArrayLiteralExpression;
|
|
235
238
|
|
|
236
239
|
result.replacements.push({
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
240
|
+
generate: (_sourceFile, text) => elements.elements.length > 0
|
|
241
|
+
? `${name}.splice(0, ${name}.length, ...${text(elements)})`
|
|
242
|
+
: `${name}.splice(0, ${name}.length)`,
|
|
243
|
+
node: assignment
|
|
241
244
|
});
|
|
242
245
|
}
|
|
243
246
|
|
package/src/compiler/bindings.ts
CHANGED
|
@@ -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 type { Origin } from '@esportsplus/typescript/compiler';
|
|
4
4
|
import { ENTRYPOINT, PACKAGE_NAME, TYPES } from './constants';
|
|
5
5
|
import type { Bindings } from './types';
|
|
@@ -7,7 +7,9 @@ import type { Bindings } from './types';
|
|
|
7
7
|
|
|
8
8
|
type Assignment = { call: ts.CallExpression; target: ts.Identifier };
|
|
9
9
|
|
|
10
|
-
// Per program snapshot: answers about declarations and types hold for the snapshot's lifetime
|
|
10
|
+
// Per program snapshot: answers about declarations and types hold for the snapshot's lifetime. `arrays`
|
|
11
|
+
// and `kinds` hold answers built from queries the shared checker does not memoize (getPropertyOfType,
|
|
12
|
+
// getNonNullableType, isArrayType), so dropping them costs a round-trip per array candidate or read
|
|
11
13
|
type Caches = {
|
|
12
14
|
arrays: Map<number, boolean>;
|
|
13
15
|
assignments: Map<string, Map<string, Assignment[]>>;
|
|
@@ -204,13 +206,19 @@ const classify = (bindings: Bindings, call: ts.CallExpression): TYPES | null =>
|
|
|
204
206
|
return type ? kindOf(bindings.checker, type) : null;
|
|
205
207
|
};
|
|
206
208
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
209
|
+
// Memoized per program (an already memoized checker is kept as is), so batched and repeated queries
|
|
210
|
+
// share one cache whoever hands the checker in
|
|
211
|
+
const create = (queries: ts.Checker, program: ts.Program, sourceFile: ts.SourceFile): Bindings => {
|
|
212
|
+
let memoized = checker.cached(queries, program);
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
checker: memoized,
|
|
216
|
+
entries: new Set(references.exported(memoized, program, PACKAGE_NAME, ENTRYPOINT).map(references.key)),
|
|
217
|
+
origins: references.origins(memoized, program, sourceFile),
|
|
218
|
+
program,
|
|
219
|
+
sourceFile
|
|
220
|
+
};
|
|
221
|
+
};
|
|
214
222
|
|
|
215
223
|
// Whether a callee expression denotes `reactive`: `reactive`, an alias, `ns.reactive`, `ns['reactive']`
|
|
216
224
|
const denotes = (bindings: Bindings, expression: ts.Expression): boolean => {
|
|
@@ -293,6 +301,37 @@ const origin = (bindings: Bindings, node: ts.Node): Origin | null => {
|
|
|
293
301
|
return bindings.origins.get(node) ?? references.origin(bindings.checker, bindings.program, node);
|
|
294
302
|
};
|
|
295
303
|
|
|
304
|
+
// Types of the arguments classify() reads from the checker, requested in one round-trip; the
|
|
305
|
+
// memoized checker then answers each classify() from that request
|
|
306
|
+
const prefetch = (bindings: Bindings, calls: ts.CallExpression[]): void => {
|
|
307
|
+
let nodes: ts.Node[] = [];
|
|
308
|
+
|
|
309
|
+
for (let i = 0, n = calls.length; i < n; i++) {
|
|
310
|
+
let arg = calls[i].arguments[0];
|
|
311
|
+
|
|
312
|
+
if (!arg || calls[i].arguments.length !== 1 || ts.isSpreadElement(arg)) {
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
let value = unwrap(arg);
|
|
317
|
+
|
|
318
|
+
if (
|
|
319
|
+
ts.isArrayLiteralExpression(value) ||
|
|
320
|
+
ts.isArrowFunction(value) ||
|
|
321
|
+
ts.isFunctionExpression(value) ||
|
|
322
|
+
ts.isObjectLiteralExpression(value)
|
|
323
|
+
) {
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
nodes.push(arg);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (nodes.length > 0) {
|
|
331
|
+
bindings.checker.getTypeAtLocation(nodes);
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
|
|
296
335
|
|
|
297
336
|
// The expression a value comes from, without parentheses, `!` and type assertions
|
|
298
337
|
const unwrap = (expression: ts.Expression): ts.Expression => {
|
|
@@ -300,4 +339,4 @@ const unwrap = (expression: ts.Expression): ts.Expression => {
|
|
|
300
339
|
};
|
|
301
340
|
|
|
302
341
|
|
|
303
|
-
export default { arrays, classify, create, denotes, holds, isReactiveCall, kind, origin, unwrap };
|
|
342
|
+
export default { arrays, classify, create, denotes, holds, isReactiveCall, kind, origin, prefetch, unwrap };
|
package/src/compiler/index.ts
CHANGED
|
@@ -9,6 +9,11 @@ import primitives from './primitives';
|
|
|
9
9
|
import scope from './bindings';
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
// A binding from this package is only ever reached through an import, so a module without one has
|
|
13
|
+
// nothing to compile and is not resolved at all
|
|
14
|
+
const REGEX_IMPORT = /\bimport\b|\brequire\s*\(/;
|
|
15
|
+
|
|
16
|
+
|
|
12
17
|
// Every use the compiler cannot lower fails the build, all of them reported at once: shipping one
|
|
13
18
|
// would leave an uncompiled reactive() or an unread signal in the output
|
|
14
19
|
function failed(sourceFile: ts.SourceFile, failures: Failure[]): Error {
|
|
@@ -64,13 +69,14 @@ function isLowered(site: ts.Node): boolean {
|
|
|
64
69
|
}
|
|
65
70
|
|
|
66
71
|
|
|
67
|
-
// `reactive`, or the member access naming it (`ns.reactive`, `ns['reactive']`)
|
|
72
|
+
// `reactive`, or the member access naming it (`ns.reactive`, `ns['reactive']`); in `list[reactive]`
|
|
73
|
+
// the key is the use
|
|
68
74
|
function siteOf(identifier: ts.Node): ts.Node {
|
|
69
75
|
let parent = identifier.parent!;
|
|
70
76
|
|
|
71
77
|
if (
|
|
72
78
|
(ts.isPropertyAccessExpression(parent) && parent.name === identifier) ||
|
|
73
|
-
(ts.isElementAccessExpression(parent) && parent.argumentExpression === identifier)
|
|
79
|
+
(ts.isElementAccessExpression(parent) && parent.argumentExpression === identifier && (ts.isStringLiteral(identifier) || ts.isNoSubstitutionTemplateLiteral(identifier)))
|
|
74
80
|
) {
|
|
75
81
|
return parent;
|
|
76
82
|
}
|
|
@@ -79,34 +85,30 @@ function siteOf(identifier: ts.Node): ts.Node {
|
|
|
79
85
|
}
|
|
80
86
|
|
|
81
87
|
|
|
82
|
-
// No
|
|
83
|
-
// (a barrel's rename, a default re-export, a namespace), so every module
|
|
84
|
-
// program and compiled exactly where its values resolve to reactive declarations
|
|
88
|
+
// No name patterns: a module can reach `reactive`, or read a reactive binding, under any name
|
|
89
|
+
// (a barrel's rename, a default re-export, a namespace), so every module that imports anything is
|
|
90
|
+
// resolved against the program and compiled exactly where its values resolve to reactive declarations
|
|
85
91
|
export default {
|
|
92
|
+
name: PACKAGE_NAME,
|
|
86
93
|
transform: (ctx: TransformContext) => {
|
|
94
|
+
if (!REGEX_IMPORT.test(ctx.code)) {
|
|
95
|
+
return {};
|
|
96
|
+
}
|
|
97
|
+
|
|
87
98
|
let { checker, program, sourceFile } = ctx,
|
|
88
99
|
bindings = scope.create(checker, program, sourceFile),
|
|
89
|
-
calls = new Set<ts.
|
|
100
|
+
calls = new Set<ts.CallExpression>(),
|
|
90
101
|
dependencies = new Set<string>(),
|
|
102
|
+
entries = new Set<ts.Node>(),
|
|
91
103
|
failures: Failure[] = [],
|
|
92
104
|
self = sourceFile.fileName.toLowerCase();
|
|
93
105
|
|
|
94
106
|
for (let [identifier, found] of bindings.origins) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (!entry && scope.kind(bindings, found) === null) {
|
|
107
|
+
if (!scope.holds(bindings, found)) {
|
|
98
108
|
continue;
|
|
99
109
|
}
|
|
100
110
|
|
|
101
|
-
|
|
102
|
-
if (file.toLowerCase() !== self) {
|
|
103
|
-
dependencies.add(file);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (!entry) {
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
111
|
+
entries.add(identifier);
|
|
110
112
|
|
|
111
113
|
let site = siteOf(identifier),
|
|
112
114
|
parent = site.parent;
|
|
@@ -126,7 +128,22 @@ export default {
|
|
|
126
128
|
}
|
|
127
129
|
}
|
|
128
130
|
|
|
129
|
-
|
|
131
|
+
// Every call is known before any binding is classified, so their argument types arrive in one request
|
|
132
|
+
scope.prefetch(bindings, [...calls]);
|
|
133
|
+
|
|
134
|
+
for (let [identifier, found] of bindings.origins) {
|
|
135
|
+
if (!entries.has(identifier) && scope.kind(bindings, found) === null) {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
for (let file of [...found.through, found.declaration.path]) {
|
|
140
|
+
if (file.toLowerCase() !== self) {
|
|
141
|
+
dependencies.add(file);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
let isReactiveCall = (node: ts.Node): node is ts.CallExpression => calls.has(node as ts.CallExpression),
|
|
130
147
|
intents = {
|
|
131
148
|
imports: [] as ImportIntent[],
|
|
132
149
|
prepend: [] as string[],
|
|
@@ -179,7 +196,7 @@ export default {
|
|
|
179
196
|
}
|
|
180
197
|
|
|
181
198
|
intents.replacements.push({
|
|
182
|
-
generate: () => `${NAMESPACE}.reactive(${call.arguments.map(
|
|
199
|
+
generate: (_sourceFile, text) => `${NAMESPACE}.reactive(${call.arguments.map(text).join(', ')})`,
|
|
183
200
|
node: call
|
|
184
201
|
});
|
|
185
202
|
}
|
package/src/compiler/object.ts
CHANGED
|
@@ -9,7 +9,7 @@ interface AnalyzedProperty {
|
|
|
9
9
|
isStatic: boolean;
|
|
10
10
|
key: string;
|
|
11
11
|
type: TYPES;
|
|
12
|
-
|
|
12
|
+
value: ts.Expression;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
type ObjectTransformResult = {
|
|
@@ -34,8 +34,6 @@ interface VisitContext {
|
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
function analyzeProperty(prop: ts.ObjectLiteralElementLike, bindings: Bindings): AnalyzedProperty | null {
|
|
37
|
-
let sourceFile = bindings.sourceFile;
|
|
38
|
-
|
|
39
37
|
if (!ts.isPropertyAssignment(prop)) {
|
|
40
38
|
return null;
|
|
41
39
|
}
|
|
@@ -50,8 +48,7 @@ function analyzeProperty(prop: ts.ObjectLiteralElementLike, bindings: Bindings):
|
|
|
50
48
|
}
|
|
51
49
|
|
|
52
50
|
let unwrapped = prop.initializer,
|
|
53
|
-
value = unwrapped
|
|
54
|
-
valueText = value.getText(sourceFile);
|
|
51
|
+
value = unwrapped;
|
|
55
52
|
|
|
56
53
|
while (ts.isAsExpression(unwrapped) || ts.isTypeAssertion(unwrapped) || ts.isParenthesizedExpression(unwrapped)) {
|
|
57
54
|
unwrapped = unwrapped.expression;
|
|
@@ -68,12 +65,12 @@ function analyzeProperty(prop: ts.ObjectLiteralElementLike, bindings: Bindings):
|
|
|
68
65
|
type.typeName.text === 'Array'
|
|
69
66
|
)
|
|
70
67
|
) {
|
|
71
|
-
return { isStatic: false, key, type: TYPES.Array,
|
|
68
|
+
return { isStatic: false, key, type: TYPES.Array, value };
|
|
72
69
|
}
|
|
73
70
|
}
|
|
74
71
|
|
|
75
72
|
if (ts.isArrowFunction(unwrapped) || ts.isFunctionExpression(unwrapped)) {
|
|
76
|
-
return { isStatic: false, key, type: TYPES.Computed,
|
|
73
|
+
return { isStatic: false, key, type: TYPES.Computed, value };
|
|
77
74
|
}
|
|
78
75
|
|
|
79
76
|
if (ts.isArrayLiteralExpression(unwrapped)) {
|
|
@@ -86,7 +83,7 @@ function analyzeProperty(prop: ts.ObjectLiteralElementLike, bindings: Bindings):
|
|
|
86
83
|
}
|
|
87
84
|
}
|
|
88
85
|
|
|
89
|
-
return { isStatic, key, type: TYPES.Array,
|
|
86
|
+
return { isStatic, key, type: TYPES.Array, value };
|
|
90
87
|
}
|
|
91
88
|
|
|
92
89
|
// A non-literal array is typed Reactive<T[]> on the object, so it must become a ReactiveArray
|
|
@@ -94,18 +91,18 @@ function analyzeProperty(prop: ts.ObjectLiteralElementLike, bindings: Bindings):
|
|
|
94
91
|
let type = isStaticValue(unwrapped) ? undefined : bindings.checker.getTypeAtLocation(value);
|
|
95
92
|
|
|
96
93
|
if (type && (bindings.checker.isArrayType(type) || bindings.checker.isTupleType(type))) {
|
|
97
|
-
return { isStatic: false, key, type: TYPES.Array,
|
|
94
|
+
return { isStatic: false, key, type: TYPES.Array, value };
|
|
98
95
|
}
|
|
99
96
|
|
|
100
97
|
return {
|
|
101
98
|
isStatic: isStaticValue(value),
|
|
102
99
|
key,
|
|
103
100
|
type: TYPES.Signal,
|
|
104
|
-
|
|
101
|
+
value
|
|
105
102
|
};
|
|
106
103
|
}
|
|
107
104
|
|
|
108
|
-
function buildClassCode(classname: string, properties: AnalyzedProperty[], typehint: string | null): string {
|
|
105
|
+
function buildClassCode(classname: string, properties: AnalyzedProperty[], sourceFile: ts.SourceFile, typehint: string | null): string {
|
|
109
106
|
let accessors: string[] = [],
|
|
110
107
|
body: string[] = [],
|
|
111
108
|
constraint: string[] = [],
|
|
@@ -115,7 +112,7 @@ function buildClassCode(classname: string, properties: AnalyzedProperty[], typeh
|
|
|
115
112
|
setters = 0;
|
|
116
113
|
|
|
117
114
|
for (let i = 0, n = properties.length; i < n; i++) {
|
|
118
|
-
let { isStatic, key, type,
|
|
115
|
+
let { isStatic, key, type, value: initializer } = properties[i],
|
|
119
116
|
generic = typehint ? `T['${key}']` : `T${parameters.length}`,
|
|
120
117
|
parameter = `_p${parameters.length}`;
|
|
121
118
|
|
|
@@ -137,7 +134,8 @@ function buildClassCode(classname: string, properties: AnalyzedProperty[], typeh
|
|
|
137
134
|
${NAMESPACE}.write(this.#${key}, ${value});
|
|
138
135
|
}
|
|
139
136
|
`);
|
|
140
|
-
|
|
137
|
+
// A static value is a literal, which no plugin rewrites, so its source is the output
|
|
138
|
+
fields.push(`#${key} = this[${NAMESPACE}.SIGNAL](${initializer.getText(sourceFile)});`);
|
|
141
139
|
}
|
|
142
140
|
else {
|
|
143
141
|
accessors.push(`
|
|
@@ -286,13 +284,13 @@ export default (sourceFile: ts.SourceFile, bindings: Bindings, isReactiveCall: I
|
|
|
286
284
|
let call = ctx.calls[i],
|
|
287
285
|
typehint = call.typehint;
|
|
288
286
|
|
|
289
|
-
prepend.push(buildClassCode(call.classname, call.properties, typehint));
|
|
287
|
+
prepend.push(buildClassCode(call.classname, call.properties, sourceFile, typehint));
|
|
290
288
|
replacements.push({
|
|
291
|
-
generate: () => {
|
|
289
|
+
generate: (_sourceFile, text) => {
|
|
292
290
|
// Every property but a static signal (baked into its field) is a constructor parameter
|
|
293
291
|
let args = call.properties
|
|
294
292
|
.filter(({ isStatic, type }) => typehint || !isStatic || type !== TYPES.Signal)
|
|
295
|
-
.map(p => p.
|
|
293
|
+
.map(p => text(p.value))
|
|
296
294
|
.join(', ');
|
|
297
295
|
|
|
298
296
|
return typehint
|
|
@@ -144,13 +144,14 @@ function isPatternTarget(node: ts.Node): boolean {
|
|
|
144
144
|
((ts.isForOfStatement(parent) || ts.isForInStatement(parent)) && parent.initializer === current);
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
// A binding read as `ns.count` / `ns['count']` is rewritten as a whole
|
|
147
|
+
// A binding read as `ns.count` / `ns['count']` is rewritten as a whole; in `list[count]` the key is
|
|
148
|
+
// the read
|
|
148
149
|
function readNode(identifier: ts.Node): ts.Expression {
|
|
149
150
|
let parent = identifier.parent!;
|
|
150
151
|
|
|
151
152
|
if (
|
|
152
153
|
(ts.isPropertyAccessExpression(parent) && parent.name === identifier) ||
|
|
153
|
-
(ts.isElementAccessExpression(parent) && parent.argumentExpression === identifier)
|
|
154
|
+
(ts.isElementAccessExpression(parent) && parent.argumentExpression === identifier && (ts.isStringLiteral(identifier) || ts.isNoSubstitutionTemplateLiteral(identifier)))
|
|
154
155
|
) {
|
|
155
156
|
return parent;
|
|
156
157
|
}
|
|
@@ -175,11 +176,13 @@ function reference(ctx: TransformContext, node: ts.Expression, kind: TYPES): voi
|
|
|
175
176
|
return;
|
|
176
177
|
}
|
|
177
178
|
|
|
178
|
-
|
|
179
|
+
// The binding itself (`count`, `ns.count`, `ns['count']`) holds nothing another rewrite replaces,
|
|
180
|
+
// so its source is copied as is: text() walks the file's intents on every call
|
|
181
|
+
let binding = node.getText(ctx.bindings.sourceFile);
|
|
179
182
|
|
|
180
183
|
if (ts.isShorthandPropertyAssignment(parent)) {
|
|
181
184
|
ctx.replacements.push({
|
|
182
|
-
generate: () => `${
|
|
185
|
+
generate: () => `${binding}: ${NAMESPACE}.read(${binding})`,
|
|
183
186
|
node: parent
|
|
184
187
|
});
|
|
185
188
|
|
|
@@ -208,7 +211,7 @@ function reference(ctx: TransformContext, node: ts.Expression, kind: TYPES): voi
|
|
|
208
211
|
|
|
209
212
|
if (!writeCtx) {
|
|
210
213
|
ctx.replacements.push({
|
|
211
|
-
generate: () => `${NAMESPACE}.read(${
|
|
214
|
+
generate: () => `${NAMESPACE}.read(${binding})`,
|
|
212
215
|
node
|
|
213
216
|
});
|
|
214
217
|
|
|
@@ -224,7 +227,7 @@ function reference(ctx: TransformContext, node: ts.Expression, kind: TYPES): voi
|
|
|
224
227
|
let right = parent.right;
|
|
225
228
|
|
|
226
229
|
ctx.replacements.push({
|
|
227
|
-
generate: (
|
|
230
|
+
generate: (_sourceFile, text) => `${NAMESPACE}.write(${binding}, ${text(right)})`,
|
|
228
231
|
node: parent
|
|
229
232
|
});
|
|
230
233
|
}
|
|
@@ -233,7 +236,7 @@ function reference(ctx: TransformContext, node: ts.Expression, kind: TYPES): voi
|
|
|
233
236
|
right = parent.right;
|
|
234
237
|
|
|
235
238
|
ctx.replacements.push({
|
|
236
|
-
generate: (
|
|
239
|
+
generate: (_sourceFile, text) => `${NAMESPACE}.write(${binding}, ${binding}.value ${op} ${text(right)})`,
|
|
237
240
|
node: parent
|
|
238
241
|
});
|
|
239
242
|
}
|
|
@@ -243,13 +246,13 @@ function reference(ctx: TransformContext, node: ts.Expression, kind: TYPES): voi
|
|
|
243
246
|
|
|
244
247
|
if (ts.isExpressionStatement(unary.parent)) {
|
|
245
248
|
ctx.replacements.push({
|
|
246
|
-
generate: () => `${NAMESPACE}.write(${
|
|
249
|
+
generate: () => `${NAMESPACE}.write(${binding}, ${binding}.value ${delta})`,
|
|
247
250
|
node: unary
|
|
248
251
|
});
|
|
249
252
|
}
|
|
250
253
|
else if (ts.isPrefixUnaryExpression(unary)) {
|
|
251
254
|
ctx.replacements.push({
|
|
252
|
-
generate: () => `(${NAMESPACE}.write(${
|
|
255
|
+
generate: () => `(${NAMESPACE}.write(${binding}, ${binding}.value ${delta}), ${binding}.value)`,
|
|
253
256
|
node: unary
|
|
254
257
|
});
|
|
255
258
|
}
|
|
@@ -257,7 +260,7 @@ function reference(ctx: TransformContext, node: ts.Expression, kind: TYPES): voi
|
|
|
257
260
|
let tmp = `_t${ctx.tmpCounter++}`;
|
|
258
261
|
|
|
259
262
|
ctx.replacements.push({
|
|
260
|
-
generate: () => `((${tmp}) => (${NAMESPACE}.write(${
|
|
263
|
+
generate: () => `((${tmp}) => (${NAMESPACE}.write(${binding}, ${tmp} ${delta}), ${tmp}))(${binding}.value)`,
|
|
261
264
|
node: unary
|
|
262
265
|
});
|
|
263
266
|
}
|
package/src/reactive/array.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { isArray } from '@esportsplus/utilities';
|
|
2
|
-
import { PACKAGE_NAME, REACTIVE_ARRAY } from '
|
|
3
|
-
import { effect, read, root, signal, write } from '
|
|
4
|
-
import type { Signal } from '~/types';
|
|
2
|
+
import { PACKAGE_NAME, REACTIVE_ARRAY } from '../constants';
|
|
3
|
+
import { effect, read, root, signal, write } from '../system';
|
|
5
4
|
import { isReactiveObject } from './object';
|
|
5
|
+
import type { Signal } from '../types';
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
// One mutation as the batch event reports it: the matching event's payload, named by 'op'
|
|
@@ -431,6 +431,7 @@ class ReactiveArray<T> extends Array<T> {
|
|
|
431
431
|
}
|
|
432
432
|
else {
|
|
433
433
|
let n = this.length,
|
|
434
|
+
// oxlint-disable-next-line unicorn/no-new-array -- preallocated to n, every slot is written next
|
|
434
435
|
before = new Array(n) as T[];
|
|
435
436
|
|
|
436
437
|
for (let i = 0; i < n; i++) {
|
|
@@ -440,6 +441,7 @@ class ReactiveArray<T> extends Array<T> {
|
|
|
440
441
|
super.sort(fn);
|
|
441
442
|
|
|
442
443
|
let buckets = new Map<T, { indices: number[]; next: number }>(),
|
|
444
|
+
// oxlint-disable-next-line unicorn/no-new-array -- preallocated to n, every slot is written next
|
|
443
445
|
order = new Array(n);
|
|
444
446
|
|
|
445
447
|
for (let i = 0; i < n; i++) {
|
package/src/reactive/index.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { isArray, isObject } from '@esportsplus/utilities';
|
|
2
|
-
import { PACKAGE_NAME } from '
|
|
3
|
-
import { onCleanup, root } from '
|
|
4
|
-
import type { Reactive } from '~/types';
|
|
2
|
+
import { PACKAGE_NAME } from '../constants';
|
|
3
|
+
import { onCleanup, root } from '../system';
|
|
5
4
|
import { ReactiveArray } from './array';
|
|
6
5
|
import { ReactiveObject } from './object';
|
|
6
|
+
import type { Reactive } from '../types';
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
type Guard<T> =
|
package/src/reactive/object.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { defineProperty, isArray } from '@esportsplus/utilities';
|
|
2
|
-
import { COMPUTED, REACTIVE_ARRAY, REACTIVE_OBJECT, SIGNAL } from '
|
|
3
|
-
import { computed, dispose, read, root, signal, write } from '
|
|
4
|
-
import { Computed } from '~/types';
|
|
2
|
+
import { COMPUTED, REACTIVE_ARRAY, REACTIVE_OBJECT, SIGNAL } from '../constants';
|
|
3
|
+
import { computed, dispose, read, root, signal, write } from '../system';
|
|
5
4
|
import { ReactiveArray } from './array';
|
|
5
|
+
import type { Computed } from '../types';
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class ReactiveObject<T extends Record<PropertyKey, unknown>> {
|
|
@@ -87,7 +87,7 @@ class ReactiveObject<T extends Record<PropertyKey, unknown>> {
|
|
|
87
87
|
return;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
while (disposer = disposers.pop()) {
|
|
90
|
+
while ((disposer = disposers.pop())) {
|
|
91
91
|
disposer();
|
|
92
92
|
}
|
|
93
93
|
}
|