@esportsplus/reactivity 0.23.0 → 0.23.1

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.
Files changed (39) hide show
  1. package/build/transformer/detector.js +38 -0
  2. package/build/transformer/{core/index.d.ts → index.d.ts} +1 -1
  3. package/build/transformer/plugins/esbuild.js +3 -2
  4. package/build/transformer/plugins/tsc.js +1 -1
  5. package/build/transformer/plugins/vite.js +2 -2
  6. package/build/transformer/transforms/auto-dispose.js +119 -0
  7. package/build/transformer/{core/transforms → transforms}/reactive-array.d.ts +1 -1
  8. package/build/transformer/transforms/reactive-array.js +93 -0
  9. package/build/transformer/{core/transforms → transforms}/reactive-object.d.ts +1 -1
  10. package/build/transformer/transforms/reactive-object.js +164 -0
  11. package/build/transformer/{core/transforms → transforms}/reactive-primitives.d.ts +1 -1
  12. package/build/transformer/transforms/reactive-primitives.js +335 -0
  13. package/build/transformer/{core/transforms → transforms}/utilities.d.ts +1 -2
  14. package/build/transformer/transforms/utilities.js +73 -0
  15. package/package.json +8 -12
  16. package/src/transformer/detector.ts +65 -0
  17. package/src/transformer/{core/index.ts → index.ts} +1 -5
  18. package/src/transformer/plugins/esbuild.ts +3 -2
  19. package/src/transformer/plugins/tsc.ts +1 -1
  20. package/src/transformer/plugins/vite.ts +2 -4
  21. package/src/transformer/transforms/auto-dispose.ts +191 -0
  22. package/src/transformer/transforms/reactive-array.ts +143 -0
  23. package/src/transformer/{core/transforms → transforms}/reactive-object.ts +101 -92
  24. package/src/transformer/transforms/reactive-primitives.ts +461 -0
  25. package/src/transformer/transforms/utilities.ts +119 -0
  26. package/build/transformer/core/detector.js +0 -6
  27. package/build/transformer/core/transforms/auto-dispose.js +0 -116
  28. package/build/transformer/core/transforms/reactive-array.js +0 -89
  29. package/build/transformer/core/transforms/reactive-object.js +0 -155
  30. package/build/transformer/core/transforms/reactive-primitives.js +0 -325
  31. package/build/transformer/core/transforms/utilities.js +0 -57
  32. package/src/transformer/core/detector.ts +0 -12
  33. package/src/transformer/core/transforms/auto-dispose.ts +0 -194
  34. package/src/transformer/core/transforms/reactive-array.ts +0 -140
  35. package/src/transformer/core/transforms/reactive-primitives.ts +0 -459
  36. package/src/transformer/core/transforms/utilities.ts +0 -95
  37. /package/build/transformer/{core/detector.d.ts → detector.d.ts} +0 -0
  38. /package/build/transformer/{core/index.js → index.js} +0 -0
  39. /package/build/transformer/{core/transforms → transforms}/auto-dispose.d.ts +0 -0
@@ -0,0 +1,461 @@
1
+ import { uid, type Range } from '@esportsplus/typescript/transformer';
2
+ import type { BindingType, Bindings } from '~/types';
3
+ import { addMissingImports, applyReplacements, Replacement } from './utilities';
4
+ import ts from 'typescript';
5
+
6
+
7
+ interface ArgContext {
8
+ argStart: number;
9
+ innerReplacements: Replacement[];
10
+ neededImports: Set<string>;
11
+ scopedBindings: ScopeBinding[];
12
+ sourceFile: ts.SourceFile;
13
+ }
14
+
15
+ interface ScopeBinding {
16
+ name: string;
17
+ scope: ts.Node;
18
+ type: BindingType;
19
+ }
20
+
21
+ interface TransformContext {
22
+ bindings: Bindings;
23
+ computedArgRanges: Range[];
24
+ hasReactiveImport: boolean;
25
+ neededImports: Set<string>;
26
+ replacements: Replacement[];
27
+ scopedBindings: ScopeBinding[];
28
+ sourceFile: ts.SourceFile;
29
+ }
30
+
31
+
32
+ function classifyReactiveArg(arg: ts.Expression): 'computed' | 'signal' | null {
33
+ if (ts.isArrowFunction(arg) || ts.isFunctionExpression(arg)) {
34
+ return 'computed';
35
+ }
36
+
37
+ if (ts.isObjectLiteralExpression(arg) || ts.isArrayLiteralExpression(arg)) {
38
+ return null;
39
+ }
40
+
41
+ return 'signal';
42
+ }
43
+
44
+ function findBinding(bindings: ScopeBinding[], name: string, node: ts.Node): ScopeBinding | undefined {
45
+ for (let i = 0, n = bindings.length; i < n; i++) {
46
+ let b = bindings[i];
47
+
48
+ if (b.name === name && isInScope(node, b)) {
49
+ return b;
50
+ }
51
+ }
52
+
53
+ return undefined;
54
+ }
55
+
56
+ function findEnclosingScope(node: ts.Node): ts.Node {
57
+ let current = node.parent;
58
+
59
+ while (current) {
60
+ if (
61
+ ts.isBlock(current) ||
62
+ ts.isSourceFile(current) ||
63
+ ts.isFunctionDeclaration(current) ||
64
+ ts.isFunctionExpression(current) ||
65
+ ts.isArrowFunction(current) ||
66
+ ts.isForStatement(current) ||
67
+ ts.isForInStatement(current) ||
68
+ ts.isForOfStatement(current)
69
+ ) {
70
+ return current;
71
+ }
72
+
73
+ current = current.parent;
74
+ }
75
+
76
+ return node.getSourceFile();
77
+ }
78
+
79
+ function getCompoundOperator(kind: ts.SyntaxKind): string {
80
+ if (kind === ts.SyntaxKind.PlusEqualsToken) {
81
+ return '+';
82
+ }
83
+ else if (kind === ts.SyntaxKind.MinusEqualsToken) {
84
+ return '-';
85
+ }
86
+ else if (kind === ts.SyntaxKind.AsteriskEqualsToken) {
87
+ return '*';
88
+ }
89
+ else if (kind === ts.SyntaxKind.SlashEqualsToken) {
90
+ return '/';
91
+ }
92
+ else if (kind === ts.SyntaxKind.PercentEqualsToken) {
93
+ return '%';
94
+ }
95
+ else if (kind === ts.SyntaxKind.AsteriskAsteriskEqualsToken) {
96
+ return '**';
97
+ }
98
+ else if (kind === ts.SyntaxKind.AmpersandEqualsToken) {
99
+ return '&';
100
+ }
101
+ else if (kind === ts.SyntaxKind.BarEqualsToken) {
102
+ return '|';
103
+ }
104
+ else if (kind === ts.SyntaxKind.CaretEqualsToken) {
105
+ return '^';
106
+ }
107
+ else if (kind === ts.SyntaxKind.LessThanLessThanEqualsToken) {
108
+ return '<<';
109
+ }
110
+ else if (kind === ts.SyntaxKind.GreaterThanGreaterThanEqualsToken) {
111
+ return '>>';
112
+ }
113
+ else if (kind === ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken) {
114
+ return '>>>';
115
+ }
116
+ else if (kind === ts.SyntaxKind.AmpersandAmpersandEqualsToken) {
117
+ return '&&';
118
+ }
119
+ else if (kind === ts.SyntaxKind.BarBarEqualsToken) {
120
+ return '||';
121
+ }
122
+ else if (kind === ts.SyntaxKind.QuestionQuestionEqualsToken) {
123
+ return '??';
124
+ }
125
+ else {
126
+ return '+';
127
+ }
128
+ }
129
+
130
+ function isInComputedRange(ranges: Range[], start: number, end: number): boolean {
131
+ for (let i = 0, n = ranges.length; i < n; i++) {
132
+ let r = ranges[i];
133
+
134
+ if (start >= r.start && end <= r.end) {
135
+ return true;
136
+ }
137
+ }
138
+
139
+ return false;
140
+ }
141
+
142
+ function isInDeclarationInit(node: ts.Node): boolean {
143
+ let parent = node.parent;
144
+
145
+ if (ts.isVariableDeclaration(parent) && parent.initializer === node) {
146
+ return true;
147
+ }
148
+
149
+ return false;
150
+ }
151
+
152
+ function isInScope(reference: ts.Node, binding: ScopeBinding): boolean {
153
+ let current: ts.Node | undefined = reference;
154
+
155
+ while (current) {
156
+ if (current === binding.scope) {
157
+ return true;
158
+ }
159
+
160
+ current = current.parent;
161
+ }
162
+
163
+ return false;
164
+ }
165
+
166
+ function isReactiveReassignment(node: ts.Node): boolean {
167
+ let parent = node.parent;
168
+
169
+ if (
170
+ ts.isBinaryExpression(parent) &&
171
+ parent.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
172
+ parent.right === node &&
173
+ ts.isCallExpression(node) &&
174
+ ts.isIdentifier((node as ts.CallExpression).expression) &&
175
+ ((node as ts.CallExpression).expression as ts.Identifier).text === 'reactive'
176
+ ) {
177
+ return true;
178
+ }
179
+
180
+ return false;
181
+ }
182
+
183
+ function isWriteContext(node: ts.Identifier): 'simple' | 'compound' | 'increment' | false {
184
+ let parent = node.parent;
185
+
186
+ if (ts.isBinaryExpression(parent) && parent.left === node) {
187
+ let op = parent.operatorToken.kind;
188
+
189
+ if (op === ts.SyntaxKind.EqualsToken) {
190
+ return 'simple';
191
+ }
192
+
193
+ if (op >= ts.SyntaxKind.PlusEqualsToken && op <= ts.SyntaxKind.CaretEqualsToken) {
194
+ return 'compound';
195
+ }
196
+
197
+ if (
198
+ op === ts.SyntaxKind.AmpersandAmpersandEqualsToken ||
199
+ op === ts.SyntaxKind.BarBarEqualsToken ||
200
+ op === ts.SyntaxKind.QuestionQuestionEqualsToken
201
+ ) {
202
+ return 'compound';
203
+ }
204
+ }
205
+
206
+ if (ts.isPostfixUnaryExpression(parent) || ts.isPrefixUnaryExpression(parent)) {
207
+ let op = parent.operator;
208
+
209
+ if (op === ts.SyntaxKind.PlusPlusToken || op === ts.SyntaxKind.MinusMinusToken) {
210
+ return 'increment';
211
+ }
212
+ }
213
+
214
+ return false;
215
+ }
216
+
217
+ function visit(ctx: TransformContext, node: ts.Node): void {
218
+ if (
219
+ ts.isImportDeclaration(node) &&
220
+ ts.isStringLiteral(node.moduleSpecifier) &&
221
+ node.moduleSpecifier.text.includes('@esportsplus/reactivity')
222
+ ) {
223
+ let clause = node.importClause;
224
+
225
+ if (clause?.namedBindings && ts.isNamedImports(clause.namedBindings)) {
226
+ for (let i = 0, n = clause.namedBindings.elements.length; i < n; i++) {
227
+ if (clause.namedBindings.elements[i].name.text === 'reactive') {
228
+ ctx.hasReactiveImport = true;
229
+ break;
230
+ }
231
+ }
232
+ }
233
+ }
234
+
235
+ if (
236
+ ctx.hasReactiveImport &&
237
+ ts.isCallExpression(node) &&
238
+ ts.isIdentifier(node.expression) &&
239
+ node.expression.text === 'reactive' &&
240
+ node.arguments.length > 0
241
+ ) {
242
+ let arg = node.arguments[0],
243
+ classification = classifyReactiveArg(arg);
244
+
245
+ if (classification) {
246
+ let varName: string | null = null;
247
+
248
+ if (ts.isVariableDeclaration(node.parent) && ts.isIdentifier(node.parent.name)) {
249
+ varName = node.parent.name.text;
250
+ }
251
+ else if (
252
+ ts.isBinaryExpression(node.parent) &&
253
+ node.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
254
+ ts.isIdentifier(node.parent.left)
255
+ ) {
256
+ varName = node.parent.left.text;
257
+ }
258
+
259
+ if (varName) {
260
+ let scope = findEnclosingScope(node);
261
+
262
+ ctx.scopedBindings.push({ name: varName, scope, type: classification });
263
+ ctx.bindings.set(varName, classification);
264
+ }
265
+
266
+ if (classification === 'computed') {
267
+ ctx.computedArgRanges.push({
268
+ end: arg.end,
269
+ start: arg.getStart(ctx.sourceFile)
270
+ });
271
+
272
+ let argCtx: ArgContext = {
273
+ argStart: arg.getStart(ctx.sourceFile),
274
+ innerReplacements: [],
275
+ neededImports: ctx.neededImports,
276
+ scopedBindings: ctx.scopedBindings,
277
+ sourceFile: ctx.sourceFile
278
+ };
279
+
280
+ visitArg(argCtx, arg);
281
+
282
+ let argText = applyReplacements(arg.getText(ctx.sourceFile), argCtx.innerReplacements);
283
+
284
+ ctx.replacements.push({
285
+ end: node.end,
286
+ newText: `computed(${argText})`,
287
+ start: node.pos
288
+ });
289
+
290
+ ctx.neededImports.add('computed');
291
+ }
292
+ else {
293
+ let argText = arg.getText(ctx.sourceFile);
294
+
295
+ ctx.replacements.push({
296
+ end: node.end,
297
+ newText: `signal(${argText})`,
298
+ start: node.pos
299
+ });
300
+
301
+ ctx.neededImports.add('signal');
302
+ }
303
+ }
304
+ }
305
+
306
+ if (ts.isIdentifier(node) && !isInDeclarationInit(node.parent)) {
307
+ if (ts.isPropertyAccessExpression(node.parent) && node.parent.name === node) {
308
+ ts.forEachChild(node, n => visit(ctx, n));
309
+ return;
310
+ }
311
+
312
+ let nodeStart = node.getStart(ctx.sourceFile);
313
+
314
+ if (isInComputedRange(ctx.computedArgRanges, nodeStart, node.end)) {
315
+ ts.forEachChild(node, n => visit(ctx, n));
316
+ return;
317
+ }
318
+
319
+ let binding = findBinding(ctx.scopedBindings, node.text, node),
320
+ name = node.text;
321
+
322
+ if (binding) {
323
+ if (
324
+ !isReactiveReassignment(node.parent) &&
325
+ !(ts.isTypeOfExpression(node.parent) && node.parent.expression === node)
326
+ ) {
327
+ let writeCtx = isWriteContext(node);
328
+
329
+ if (writeCtx) {
330
+ if (binding.type !== 'computed') {
331
+ ctx.neededImports.add('set');
332
+
333
+ let parent = node.parent;
334
+
335
+ if (writeCtx === 'simple' && ts.isBinaryExpression(parent)) {
336
+ let valueText = parent.right.getText(ctx.sourceFile);
337
+
338
+ ctx.replacements.push({
339
+ end: parent.end,
340
+ newText: `set(${name}, ${valueText})`,
341
+ start: parent.pos
342
+ });
343
+ }
344
+ else if (writeCtx === 'compound' && ts.isBinaryExpression(parent)) {
345
+ let op = getCompoundOperator(parent.operatorToken.kind),
346
+ valueText = parent.right.getText(ctx.sourceFile);
347
+
348
+ ctx.replacements.push({
349
+ end: parent.end,
350
+ newText: `set(${name}, ${name}.value ${op} ${valueText})`,
351
+ start: parent.pos
352
+ });
353
+ }
354
+ else if (writeCtx === 'increment') {
355
+ let isPrefix = ts.isPrefixUnaryExpression(parent),
356
+ op = (parent as ts.PrefixUnaryExpression | ts.PostfixUnaryExpression).operator,
357
+ delta = op === ts.SyntaxKind.PlusPlusToken ? '+ 1' : '- 1';
358
+
359
+ if (ts.isExpressionStatement(parent.parent)) {
360
+ ctx.replacements.push({
361
+ end: parent.end,
362
+ newText: `set(${name}, ${name}.value ${delta})`,
363
+ start: parent.pos
364
+ });
365
+ }
366
+ else if (isPrefix) {
367
+ ctx.replacements.push({
368
+ end: parent.end,
369
+ newText: `(set(${name}, ${name}.value ${delta}), ${name}.value)`,
370
+ start: parent.pos
371
+ });
372
+ }
373
+ else {
374
+ let tmp = uid('tmp');
375
+
376
+ ctx.replacements.push({
377
+ end: parent.end,
378
+ newText: `((${tmp}) => (set(${name}, ${tmp} ${delta}), ${tmp}))(${name}.value)`,
379
+ start: parent.pos
380
+ });
381
+ }
382
+ }
383
+ }
384
+ }
385
+ else {
386
+ ctx.neededImports.add('read');
387
+
388
+ ctx.replacements.push({
389
+ end: node.end,
390
+ newText: `read(${name})`,
391
+ start: node.pos
392
+ });
393
+ }
394
+ }
395
+ }
396
+ }
397
+
398
+ ts.forEachChild(node, n => visit(ctx, n));
399
+ }
400
+
401
+ function visitArg(ctx: ArgContext, node: ts.Node): void {
402
+ if (ts.isIdentifier(node)) {
403
+ if (ts.isPropertyAccessExpression(node.parent) && node.parent.name === node) {
404
+ ts.forEachChild(node, n => visitArg(ctx, n));
405
+ return;
406
+ }
407
+
408
+ if (ts.isCallExpression(node.parent) && node.parent.expression === node) {
409
+ ts.forEachChild(node, n => visitArg(ctx, n));
410
+ return;
411
+ }
412
+
413
+ let binding = findBinding(ctx.scopedBindings, node.text, node);
414
+
415
+ if (binding) {
416
+ ctx.neededImports.add('read');
417
+
418
+ ctx.innerReplacements.push({
419
+ end: node.end - ctx.argStart,
420
+ newText: `read(${node.text})`,
421
+ start: node.getStart(ctx.sourceFile) - ctx.argStart
422
+ });
423
+ }
424
+ }
425
+
426
+ ts.forEachChild(node, n => visitArg(ctx, n));
427
+ }
428
+
429
+
430
+ const transformReactivePrimitives = (
431
+ sourceFile: ts.SourceFile,
432
+ bindings: Bindings
433
+ ): string => {
434
+ let code = sourceFile.getFullText(),
435
+ ctx: TransformContext = {
436
+ bindings,
437
+ computedArgRanges: [],
438
+ hasReactiveImport: false,
439
+ neededImports: new Set<string>(),
440
+ replacements: [],
441
+ scopedBindings: [],
442
+ sourceFile
443
+ };
444
+
445
+ visit(ctx, sourceFile);
446
+
447
+ if (ctx.replacements.length === 0) {
448
+ return code;
449
+ }
450
+
451
+ let result = applyReplacements(code, ctx.replacements);
452
+
453
+ if (ctx.neededImports.size > 0) {
454
+ result = addMissingImports(result, ctx.neededImports);
455
+ }
456
+
457
+ return result;
458
+ };
459
+
460
+
461
+ export { transformReactivePrimitives };
@@ -0,0 +1,119 @@
1
+ import { applyReplacements, type Replacement } from '@esportsplus/typescript/transformer';
2
+ import ts from 'typescript';
3
+
4
+
5
+ type ExtraImport = {
6
+ module: string;
7
+ specifier: string;
8
+ };
9
+
10
+
11
+ function findReactivityImport(sourceFile: ts.SourceFile): ts.ImportDeclaration | null {
12
+ for (let i = 0, n = sourceFile.statements.length; i < n; i++) {
13
+ let stmt = sourceFile.statements[i];
14
+
15
+ if (
16
+ ts.isImportDeclaration(stmt) &&
17
+ ts.isStringLiteral(stmt.moduleSpecifier) &&
18
+ stmt.moduleSpecifier.text === '@esportsplus/reactivity' &&
19
+ stmt.importClause?.namedBindings &&
20
+ ts.isNamedImports(stmt.importClause.namedBindings)
21
+ ) {
22
+ return stmt;
23
+ }
24
+ }
25
+
26
+ return null;
27
+ }
28
+
29
+ function getExistingSpecifiers(namedImports: ts.NamedImports): Set<string> {
30
+ let existing = new Set<string>();
31
+
32
+ for (let i = 0, n = namedImports.elements.length; i < n; i++) {
33
+ let el = namedImports.elements[i],
34
+ name = el.propertyName?.text ?? el.name.text;
35
+
36
+ existing.add(name);
37
+ }
38
+
39
+ return existing;
40
+ }
41
+
42
+ function getFirstImportPos(sourceFile: ts.SourceFile): number {
43
+ for (let i = 0, n = sourceFile.statements.length; i < n; i++) {
44
+ if (ts.isImportDeclaration(sourceFile.statements[i])) {
45
+ return sourceFile.statements[i].getStart(sourceFile);
46
+ }
47
+ }
48
+
49
+ return 0;
50
+ }
51
+
52
+
53
+ const addMissingImports = (code: string, needed: Set<string>, extraImports?: ExtraImport[]): string => {
54
+ let sourceFile = ts.createSourceFile('temp.ts', code, ts.ScriptTarget.Latest, true),
55
+ reactivityImport = findReactivityImport(sourceFile);
56
+
57
+ if (!reactivityImport) {
58
+ return code;
59
+ }
60
+
61
+ let extraSpecifiers = new Set<string>(),
62
+ namedImports = reactivityImport.importClause!.namedBindings as ts.NamedImports,
63
+ existing = getExistingSpecifiers(namedImports),
64
+ toAdd: string[] = [];
65
+
66
+ if (extraImports) {
67
+ for (let i = 0, n = extraImports.length; i < n; i++) {
68
+ extraSpecifiers.add(extraImports[i].specifier);
69
+ }
70
+ }
71
+
72
+ for (let imp of needed) {
73
+ if (!extraSpecifiers.has(imp) && !existing.has(imp)) {
74
+ toAdd.push(imp);
75
+ }
76
+ }
77
+
78
+ if (toAdd.length > 0) {
79
+ let combined: string[] = [];
80
+
81
+ for (let item of existing) {
82
+ combined.push(item);
83
+ }
84
+
85
+ for (let i = 0, n = toAdd.length; i < n; i++) {
86
+ combined.push(toAdd[i]);
87
+ }
88
+
89
+ combined.sort();
90
+
91
+ let newSpecifiers = `{ ${combined.join(', ')} }`,
92
+ bindingsStart = namedImports.getStart(sourceFile),
93
+ bindingsEnd = namedImports.getEnd();
94
+
95
+ code = code.substring(0, bindingsStart) + newSpecifiers + code.substring(bindingsEnd);
96
+ }
97
+
98
+ if (extraImports) {
99
+ let insertPos = getFirstImportPos(
100
+ ts.createSourceFile('temp.ts', code, ts.ScriptTarget.Latest, true)
101
+ );
102
+
103
+ for (let i = 0, n = extraImports.length; i < n; i++) {
104
+ let extra = extraImports[i];
105
+
106
+ if (needed.has(extra.specifier) && !code.includes(extra.module)) {
107
+ code = code.substring(0, insertPos) +
108
+ `import { ${extra.specifier} } from '${extra.module}';\n` +
109
+ code.substring(insertPos);
110
+ }
111
+ }
112
+ }
113
+
114
+ return code;
115
+ };
116
+
117
+
118
+ export { addMissingImports, applyReplacements };
119
+ export type { ExtraImport, Replacement };
@@ -1,6 +0,0 @@
1
- import { mightNeedTransform as checkTransform } from '@esportsplus/typescript/transformer';
2
- let regex = /import\s*\{[^}]*\breactive\b[^}]*\}\s*from\s*['"]@esportsplus\/reactivity/;
3
- const mightNeedTransform = (code) => {
4
- return checkTransform(code, { regex });
5
- };
6
- export { mightNeedTransform };
@@ -1,116 +0,0 @@
1
- import { uid } from '@esportsplus/typescript/transformer';
2
- import ts from 'typescript';
3
- import { applyReplacements } from './utilities.js';
4
- const TRAILING_SEMICOLON = /;$/;
5
- function visitFunctionBody(body, parentBody) {
6
- let disposables = [], effectsToCapture = [], returnStatement = null;
7
- function visit(n) {
8
- if (ts.isVariableDeclaration(n) &&
9
- ts.isIdentifier(n.name) &&
10
- n.initializer &&
11
- ts.isCallExpression(n.initializer) &&
12
- ts.isIdentifier(n.initializer.expression) &&
13
- n.initializer.expression.text === 'reactive') {
14
- disposables.push({ name: n.name.text, type: 'reactive' });
15
- }
16
- if (ts.isCallExpression(n) &&
17
- ts.isIdentifier(n.expression) &&
18
- n.expression.text === 'effect') {
19
- if (ts.isVariableDeclaration(n.parent) && ts.isIdentifier(n.parent.name)) {
20
- disposables.push({ name: n.parent.name.text, type: 'effect' });
21
- }
22
- else if (ts.isExpressionStatement(n.parent)) {
23
- let name = uid('effect');
24
- effectsToCapture.push({
25
- end: n.parent.end,
26
- name,
27
- start: n.parent.pos
28
- });
29
- disposables.push({ name, type: 'effect' });
30
- }
31
- }
32
- if (ts.isReturnStatement(n) &&
33
- n.expression &&
34
- (ts.isArrowFunction(n.expression) || ts.isFunctionExpression(n.expression)) &&
35
- n.parent === parentBody) {
36
- returnStatement = n;
37
- }
38
- ts.forEachChild(n, visit);
39
- }
40
- visit(body);
41
- return { disposables, effectsToCapture, returnStatement };
42
- }
43
- function processFunction(node, sourceFile, edits) {
44
- if (!node.body || !ts.isBlock(node.body)) {
45
- return;
46
- }
47
- let result = visitFunctionBody(node.body, node.body), disposables = result.disposables, effectsToCapture = result.effectsToCapture, returnStatement = result.returnStatement;
48
- if (disposables.length === 0 || !returnStatement || !returnStatement.expression) {
49
- return;
50
- }
51
- let cleanupFn = returnStatement.expression;
52
- if (!cleanupFn.body) {
53
- return;
54
- }
55
- let disposeStatements = [];
56
- for (let i = disposables.length - 1; i >= 0; i--) {
57
- let d = disposables[i];
58
- if (d.type === 'reactive') {
59
- disposeStatements.push(`${d.name}.dispose();`);
60
- }
61
- else {
62
- disposeStatements.push(`${d.name}();`);
63
- }
64
- }
65
- let disposeCode = disposeStatements.join('\n');
66
- if (ts.isBlock(cleanupFn.body)) {
67
- edits.push({
68
- cleanupBodyEnd: cleanupFn.body.statements[0]?.pos ?? cleanupFn.body.end - 1,
69
- cleanupBodyStart: cleanupFn.body.pos + 1,
70
- disposeCode,
71
- effectsToCapture
72
- });
73
- }
74
- else {
75
- edits.push({
76
- cleanupBodyEnd: cleanupFn.body.end,
77
- cleanupBodyStart: cleanupFn.body.pos,
78
- disposeCode: `{ ${disposeCode}\n return ${cleanupFn.body.getText(sourceFile)}; }`,
79
- effectsToCapture
80
- });
81
- }
82
- }
83
- const injectAutoDispose = (sourceFile) => {
84
- let code = sourceFile.getFullText(), edits = [];
85
- function visit(node) {
86
- if (ts.isFunctionDeclaration(node) ||
87
- ts.isFunctionExpression(node) ||
88
- ts.isArrowFunction(node)) {
89
- processFunction(node, sourceFile, edits);
90
- }
91
- ts.forEachChild(node, visit);
92
- }
93
- visit(sourceFile);
94
- if (edits.length === 0) {
95
- return code;
96
- }
97
- let replacements = [];
98
- for (let i = 0, n = edits.length; i < n; i++) {
99
- let edit = edits[i], effects = edit.effectsToCapture;
100
- for (let j = 0, m = effects.length; j < m; j++) {
101
- let effect = effects[j], original = code.substring(effect.start, effect.end).trim();
102
- replacements.push({
103
- end: effect.end,
104
- newText: `const ${effect.name} = ${original.replace(TRAILING_SEMICOLON, '')}`,
105
- start: effect.start
106
- });
107
- }
108
- replacements.push({
109
- end: edit.cleanupBodyEnd,
110
- newText: `\n${edit.disposeCode}`,
111
- start: edit.cleanupBodyStart
112
- });
113
- }
114
- return applyReplacements(code, replacements);
115
- };
116
- export { injectAutoDispose };