@esportsplus/reactivity 0.22.3 → 0.23.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/build/index.d.ts +1 -1
- package/build/index.js +1 -1
- package/build/reactive/array.d.ts +3 -0
- package/build/reactive/array.js +32 -2
- package/build/reactive/index.d.ts +17 -14
- package/build/reactive/index.js +7 -25
- package/build/system.js +1 -1
- package/build/transformer/core/detector.d.ts +2 -0
- package/build/transformer/core/detector.js +6 -0
- package/build/transformer/core/index.d.ts +10 -0
- package/build/transformer/core/index.js +55 -0
- package/build/transformer/core/transforms/auto-dispose.d.ts +3 -0
- package/build/transformer/core/transforms/auto-dispose.js +116 -0
- package/build/transformer/core/transforms/reactive-array.d.ts +4 -0
- package/build/transformer/core/transforms/reactive-array.js +89 -0
- package/build/transformer/core/transforms/reactive-object.d.ts +4 -0
- package/build/transformer/core/transforms/reactive-object.js +155 -0
- package/build/transformer/core/transforms/reactive-primitives.d.ts +4 -0
- package/build/transformer/core/transforms/reactive-primitives.js +325 -0
- package/build/transformer/core/transforms/utilities.d.ts +9 -0
- package/build/transformer/core/transforms/utilities.js +57 -0
- package/build/transformer/plugins/esbuild.d.ts +5 -0
- package/build/transformer/plugins/esbuild.js +30 -0
- package/build/transformer/plugins/tsc.d.ts +3 -0
- package/build/transformer/plugins/tsc.js +4 -0
- package/build/transformer/plugins/vite.d.ts +5 -0
- package/build/transformer/plugins/vite.js +28 -0
- package/build/types.d.ts +14 -4
- package/package.json +34 -3
- package/readme.md +276 -2
- package/src/constants.ts +1 -1
- package/src/index.ts +1 -1
- package/src/reactive/array.ts +49 -2
- package/src/reactive/index.ts +33 -57
- package/src/system.ts +14 -5
- package/src/transformer/core/detector.ts +12 -0
- package/src/transformer/core/index.ts +82 -0
- package/src/transformer/core/transforms/auto-dispose.ts +194 -0
- package/src/transformer/core/transforms/reactive-array.ts +140 -0
- package/src/transformer/core/transforms/reactive-object.ts +244 -0
- package/src/transformer/core/transforms/reactive-primitives.ts +459 -0
- package/src/transformer/core/transforms/utilities.ts +95 -0
- package/src/transformer/plugins/esbuild.ts +46 -0
- package/src/transformer/plugins/tsc.ts +8 -0
- package/src/transformer/plugins/vite.ts +41 -0
- package/src/types.ts +24 -5
- package/test/arrays.ts +146 -0
- package/test/effects.ts +168 -0
- package/test/index.ts +8 -0
- package/test/nested.ts +201 -0
- package/test/objects.ts +106 -0
- package/test/primitives.ts +171 -0
- package/test/vite.config.ts +40 -0
- package/build/reactive/object.d.ts +0 -7
- package/build/reactive/object.js +0 -79
- package/src/reactive/object.ts +0 -116
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
import type { BindingType, Bindings } from '~/types';
|
|
2
|
+
import { uid } from '@esportsplus/typescript/transformer';
|
|
3
|
+
import ts from 'typescript';
|
|
4
|
+
import { addMissingImports, applyReplacements, Replacement } from './utilities';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
interface ComputedArgRange {
|
|
8
|
+
end: number;
|
|
9
|
+
start: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface ScopeBinding {
|
|
13
|
+
name: string;
|
|
14
|
+
scope: ts.Node;
|
|
15
|
+
type: BindingType;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
function findEnclosingScope(node: ts.Node): ts.Node {
|
|
20
|
+
let current = node.parent;
|
|
21
|
+
|
|
22
|
+
while (current) {
|
|
23
|
+
if (
|
|
24
|
+
ts.isBlock(current) ||
|
|
25
|
+
ts.isSourceFile(current) ||
|
|
26
|
+
ts.isFunctionDeclaration(current) ||
|
|
27
|
+
ts.isFunctionExpression(current) ||
|
|
28
|
+
ts.isArrowFunction(current) ||
|
|
29
|
+
ts.isForStatement(current) ||
|
|
30
|
+
ts.isForInStatement(current) ||
|
|
31
|
+
ts.isForOfStatement(current)
|
|
32
|
+
) {
|
|
33
|
+
return current;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
current = current.parent;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return node.getSourceFile();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function findBinding(bindings: ScopeBinding[], name: string, node: ts.Node): ScopeBinding | undefined {
|
|
43
|
+
for (let i = 0, n = bindings.length; i < n; i++) {
|
|
44
|
+
let b = bindings[i];
|
|
45
|
+
|
|
46
|
+
if (b.name === name && isInScope(node, b)) {
|
|
47
|
+
return b;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isInComputedRange(ranges: ComputedArgRange[], start: number, end: number): boolean {
|
|
55
|
+
for (let i = 0, n = ranges.length; i < n; i++) {
|
|
56
|
+
let r = ranges[i];
|
|
57
|
+
|
|
58
|
+
if (start >= r.start && end <= r.end) {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isInScope(reference: ts.Node, binding: ScopeBinding): boolean {
|
|
67
|
+
let current: ts.Node | undefined = reference;
|
|
68
|
+
|
|
69
|
+
while (current) {
|
|
70
|
+
if (current === binding.scope) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
current = current.parent;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function classifyReactiveArg(arg: ts.Expression): 'computed' | 'signal' | null {
|
|
81
|
+
if (ts.isArrowFunction(arg) || ts.isFunctionExpression(arg)) {
|
|
82
|
+
return 'computed';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (ts.isObjectLiteralExpression(arg) || ts.isArrayLiteralExpression(arg)) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return 'signal';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isInDeclarationInit(node: ts.Node): boolean {
|
|
93
|
+
let parent = node.parent;
|
|
94
|
+
|
|
95
|
+
if (ts.isVariableDeclaration(parent) && parent.initializer === node) {
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function isReactiveReassignment(node: ts.Node): boolean {
|
|
103
|
+
let parent = node.parent;
|
|
104
|
+
|
|
105
|
+
if (ts.isBinaryExpression(parent) &&
|
|
106
|
+
parent.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
|
107
|
+
parent.right === node &&
|
|
108
|
+
ts.isCallExpression(node) &&
|
|
109
|
+
ts.isIdentifier((node as ts.CallExpression).expression) &&
|
|
110
|
+
((node as ts.CallExpression).expression as ts.Identifier).text === 'reactive') {
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function isWriteContext(node: ts.Identifier): 'simple' | 'compound' | 'increment' | false {
|
|
118
|
+
let parent = node.parent;
|
|
119
|
+
|
|
120
|
+
if (ts.isBinaryExpression(parent) && parent.left === node) {
|
|
121
|
+
let op = parent.operatorToken.kind;
|
|
122
|
+
|
|
123
|
+
if (op === ts.SyntaxKind.EqualsToken) {
|
|
124
|
+
return 'simple';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (op >= ts.SyntaxKind.PlusEqualsToken && op <= ts.SyntaxKind.CaretEqualsToken) {
|
|
128
|
+
return 'compound';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (
|
|
132
|
+
op === ts.SyntaxKind.AmpersandAmpersandEqualsToken ||
|
|
133
|
+
op === ts.SyntaxKind.BarBarEqualsToken ||
|
|
134
|
+
op === ts.SyntaxKind.QuestionQuestionEqualsToken
|
|
135
|
+
) {
|
|
136
|
+
return 'compound';
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (ts.isPostfixUnaryExpression(parent) || ts.isPrefixUnaryExpression(parent)) {
|
|
141
|
+
let op = parent.operator;
|
|
142
|
+
|
|
143
|
+
if (op === ts.SyntaxKind.PlusPlusToken || op === ts.SyntaxKind.MinusMinusToken) {
|
|
144
|
+
return 'increment';
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function getCompoundOperator(kind: ts.SyntaxKind): string {
|
|
152
|
+
if (kind === ts.SyntaxKind.PlusEqualsToken) {
|
|
153
|
+
return '+';
|
|
154
|
+
}
|
|
155
|
+
else if (kind === ts.SyntaxKind.MinusEqualsToken) {
|
|
156
|
+
return '-';
|
|
157
|
+
}
|
|
158
|
+
else if (kind === ts.SyntaxKind.AsteriskEqualsToken) {
|
|
159
|
+
return '*';
|
|
160
|
+
}
|
|
161
|
+
else if (kind === ts.SyntaxKind.SlashEqualsToken) {
|
|
162
|
+
return '/';
|
|
163
|
+
}
|
|
164
|
+
else if (kind === ts.SyntaxKind.PercentEqualsToken) {
|
|
165
|
+
return '%';
|
|
166
|
+
}
|
|
167
|
+
else if (kind === ts.SyntaxKind.AsteriskAsteriskEqualsToken) {
|
|
168
|
+
return '**';
|
|
169
|
+
}
|
|
170
|
+
else if (kind === ts.SyntaxKind.AmpersandEqualsToken) {
|
|
171
|
+
return '&';
|
|
172
|
+
}
|
|
173
|
+
else if (kind === ts.SyntaxKind.BarEqualsToken) {
|
|
174
|
+
return '|';
|
|
175
|
+
}
|
|
176
|
+
else if (kind === ts.SyntaxKind.CaretEqualsToken) {
|
|
177
|
+
return '^';
|
|
178
|
+
}
|
|
179
|
+
else if (kind === ts.SyntaxKind.LessThanLessThanEqualsToken) {
|
|
180
|
+
return '<<';
|
|
181
|
+
}
|
|
182
|
+
else if (kind === ts.SyntaxKind.GreaterThanGreaterThanEqualsToken) {
|
|
183
|
+
return '>>';
|
|
184
|
+
}
|
|
185
|
+
else if (kind === ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken) {
|
|
186
|
+
return '>>>';
|
|
187
|
+
}
|
|
188
|
+
else if (kind === ts.SyntaxKind.AmpersandAmpersandEqualsToken) {
|
|
189
|
+
return '&&';
|
|
190
|
+
}
|
|
191
|
+
else if (kind === ts.SyntaxKind.BarBarEqualsToken) {
|
|
192
|
+
return '||';
|
|
193
|
+
}
|
|
194
|
+
else if (kind === ts.SyntaxKind.QuestionQuestionEqualsToken) {
|
|
195
|
+
return '??';
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
return '+';
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function transformComputedArg(
|
|
203
|
+
arg: ts.Expression,
|
|
204
|
+
scopedBindings: ScopeBinding[],
|
|
205
|
+
sourceFile: ts.SourceFile,
|
|
206
|
+
neededImports: Set<string>
|
|
207
|
+
): string {
|
|
208
|
+
let argStart = arg.getStart(sourceFile),
|
|
209
|
+
innerReplacements: Replacement[] = [],
|
|
210
|
+
text = arg.getText(sourceFile);
|
|
211
|
+
|
|
212
|
+
function visitArg(node: ts.Node): void {
|
|
213
|
+
// Only transform identifiers that are signal bindings
|
|
214
|
+
if (ts.isIdentifier(node)) {
|
|
215
|
+
// Skip if it's a property name in property access (obj.prop - skip prop)
|
|
216
|
+
if (ts.isPropertyAccessExpression(node.parent) && node.parent.name === node) {
|
|
217
|
+
ts.forEachChild(node, visitArg);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Skip if it's the function name in a call expression
|
|
222
|
+
if (ts.isCallExpression(node.parent) && node.parent.expression === node) {
|
|
223
|
+
ts.forEachChild(node, visitArg);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
let binding = findBinding(scopedBindings, node.text, node);
|
|
228
|
+
|
|
229
|
+
if (binding) {
|
|
230
|
+
neededImports.add('read');
|
|
231
|
+
|
|
232
|
+
innerReplacements.push({
|
|
233
|
+
end: node.end - argStart,
|
|
234
|
+
newText: `read(${node.text})`,
|
|
235
|
+
start: node.getStart(sourceFile) - argStart
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
ts.forEachChild(node, visitArg);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
visitArg(arg);
|
|
244
|
+
|
|
245
|
+
return applyReplacements(text, innerReplacements);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
const transformReactivePrimitives = (
|
|
250
|
+
sourceFile: ts.SourceFile,
|
|
251
|
+
bindings: Bindings
|
|
252
|
+
): string => {
|
|
253
|
+
let code = sourceFile.getFullText(),
|
|
254
|
+
computedArgRanges: ComputedArgRange[] = [],
|
|
255
|
+
hasReactiveImport = false,
|
|
256
|
+
neededImports = new Set<string>(),
|
|
257
|
+
replacements: Replacement[] = [],
|
|
258
|
+
scopedBindings: ScopeBinding[] = [];
|
|
259
|
+
|
|
260
|
+
// Single-pass visitor: detect imports, bindings, and usages together
|
|
261
|
+
function visit(node: ts.Node): void {
|
|
262
|
+
if (
|
|
263
|
+
ts.isImportDeclaration(node) &&
|
|
264
|
+
ts.isStringLiteral(node.moduleSpecifier) &&
|
|
265
|
+
node.moduleSpecifier.text.includes('@esportsplus/reactivity')
|
|
266
|
+
) {
|
|
267
|
+
let clause = node.importClause;
|
|
268
|
+
|
|
269
|
+
if (clause?.namedBindings && ts.isNamedImports(clause.namedBindings)) {
|
|
270
|
+
for (let i = 0, n = clause.namedBindings.elements.length; i < n; i++) {
|
|
271
|
+
if (clause.namedBindings.elements[i].name.text === 'reactive') {
|
|
272
|
+
hasReactiveImport = true;
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (
|
|
280
|
+
hasReactiveImport &&
|
|
281
|
+
ts.isCallExpression(node) &&
|
|
282
|
+
ts.isIdentifier(node.expression) &&
|
|
283
|
+
node.expression.text === 'reactive' &&
|
|
284
|
+
node.arguments.length > 0
|
|
285
|
+
) {
|
|
286
|
+
|
|
287
|
+
let arg = node.arguments[0],
|
|
288
|
+
classification = classifyReactiveArg(arg);
|
|
289
|
+
|
|
290
|
+
if (classification) {
|
|
291
|
+
let varName: string | null = null;
|
|
292
|
+
|
|
293
|
+
if (ts.isVariableDeclaration(node.parent) && ts.isIdentifier(node.parent.name)) {
|
|
294
|
+
varName = node.parent.name.text;
|
|
295
|
+
}
|
|
296
|
+
else if (
|
|
297
|
+
ts.isBinaryExpression(node.parent) &&
|
|
298
|
+
node.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
|
299
|
+
ts.isIdentifier(node.parent.left)
|
|
300
|
+
) {
|
|
301
|
+
varName = node.parent.left.text;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (varName) {
|
|
305
|
+
let scope = findEnclosingScope(node);
|
|
306
|
+
|
|
307
|
+
scopedBindings.push({ name: varName, scope, type: classification });
|
|
308
|
+
bindings.set(varName, classification);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (classification === 'computed') {
|
|
312
|
+
// Track range to skip identifiers inside when visitor continues
|
|
313
|
+
computedArgRanges.push({
|
|
314
|
+
end: arg.end,
|
|
315
|
+
start: arg.getStart(sourceFile)
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// Transform signal references inside the computed arg
|
|
319
|
+
let argText = transformComputedArg(arg, scopedBindings, sourceFile, neededImports);
|
|
320
|
+
|
|
321
|
+
replacements.push({
|
|
322
|
+
end: node.end,
|
|
323
|
+
newText: `computed(${argText})`,
|
|
324
|
+
start: node.pos
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
neededImports.add('computed');
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
let argText = arg.getText(sourceFile);
|
|
331
|
+
|
|
332
|
+
replacements.push({
|
|
333
|
+
end: node.end,
|
|
334
|
+
newText: `signal(${argText})`,
|
|
335
|
+
start: node.pos
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
neededImports.add('signal');
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Transform identifier usages
|
|
344
|
+
if (ts.isIdentifier(node) && !isInDeclarationInit(node.parent)) {
|
|
345
|
+
// Skip if it's a property name in property access (obj.prop - skip prop)
|
|
346
|
+
if (ts.isPropertyAccessExpression(node.parent) && node.parent.name === node) {
|
|
347
|
+
ts.forEachChild(node, visit);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
let nodeStart = node.getStart(sourceFile);
|
|
352
|
+
|
|
353
|
+
// Skip if inside a computed arg we already transformed
|
|
354
|
+
let insideComputedArg = isInComputedRange(computedArgRanges, nodeStart, node.end);
|
|
355
|
+
|
|
356
|
+
if (insideComputedArg) {
|
|
357
|
+
ts.forEachChild(node, visit);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
let binding = findBinding(scopedBindings, node.text, node),
|
|
362
|
+
name = node.text;
|
|
363
|
+
|
|
364
|
+
if (binding) {
|
|
365
|
+
if (
|
|
366
|
+
!isReactiveReassignment(node.parent) &&
|
|
367
|
+
!(ts.isTypeOfExpression(node.parent) && node.parent.expression === node)
|
|
368
|
+
) {
|
|
369
|
+
let writeCtx = isWriteContext(node);
|
|
370
|
+
|
|
371
|
+
if (writeCtx) {
|
|
372
|
+
if (binding.type !== 'computed') {
|
|
373
|
+
neededImports.add('set');
|
|
374
|
+
|
|
375
|
+
let parent = node.parent;
|
|
376
|
+
|
|
377
|
+
if (writeCtx === 'simple' && ts.isBinaryExpression(parent)) {
|
|
378
|
+
let valueText = parent.right.getText(sourceFile);
|
|
379
|
+
|
|
380
|
+
replacements.push({
|
|
381
|
+
end: parent.end,
|
|
382
|
+
newText: `set(${name}, ${valueText})`,
|
|
383
|
+
start: parent.pos
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
else if (writeCtx === 'compound' && ts.isBinaryExpression(parent)) {
|
|
387
|
+
let op = getCompoundOperator(parent.operatorToken.kind),
|
|
388
|
+
valueText = parent.right.getText(sourceFile);
|
|
389
|
+
|
|
390
|
+
replacements.push({
|
|
391
|
+
end: parent.end,
|
|
392
|
+
newText: `set(${name}, ${name}.value ${op} ${valueText})`,
|
|
393
|
+
start: parent.pos
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
else if (writeCtx === 'increment') {
|
|
397
|
+
let isPrefix = ts.isPrefixUnaryExpression(parent),
|
|
398
|
+
op = (parent as ts.PrefixUnaryExpression | ts.PostfixUnaryExpression).operator,
|
|
399
|
+
delta = op === ts.SyntaxKind.PlusPlusToken ? '+ 1' : '- 1';
|
|
400
|
+
|
|
401
|
+
if (ts.isExpressionStatement(parent.parent)) {
|
|
402
|
+
replacements.push({
|
|
403
|
+
end: parent.end,
|
|
404
|
+
newText: `set(${name}, ${name}.value ${delta})`,
|
|
405
|
+
start: parent.pos
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
else if (isPrefix) {
|
|
409
|
+
replacements.push({
|
|
410
|
+
end: parent.end,
|
|
411
|
+
newText: `(set(${name}, ${name}.value ${delta}), ${name}.value)`,
|
|
412
|
+
start: parent.pos
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
else {
|
|
416
|
+
let tmp = uid('tmp');
|
|
417
|
+
|
|
418
|
+
replacements.push({
|
|
419
|
+
end: parent.end,
|
|
420
|
+
newText: `((${tmp}) => (set(${name}, ${tmp} ${delta}), ${tmp}))(${name}.value)`,
|
|
421
|
+
start: parent.pos
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
neededImports.add('read');
|
|
429
|
+
|
|
430
|
+
replacements.push({
|
|
431
|
+
end: node.end,
|
|
432
|
+
newText: `read(${name})`,
|
|
433
|
+
start: node.pos
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
ts.forEachChild(node, visit);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
visit(sourceFile);
|
|
444
|
+
|
|
445
|
+
if (replacements.length === 0) {
|
|
446
|
+
return code;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
let result = applyReplacements(code, replacements);
|
|
450
|
+
|
|
451
|
+
if (neededImports.size > 0) {
|
|
452
|
+
result = addMissingImports(result, neededImports);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return result;
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
export { transformReactivePrimitives };
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { Replacement } from '@esportsplus/typescript/transformer';
|
|
2
|
+
import { applyReplacements } from '@esportsplus/typescript/transformer';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
type ExtraImport = {
|
|
6
|
+
module: string;
|
|
7
|
+
specifier: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
const BRACES_CONTENT_REGEX = /\{([^}]*)\}/;
|
|
12
|
+
|
|
13
|
+
const REACTIVITY_IMPORT_REGEX = /(import\s*\{[^}]*\}\s*from\s*['"]@esportsplus\/reactivity['"])/;
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
const addMissingImports = (code: string, needed: Set<string>, extraImports?: ExtraImport[]): string => {
|
|
17
|
+
let reactivityImportMatch = code.match(REACTIVITY_IMPORT_REGEX);
|
|
18
|
+
|
|
19
|
+
if (!reactivityImportMatch) {
|
|
20
|
+
return code;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let bracesMatch = reactivityImportMatch[1].match(BRACES_CONTENT_REGEX),
|
|
24
|
+
existing = new Set<string>(),
|
|
25
|
+
existingImport = reactivityImportMatch[1],
|
|
26
|
+
extraSpecifiers = new Set<string>(),
|
|
27
|
+
toAdd: string[] = [];
|
|
28
|
+
|
|
29
|
+
if (bracesMatch?.[1]) {
|
|
30
|
+
let parts = bracesMatch[1].split(',');
|
|
31
|
+
|
|
32
|
+
for (let i = 0, n = parts.length; i < n; i++) {
|
|
33
|
+
let trimmed = parts[i].trim();
|
|
34
|
+
|
|
35
|
+
if (trimmed) {
|
|
36
|
+
existing.add(trimmed);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (extraImports) {
|
|
42
|
+
for (let i = 0, n = extraImports.length; i < n; i++) {
|
|
43
|
+
extraSpecifiers.add(extraImports[i].specifier);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
for (let imp of needed) {
|
|
48
|
+
if (!extraSpecifiers.has(imp) && !existing.has(imp)) {
|
|
49
|
+
toAdd.push(imp);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (toAdd.length > 0) {
|
|
54
|
+
let combined: string[] = [];
|
|
55
|
+
|
|
56
|
+
for (let item of existing) {
|
|
57
|
+
if (item) {
|
|
58
|
+
combined.push(item);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
for (let i = 0, n = toAdd.length; i < n; i++) {
|
|
63
|
+
if (toAdd[i]) {
|
|
64
|
+
combined.push(toAdd[i]);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
combined.sort();
|
|
69
|
+
|
|
70
|
+
code = code.replace(
|
|
71
|
+
existingImport,
|
|
72
|
+
existingImport.replace(BRACES_CONTENT_REGEX, `{ ${combined.join(', ')} }`)
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (extraImports) {
|
|
77
|
+
for (let i = 0, n = extraImports.length; i < n; i++) {
|
|
78
|
+
let extra = extraImports[i];
|
|
79
|
+
|
|
80
|
+
if (needed.has(extra.specifier) && !code.includes(extra.module)) {
|
|
81
|
+
let insertPos = code.indexOf('import');
|
|
82
|
+
|
|
83
|
+
code = code.substring(0, insertPos) +
|
|
84
|
+
`import { ${extra.specifier} } from '${extra.module}';\n` +
|
|
85
|
+
code.substring(insertPos);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return code;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
export { addMissingImports, applyReplacements };
|
|
95
|
+
export type { ExtraImport, Replacement };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { mightNeedTransform, transform } from '~/transformer/core';
|
|
2
|
+
import type { OnLoadArgs, Plugin, PluginBuild } from 'esbuild';
|
|
3
|
+
import type { TransformOptions } from '~/types';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import ts from 'typescript';
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
export default (options?: TransformOptions): Plugin => {
|
|
9
|
+
return {
|
|
10
|
+
name: '@esportsplus/reactivity/plugin-esbuild',
|
|
11
|
+
|
|
12
|
+
setup(build: PluginBuild) {
|
|
13
|
+
build.onLoad({ filter: /\.[tj]sx?$/ }, async (args: OnLoadArgs) => {
|
|
14
|
+
let code = await fs.promises.readFile(args.path, 'utf8');
|
|
15
|
+
|
|
16
|
+
if (!mightNeedTransform(code)) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
let sourceFile = ts.createSourceFile(
|
|
22
|
+
args.path,
|
|
23
|
+
code,
|
|
24
|
+
ts.ScriptTarget.Latest,
|
|
25
|
+
true
|
|
26
|
+
),
|
|
27
|
+
result = transform(sourceFile, options);
|
|
28
|
+
|
|
29
|
+
if (!result.transformed) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
contents: result.code,
|
|
35
|
+
loader: args.path.endsWith('x') ? 'tsx' : 'ts'
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
console.error(`@esportsplus/reactivity: Error transforming ${args.path}:`, error);
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
export type { TransformOptions as PluginOptions };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { createTransformer } from '~/transformer/core';
|
|
2
|
+
import ts from 'typescript';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
// TypeScript custom transformers API requires program parameter, but we don't use it
|
|
6
|
+
export default (_program: ts.Program): ts.TransformerFactory<ts.SourceFile> => {
|
|
7
|
+
return createTransformer();
|
|
8
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { mightNeedTransform, transform } from '~/transformer/core';
|
|
2
|
+
import type { Plugin } from 'vite';
|
|
3
|
+
import type { TransformOptions } from '~/types';
|
|
4
|
+
import ts from 'typescript';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
const TRANSFORM_PATTERN = /\.[tj]sx?$/;
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
export default (options?: TransformOptions): Plugin => {
|
|
11
|
+
return {
|
|
12
|
+
enforce: 'pre',
|
|
13
|
+
name: '@esportsplus/reactivity/plugin-vite',
|
|
14
|
+
|
|
15
|
+
transform(code: string, id: string) {
|
|
16
|
+
if (!TRANSFORM_PATTERN.test(id) || id.includes('node_modules')) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!mightNeedTransform(code)) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
let sourceFile = ts.createSourceFile(id, code, ts.ScriptTarget.Latest, true),
|
|
26
|
+
result = transform(sourceFile, options);
|
|
27
|
+
|
|
28
|
+
if (!result.transformed) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return { code: result.code, map: null };
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
console.error(`@esportsplus/reactivity: Error transforming ${id}:`, error);
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
export type { TransformOptions as PluginOptions };
|
package/src/types.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { COMPUTED, SIGNAL, STATE_CHECK, STATE_DIRTY, STATE_IN_HEAP, STATE_NONE, STATE_RECOMPUTING } from './constants';
|
|
2
|
-
import { ReactiveArray } from './reactive
|
|
3
|
-
import
|
|
2
|
+
import { ReactiveArray, ReactiveObject } from './reactive';
|
|
3
|
+
import ts from 'typescript';
|
|
4
4
|
|
|
5
5
|
|
|
6
|
+
type BindingType = 'array' | 'computed' | 'object' | 'signal';
|
|
7
|
+
|
|
8
|
+
type Bindings = Map<string, BindingType>;
|
|
9
|
+
|
|
6
10
|
interface Computed<T> {
|
|
7
11
|
cleanup: VoidFunction | VoidFunction[] | null;
|
|
8
12
|
deps: Link | null;
|
|
@@ -25,10 +29,10 @@ interface Computed<T> {
|
|
|
25
29
|
|
|
26
30
|
interface Link {
|
|
27
31
|
dep: Signal<unknown> | Computed<unknown>;
|
|
28
|
-
sub: Computed<unknown>;
|
|
29
32
|
nextDep: Link | null;
|
|
30
33
|
nextSub: Link | null;
|
|
31
34
|
prevSub: Link | null;
|
|
35
|
+
sub: Computed<unknown>;
|
|
32
36
|
version: number;
|
|
33
37
|
}
|
|
34
38
|
|
|
@@ -39,10 +43,25 @@ type Signal<T> = {
|
|
|
39
43
|
value: T;
|
|
40
44
|
};
|
|
41
45
|
|
|
46
|
+
interface TransformOptions {
|
|
47
|
+
autoDispose?: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface TransformResult {
|
|
51
|
+
code: string;
|
|
52
|
+
sourceFile: ts.SourceFile;
|
|
53
|
+
transformed: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
42
56
|
|
|
43
57
|
export type {
|
|
58
|
+
BindingType,
|
|
59
|
+
Bindings,
|
|
44
60
|
Computed,
|
|
45
61
|
Link,
|
|
62
|
+
ReactiveArray,
|
|
63
|
+
ReactiveObject,
|
|
46
64
|
Signal,
|
|
47
|
-
|
|
48
|
-
|
|
65
|
+
TransformOptions,
|
|
66
|
+
TransformResult
|
|
67
|
+
};
|