@esportsplus/reactivity 0.27.0 → 0.27.2

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.
@@ -1,10 +1,38 @@
1
- import { ast, code as c, type Replacement } from '@esportsplus/typescript/compiler';
2
1
  import { ts } from '@esportsplus/typescript';
3
- import { COMPILER_TYPES } from '~/constants';
2
+ import { ast, code as c, type Replacement } from '@esportsplus/typescript/compiler';
3
+ import { COMPILER_NAMESPACE, COMPILER_TYPES } from '~/constants';
4
4
  import type { Bindings } from '~/types';
5
+ import { isReactiveCall } from '.';
6
+
7
+
8
+ interface TransformContext {
9
+ bindings: Bindings;
10
+ checker?: ts.TypeChecker;
11
+ replacements: Replacement[];
12
+ sourceFile: ts.SourceFile;
13
+ }
14
+
15
+
16
+ function visit(ctx: TransformContext, node: ts.Node): void {
17
+ if (ts.isCallExpression(node) && isReactiveCall(node, ctx.checker) && node.arguments.length > 0) {
18
+ let arg = node.arguments[0],
19
+ expression = ts.isAsExpression(arg) ? arg.expression : arg;
20
+
21
+ if (ts.isArrayLiteralExpression(expression)) {
22
+ if (node.parent && ts.isVariableDeclaration(node.parent) && ts.isIdentifier(node.parent.name)) {
23
+ ctx.bindings.set(node.parent.name.text, COMPILER_TYPES.Array);
24
+ }
5
25
 
26
+ ctx.replacements.push({
27
+ end: node.end,
28
+ newText: expression.elements.length > 0
29
+ ? ` new ${COMPILER_NAMESPACE}.ReactiveArray(...${expression.getText(ctx.sourceFile)})`
30
+ : ` new ${COMPILER_NAMESPACE}.ReactiveArray()`,
31
+ start: node.pos
32
+ });
33
+ }
34
+ }
6
35
 
7
- function visit(ctx: { bindings: Bindings, replacements: Replacement[], sourceFile: ts.SourceFile }, node: ts.Node): void {
8
36
  if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
9
37
  if (ts.isIdentifier(node.initializer) && ctx.bindings.get(node.initializer.text) === COMPILER_TYPES.Array) {
10
38
  ctx.bindings.set(node.name.text, COMPILER_TYPES.Array);
@@ -61,17 +89,16 @@ function visit(ctx: { bindings: Bindings, replacements: Replacement[], sourceFil
61
89
  node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
62
90
  ts.isElementAccessExpression(node.left)
63
91
  ) {
64
- let elemAccess = node.left,
65
- objName = ast.getExpressionName(elemAccess.expression);
92
+ let element = node.left,
93
+ name = ast.getExpressionName(element.expression);
66
94
 
67
- if (objName && ctx.bindings.get(objName) === COMPILER_TYPES.Array) {
68
- let index = elemAccess.argumentExpression.getText(ctx.sourceFile),
69
- obj = elemAccess.expression.getText(ctx.sourceFile),
95
+ if (name && ctx.bindings.get(name) === COMPILER_TYPES.Array) {
96
+ let index = element.argumentExpression.getText(ctx.sourceFile),
70
97
  value = node.right.getText(ctx.sourceFile);
71
98
 
72
99
  ctx.replacements.push({
73
100
  end: node.end,
74
- newText: `${obj}.$set(${index}, ${value})`,
101
+ newText: `${element.expression.getText(ctx.sourceFile)}.$set(${index}, ${value})`,
75
102
  start: node.pos
76
103
  });
77
104
  }
@@ -81,15 +108,20 @@ function visit(ctx: { bindings: Bindings, replacements: Replacement[], sourceFil
81
108
  }
82
109
 
83
110
 
84
- export default (sourceFile: ts.SourceFile, bindings: Bindings, _ns: string, _checker?: ts.TypeChecker): string => {
111
+ export default (sourceFile: ts.SourceFile, bindings: Bindings, checker?: ts.TypeChecker): string => {
85
112
  let code = sourceFile.getFullText(),
86
- ctx = {
113
+ ctx: TransformContext = {
87
114
  bindings,
115
+ checker,
88
116
  replacements: [],
89
117
  sourceFile
90
118
  };
91
119
 
92
120
  visit(ctx, sourceFile);
93
121
 
122
+ if (ctx.replacements.length === 0) {
123
+ return code;
124
+ }
125
+
94
126
  return c.replace(code, ctx.replacements);
95
- };
127
+ };
@@ -1,83 +1,101 @@
1
+ import type { PluginContext } from '@esportsplus/typescript/compiler';
1
2
  import { ts } from '@esportsplus/typescript';
2
- import { code as c, imports } from '@esportsplus/typescript/compiler';
3
- import { COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REGEX, COMPILER_NAMESPACE, PACKAGE } from '~/constants';
3
+ import { ast, imports } from '@esportsplus/typescript/compiler';
4
+ import { COMPILER_ENTRYPOINT, COMPILER_NAMESPACE, PACKAGE } from '~/constants';
4
5
  import type { Bindings, TransformResult } from '~/types';
5
6
  import array from './array';
6
7
  import object from './object';
7
- import primitives from './primitives';
8
8
 
9
9
 
10
- let transforms = [object, array, primitives];
10
+ type AnalyzedFile = {
11
+ hasReactiveImport: boolean;
12
+ };
11
13
 
12
14
 
13
- function hasReactiveImport(sourceFile: ts.SourceFile): boolean {
14
- let found = imports.find(sourceFile, PACKAGE);
15
+ const CONTEXT_KEY = 'reactivity:analyzed';
15
16
 
16
- for (let i = 0, n = found.length; i < n; i++) {
17
- if (found[i].specifiers.has(COMPILER_ENTRYPOINT)) {
18
- return true;
19
- }
20
- }
21
17
 
22
- return false;
18
+ let transforms = [object, array];
19
+
20
+
21
+ function getAnalyzedFile(context: PluginContext | undefined, filename: string): AnalyzedFile | undefined {
22
+ return (context?.get(CONTEXT_KEY) as Map<string, AnalyzedFile> | undefined)?.get(filename);
23
23
  }
24
24
 
25
- function hasReactiveUsage(code: string): boolean {
26
- if (!c.contains(code, { regex: COMPILER_ENTRYPOINT_REGEX })) {
27
- return false;
28
- }
25
+ function hasReactiveImport(sourceFile: ts.SourceFile): boolean {
26
+ return imports.find(sourceFile, PACKAGE).some(i => i.specifiers.has(COMPILER_ENTRYPOINT));
27
+ }
29
28
 
30
- let sourceFile = ts.createSourceFile('detect.ts', code, ts.ScriptTarget.Latest, false),
31
- used = false;
29
+ function isReactiveCallNode(node: ts.Node): boolean {
30
+ return ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === COMPILER_ENTRYPOINT;
31
+ }
32
32
 
33
- function visit(node: ts.Node): void {
34
- if (used) {
35
- return;
36
- }
37
33
 
38
- if (
39
- ts.isCallExpression(node) &&
40
- ts.isIdentifier(node.expression) &&
41
- node.expression.text === COMPILER_ENTRYPOINT
42
- ) {
43
- used = true;
44
- return;
45
- }
34
+ const analyze = (sourceFile: ts.SourceFile, _program: ts.Program, context: PluginContext): void => {
35
+ if (!hasReactiveImport(sourceFile)) {
36
+ return;
37
+ }
46
38
 
47
- ts.forEachChild(node, visit);
39
+ let files = context.get(CONTEXT_KEY) as Map<string, AnalyzedFile> | undefined;
40
+
41
+ if (!files) {
42
+ files = new Map();
43
+ context.set(CONTEXT_KEY, files);
48
44
  }
49
45
 
50
- visit(sourceFile);
46
+ files.set(sourceFile.fileName, {
47
+ hasReactiveImport: true
48
+ });
49
+ };
51
50
 
52
- return used;
53
- }
51
+ const isReactiveCall = (node: ts.CallExpression, _checker?: ts.TypeChecker): boolean => {
52
+ if (!ts.isIdentifier(node.expression)) {
53
+ return false;
54
+ }
54
55
 
56
+ return node.expression.text === COMPILER_ENTRYPOINT;
57
+ };
55
58
 
56
- const transform = (sourceFile: ts.SourceFile, program: ts.Program): TransformResult => {
59
+ const transform = (sourceFile: ts.SourceFile, program: ts.Program, context?: PluginContext): TransformResult => {
57
60
  let bindings: Bindings = new Map(),
58
61
  changed = false,
59
62
  checker = program.getTypeChecker(),
60
63
  code = sourceFile.getFullText(),
61
64
  current = sourceFile,
65
+ filename = sourceFile.fileName,
62
66
  result: string;
63
67
 
64
- if (!hasReactiveImport(sourceFile) || !hasReactiveUsage(code)) {
68
+ // Try to get pre-analyzed data from context
69
+ let analyzed = getAnalyzedFile(context, filename);
70
+
71
+ // Fall back to inline check (for Vite or when context unavailable)
72
+ if (!analyzed) {
73
+ if (!hasReactiveImport(sourceFile)) {
74
+ return { changed: false, code, sourceFile };
75
+ }
76
+ }
77
+ else if (!analyzed.hasReactiveImport) {
65
78
  return { changed: false, code, sourceFile };
66
79
  }
67
80
 
68
81
  for (let i = 0, n = transforms.length; i < n; i++) {
69
- result = transforms[i](current, bindings, COMPILER_NAMESPACE, checker);
82
+ result = transforms[i](current, bindings, checker);
70
83
 
71
84
  if (result !== code) {
72
- current = ts.createSourceFile(sourceFile.fileName, result, sourceFile.languageVersion, true);
73
85
  code = result;
74
86
  changed = true;
87
+ current = ts.createSourceFile(sourceFile.fileName, result, sourceFile.languageVersion, true);
75
88
  }
76
89
  }
77
90
 
78
91
  if (changed) {
79
- code = imports.modify(code, current, PACKAGE, { remove: [COMPILER_ENTRYPOINT] });
80
- code = `import * as ${COMPILER_NAMESPACE} from '@esportsplus/reactivity';\n` + code;
92
+ let remove: string[] = [];
93
+
94
+ if (!ast.hasMatch(current, isReactiveCallNode)) {
95
+ remove.push(COMPILER_ENTRYPOINT);
96
+ }
97
+
98
+ code = imports.modify(code, current, PACKAGE, { namespace: COMPILER_NAMESPACE, remove });
81
99
  sourceFile = ts.createSourceFile(sourceFile.fileName, code, sourceFile.languageVersion, true);
82
100
  }
83
101
 
@@ -85,4 +103,4 @@ const transform = (sourceFile: ts.SourceFile, program: ts.Program): TransformRes
85
103
  };
86
104
 
87
105
 
88
- export { transform };
106
+ export { analyze, isReactiveCall, transform };
@@ -1,10 +1,12 @@
1
1
  import { ts } from '@esportsplus/typescript';
2
- import { code as c, imports, type Replacement } from '@esportsplus/typescript/compiler';
3
- import { COMPILER_ENTRYPOINT, COMPILER_TYPES, PACKAGE } from '~/constants';
2
+ import { code as c, uid, type Replacement } from '@esportsplus/typescript/compiler';
3
+ import { COMPILER_NAMESPACE, COMPILER_TYPES } from '~/constants';
4
4
  import type { Bindings } from '~/types';
5
+ import { isReactiveCall } from '.';
5
6
 
6
7
 
7
8
  interface AnalyzedProperty {
9
+ isStatic: boolean;
8
10
  key: string;
9
11
  type: COMPILER_TYPES;
10
12
  valueText: string;
@@ -24,7 +26,6 @@ interface TransformContext {
24
26
  checker?: ts.TypeChecker;
25
27
  classCounter: number;
26
28
  lastImportEnd: number;
27
- ns: string;
28
29
  sourceFile: ts.SourceFile;
29
30
  }
30
31
 
@@ -43,82 +44,129 @@ function analyzeProperty(prop: ts.ObjectLiteralElementLike, sourceFile: ts.Sourc
43
44
  return null;
44
45
  }
45
46
 
46
- let value = prop.initializer,
47
+ let unwrapped = prop.initializer,
48
+ value = unwrapped,
47
49
  valueText = value.getText(sourceFile);
48
50
 
49
- if (ts.isArrowFunction(value) || ts.isFunctionExpression(value)) {
50
- return { key, type: COMPILER_TYPES.Computed, valueText };
51
+ while (ts.isAsExpression(unwrapped) || ts.isTypeAssertionExpression(unwrapped) || ts.isParenthesizedExpression(unwrapped)) {
52
+ unwrapped = unwrapped.expression;
51
53
  }
52
54
 
53
- if (ts.isArrayLiteralExpression(value)) {
54
- let elements = value.elements,
55
- elementsText = '';
55
+ if (ts.isArrowFunction(unwrapped) || ts.isFunctionExpression(unwrapped)) {
56
+ return { isStatic: false, key, type: COMPILER_TYPES.Computed, valueText };
57
+ }
58
+
59
+ if (ts.isArrayLiteralExpression(unwrapped)) {
60
+ let elements = unwrapped.elements,
61
+ isStatic = value === unwrapped;
56
62
 
57
63
  for (let i = 0, n = elements.length; i < n; i++) {
58
- if (i > 0) {
59
- elementsText += ', ';
64
+ if (isStatic && !isStaticValue(elements[i])) {
65
+ isStatic = false;
60
66
  }
61
-
62
- elementsText += elements[i].getText(sourceFile);
63
67
  }
64
68
 
65
- return { key, type: COMPILER_TYPES.Array, valueText: elementsText };
69
+ return { isStatic, key, type: COMPILER_TYPES.Array, valueText };
66
70
  }
67
71
 
68
- return { key, type: COMPILER_TYPES.Signal, valueText };
72
+ return { isStatic: isStaticValue(value), key, type: COMPILER_TYPES.Signal, valueText };
69
73
  }
70
74
 
71
- function buildClassCode(className: string, properties: AnalyzedProperty[], ns: string): string {
75
+ function buildClassCode(className: string, properties: AnalyzedProperty[]): string {
72
76
  let accessors: string[] = [],
73
- disposeStatements: string[] = [],
77
+ body: string[] = [],
74
78
  fields: string[] = [],
75
- paramCounter = 0;
76
-
77
- fields.push(`[${ns}.REACTIVE_OBJECT] = true;`);
79
+ generics: string[] = [],
80
+ parameters: string[] = [],
81
+ setters = 0;
78
82
 
79
83
  for (let i = 0, n = properties.length; i < n; i++) {
80
- let { key, type, valueText } = properties[i];
84
+ let { isStatic, key, type, valueText } = properties[i],
85
+ generic = `T${parameters.length}`,
86
+ parameter = `_p${parameters.length}`;
81
87
 
82
88
  if (type === COMPILER_TYPES.Signal) {
83
- let param = `_v${paramCounter++}`;
84
-
85
- accessors.push(`get ${key}() { return ${ns}.read(this.#${key}); }`);
86
- accessors.push(`set ${key}(${param}) { ${ns}.write(this.#${key}, ${param}); }`);
87
- fields.push(`#${key} = ${ns}.signal(${valueText});`);
89
+ let value = `_v${setters++}`;
90
+
91
+ if (isStatic) {
92
+ accessors.push(`
93
+ get ${key}() {
94
+ return ${COMPILER_NAMESPACE}.read(this.#${key});
95
+ }
96
+ set ${key}(${value}) {
97
+ ${COMPILER_NAMESPACE}.write(this.#${key}, ${value});
98
+ }
99
+ `);
100
+ fields.push(`#${key} = this[${COMPILER_NAMESPACE}.SIGNAL](${valueText});`);
101
+ }
102
+ else {
103
+ accessors.push(`
104
+ get ${key}() {
105
+ return ${COMPILER_NAMESPACE}.read(this.#${key}) as ${generic};
106
+ }
107
+ set ${key}(${value}) {
108
+ ${COMPILER_NAMESPACE}.write(this.#${key}, ${value});
109
+ }
110
+ `);
111
+ body.push(`this.#${key} = this[${COMPILER_NAMESPACE}.SIGNAL](${parameter});`);
112
+ fields.push(`#${key};`);
113
+ generics.push(generic);
114
+ parameters.push(`${parameter}: ${generic}`);
115
+ }
88
116
  }
89
117
  else if (type === COMPILER_TYPES.Array) {
90
- disposeStatements.push(`this.${key}.dispose();`);
91
- fields.push(`${key} = new ${ns}.ReactiveArray(${valueText});`);
118
+ accessors.push(`
119
+ get ${key}() {
120
+ return this.#${key};
121
+ }
122
+ `);
123
+ body.push(`this.#${key} = this[${COMPILER_NAMESPACE}.REACTIVE_ARRAY](${parameter});`);
124
+ fields.push(`#${key};`);
125
+ generics.push(`${generic} extends unknown[]`);
126
+ parameters.push(`${parameter}: ${generic}`);
92
127
  }
93
128
  else if (type === COMPILER_TYPES.Computed) {
94
- accessors.push(`get ${key}() { return ${ns}.read(this.#${key} ??= ${ns}.computed(${valueText})); }`);
95
- disposeStatements.push(`if (this.#${key}) ${ns}.dispose(this.#${key});`);
96
- fields.push(`#${key} = null;`);
129
+ accessors.push(`
130
+ get ${key}() {
131
+ return ${COMPILER_NAMESPACE}.read(this.#${key});
132
+ }
133
+ `);
134
+ body.push(`this.#${key} = this[${COMPILER_NAMESPACE}.COMPUTED](${parameter});`);
135
+ fields.push(`#${key};`);
136
+ generics.push(`${generic} extends ${COMPILER_NAMESPACE}.Computed<ReturnType<${generic}>>['fn']`);
137
+ parameters.push(`${parameter}: ${generic}`);
97
138
  }
98
139
  }
99
140
 
100
141
  return `
101
- class ${className} {
142
+ class ${className}${generics.length > 0 ? `<${generics.join(', ')}>` : ''} extends ${COMPILER_NAMESPACE}.ReactiveObject<any> {
102
143
  ${fields.join('\n')}
103
- ${accessors.join('\n')}
104
-
105
- dispose() {
106
- ${disposeStatements.length > 0 ? disposeStatements.join('\n') : ''}
144
+ constructor(${parameters.join(', ')}) {
145
+ super(null);
146
+ ${body.join('\n')}
107
147
  }
148
+ ${accessors.join('\n')}
108
149
  }
109
150
  `;
110
151
  }
111
152
 
112
- function isReactiveCall(node: ts.CallExpression, checker?: ts.TypeChecker): boolean {
113
- if (!ts.isIdentifier(node.expression)) {
114
- return false;
153
+ function isStaticValue(node: ts.Node): boolean {
154
+ if (
155
+ ts.isNumericLiteral(node) ||
156
+ ts.isStringLiteral(node) ||
157
+ node.kind === ts.SyntaxKind.TrueKeyword ||
158
+ node.kind === ts.SyntaxKind.FalseKeyword ||
159
+ node.kind === ts.SyntaxKind.NullKeyword ||
160
+ node.kind === ts.SyntaxKind.UndefinedKeyword
161
+ ) {
162
+ return true;
115
163
  }
116
164
 
117
- if (node.expression.text !== COMPILER_ENTRYPOINT) {
118
- return false;
165
+ if (ts.isPrefixUnaryExpression(node) && ts.isNumericLiteral(node.operand)) {
166
+ return true;
119
167
  }
120
168
 
121
- return imports.isFromPackage(node.expression, PACKAGE, checker);
169
+ return false;
122
170
  }
123
171
 
124
172
  function visit(ctx: TransformContext, node: ts.Node): void {
@@ -165,7 +213,7 @@ function visit(ctx: TransformContext, node: ts.Node): void {
165
213
  }
166
214
 
167
215
  ctx.calls.push({
168
- className: `_RO${ctx.classCounter++}`,
216
+ className: uid('ReactiveObject'),
169
217
  end: node.end,
170
218
  properties,
171
219
  start: node.pos,
@@ -178,7 +226,7 @@ function visit(ctx: TransformContext, node: ts.Node): void {
178
226
  }
179
227
 
180
228
 
181
- export default (sourceFile: ts.SourceFile, bindings: Bindings, ns: string, checker?: ts.TypeChecker): string => {
229
+ export default (sourceFile: ts.SourceFile, bindings: Bindings, checker?: ts.TypeChecker): string => {
182
230
  let code = sourceFile.getFullText(),
183
231
  ctx: TransformContext = {
184
232
  bindings,
@@ -186,7 +234,6 @@ export default (sourceFile: ts.SourceFile, bindings: Bindings, ns: string, check
186
234
  checker,
187
235
  classCounter: 0,
188
236
  lastImportEnd: 0,
189
- ns,
190
237
  sourceFile
191
238
  };
192
239
 
@@ -196,7 +243,7 @@ export default (sourceFile: ts.SourceFile, bindings: Bindings, ns: string, check
196
243
  return code;
197
244
  }
198
245
 
199
- let classes = ctx.calls.map(c => buildClassCode(c.className, c.properties, ns)).join('\n'),
246
+ let classes = ctx.calls.map(c => buildClassCode(c.className, c.properties)).join('\n'),
200
247
  replacements: Replacement[] = [];
201
248
 
202
249
  replacements.push({
@@ -210,11 +257,15 @@ export default (sourceFile: ts.SourceFile, bindings: Bindings, ns: string, check
210
257
 
211
258
  replacements.push({
212
259
  end: call.end,
213
- newText: ` new ${call.className}()`,
260
+ newText: ` new ${call.className}(${
261
+ call.properties
262
+ .filter(({ isStatic, type }) => !isStatic || type === COMPILER_TYPES.Computed)
263
+ .map(p => p.valueText)
264
+ .join(', ')
265
+ })`,
214
266
  start: call.start
215
267
  });
216
268
  }
217
269
 
218
270
  return c.replace(code, replacements);
219
271
  };
220
-
@@ -1,5 +1,5 @@
1
1
  import { plugin } from '@esportsplus/typescript/compiler';
2
- import { transform } from '..';
2
+ import { analyze, transform } from '..';
3
3
 
4
4
 
5
- export default plugin.tsc(transform) as ReturnType<typeof plugin.tsc>;
5
+ export default plugin.tsc({ analyze, transform }) as ReturnType<typeof plugin.tsc>;
@@ -1,9 +1,10 @@
1
1
  import { PACKAGE } from '../../constants';
2
2
  import { plugin } from '@esportsplus/typescript/compiler';
3
- import { transform } from '..';
3
+ import { analyze, transform } from '..';
4
4
 
5
5
 
6
6
  export default plugin.vite({
7
+ analyze,
7
8
  name: PACKAGE,
8
9
  transform
9
10
  });
package/src/constants.ts CHANGED
@@ -5,7 +5,7 @@ const COMPILER_ENTRYPOINT = 'reactive';
5
5
 
6
6
  const COMPILER_ENTRYPOINT_REGEX = /\breactive\b/;
7
7
 
8
- const COMPILER_NAMESPACE = uid(COMPILER_ENTRYPOINT);
8
+ const COMPILER_NAMESPACE = uid('reactivity');
9
9
 
10
10
  const enum COMPILER_TYPES {
11
11
  Array,
@@ -14,7 +14,6 @@ const enum COMPILER_TYPES {
14
14
  Signal
15
15
  }
16
16
 
17
-
18
17
  const COMPUTED = Symbol('reactivity.computed');
19
18
 
20
19
  const PACKAGE = '@esportsplus/reactivity';
@@ -25,7 +24,6 @@ const REACTIVE_OBJECT = Symbol('reactivity.reactive.object');
25
24
 
26
25
  const SIGNAL = Symbol('reactivity.signal');
27
26
 
28
-
29
27
  const STABILIZER_IDLE = 0;
30
28
 
31
29
  const STABILIZER_RESCHEDULE = 1;
@@ -34,7 +32,6 @@ const STABILIZER_RUNNING = 2;
34
32
 
35
33
  const STABILIZER_SCHEDULED = 3;
36
34
 
37
-
38
35
  const STATE_NONE = 0;
39
36
 
40
37
  const STATE_CHECK = 1 << 0;
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
- export { REACTIVE_OBJECT } from './constants';
2
- export { default as reactive, ReactiveArray } from './reactive';
1
+ export { isPromise } from '@esportsplus/utilities';
2
+ export { COMPUTED, REACTIVE_ARRAY, REACTIVE_OBJECT, SIGNAL } from './constants';
3
+ export { default as reactive, ReactiveArray, ReactiveObject } from './reactive';
3
4
  export * from './system';
4
5
  export * from './types';
@@ -1,7 +1,8 @@
1
1
  import { isArray } from '@esportsplus/utilities';
2
2
  import { read, signal, write } from '~/system';
3
- import { REACTIVE_ARRAY, REACTIVE_OBJECT } from '~/constants';
3
+ import { REACTIVE_ARRAY } from '~/constants';
4
4
  import type { Signal } from '~/types';
5
+ import { isReactiveObject } from './object';
5
6
 
6
7
 
7
8
  type Events<T> = {
@@ -45,7 +46,7 @@ type Listeners = Record<string, (Listener<any> | null)[]>;
45
46
 
46
47
 
47
48
  function dispose(value: unknown) {
48
- if (value !== null && typeof value === 'object' && (value as any)[REACTIVE_OBJECT] === true) {
49
+ if (isReactiveObject(value)) {
49
50
  (value as { dispose(): void }).dispose();
50
51
  }
51
52
  }
@@ -83,6 +84,7 @@ class ReactiveArray<T> extends Array<T> {
83
84
  this.dispatch('set', { index: i, item: value });
84
85
  }
85
86
 
87
+
86
88
  clear() {
87
89
  this.dispose();
88
90
  write(this._length, 0);
@@ -1,6 +1,9 @@
1
- import { ReactiveArray } from './array';
2
- import { COMPILER_ENTRYPOINT, PACKAGE } from '~/constants';
1
+ import { onCleanup, root } from '@esportsplus/reactivity';
2
+ import { isArray, isObject } from '@esportsplus/utilities';
3
3
  import { Reactive } from '~/types';
4
+ import { ReactiveArray } from './array';
5
+ import { ReactiveObject } from './object';
6
+ import { PACKAGE } from '~/constants';
4
7
 
5
8
 
6
9
  type Guard<T> =
@@ -11,14 +14,38 @@ type Guard<T> =
11
14
  : never;
12
15
 
13
16
 
14
- function reactive<T extends Record<PropertyKey, any>>(_input: Guard<T>): Reactive<T>;
15
- function reactive<T>(_input: T): Reactive<T> {
16
- throw new Error(
17
- `${PACKAGE}: ${COMPILER_ENTRYPOINT}() called at runtime. ` +
18
- 'Ensure vite plugin is configured.'
19
- );
17
+ function reactive<T extends unknown[]>(input: T): Reactive<T>;
18
+ function reactive<T extends Record<PropertyKey, unknown>>(input: Guard<T>): Reactive<T>;
19
+ function reactive<T extends unknown[] | Record<PropertyKey, unknown>>(input: T): Reactive<T> {
20
+ let dispose = false,
21
+ value = root(() => {
22
+ let response: Reactive<T> | undefined;
23
+
24
+ if (isObject(input)) {
25
+ response = new ReactiveObject(input) as any as Reactive<T>;
26
+ }
27
+ else if (isArray(input)) {
28
+ response = new ReactiveArray(...input) as any as Reactive<T>;
29
+ }
30
+
31
+ if (response) {
32
+ if (root.disposables) {
33
+ dispose = true;
34
+ }
35
+
36
+ return response;
37
+ }
38
+
39
+ throw new Error(`${PACKAGE}: 'reactive' received invalid input - ${JSON.stringify(input)}`);
40
+ });
41
+
42
+ if (dispose) {
43
+ onCleanup(() => value.dispose());
44
+ }
45
+
46
+ return value;
20
47
  }
21
48
 
22
49
 
23
50
  export default reactive;
24
- export { reactive, ReactiveArray };
51
+ export { reactive, ReactiveArray, ReactiveObject };