@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,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, _checker?: ts.TypeChecker) => 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, _checker) => {
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, program: ts.Program) => 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,57 +1,65 @@
1
1
  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.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];
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
+ }
8
11
  function hasReactiveImport(sourceFile) {
9
- let found = imports.find(sourceFile, PACKAGE);
10
- for (let i = 0, n = found.length; i < n; i++) {
11
- if (found[i].specifiers.has(COMPILER_ENTRYPOINT)) {
12
- return true;
13
- }
14
- }
15
- return false;
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;
16
16
  }
17
- function hasReactiveUsage(code) {
18
- if (!c.contains(code, { regex: COMPILER_ENTRYPOINT_REGEX })) {
17
+ const analyze = (sourceFile, _program, context) => {
18
+ if (!hasReactiveImport(sourceFile)) {
19
+ return;
20
+ }
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)) {
19
32
  return false;
20
33
  }
21
- let sourceFile = ts.createSourceFile('detect.ts', code, ts.ScriptTarget.Latest, false), used = false;
22
- function visit(node) {
23
- if (used) {
24
- return;
25
- }
26
- if (ts.isCallExpression(node) &&
27
- ts.isIdentifier(node.expression) &&
28
- node.expression.text === COMPILER_ENTRYPOINT) {
29
- used = true;
30
- return;
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 };
31
42
  }
32
- ts.forEachChild(node, visit);
33
43
  }
34
- visit(sourceFile);
35
- return used;
36
- }
37
- const transform = (sourceFile, program) => {
38
- let bindings = new Map(), changed = false, checker = program.getTypeChecker(), code = sourceFile.getFullText(), current = sourceFile, result;
39
- if (!hasReactiveImport(sourceFile) || !hasReactiveUsage(code)) {
44
+ else if (!analyzed.hasReactiveImport) {
40
45
  return { changed: false, code, sourceFile };
41
46
  }
42
47
  for (let i = 0, n = transforms.length; i < n; i++) {
43
- result = transforms[i](current, bindings, COMPILER_NAMESPACE, checker);
48
+ result = transforms[i](current, bindings, checker);
44
49
  if (result !== code) {
45
- current = ts.createSourceFile(sourceFile.fileName, result, sourceFile.languageVersion, true);
46
50
  code = result;
47
51
  changed = true;
52
+ current = ts.createSourceFile(sourceFile.fileName, result, sourceFile.languageVersion, true);
48
53
  }
49
54
  }
50
55
  if (changed) {
51
- code = imports.modify(code, current, PACKAGE, { remove: [COMPILER_ENTRYPOINT] });
52
- 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 });
53
61
  sourceFile = ts.createSourceFile(sourceFile.fileName, code, sourceFile.languageVersion, true);
54
62
  }
55
63
  return { changed, code, sourceFile };
56
64
  };
57
- 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, checker?: ts.TypeChecker) => 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, imports } from '@esportsplus/typescript/compiler';
3
- import { COMPILER_ENTRYPOINT, 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,62 +13,103 @@ 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
  }
63
- function isReactiveCall(node, checker) {
64
- if (!ts.isIdentifier(node.expression)) {
65
- return false;
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;
66
108
  }
67
- if (node.expression.text !== COMPILER_ENTRYPOINT) {
68
- return false;
109
+ if (ts.isPrefixUnaryExpression(node) && ts.isNumericLiteral(node.operand)) {
110
+ return true;
69
111
  }
70
- return imports.isFromPackage(node.expression, PACKAGE, checker);
112
+ return false;
71
113
  }
72
114
  function visit(ctx, node) {
73
115
  if (ts.isImportDeclaration(node)) {
@@ -99,7 +141,7 @@ function visit(ctx, node) {
99
141
  }
100
142
  }
101
143
  ctx.calls.push({
102
- className: `_RO${ctx.classCounter++}`,
144
+ className: uid('ReactiveObject'),
103
145
  end: node.end,
104
146
  properties,
105
147
  start: node.pos,
@@ -109,21 +151,20 @@ function visit(ctx, node) {
109
151
  }
110
152
  ts.forEachChild(node, n => visit(ctx, n));
111
153
  }
112
- export default (sourceFile, bindings, ns, checker) => {
154
+ export default (sourceFile, bindings, checker) => {
113
155
  let code = sourceFile.getFullText(), ctx = {
114
156
  bindings,
115
157
  calls: [],
116
158
  checker,
117
159
  classCounter: 0,
118
160
  lastImportEnd: 0,
119
- ns,
120
161
  sourceFile
121
162
  };
122
163
  visit(ctx, sourceFile);
123
164
  if (ctx.calls.length === 0) {
124
165
  return code;
125
166
  }
126
- 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 = [];
127
168
  replacements.push({
128
169
  end: ctx.lastImportEnd,
129
170
  newText: code.substring(0, ctx.lastImportEnd) + '\n' + classes + '\n',
@@ -133,7 +174,10 @@ export default (sourceFile, bindings, ns, checker) => {
133
174
  let call = ctx.calls[i];
134
175
  replacements.push({
135
176
  end: call.end,
136
- 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(', ')})`,
137
181
  start: call.start
138
182
  });
139
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.22.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.27.0",
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",