@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.
Files changed (56) hide show
  1. package/build/index.d.ts +1 -1
  2. package/build/index.js +1 -1
  3. package/build/reactive/array.d.ts +3 -0
  4. package/build/reactive/array.js +32 -2
  5. package/build/reactive/index.d.ts +17 -14
  6. package/build/reactive/index.js +7 -25
  7. package/build/system.js +1 -1
  8. package/build/transformer/detector.d.ts +2 -0
  9. package/build/transformer/detector.js +38 -0
  10. package/build/transformer/index.d.ts +10 -0
  11. package/build/transformer/index.js +55 -0
  12. package/build/transformer/plugins/esbuild.d.ts +5 -0
  13. package/build/transformer/plugins/esbuild.js +31 -0
  14. package/build/transformer/plugins/tsc.d.ts +3 -0
  15. package/build/transformer/plugins/tsc.js +4 -0
  16. package/build/transformer/plugins/vite.d.ts +5 -0
  17. package/build/transformer/plugins/vite.js +28 -0
  18. package/build/transformer/transforms/auto-dispose.d.ts +3 -0
  19. package/build/transformer/transforms/auto-dispose.js +119 -0
  20. package/build/transformer/transforms/reactive-array.d.ts +4 -0
  21. package/build/transformer/transforms/reactive-array.js +93 -0
  22. package/build/transformer/transforms/reactive-object.d.ts +4 -0
  23. package/build/transformer/transforms/reactive-object.js +164 -0
  24. package/build/transformer/transforms/reactive-primitives.d.ts +4 -0
  25. package/build/transformer/transforms/reactive-primitives.js +335 -0
  26. package/build/transformer/transforms/utilities.d.ts +8 -0
  27. package/build/transformer/transforms/utilities.js +73 -0
  28. package/build/types.d.ts +14 -4
  29. package/package.json +30 -3
  30. package/readme.md +276 -2
  31. package/src/constants.ts +1 -1
  32. package/src/index.ts +1 -1
  33. package/src/reactive/array.ts +49 -2
  34. package/src/reactive/index.ts +33 -57
  35. package/src/system.ts +14 -5
  36. package/src/transformer/detector.ts +65 -0
  37. package/src/transformer/index.ts +78 -0
  38. package/src/transformer/plugins/esbuild.ts +47 -0
  39. package/src/transformer/plugins/tsc.ts +8 -0
  40. package/src/transformer/plugins/vite.ts +39 -0
  41. package/src/transformer/transforms/auto-dispose.ts +191 -0
  42. package/src/transformer/transforms/reactive-array.ts +143 -0
  43. package/src/transformer/transforms/reactive-object.ts +253 -0
  44. package/src/transformer/transforms/reactive-primitives.ts +461 -0
  45. package/src/transformer/transforms/utilities.ts +119 -0
  46. package/src/types.ts +24 -5
  47. package/test/arrays.ts +146 -0
  48. package/test/effects.ts +168 -0
  49. package/test/index.ts +8 -0
  50. package/test/nested.ts +201 -0
  51. package/test/objects.ts +106 -0
  52. package/test/primitives.ts +171 -0
  53. package/test/vite.config.ts +40 -0
  54. package/build/reactive/object.d.ts +0 -7
  55. package/build/reactive/object.js +0 -79
  56. 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>;
@@ -1,13 +1,33 @@
1
1
  import { isArray } from '@esportsplus/utilities';
2
- import { REACTIVE_ARRAY } from '../constants.js';
3
- import { isReactiveObject } from './object.js';
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 { Prettify } from '@esportsplus/utilities';
1
+ import { REACTIVE_OBJECT } from '../constants.js';
2
2
  import { ReactiveArray } from './array.js';
3
- import { ReactiveObject } from './object.js';
4
- type API<T> = T extends Record<PropertyKey, unknown> ? Prettify<{
5
- [K in keyof T]: Infer<T[K]>;
6
- } & {
7
- dispose: VoidFunction;
8
- }> : T extends (infer U)[] ? ReactiveArray<U> : never;
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 ] are reserved keys';
11
+ never: '[ dispose ] is a reserved key';
13
12
  } : T;
14
- type Infer<T> = T extends (...args: unknown[]) => Promise<infer R> ? R | undefined : T extends (...args: any[]) => infer R ? R : T extends (infer U)[] ? ReactiveArray<U> : T extends ReactiveObject<any> ? T : T extends Record<PropertyKey, unknown> ? {
15
- [K in keyof T]: T[K];
16
- } : T;
17
- declare const _default: <T extends Record<PropertyKey, any> | unknown[]>(input: Guard<T>) => API<T>;
18
- export default _default;
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 };
@@ -1,26 +1,8 @@
1
- import { isArray, isObject } from '@esportsplus/utilities';
2
- import { onCleanup, root } from '../system.js';
1
+ import { REACTIVE_OBJECT } from '../constants.js';
3
2
  import { ReactiveArray } from './array.js';
4
- import { ReactiveObject } from './object.js';
5
- export default (input) => {
6
- let dispose = false, value = root(() => {
7
- let response;
8
- if (isObject(input)) {
9
- response = new ReactiveObject(input);
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,2 @@
1
+ declare const mightNeedTransform: (code: string) => boolean;
2
+ export { mightNeedTransform };
@@ -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,5 @@
1
+ import type { Plugin } from 'esbuild';
2
+ import type { TransformOptions } from '../../types.js';
3
+ declare const _default: (options?: TransformOptions) => Plugin;
4
+ export default _default;
5
+ export type { TransformOptions as PluginOptions };
@@ -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,3 @@
1
+ import ts from 'typescript';
2
+ declare const _default: (_program: ts.Program) => ts.TransformerFactory<ts.SourceFile>;
3
+ export default _default;
@@ -0,0 +1,4 @@
1
+ import { createTransformer } from '../../transformer/index.js';
2
+ export default (_program) => {
3
+ return createTransformer();
4
+ };
@@ -0,0 +1,5 @@
1
+ import type { Plugin } from 'vite';
2
+ import type { TransformOptions } from '../../types.js';
3
+ declare const _default: (options?: TransformOptions) => Plugin;
4
+ export default _default;
5
+ export type { TransformOptions as PluginOptions };
@@ -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,3 @@
1
+ import ts from 'typescript';
2
+ declare const injectAutoDispose: (sourceFile: ts.SourceFile) => string;
3
+ export { injectAutoDispose };
@@ -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,4 @@
1
+ import type { Bindings } from '../../types.js';
2
+ import ts from 'typescript';
3
+ declare const transformReactiveArrays: (sourceFile: ts.SourceFile, bindings: Bindings) => string;
4
+ export { transformReactiveArrays };
@@ -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 };
@@ -0,0 +1,4 @@
1
+ import type { Bindings } from '../../types.js';
2
+ import ts from 'typescript';
3
+ declare const transformReactiveObjects: (sourceFile: ts.SourceFile, bindings: Bindings) => string;
4
+ export { transformReactiveObjects };