@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,4 +1,4 @@
1
1
  import { ts } from '@esportsplus/typescript';
2
2
  import type { Bindings } from '../types.js';
3
- declare const _default: (sourceFile: ts.SourceFile, bindings: Bindings, _ns: string) => string;
3
+ declare const _default: (sourceFile: ts.SourceFile, bindings: Bindings, checker?: ts.TypeChecker) => string;
4
4
  export default _default;
@@ -1,7 +1,23 @@
1
- import { ast, code as c } from '@esportsplus/typescript/compiler';
2
1
  import { ts } from '@esportsplus/typescript';
3
- import { COMPILER_TYPES } from '../constants.js';
2
+ import { ast, code as c } from '@esportsplus/typescript/compiler';
3
+ import { COMPILER_NAMESPACE, COMPILER_TYPES } from '../constants.js';
4
+ import { isReactiveCall } from './index.js';
4
5
  function visit(ctx, node) {
6
+ if (ts.isCallExpression(node) && isReactiveCall(node, ctx.checker) && node.arguments.length > 0) {
7
+ let arg = node.arguments[0], expression = ts.isAsExpression(arg) ? arg.expression : arg;
8
+ if (ts.isArrayLiteralExpression(expression)) {
9
+ if (node.parent && ts.isVariableDeclaration(node.parent) && ts.isIdentifier(node.parent.name)) {
10
+ ctx.bindings.set(node.parent.name.text, COMPILER_TYPES.Array);
11
+ }
12
+ ctx.replacements.push({
13
+ end: node.end,
14
+ newText: expression.elements.length > 0
15
+ ? ` new ${COMPILER_NAMESPACE}.ReactiveArray(...${expression.getText(ctx.sourceFile)})`
16
+ : ` new ${COMPILER_NAMESPACE}.ReactiveArray()`,
17
+ start: node.pos
18
+ });
19
+ }
20
+ }
5
21
  if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
6
22
  if (ts.isIdentifier(node.initializer) && ctx.bindings.get(node.initializer.text) === COMPILER_TYPES.Array) {
7
23
  ctx.bindings.set(node.name.text, COMPILER_TYPES.Array);
@@ -42,24 +58,28 @@ function visit(ctx, node) {
42
58
  if (ts.isBinaryExpression(node) &&
43
59
  node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
44
60
  ts.isElementAccessExpression(node.left)) {
45
- let elemAccess = node.left, objName = ast.getExpressionName(elemAccess.expression);
46
- if (objName && ctx.bindings.get(objName) === COMPILER_TYPES.Array) {
47
- let index = elemAccess.argumentExpression.getText(ctx.sourceFile), obj = elemAccess.expression.getText(ctx.sourceFile), value = node.right.getText(ctx.sourceFile);
61
+ let element = node.left, name = ast.getExpressionName(element.expression);
62
+ if (name && ctx.bindings.get(name) === COMPILER_TYPES.Array) {
63
+ let index = element.argumentExpression.getText(ctx.sourceFile), value = node.right.getText(ctx.sourceFile);
48
64
  ctx.replacements.push({
49
65
  end: node.end,
50
- newText: `${obj}.$set(${index}, ${value})`,
66
+ newText: `${element.expression.getText(ctx.sourceFile)}.$set(${index}, ${value})`,
51
67
  start: node.pos
52
68
  });
53
69
  }
54
70
  }
55
71
  ts.forEachChild(node, n => visit(ctx, n));
56
72
  }
57
- export default (sourceFile, bindings, _ns) => {
73
+ export default (sourceFile, bindings, checker) => {
58
74
  let code = sourceFile.getFullText(), ctx = {
59
75
  bindings,
76
+ checker,
60
77
  replacements: [],
61
78
  sourceFile
62
79
  };
63
80
  visit(ctx, sourceFile);
81
+ if (ctx.replacements.length === 0) {
82
+ return code;
83
+ }
64
84
  return c.replace(code, ctx.replacements);
65
85
  };
@@ -1,4 +1,7 @@
1
+ import type { PluginContext } from '@esportsplus/typescript/compiler';
1
2
  import { ts } from '@esportsplus/typescript';
2
3
  import type { TransformResult } from '../types.js';
3
- declare const transform: (sourceFile: ts.SourceFile) => TransformResult;
4
- export { transform };
4
+ declare const analyze: (sourceFile: ts.SourceFile, _program: ts.Program, context: PluginContext) => void;
5
+ declare const isReactiveCall: (node: ts.CallExpression, _checker?: ts.TypeChecker) => boolean;
6
+ declare const transform: (sourceFile: ts.SourceFile, program: ts.Program, context?: PluginContext) => TransformResult;
7
+ export { analyze, isReactiveCall, transform };
@@ -1,61 +1,65 @@
1
1
  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.js';
2
+ import { ast, imports } from '@esportsplus/typescript/compiler';
3
+ import { COMPILER_ENTRYPOINT, COMPILER_NAMESPACE, PACKAGE } from '../constants.js';
4
4
  import array from './array.js';
5
5
  import object from './object.js';
6
- import primitives from './primitives.js';
7
- let transforms = [object, array, primitives];
8
- function contains(code) {
9
- if (!c.contains(code, { regex: COMPILER_ENTRYPOINT_REGEX })) {
10
- return false;
11
- }
12
- let ctx = {
13
- imported: false,
14
- used: false
15
- };
16
- visit(ctx, ts.createSourceFile('detect.ts', code, ts.ScriptTarget.Latest, false));
17
- return ctx.imported && ctx.used;
6
+ const CONTEXT_KEY = 'reactivity:analyzed';
7
+ let transforms = [object, array];
8
+ function getAnalyzedFile(context, filename) {
9
+ return context?.get(CONTEXT_KEY)?.get(filename);
10
+ }
11
+ function hasReactiveImport(sourceFile) {
12
+ return imports.find(sourceFile, PACKAGE).some(i => i.specifiers.has(COMPILER_ENTRYPOINT));
13
+ }
14
+ function isReactiveCallNode(node) {
15
+ return ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === COMPILER_ENTRYPOINT;
18
16
  }
19
- function visit(ctx, node) {
20
- if (ctx.imported && ctx.used) {
17
+ const analyze = (sourceFile, _program, context) => {
18
+ if (!hasReactiveImport(sourceFile)) {
21
19
  return;
22
20
  }
23
- if (ts.isImportDeclaration(node) &&
24
- node.importClause?.namedBindings &&
25
- ts.isNamedImports(node.importClause.namedBindings)) {
26
- let elements = node.importClause.namedBindings.elements;
27
- for (let i = 0, n = elements.length; i < n; i++) {
28
- let element = elements[i];
29
- if ((element.propertyName?.text ?? element.name.text) === COMPILER_ENTRYPOINT) {
30
- ctx.imported = true;
31
- break;
32
- }
33
- }
21
+ let files = context.get(CONTEXT_KEY);
22
+ if (!files) {
23
+ files = new Map();
24
+ context.set(CONTEXT_KEY, files);
25
+ }
26
+ files.set(sourceFile.fileName, {
27
+ hasReactiveImport: true
28
+ });
29
+ };
30
+ const isReactiveCall = (node, _checker) => {
31
+ if (!ts.isIdentifier(node.expression)) {
32
+ return false;
34
33
  }
35
- if (ts.isCallExpression(node) &&
36
- ts.isIdentifier(node.expression) &&
37
- node.expression.text === COMPILER_ENTRYPOINT) {
38
- ctx.used = true;
34
+ return node.expression.text === COMPILER_ENTRYPOINT;
35
+ };
36
+ const transform = (sourceFile, program, context) => {
37
+ let bindings = new Map(), changed = false, checker = program.getTypeChecker(), code = sourceFile.getFullText(), current = sourceFile, filename = sourceFile.fileName, result;
38
+ let analyzed = getAnalyzedFile(context, filename);
39
+ if (!analyzed) {
40
+ if (!hasReactiveImport(sourceFile)) {
41
+ return { changed: false, code, sourceFile };
42
+ }
39
43
  }
40
- ts.forEachChild(node, n => visit(ctx, n));
41
- }
42
- const transform = (sourceFile) => {
43
- let bindings = new Map(), changed = false, code = sourceFile.getFullText(), current = sourceFile, result;
44
- if (!contains(code)) {
44
+ else if (!analyzed.hasReactiveImport) {
45
45
  return { changed: false, code, sourceFile };
46
46
  }
47
47
  for (let i = 0, n = transforms.length; i < n; i++) {
48
- result = transforms[i](current, bindings, COMPILER_NAMESPACE);
48
+ result = transforms[i](current, bindings, checker);
49
49
  if (result !== code) {
50
- current = ts.createSourceFile(sourceFile.fileName, result, sourceFile.languageVersion, true);
51
50
  code = result;
52
51
  changed = true;
52
+ current = ts.createSourceFile(sourceFile.fileName, result, sourceFile.languageVersion, true);
53
53
  }
54
54
  }
55
55
  if (changed) {
56
- code = `import * as ${COMPILER_NAMESPACE} from '@esportsplus/reactivity';\n` + code;
56
+ let remove = [];
57
+ if (!ast.hasMatch(current, isReactiveCallNode)) {
58
+ remove.push(COMPILER_ENTRYPOINT);
59
+ }
60
+ code = imports.modify(code, current, PACKAGE, { namespace: COMPILER_NAMESPACE, remove });
57
61
  sourceFile = ts.createSourceFile(sourceFile.fileName, code, sourceFile.languageVersion, true);
58
62
  }
59
63
  return { changed, code, sourceFile };
60
64
  };
61
- export { transform };
65
+ export { analyze, isReactiveCall, transform };
@@ -1,4 +1,4 @@
1
1
  import { ts } from '@esportsplus/typescript';
2
2
  import type { Bindings } from '../types.js';
3
- declare const _default: (sourceFile: ts.SourceFile, bindings: Bindings, ns: string) => string;
3
+ declare const _default: (sourceFile: ts.SourceFile, bindings: Bindings, checker?: ts.TypeChecker) => string;
4
4
  export default _default;
@@ -1,6 +1,7 @@
1
1
  import { ts } from '@esportsplus/typescript';
2
- import { code as c } from '@esportsplus/typescript/compiler';
3
- import { COMPILER_TYPES, PACKAGE } from '../constants.js';
2
+ import { code as c, uid } from '@esportsplus/typescript/compiler';
3
+ import { COMPILER_NAMESPACE, COMPILER_TYPES } from '../constants.js';
4
+ import { isReactiveCall } from './index.js';
4
5
  function analyzeProperty(prop, sourceFile) {
5
6
  if (!ts.isPropertyAssignment(prop)) {
6
7
  return null;
@@ -12,75 +13,110 @@ function analyzeProperty(prop, sourceFile) {
12
13
  else {
13
14
  return null;
14
15
  }
15
- let value = prop.initializer, valueText = value.getText(sourceFile);
16
- if (ts.isArrowFunction(value) || ts.isFunctionExpression(value)) {
17
- return { key, type: COMPILER_TYPES.Computed, valueText };
16
+ let unwrapped = prop.initializer, value = unwrapped, valueText = value.getText(sourceFile);
17
+ while (ts.isAsExpression(unwrapped) || ts.isTypeAssertionExpression(unwrapped) || ts.isParenthesizedExpression(unwrapped)) {
18
+ unwrapped = unwrapped.expression;
18
19
  }
19
- if (ts.isArrayLiteralExpression(value)) {
20
- let elements = value.elements, elementsText = '';
20
+ if (ts.isArrowFunction(unwrapped) || ts.isFunctionExpression(unwrapped)) {
21
+ return { isStatic: false, key, type: COMPILER_TYPES.Computed, valueText };
22
+ }
23
+ if (ts.isArrayLiteralExpression(unwrapped)) {
24
+ let elements = unwrapped.elements, isStatic = value === unwrapped;
21
25
  for (let i = 0, n = elements.length; i < n; i++) {
22
- if (i > 0) {
23
- elementsText += ', ';
26
+ if (isStatic && !isStaticValue(elements[i])) {
27
+ isStatic = false;
24
28
  }
25
- elementsText += elements[i].getText(sourceFile);
26
29
  }
27
- return { key, type: COMPILER_TYPES.Array, valueText: elementsText };
30
+ return { isStatic, key, type: COMPILER_TYPES.Array, valueText };
28
31
  }
29
- return { key, type: COMPILER_TYPES.Signal, valueText };
32
+ return { isStatic: isStaticValue(value), key, type: COMPILER_TYPES.Signal, valueText };
30
33
  }
31
- function buildClassCode(className, properties, ns) {
32
- let accessors = [], disposeStatements = [], fields = [], paramCounter = 0;
33
- fields.push(`[${ns}.REACTIVE_OBJECT] = true;`);
34
+ function buildClassCode(className, properties) {
35
+ let accessors = [], body = [], fields = [], generics = [], parameters = [], setters = 0;
34
36
  for (let i = 0, n = properties.length; i < n; i++) {
35
- let { key, type, valueText } = properties[i];
37
+ let { isStatic, key, type, valueText } = properties[i], generic = `T${parameters.length}`, parameter = `_p${parameters.length}`;
36
38
  if (type === COMPILER_TYPES.Signal) {
37
- let param = `_v${paramCounter++}`;
38
- accessors.push(`get ${key}() { return ${ns}.read(this.#${key}); }`);
39
- accessors.push(`set ${key}(${param}) { ${ns}.write(this.#${key}, ${param}); }`);
40
- fields.push(`#${key} = ${ns}.signal(${valueText});`);
39
+ let value = `_v${setters++}`;
40
+ if (isStatic) {
41
+ accessors.push(`
42
+ get ${key}() {
43
+ return ${COMPILER_NAMESPACE}.read(this.#${key});
44
+ }
45
+ set ${key}(${value}) {
46
+ ${COMPILER_NAMESPACE}.write(this.#${key}, ${value});
47
+ }
48
+ `);
49
+ fields.push(`#${key} = this[${COMPILER_NAMESPACE}.SIGNAL](${valueText});`);
50
+ }
51
+ else {
52
+ accessors.push(`
53
+ get ${key}() {
54
+ return ${COMPILER_NAMESPACE}.read(this.#${key}) as ${generic};
55
+ }
56
+ set ${key}(${value}) {
57
+ ${COMPILER_NAMESPACE}.write(this.#${key}, ${value});
58
+ }
59
+ `);
60
+ body.push(`this.#${key} = this[${COMPILER_NAMESPACE}.SIGNAL](${parameter});`);
61
+ fields.push(`#${key};`);
62
+ generics.push(generic);
63
+ parameters.push(`${parameter}: ${generic}`);
64
+ }
41
65
  }
42
66
  else if (type === COMPILER_TYPES.Array) {
43
- disposeStatements.push(`this.${key}.dispose();`);
44
- fields.push(`${key} = new ${ns}.ReactiveArray(${valueText});`);
67
+ accessors.push(`
68
+ get ${key}() {
69
+ return this.#${key};
70
+ }
71
+ `);
72
+ body.push(`this.#${key} = this[${COMPILER_NAMESPACE}.REACTIVE_ARRAY](${parameter});`);
73
+ fields.push(`#${key};`);
74
+ generics.push(`${generic} extends unknown[]`);
75
+ parameters.push(`${parameter}: ${generic}`);
45
76
  }
46
77
  else if (type === COMPILER_TYPES.Computed) {
47
- accessors.push(`get ${key}() { return ${ns}.read(this.#${key} ??= ${ns}.computed(${valueText})); }`);
48
- disposeStatements.push(`if (this.#${key}) ${ns}.dispose(this.#${key});`);
49
- fields.push(`#${key} = null;`);
78
+ accessors.push(`
79
+ get ${key}() {
80
+ return ${COMPILER_NAMESPACE}.read(this.#${key});
81
+ }
82
+ `);
83
+ body.push(`this.#${key} = this[${COMPILER_NAMESPACE}.COMPUTED](${parameter});`);
84
+ fields.push(`#${key};`);
85
+ generics.push(`${generic} extends ${COMPILER_NAMESPACE}.Computed<ReturnType<${generic}>>['fn']`);
86
+ parameters.push(`${parameter}: ${generic}`);
50
87
  }
51
88
  }
52
89
  return `
53
- class ${className} {
90
+ class ${className}${generics.length > 0 ? `<${generics.join(', ')}>` : ''} extends ${COMPILER_NAMESPACE}.ReactiveObject<any> {
54
91
  ${fields.join('\n')}
55
- ${accessors.join('\n')}
56
-
57
- dispose() {
58
- ${disposeStatements.length > 0 ? disposeStatements.join('\n') : ''}
92
+ constructor(${parameters.join(', ')}) {
93
+ super(null);
94
+ ${body.join('\n')}
59
95
  }
96
+ ${accessors.join('\n')}
60
97
  }
61
98
  `;
62
99
  }
100
+ function isStaticValue(node) {
101
+ if (ts.isNumericLiteral(node) ||
102
+ ts.isStringLiteral(node) ||
103
+ node.kind === ts.SyntaxKind.TrueKeyword ||
104
+ node.kind === ts.SyntaxKind.FalseKeyword ||
105
+ node.kind === ts.SyntaxKind.NullKeyword ||
106
+ node.kind === ts.SyntaxKind.UndefinedKeyword) {
107
+ return true;
108
+ }
109
+ if (ts.isPrefixUnaryExpression(node) && ts.isNumericLiteral(node.operand)) {
110
+ return true;
111
+ }
112
+ return false;
113
+ }
63
114
  function visit(ctx, node) {
64
115
  if (ts.isImportDeclaration(node)) {
65
116
  ctx.lastImportEnd = node.end;
66
- if (ts.isStringLiteral(node.moduleSpecifier) &&
67
- node.moduleSpecifier.text.includes(PACKAGE)) {
68
- let clause = node.importClause;
69
- if (clause?.namedBindings && ts.isNamedImports(clause.namedBindings)) {
70
- let elements = clause.namedBindings.elements;
71
- for (let i = 0, n = elements.length; i < n; i++) {
72
- if (elements[i].name.text === 'reactive') {
73
- ctx.hasReactiveImport = true;
74
- break;
75
- }
76
- }
77
- }
78
- }
79
117
  }
80
- if (ctx.hasReactiveImport &&
81
- ts.isCallExpression(node) &&
82
- ts.isIdentifier(node.expression) &&
83
- node.expression.text === 'reactive') {
118
+ if (ts.isCallExpression(node) &&
119
+ isReactiveCall(node, ctx.checker)) {
84
120
  let arg = node.arguments[0];
85
121
  if (arg && ts.isObjectLiteralExpression(arg)) {
86
122
  let properties = [], props = arg.properties, varName = null;
@@ -105,7 +141,7 @@ function visit(ctx, node) {
105
141
  }
106
142
  }
107
143
  ctx.calls.push({
108
- className: `_RO${ctx.classCounter++}`,
144
+ className: uid('ReactiveObject'),
109
145
  end: node.end,
110
146
  properties,
111
147
  start: node.pos,
@@ -115,21 +151,20 @@ function visit(ctx, node) {
115
151
  }
116
152
  ts.forEachChild(node, n => visit(ctx, n));
117
153
  }
118
- export default (sourceFile, bindings, ns) => {
154
+ export default (sourceFile, bindings, checker) => {
119
155
  let code = sourceFile.getFullText(), ctx = {
120
156
  bindings,
121
157
  calls: [],
158
+ checker,
122
159
  classCounter: 0,
123
- hasReactiveImport: false,
124
160
  lastImportEnd: 0,
125
- ns,
126
161
  sourceFile
127
162
  };
128
163
  visit(ctx, sourceFile);
129
164
  if (ctx.calls.length === 0) {
130
165
  return code;
131
166
  }
132
- let classes = ctx.calls.map(c => buildClassCode(c.className, c.properties, ns)).join('\n'), replacements = [];
167
+ let classes = ctx.calls.map(c => buildClassCode(c.className, c.properties)).join('\n'), replacements = [];
133
168
  replacements.push({
134
169
  end: ctx.lastImportEnd,
135
170
  newText: code.substring(0, ctx.lastImportEnd) + '\n' + classes + '\n',
@@ -139,7 +174,10 @@ export default (sourceFile, bindings, ns) => {
139
174
  let call = ctx.calls[i];
140
175
  replacements.push({
141
176
  end: call.end,
142
- newText: ` new ${call.className}()`,
177
+ newText: ` new ${call.className}(${call.properties
178
+ .filter(({ isStatic, type }) => !isStatic || type === COMPILER_TYPES.Computed)
179
+ .map(p => p.valueText)
180
+ .join(', ')})`,
143
181
  start: call.start
144
182
  });
145
183
  }
@@ -1,3 +1,3 @@
1
1
  import { plugin } from '@esportsplus/typescript/compiler';
2
- import { transform } from '../index.js';
3
- export default plugin.tsc(transform);
2
+ import { analyze, transform } from '../index.js';
3
+ export default plugin.tsc({ analyze, transform });
@@ -1,7 +1,8 @@
1
1
  import { PACKAGE } from '../../constants.js';
2
2
  import { plugin } from '@esportsplus/typescript/compiler';
3
- import { transform } from '../index.js';
3
+ import { analyze, transform } from '../index.js';
4
4
  export default plugin.vite({
5
+ analyze,
5
6
  name: PACKAGE,
6
7
  transform
7
8
  });
@@ -1,7 +1,7 @@
1
1
  import { uid } from '@esportsplus/typescript/compiler';
2
2
  const COMPILER_ENTRYPOINT = 'reactive';
3
3
  const COMPILER_ENTRYPOINT_REGEX = /\breactive\b/;
4
- const COMPILER_NAMESPACE = uid(COMPILER_ENTRYPOINT);
4
+ const COMPILER_NAMESPACE = uid('reactivity');
5
5
  var COMPILER_TYPES;
6
6
  (function (COMPILER_TYPES) {
7
7
  COMPILER_TYPES[COMPILER_TYPES["Array"] = 0] = "Array";
package/build/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- export { REACTIVE_OBJECT } from './constants.js';
2
- export { default as reactive, ReactiveArray } from './reactive/index.js';
1
+ export { isPromise } from '@esportsplus/utilities';
2
+ export { COMPUTED, REACTIVE_ARRAY, REACTIVE_OBJECT, SIGNAL } from './constants.js';
3
+ export { default as reactive, ReactiveArray, ReactiveObject } from './reactive/index.js';
3
4
  export * from './system.js';
4
5
  export * from './types.js';
package/build/index.js CHANGED
@@ -1,4 +1,5 @@
1
- export { REACTIVE_OBJECT } from './constants.js';
2
- export { default as reactive, ReactiveArray } from './reactive/index.js';
1
+ export { isPromise } from '@esportsplus/utilities';
2
+ export { COMPUTED, REACTIVE_ARRAY, REACTIVE_OBJECT, SIGNAL } from './constants.js';
3
+ export { default as reactive, ReactiveArray, ReactiveObject } from './reactive/index.js';
3
4
  export * from './system.js';
4
5
  export * from './types.js';
@@ -1,8 +1,9 @@
1
1
  import { isArray } from '@esportsplus/utilities';
2
2
  import { read, signal, write } from '../system.js';
3
- import { REACTIVE_ARRAY, REACTIVE_OBJECT } from '../constants.js';
3
+ import { REACTIVE_ARRAY } from '../constants.js';
4
+ import { isReactiveObject } from './object.js';
4
5
  function dispose(value) {
5
- if (value !== null && typeof value === 'object' && value[REACTIVE_OBJECT] === true) {
6
+ if (isReactiveObject(value)) {
6
7
  value.dispose();
7
8
  }
8
9
  }
@@ -1,10 +1,12 @@
1
- import { ReactiveArray } from './array.js';
2
1
  import { Reactive } from '../types.js';
2
+ import { ReactiveArray } from './array.js';
3
+ import { ReactiveObject } from './object.js';
3
4
  type Guard<T> = T extends Record<PropertyKey, unknown> ? T extends {
4
5
  dispose: any;
5
6
  } ? {
6
7
  never: '[ dispose ] is a reserved key';
7
8
  } : T : never;
8
- declare function reactive<T extends Record<PropertyKey, any>>(_input: Guard<T>): Reactive<T>;
9
+ declare function reactive<T extends unknown[]>(input: T): Reactive<T>;
10
+ declare function reactive<T extends Record<PropertyKey, unknown>>(input: Guard<T>): Reactive<T>;
9
11
  export default reactive;
10
- export { reactive, ReactiveArray };
12
+ export { reactive, ReactiveArray, ReactiveObject };
@@ -1,8 +1,29 @@
1
+ import { onCleanup, root } from '@esportsplus/reactivity';
2
+ import { isArray, isObject } from '@esportsplus/utilities';
1
3
  import { ReactiveArray } from './array.js';
2
- import { COMPILER_ENTRYPOINT, PACKAGE } from '../constants.js';
3
- function reactive(_input) {
4
- throw new Error(`${PACKAGE}: ${COMPILER_ENTRYPOINT}() called at runtime. ` +
5
- 'Ensure vite plugin is configured.');
4
+ import { ReactiveObject } from './object.js';
5
+ import { PACKAGE } from '../constants.js';
6
+ function reactive(input) {
7
+ let dispose = false, value = root(() => {
8
+ let response;
9
+ if (isObject(input)) {
10
+ response = new ReactiveObject(input);
11
+ }
12
+ else if (isArray(input)) {
13
+ response = new ReactiveArray(...input);
14
+ }
15
+ if (response) {
16
+ if (root.disposables) {
17
+ dispose = true;
18
+ }
19
+ return response;
20
+ }
21
+ throw new Error(`${PACKAGE}: 'reactive' received invalid input - ${JSON.stringify(input)}`);
22
+ });
23
+ if (dispose) {
24
+ onCleanup(() => value.dispose());
25
+ }
26
+ return value;
6
27
  }
7
28
  export default reactive;
8
- export { reactive, ReactiveArray };
29
+ export { reactive, ReactiveArray, ReactiveObject };
@@ -0,0 +1,13 @@
1
+ import { Computed, Signal } from '../types.js';
2
+ import { COMPUTED, REACTIVE_ARRAY, SIGNAL } from '../constants.js';
3
+ import { ReactiveArray } from './array.js';
4
+ declare class ReactiveObject<T extends Record<PropertyKey, unknown>> {
5
+ protected disposers: VoidFunction[] | null;
6
+ constructor(data: T | null);
7
+ protected [REACTIVE_ARRAY]<U>(value: U[]): ReactiveArray<U>;
8
+ protected [COMPUTED]<T extends Computed<ReturnType<T>>['fn']>(value: T): Computed<ReturnType<T>> | Signal<ReturnType<T> | undefined>;
9
+ protected [SIGNAL]<T>(value: T): Signal<T>;
10
+ dispose(): void;
11
+ }
12
+ declare const isReactiveObject: (value: any) => value is ReactiveObject<any>;
13
+ export { isReactiveObject, ReactiveObject };
@@ -0,0 +1,86 @@
1
+ import { defineProperty, isArray, isPromise } from '@esportsplus/utilities';
2
+ import { computed, dispose, effect, read, root, signal, write } from '../system.js';
3
+ import { COMPUTED, REACTIVE_ARRAY, REACTIVE_OBJECT, SIGNAL } from '../constants.js';
4
+ import { ReactiveArray } from './array.js';
5
+ class ReactiveObject {
6
+ disposers = null;
7
+ constructor(data) {
8
+ if (data == null) {
9
+ return;
10
+ }
11
+ for (let key in data) {
12
+ let value = data[key], type = typeof value;
13
+ if (type === 'function') {
14
+ let node = this[COMPUTED](value);
15
+ defineProperty(this, key, {
16
+ enumerable: true,
17
+ get: () => read(node)
18
+ });
19
+ continue;
20
+ }
21
+ if (value == null || type !== 'object') {
22
+ }
23
+ else if (isArray(value)) {
24
+ defineProperty(this, key, {
25
+ enumerable: true,
26
+ value: this[REACTIVE_ARRAY](value)
27
+ });
28
+ continue;
29
+ }
30
+ let node = signal(value);
31
+ defineProperty(this, key, {
32
+ enumerable: true,
33
+ get() {
34
+ return read(node);
35
+ },
36
+ set(v) {
37
+ write(node, v);
38
+ }
39
+ });
40
+ }
41
+ }
42
+ [REACTIVE_ARRAY](value) {
43
+ let node = new ReactiveArray(...value);
44
+ (this.disposers ??= []).push(() => node.dispose());
45
+ return node;
46
+ }
47
+ [COMPUTED](value) {
48
+ return root(() => {
49
+ let node = computed(value);
50
+ if (isPromise(node.value)) {
51
+ let factory = node, version = 0;
52
+ node = signal(undefined);
53
+ (this.disposers ??= []).push(effect(() => {
54
+ let id = ++version;
55
+ read(factory).then((v) => {
56
+ if (id !== version) {
57
+ return;
58
+ }
59
+ write(node, v);
60
+ });
61
+ }));
62
+ }
63
+ else {
64
+ (this.disposers ??= []).push(() => dispose(node));
65
+ }
66
+ return node;
67
+ });
68
+ }
69
+ [SIGNAL](value) {
70
+ return signal(value);
71
+ }
72
+ dispose() {
73
+ let disposers = this.disposers, disposer;
74
+ if (!disposers) {
75
+ return;
76
+ }
77
+ while (disposer = disposers.pop()) {
78
+ disposer();
79
+ }
80
+ }
81
+ }
82
+ Object.defineProperty(ReactiveObject.prototype, REACTIVE_OBJECT, { value: true });
83
+ const isReactiveObject = (value) => {
84
+ return typeof value === 'object' && value !== null && value[REACTIVE_OBJECT] === true;
85
+ };
86
+ export { isReactiveObject, ReactiveObject };
package/build/types.d.ts CHANGED
@@ -29,8 +29,8 @@ type Reactive<T> = T extends (...args: unknown[]) => Promise<infer R> ? (R | und
29
29
  readonly [READONLY]: true;
30
30
  } : T extends (...args: any[]) => infer R ? R & {
31
31
  readonly [READONLY]: true;
32
- } : T extends (infer U)[] ? U[] & Pick<ReactiveArray<U>, 'clear' | 'on' | 'once'> : T extends Record<PropertyKey, unknown> ? {
33
- [K in keyof T]: T[K];
32
+ } : T extends (infer U)[] ? U[] & Pick<ReactiveArray<U>, 'clear' | 'dispose' | 'on' | 'once'> : T extends Record<PropertyKey, unknown> ? {
33
+ [K in keyof T]: Reactive<T[K]>;
34
34
  } & {
35
35
  dispose: VoidFunction;
36
36
  } : T;
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "@esportsplus/utilities": "^0.27.2"
5
5
  },
6
6
  "devDependencies": {
7
- "@esportsplus/typescript": "^0.21.0",
7
+ "@esportsplus/typescript": "^0.24.1",
8
8
  "@types/node": "^25.0.3",
9
9
  "vite": "^7.3.0"
10
10
  },
@@ -31,7 +31,7 @@
31
31
  },
32
32
  "type": "module",
33
33
  "types": "build/index.d.ts",
34
- "version": "0.26.1",
34
+ "version": "0.27.2",
35
35
  "scripts": {
36
36
  "build": "tsc",
37
37
  "build:test": "pnpm build && vite build --config test/vite.config.ts",