@esportsplus/reactivity 0.26.1 → 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): 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,87 +1,101 @@
1
+ import type { PluginContext } from '@esportsplus/typescript/compiler';
1
2
  import { ts } from '@esportsplus/typescript';
2
- import { code as c } from '@esportsplus/typescript/compiler';
3
- import { COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REGEX, COMPILER_NAMESPACE } 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 contains(code: string): boolean {
14
- if (!c.contains(code, { regex: COMPILER_ENTRYPOINT_REGEX })) {
15
- return false;
16
- }
15
+ const CONTEXT_KEY = 'reactivity:analyzed';
16
+
17
17
 
18
- let ctx = {
19
- imported: false,
20
- used: false
21
- };
18
+ let transforms = [object, array];
22
19
 
23
- visit(ctx, ts.createSourceFile('detect.ts', code, ts.ScriptTarget.Latest, false));
24
20
 
25
- return ctx.imported && ctx.used;
21
+ function getAnalyzedFile(context: PluginContext | undefined, filename: string): AnalyzedFile | undefined {
22
+ return (context?.get(CONTEXT_KEY) as Map<string, AnalyzedFile> | undefined)?.get(filename);
26
23
  }
27
24
 
28
- function visit(ctx: { imported: boolean; used: boolean; }, node: ts.Node): void {
29
- if (ctx.imported && ctx.used) {
30
- return;
31
- }
25
+ function hasReactiveImport(sourceFile: ts.SourceFile): boolean {
26
+ return imports.find(sourceFile, PACKAGE).some(i => i.specifiers.has(COMPILER_ENTRYPOINT));
27
+ }
32
28
 
33
- if (
34
- ts.isImportDeclaration(node) &&
35
- node.importClause?.namedBindings &&
36
- ts.isNamedImports(node.importClause.namedBindings)
37
- ) {
38
- let elements = node.importClause.namedBindings.elements;
29
+ function isReactiveCallNode(node: ts.Node): boolean {
30
+ return ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === COMPILER_ENTRYPOINT;
31
+ }
39
32
 
40
- for (let i = 0, n = elements.length; i < n; i++) {
41
- let element = elements[i];
42
33
 
43
- if ((element.propertyName?.text ?? element.name.text) === COMPILER_ENTRYPOINT) {
44
- ctx.imported = true;
45
- break;
46
- }
47
- }
34
+ const analyze = (sourceFile: ts.SourceFile, _program: ts.Program, context: PluginContext): void => {
35
+ if (!hasReactiveImport(sourceFile)) {
36
+ return;
48
37
  }
49
38
 
50
- if (
51
- ts.isCallExpression(node) &&
52
- ts.isIdentifier(node.expression) &&
53
- node.expression.text === COMPILER_ENTRYPOINT
54
- ) {
55
- ctx.used = true;
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);
56
44
  }
57
45
 
58
- ts.forEachChild(node, n => visit(ctx, n));
59
- }
46
+ files.set(sourceFile.fileName, {
47
+ hasReactiveImport: true
48
+ });
49
+ };
60
50
 
51
+ const isReactiveCall = (node: ts.CallExpression, _checker?: ts.TypeChecker): boolean => {
52
+ if (!ts.isIdentifier(node.expression)) {
53
+ return false;
54
+ }
55
+
56
+ return node.expression.text === COMPILER_ENTRYPOINT;
57
+ };
61
58
 
62
- const transform = (sourceFile: ts.SourceFile): TransformResult => {
59
+ const transform = (sourceFile: ts.SourceFile, program: ts.Program, context?: PluginContext): TransformResult => {
63
60
  let bindings: Bindings = new Map(),
64
61
  changed = false,
62
+ checker = program.getTypeChecker(),
65
63
  code = sourceFile.getFullText(),
66
64
  current = sourceFile,
65
+ filename = sourceFile.fileName,
67
66
  result: string;
68
67
 
69
- if (!contains(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) {
70
78
  return { changed: false, code, sourceFile };
71
79
  }
72
80
 
73
81
  for (let i = 0, n = transforms.length; i < n; i++) {
74
- result = transforms[i](current, bindings, COMPILER_NAMESPACE);
82
+ result = transforms[i](current, bindings, checker);
75
83
 
76
84
  if (result !== code) {
77
- current = ts.createSourceFile(sourceFile.fileName, result, sourceFile.languageVersion, true);
78
85
  code = result;
79
86
  changed = true;
87
+ current = ts.createSourceFile(sourceFile.fileName, result, sourceFile.languageVersion, true);
80
88
  }
81
89
  }
82
90
 
83
91
  if (changed) {
84
- 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 });
85
99
  sourceFile = ts.createSourceFile(sourceFile.fileName, code, sourceFile.languageVersion, true);
86
100
  }
87
101
 
@@ -89,4 +103,4 @@ const transform = (sourceFile: ts.SourceFile): TransformResult => {
89
103
  };
90
104
 
91
105
 
92
- export { transform };
106
+ export { analyze, isReactiveCall, transform };
@@ -1,10 +1,12 @@
1
1
  import { ts } from '@esportsplus/typescript';
2
- import { code as c, type Replacement } from '@esportsplus/typescript/compiler';
3
- import { 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;
@@ -21,10 +23,9 @@ interface ReactiveObjectCall {
21
23
  interface TransformContext {
22
24
  bindings: Bindings;
23
25
  calls: ReactiveObjectCall[];
26
+ checker?: ts.TypeChecker;
24
27
  classCounter: number;
25
- hasReactiveImport: boolean;
26
28
  lastImportEnd: number;
27
- ns: string;
28
29
  sourceFile: ts.SourceFile;
29
30
  }
30
31
 
@@ -43,100 +44,139 @@ 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++}`;
89
+ let value = `_v${setters++}`;
84
90
 
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});`);
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 visit(ctx: TransformContext, node: ts.Node): void {
113
- if (ts.isImportDeclaration(node)) {
114
- ctx.lastImportEnd = node.end;
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;
163
+ }
115
164
 
116
- if (
117
- ts.isStringLiteral(node.moduleSpecifier) &&
118
- node.moduleSpecifier.text.includes(PACKAGE)
119
- ) {
120
- let clause = node.importClause;
165
+ if (ts.isPrefixUnaryExpression(node) && ts.isNumericLiteral(node.operand)) {
166
+ return true;
167
+ }
121
168
 
122
- if (clause?.namedBindings && ts.isNamedImports(clause.namedBindings)) {
123
- let elements = clause.namedBindings.elements;
169
+ return false;
170
+ }
124
171
 
125
- for (let i = 0, n = elements.length; i < n; i++) {
126
- if (elements[i].name.text === 'reactive') {
127
- ctx.hasReactiveImport = true;
128
- break;
129
- }
130
- }
131
- }
132
- }
172
+ function visit(ctx: TransformContext, node: ts.Node): void {
173
+ if (ts.isImportDeclaration(node)) {
174
+ ctx.lastImportEnd = node.end;
133
175
  }
134
176
 
135
177
  if (
136
- ctx.hasReactiveImport &&
137
178
  ts.isCallExpression(node) &&
138
- ts.isIdentifier(node.expression) &&
139
- node.expression.text === 'reactive'
179
+ isReactiveCall(node, ctx.checker)
140
180
  ) {
141
181
  let arg = node.arguments[0];
142
182
 
@@ -173,7 +213,7 @@ function visit(ctx: TransformContext, node: ts.Node): void {
173
213
  }
174
214
 
175
215
  ctx.calls.push({
176
- className: `_RO${ctx.classCounter++}`,
216
+ className: uid('ReactiveObject'),
177
217
  end: node.end,
178
218
  properties,
179
219
  start: node.pos,
@@ -186,15 +226,14 @@ function visit(ctx: TransformContext, node: ts.Node): void {
186
226
  }
187
227
 
188
228
 
189
- export default (sourceFile: ts.SourceFile, bindings: Bindings, ns: string): string => {
229
+ export default (sourceFile: ts.SourceFile, bindings: Bindings, checker?: ts.TypeChecker): string => {
190
230
  let code = sourceFile.getFullText(),
191
231
  ctx: TransformContext = {
192
232
  bindings,
193
233
  calls: [],
234
+ checker,
194
235
  classCounter: 0,
195
- hasReactiveImport: false,
196
236
  lastImportEnd: 0,
197
- ns,
198
237
  sourceFile
199
238
  };
200
239
 
@@ -204,7 +243,7 @@ export default (sourceFile: ts.SourceFile, bindings: Bindings, ns: string): stri
204
243
  return code;
205
244
  }
206
245
 
207
- 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'),
208
247
  replacements: Replacement[] = [];
209
248
 
210
249
  replacements.push({
@@ -218,10 +257,15 @@ export default (sourceFile: ts.SourceFile, bindings: Bindings, ns: string): stri
218
257
 
219
258
  replacements.push({
220
259
  end: call.end,
221
- 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
+ })`,
222
266
  start: call.start
223
267
  });
224
268
  }
225
269
 
226
270
  return c.replace(code, replacements);
227
- };
271
+ };
@@ -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 };