@esportsplus/reactivity 0.22.3 → 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.
- 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/detector.d.ts +2 -0
- package/build/transformer/detector.js +38 -0
- package/build/transformer/index.d.ts +10 -0
- package/build/transformer/index.js +55 -0
- package/build/transformer/plugins/esbuild.d.ts +5 -0
- package/build/transformer/plugins/esbuild.js +31 -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/transformer/transforms/auto-dispose.d.ts +3 -0
- package/build/transformer/transforms/auto-dispose.js +119 -0
- package/build/transformer/transforms/reactive-array.d.ts +4 -0
- package/build/transformer/transforms/reactive-array.js +93 -0
- package/build/transformer/transforms/reactive-object.d.ts +4 -0
- package/build/transformer/transforms/reactive-object.js +164 -0
- package/build/transformer/transforms/reactive-primitives.d.ts +4 -0
- package/build/transformer/transforms/reactive-primitives.js +335 -0
- package/build/transformer/transforms/utilities.d.ts +8 -0
- package/build/transformer/transforms/utilities.js +73 -0
- package/build/types.d.ts +14 -4
- package/package.json +30 -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/detector.ts +65 -0
- package/src/transformer/index.ts +78 -0
- package/src/transformer/plugins/esbuild.ts +47 -0
- package/src/transformer/plugins/tsc.ts +8 -0
- package/src/transformer/plugins/vite.ts +39 -0
- package/src/transformer/transforms/auto-dispose.ts +191 -0
- package/src/transformer/transforms/reactive-array.ts +143 -0
- package/src/transformer/transforms/reactive-object.ts +253 -0
- package/src/transformer/transforms/reactive-primitives.ts +461 -0
- package/src/transformer/transforms/utilities.ts +119 -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
package/build/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { default as reactive } from './reactive/index.js';
|
|
2
1
|
export { STABILIZER_IDLE, STABILIZER_RESCHEDULE, STABILIZER_RUNNING, STABILIZER_SCHEDULED } from './constants.js';
|
|
2
|
+
export { default as reactive } from './reactive/index.js';
|
|
3
3
|
export * from './system.js';
|
|
4
4
|
export * from './types.js';
|
package/build/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { default as reactive } from './reactive/index.js';
|
|
2
1
|
export { STABILIZER_IDLE, STABILIZER_RESCHEDULE, STABILIZER_RUNNING, STABILIZER_SCHEDULED } from './constants.js';
|
|
2
|
+
export { default as reactive } from './reactive/index.js';
|
|
3
3
|
export * from './system.js';
|
|
4
4
|
export * from './types.js';
|
|
@@ -35,8 +35,11 @@ type Listener<V> = {
|
|
|
35
35
|
};
|
|
36
36
|
type Listeners = Record<string, (Listener<any> | null)[]>;
|
|
37
37
|
declare class ReactiveArray<T> extends Array<T> {
|
|
38
|
+
private _length;
|
|
38
39
|
listeners: Listeners;
|
|
39
40
|
constructor(...items: T[]);
|
|
41
|
+
$length(): number;
|
|
42
|
+
$set(i: number, value: T): void;
|
|
40
43
|
clear(): void;
|
|
41
44
|
concat(...items: ConcatArray<T>[]): ReactiveArray<T>;
|
|
42
45
|
concat(...items: (T | ConcatArray<T>)[]): ReactiveArray<T>;
|
package/build/reactive/array.js
CHANGED
|
@@ -1,13 +1,33 @@
|
|
|
1
1
|
import { isArray } from '@esportsplus/utilities';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { read, set, signal } from '../system.js';
|
|
3
|
+
import { REACTIVE_ARRAY, REACTIVE_OBJECT } from '../constants.js';
|
|
4
|
+
function isReactiveObject(value) {
|
|
5
|
+
return value !== null && typeof value === 'object' && value[REACTIVE_OBJECT] === true;
|
|
6
|
+
}
|
|
4
7
|
class ReactiveArray extends Array {
|
|
8
|
+
_length;
|
|
5
9
|
listeners = {};
|
|
6
10
|
constructor(...items) {
|
|
7
11
|
super(...items);
|
|
12
|
+
this._length = signal(items.length);
|
|
13
|
+
}
|
|
14
|
+
$length() {
|
|
15
|
+
return read(this._length);
|
|
16
|
+
}
|
|
17
|
+
$set(i, value) {
|
|
18
|
+
let prev = this[i];
|
|
19
|
+
if (prev === value) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
this[i] = value;
|
|
23
|
+
if (i >= super.length) {
|
|
24
|
+
set(this._length, i + 1);
|
|
25
|
+
}
|
|
26
|
+
this.dispatch('set', { index: i, item: value });
|
|
8
27
|
}
|
|
9
28
|
clear() {
|
|
10
29
|
this.dispose();
|
|
30
|
+
set(this._length, 0);
|
|
11
31
|
this.dispatch('clear');
|
|
12
32
|
}
|
|
13
33
|
concat(...items) {
|
|
@@ -26,6 +46,7 @@ class ReactiveArray extends Array {
|
|
|
26
46
|
}
|
|
27
47
|
}
|
|
28
48
|
if (added.length) {
|
|
49
|
+
set(this._length, super.length);
|
|
29
50
|
this.dispatch('concat', { items: added });
|
|
30
51
|
}
|
|
31
52
|
return this;
|
|
@@ -62,6 +83,7 @@ class ReactiveArray extends Array {
|
|
|
62
83
|
item.dispose();
|
|
63
84
|
}
|
|
64
85
|
}
|
|
86
|
+
set(this._length, 0);
|
|
65
87
|
}
|
|
66
88
|
on(event, listener) {
|
|
67
89
|
let listeners = this.listeners[event];
|
|
@@ -92,6 +114,7 @@ class ReactiveArray extends Array {
|
|
|
92
114
|
pop() {
|
|
93
115
|
let item = super.pop();
|
|
94
116
|
if (item !== undefined) {
|
|
117
|
+
set(this._length, super.length);
|
|
95
118
|
if (isReactiveObject(item)) {
|
|
96
119
|
item.dispose();
|
|
97
120
|
}
|
|
@@ -100,7 +123,11 @@ class ReactiveArray extends Array {
|
|
|
100
123
|
return item;
|
|
101
124
|
}
|
|
102
125
|
push(...items) {
|
|
126
|
+
if (!items.length) {
|
|
127
|
+
return super.length;
|
|
128
|
+
}
|
|
103
129
|
let length = super.push(...items);
|
|
130
|
+
set(this._length, length);
|
|
104
131
|
this.dispatch('push', { items });
|
|
105
132
|
return length;
|
|
106
133
|
}
|
|
@@ -112,6 +139,7 @@ class ReactiveArray extends Array {
|
|
|
112
139
|
shift() {
|
|
113
140
|
let item = super.shift();
|
|
114
141
|
if (item !== undefined) {
|
|
142
|
+
set(this._length, super.length);
|
|
115
143
|
if (isReactiveObject(item)) {
|
|
116
144
|
item.dispose();
|
|
117
145
|
}
|
|
@@ -151,6 +179,7 @@ class ReactiveArray extends Array {
|
|
|
151
179
|
splice(start, deleteCount = this.length, ...items) {
|
|
152
180
|
let removed = super.splice(start, deleteCount, ...items);
|
|
153
181
|
if (items.length > 0 || removed.length > 0) {
|
|
182
|
+
set(this._length, super.length);
|
|
154
183
|
for (let i = 0, n = removed.length; i < n; i++) {
|
|
155
184
|
let item = removed[i];
|
|
156
185
|
if (isReactiveObject(item)) {
|
|
@@ -163,6 +192,7 @@ class ReactiveArray extends Array {
|
|
|
163
192
|
}
|
|
164
193
|
unshift(...items) {
|
|
165
194
|
let length = super.unshift(...items);
|
|
195
|
+
set(this._length, length);
|
|
166
196
|
this.dispatch('unshift', { items });
|
|
167
197
|
return length;
|
|
168
198
|
}
|
|
@@ -1,18 +1,21 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { REACTIVE_OBJECT } from '../constants.js';
|
|
2
2
|
import { ReactiveArray } from './array.js';
|
|
3
|
-
|
|
4
|
-
type
|
|
5
|
-
[
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
type Guard<T> = T extends {
|
|
3
|
+
declare const READONLY: unique symbol;
|
|
4
|
+
type ReactiveObject<T extends Record<PropertyKey, unknown>> = T & {
|
|
5
|
+
[REACTIVE_OBJECT]: true;
|
|
6
|
+
dispose(): void;
|
|
7
|
+
};
|
|
8
|
+
type ReactiveObjectGuard<T> = T extends {
|
|
10
9
|
dispose: any;
|
|
11
10
|
} ? {
|
|
12
|
-
never: '[ dispose ]
|
|
11
|
+
never: '[ dispose ] is a reserved key';
|
|
13
12
|
} : T;
|
|
14
|
-
|
|
15
|
-
[
|
|
16
|
-
}
|
|
17
|
-
declare
|
|
18
|
-
|
|
13
|
+
declare function reactive<T extends () => unknown>(_input: T): ReturnType<T> & {
|
|
14
|
+
readonly [READONLY]: true;
|
|
15
|
+
};
|
|
16
|
+
declare function reactive<T extends Record<PropertyKey, any>>(_input: ReactiveObjectGuard<T>): ReactiveObject<T>;
|
|
17
|
+
declare function reactive<T>(_input: T[]): ReactiveArray<T>;
|
|
18
|
+
declare function reactive<T>(_input: T): T;
|
|
19
|
+
export default reactive;
|
|
20
|
+
export { reactive, ReactiveArray };
|
|
21
|
+
export type { ReactiveObject };
|
package/build/reactive/index.js
CHANGED
|
@@ -1,26 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { onCleanup, root } from '../system.js';
|
|
1
|
+
import { REACTIVE_OBJECT } from '../constants.js';
|
|
3
2
|
import { ReactiveArray } from './array.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
else if (isArray(input)) {
|
|
12
|
-
response = new ReactiveArray(...input);
|
|
13
|
-
}
|
|
14
|
-
if (response) {
|
|
15
|
-
if (root.disposables) {
|
|
16
|
-
dispose = true;
|
|
17
|
-
}
|
|
18
|
-
return response;
|
|
19
|
-
}
|
|
20
|
-
throw new Error(`@esportsplus/reactivity: 'reactive' received invalid input - ${JSON.stringify(input)}`);
|
|
21
|
-
});
|
|
22
|
-
if (dispose) {
|
|
23
|
-
onCleanup(() => value.dispose());
|
|
24
|
-
}
|
|
25
|
-
return value;
|
|
26
|
-
};
|
|
3
|
+
function reactive(_input) {
|
|
4
|
+
throw new Error('@esportsplus/reactivity: reactive() called at runtime. ' +
|
|
5
|
+
'Ensure vite-plugin-reactivity-compile is configured.');
|
|
6
|
+
}
|
|
7
|
+
export default reactive;
|
|
8
|
+
export { reactive, ReactiveArray };
|
package/build/system.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { isObject } from '@esportsplus/utilities';
|
|
2
1
|
import { COMPUTED, SIGNAL, STABILIZER_IDLE, STABILIZER_RESCHEDULE, STABILIZER_RUNNING, STABILIZER_SCHEDULED, STATE_CHECK, STATE_DIRTY, STATE_IN_HEAP, STATE_NONE, STATE_NOTIFY_MASK, STATE_RECOMPUTING } from './constants.js';
|
|
2
|
+
import { isObject } from '@esportsplus/utilities';
|
|
3
3
|
let depth = 0, heap = new Array(64), heap_i = 0, heap_n = 0, linkPool = [], linkPoolMax = 1000, microtask = queueMicrotask, notified = false, observer = null, scope = null, stabilizer = STABILIZER_IDLE, version = 0;
|
|
4
4
|
function cleanup(computed) {
|
|
5
5
|
if (!computed.cleanup) {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { mightNeedTransform as checkTransform } from '@esportsplus/typescript/transformer';
|
|
2
|
+
import ts from 'typescript';
|
|
3
|
+
const REACTIVE_REGEX = /\breactive\b/;
|
|
4
|
+
function visit(ctx, node) {
|
|
5
|
+
if (ctx.hasImport && ctx.hasUsage) {
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
if (ts.isImportDeclaration(node) &&
|
|
9
|
+
node.importClause?.namedBindings &&
|
|
10
|
+
ts.isNamedImports(node.importClause.namedBindings)) {
|
|
11
|
+
let elements = node.importClause.namedBindings.elements;
|
|
12
|
+
for (let i = 0, n = elements.length; i < n; i++) {
|
|
13
|
+
let el = elements[i], name = el.propertyName?.text ?? el.name.text;
|
|
14
|
+
if (name === 'reactive') {
|
|
15
|
+
ctx.hasImport = true;
|
|
16
|
+
break;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
if (ts.isCallExpression(node) &&
|
|
21
|
+
ts.isIdentifier(node.expression) &&
|
|
22
|
+
node.expression.text === 'reactive') {
|
|
23
|
+
ctx.hasUsage = true;
|
|
24
|
+
}
|
|
25
|
+
ts.forEachChild(node, n => visit(ctx, n));
|
|
26
|
+
}
|
|
27
|
+
const mightNeedTransform = (code) => {
|
|
28
|
+
if (!checkTransform(code, { regex: REACTIVE_REGEX })) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
let ctx = {
|
|
32
|
+
hasImport: false,
|
|
33
|
+
hasUsage: false
|
|
34
|
+
};
|
|
35
|
+
visit(ctx, ts.createSourceFile('detect.ts', code, ts.ScriptTarget.Latest, false));
|
|
36
|
+
return ctx.hasImport && ctx.hasUsage;
|
|
37
|
+
};
|
|
38
|
+
export { mightNeedTransform };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { TransformOptions, TransformResult } from '../types.js';
|
|
2
|
+
import { mightNeedTransform } from './detector.js';
|
|
3
|
+
import ts from 'typescript';
|
|
4
|
+
declare const createTransformer: (options?: TransformOptions) => ts.TransformerFactory<ts.SourceFile>;
|
|
5
|
+
declare const transform: (sourceFile: ts.SourceFile, options?: TransformOptions) => TransformResult;
|
|
6
|
+
export { createTransformer, mightNeedTransform, transform };
|
|
7
|
+
export { injectAutoDispose } from './transforms/auto-dispose.js';
|
|
8
|
+
export { transformReactiveArrays } from './transforms/reactive-array.js';
|
|
9
|
+
export { transformReactiveObjects } from './transforms/reactive-object.js';
|
|
10
|
+
export { transformReactivePrimitives } from './transforms/reactive-primitives.js';
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { injectAutoDispose } from './transforms/auto-dispose.js';
|
|
2
|
+
import { mightNeedTransform } from './detector.js';
|
|
3
|
+
import { transformReactiveArrays } from './transforms/reactive-array.js';
|
|
4
|
+
import { transformReactiveObjects } from './transforms/reactive-object.js';
|
|
5
|
+
import { transformReactivePrimitives } from './transforms/reactive-primitives.js';
|
|
6
|
+
import ts from 'typescript';
|
|
7
|
+
const createTransformer = (options) => {
|
|
8
|
+
return () => {
|
|
9
|
+
return (sourceFile) => {
|
|
10
|
+
let result = transform(sourceFile, options);
|
|
11
|
+
return result.transformed ? result.sourceFile : sourceFile;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
const transform = (sourceFile, options) => {
|
|
16
|
+
let bindings = new Map(), code = sourceFile.getFullText(), current = sourceFile, original = code, result;
|
|
17
|
+
if (!mightNeedTransform(code)) {
|
|
18
|
+
return { code, sourceFile, transformed: false };
|
|
19
|
+
}
|
|
20
|
+
result = transformReactiveObjects(current, bindings);
|
|
21
|
+
if (result !== code) {
|
|
22
|
+
current = ts.createSourceFile(sourceFile.fileName, result, sourceFile.languageVersion, true);
|
|
23
|
+
code = result;
|
|
24
|
+
}
|
|
25
|
+
result = transformReactiveArrays(current, bindings);
|
|
26
|
+
if (result !== code) {
|
|
27
|
+
current = ts.createSourceFile(sourceFile.fileName, result, sourceFile.languageVersion, true);
|
|
28
|
+
code = result;
|
|
29
|
+
}
|
|
30
|
+
result = transformReactivePrimitives(current, bindings);
|
|
31
|
+
if (result !== code) {
|
|
32
|
+
current = ts.createSourceFile(sourceFile.fileName, result, sourceFile.languageVersion, true);
|
|
33
|
+
code = result;
|
|
34
|
+
}
|
|
35
|
+
if (options?.autoDispose) {
|
|
36
|
+
result = injectAutoDispose(current);
|
|
37
|
+
if (result !== code) {
|
|
38
|
+
current = ts.createSourceFile(sourceFile.fileName, result, sourceFile.languageVersion, true);
|
|
39
|
+
code = result;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (code === original) {
|
|
43
|
+
return { code, sourceFile, transformed: false };
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
code,
|
|
47
|
+
sourceFile: current,
|
|
48
|
+
transformed: true
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
export { createTransformer, mightNeedTransform, transform };
|
|
52
|
+
export { injectAutoDispose } from './transforms/auto-dispose.js';
|
|
53
|
+
export { transformReactiveArrays } from './transforms/reactive-array.js';
|
|
54
|
+
export { transformReactiveObjects } from './transforms/reactive-object.js';
|
|
55
|
+
export { transformReactivePrimitives } from './transforms/reactive-primitives.js';
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { TRANSFORM_PATTERN } from '@esportsplus/typescript/transformer';
|
|
2
|
+
import { mightNeedTransform, transform } from '../../transformer/index.js';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import ts from 'typescript';
|
|
5
|
+
export default (options) => {
|
|
6
|
+
return {
|
|
7
|
+
name: '@esportsplus/reactivity/plugin-esbuild',
|
|
8
|
+
setup(build) {
|
|
9
|
+
build.onLoad({ filter: TRANSFORM_PATTERN }, async (args) => {
|
|
10
|
+
let code = await fs.promises.readFile(args.path, 'utf8');
|
|
11
|
+
if (!mightNeedTransform(code)) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
let sourceFile = ts.createSourceFile(args.path, code, ts.ScriptTarget.Latest, true), result = transform(sourceFile, options);
|
|
16
|
+
if (!result.transformed) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
contents: result.code,
|
|
21
|
+
loader: args.path.endsWith('x') ? 'tsx' : 'ts'
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
console.error(`@esportsplus/reactivity: Error transforming ${args.path}:`, error);
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { TRANSFORM_PATTERN } from '@esportsplus/typescript/transformer';
|
|
2
|
+
import { mightNeedTransform, transform } from '../../transformer/index.js';
|
|
3
|
+
import ts from 'typescript';
|
|
4
|
+
export default (options) => {
|
|
5
|
+
return {
|
|
6
|
+
enforce: 'pre',
|
|
7
|
+
name: '@esportsplus/reactivity/plugin-vite',
|
|
8
|
+
transform(code, id) {
|
|
9
|
+
if (!TRANSFORM_PATTERN.test(id) || id.includes('node_modules')) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
if (!mightNeedTransform(code)) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
let sourceFile = ts.createSourceFile(id, code, ts.ScriptTarget.Latest, true), result = transform(sourceFile, options);
|
|
17
|
+
if (!result.transformed) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
return { code: result.code, map: null };
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
console.error(`@esportsplus/reactivity: Error transforming ${id}:`, error);
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { uid, TRAILING_SEMICOLON } from '@esportsplus/typescript/transformer';
|
|
2
|
+
import { applyReplacements } from './utilities.js';
|
|
3
|
+
import ts from 'typescript';
|
|
4
|
+
function processFunction(node, sourceFile, edits) {
|
|
5
|
+
if (!node.body || !ts.isBlock(node.body)) {
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
let ctx = {
|
|
9
|
+
disposables: [],
|
|
10
|
+
effectsToCapture: [],
|
|
11
|
+
parentBody: node.body,
|
|
12
|
+
returnStatement: null
|
|
13
|
+
};
|
|
14
|
+
visitBody(ctx, node.body);
|
|
15
|
+
if (ctx.disposables.length === 0 || !ctx.returnStatement || !ctx.returnStatement.expression) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
let cleanupFn = ctx.returnStatement.expression;
|
|
19
|
+
if (!cleanupFn.body) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
let disposeStatements = [];
|
|
23
|
+
for (let i = ctx.disposables.length - 1; i >= 0; i--) {
|
|
24
|
+
let d = ctx.disposables[i];
|
|
25
|
+
if (d.type === 'reactive') {
|
|
26
|
+
disposeStatements.push(`${d.name}.dispose();`);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
disposeStatements.push(`${d.name}();`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
let disposeCode = disposeStatements.join('\n');
|
|
33
|
+
if (ts.isBlock(cleanupFn.body)) {
|
|
34
|
+
edits.push({
|
|
35
|
+
cleanupBodyEnd: cleanupFn.body.statements[0]?.pos ?? cleanupFn.body.end - 1,
|
|
36
|
+
cleanupBodyStart: cleanupFn.body.pos + 1,
|
|
37
|
+
disposeCode,
|
|
38
|
+
effectsToCapture: ctx.effectsToCapture
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
edits.push({
|
|
43
|
+
cleanupBodyEnd: cleanupFn.body.end,
|
|
44
|
+
cleanupBodyStart: cleanupFn.body.pos,
|
|
45
|
+
disposeCode: `{ ${disposeCode}\n return ${cleanupFn.body.getText(sourceFile)}; }`,
|
|
46
|
+
effectsToCapture: ctx.effectsToCapture
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function visitBody(ctx, node) {
|
|
51
|
+
if (ts.isVariableDeclaration(node) &&
|
|
52
|
+
ts.isIdentifier(node.name) &&
|
|
53
|
+
node.initializer &&
|
|
54
|
+
ts.isCallExpression(node.initializer) &&
|
|
55
|
+
ts.isIdentifier(node.initializer.expression) &&
|
|
56
|
+
node.initializer.expression.text === 'reactive') {
|
|
57
|
+
ctx.disposables.push({ name: node.name.text, type: 'reactive' });
|
|
58
|
+
}
|
|
59
|
+
if (ts.isCallExpression(node) &&
|
|
60
|
+
ts.isIdentifier(node.expression) &&
|
|
61
|
+
node.expression.text === 'effect') {
|
|
62
|
+
if (ts.isVariableDeclaration(node.parent) && ts.isIdentifier(node.parent.name)) {
|
|
63
|
+
ctx.disposables.push({ name: node.parent.name.text, type: 'effect' });
|
|
64
|
+
}
|
|
65
|
+
else if (ts.isExpressionStatement(node.parent)) {
|
|
66
|
+
let name = uid('effect');
|
|
67
|
+
ctx.effectsToCapture.push({
|
|
68
|
+
end: node.parent.end,
|
|
69
|
+
name,
|
|
70
|
+
start: node.parent.pos
|
|
71
|
+
});
|
|
72
|
+
ctx.disposables.push({ name, type: 'effect' });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (ts.isReturnStatement(node) &&
|
|
76
|
+
node.expression &&
|
|
77
|
+
(ts.isArrowFunction(node.expression) || ts.isFunctionExpression(node.expression)) &&
|
|
78
|
+
node.parent === ctx.parentBody) {
|
|
79
|
+
ctx.returnStatement = node;
|
|
80
|
+
}
|
|
81
|
+
ts.forEachChild(node, n => visitBody(ctx, n));
|
|
82
|
+
}
|
|
83
|
+
function visitMain(ctx, node) {
|
|
84
|
+
if (ts.isFunctionDeclaration(node) ||
|
|
85
|
+
ts.isFunctionExpression(node) ||
|
|
86
|
+
ts.isArrowFunction(node)) {
|
|
87
|
+
processFunction(node, ctx.sourceFile, ctx.edits);
|
|
88
|
+
}
|
|
89
|
+
ts.forEachChild(node, n => visitMain(ctx, n));
|
|
90
|
+
}
|
|
91
|
+
const injectAutoDispose = (sourceFile) => {
|
|
92
|
+
let code = sourceFile.getFullText(), ctx = {
|
|
93
|
+
edits: [],
|
|
94
|
+
sourceFile
|
|
95
|
+
};
|
|
96
|
+
visitMain(ctx, sourceFile);
|
|
97
|
+
if (ctx.edits.length === 0) {
|
|
98
|
+
return code;
|
|
99
|
+
}
|
|
100
|
+
let replacements = [];
|
|
101
|
+
for (let i = 0, n = ctx.edits.length; i < n; i++) {
|
|
102
|
+
let edit = ctx.edits[i], effects = edit.effectsToCapture;
|
|
103
|
+
for (let j = 0, m = effects.length; j < m; j++) {
|
|
104
|
+
let effect = effects[j], original = code.substring(effect.start, effect.end).trim();
|
|
105
|
+
replacements.push({
|
|
106
|
+
end: effect.end,
|
|
107
|
+
newText: `const ${effect.name} = ${original.replace(TRAILING_SEMICOLON, '')}`,
|
|
108
|
+
start: effect.start
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
replacements.push({
|
|
112
|
+
end: edit.cleanupBodyEnd,
|
|
113
|
+
newText: `\n${edit.disposeCode}`,
|
|
114
|
+
start: edit.cleanupBodyStart
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return applyReplacements(code, replacements);
|
|
118
|
+
};
|
|
119
|
+
export { injectAutoDispose };
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { applyReplacements } from './utilities.js';
|
|
2
|
+
import ts from 'typescript';
|
|
3
|
+
function getExpressionName(node) {
|
|
4
|
+
if (ts.isIdentifier(node)) {
|
|
5
|
+
return node.text;
|
|
6
|
+
}
|
|
7
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
8
|
+
return getPropertyPath(node);
|
|
9
|
+
}
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
function getPropertyPath(node) {
|
|
13
|
+
let current = node, parts = [];
|
|
14
|
+
while (ts.isPropertyAccessExpression(current)) {
|
|
15
|
+
parts.unshift(current.name.text);
|
|
16
|
+
current = current.expression;
|
|
17
|
+
}
|
|
18
|
+
if (ts.isIdentifier(current)) {
|
|
19
|
+
parts.unshift(current.text);
|
|
20
|
+
return parts.join('.');
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
function isAssignmentTarget(node) {
|
|
25
|
+
let parent = node.parent;
|
|
26
|
+
if ((ts.isBinaryExpression(parent) && parent.left === node) ||
|
|
27
|
+
ts.isPostfixUnaryExpression(parent) ||
|
|
28
|
+
ts.isPrefixUnaryExpression(parent)) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
function visit(ctx, node) {
|
|
34
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
|
|
35
|
+
if (ts.isIdentifier(node.initializer) && ctx.bindings.get(node.initializer.text) === 'array') {
|
|
36
|
+
ctx.bindings.set(node.name.text, 'array');
|
|
37
|
+
}
|
|
38
|
+
if (ts.isPropertyAccessExpression(node.initializer)) {
|
|
39
|
+
let path = getPropertyPath(node.initializer);
|
|
40
|
+
if (path && ctx.bindings.get(path) === 'array') {
|
|
41
|
+
ctx.bindings.set(node.name.text, 'array');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if ((ts.isFunctionDeclaration(node) || ts.isArrowFunction(node)) && node.parameters) {
|
|
46
|
+
for (let i = 0, n = node.parameters.length; i < n; i++) {
|
|
47
|
+
let param = node.parameters[i];
|
|
48
|
+
if ((ts.isIdentifier(param.name) && param.type) &&
|
|
49
|
+
ts.isTypeReferenceNode(param.type) &&
|
|
50
|
+
ts.isIdentifier(param.type.typeName) &&
|
|
51
|
+
param.type.typeName.text === 'ReactiveArray') {
|
|
52
|
+
ctx.bindings.set(param.name.text, 'array');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (ts.isPropertyAccessExpression(node) &&
|
|
57
|
+
node.name.text === 'length' &&
|
|
58
|
+
!isAssignmentTarget(node)) {
|
|
59
|
+
let name = getExpressionName(node.expression);
|
|
60
|
+
if (name && ctx.bindings.get(name) === 'array') {
|
|
61
|
+
let objText = node.expression.getText(ctx.sourceFile);
|
|
62
|
+
ctx.replacements.push({
|
|
63
|
+
end: node.end,
|
|
64
|
+
newText: `${objText}.$length()`,
|
|
65
|
+
start: node.pos
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (ts.isBinaryExpression(node) &&
|
|
70
|
+
node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
|
71
|
+
ts.isElementAccessExpression(node.left)) {
|
|
72
|
+
let elemAccess = node.left, objName = getExpressionName(elemAccess.expression);
|
|
73
|
+
if (objName && ctx.bindings.get(objName) === 'array') {
|
|
74
|
+
let indexText = elemAccess.argumentExpression.getText(ctx.sourceFile), objText = elemAccess.expression.getText(ctx.sourceFile), valueText = node.right.getText(ctx.sourceFile);
|
|
75
|
+
ctx.replacements.push({
|
|
76
|
+
end: node.end,
|
|
77
|
+
newText: `${objText}.$set(${indexText}, ${valueText})`,
|
|
78
|
+
start: node.pos
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
ts.forEachChild(node, n => visit(ctx, n));
|
|
83
|
+
}
|
|
84
|
+
const transformReactiveArrays = (sourceFile, bindings) => {
|
|
85
|
+
let code = sourceFile.getFullText(), ctx = {
|
|
86
|
+
bindings,
|
|
87
|
+
replacements: [],
|
|
88
|
+
sourceFile
|
|
89
|
+
};
|
|
90
|
+
visit(ctx, sourceFile);
|
|
91
|
+
return applyReplacements(code, ctx.replacements);
|
|
92
|
+
};
|
|
93
|
+
export { transformReactiveArrays };
|