@esportsplus/reactivity 0.22.3 → 0.23.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/build/index.d.ts +1 -1
  2. package/build/index.js +1 -1
  3. package/build/reactive/array.d.ts +3 -0
  4. package/build/reactive/array.js +32 -2
  5. package/build/reactive/index.d.ts +17 -14
  6. package/build/reactive/index.js +7 -25
  7. package/build/system.js +1 -1
  8. package/build/transformer/detector.d.ts +2 -0
  9. package/build/transformer/detector.js +38 -0
  10. package/build/transformer/index.d.ts +10 -0
  11. package/build/transformer/index.js +55 -0
  12. package/build/transformer/plugins/esbuild.d.ts +5 -0
  13. package/build/transformer/plugins/esbuild.js +31 -0
  14. package/build/transformer/plugins/tsc.d.ts +3 -0
  15. package/build/transformer/plugins/tsc.js +4 -0
  16. package/build/transformer/plugins/vite.d.ts +5 -0
  17. package/build/transformer/plugins/vite.js +28 -0
  18. package/build/transformer/transforms/auto-dispose.d.ts +3 -0
  19. package/build/transformer/transforms/auto-dispose.js +119 -0
  20. package/build/transformer/transforms/reactive-array.d.ts +4 -0
  21. package/build/transformer/transforms/reactive-array.js +93 -0
  22. package/build/transformer/transforms/reactive-object.d.ts +4 -0
  23. package/build/transformer/transforms/reactive-object.js +164 -0
  24. package/build/transformer/transforms/reactive-primitives.d.ts +4 -0
  25. package/build/transformer/transforms/reactive-primitives.js +335 -0
  26. package/build/transformer/transforms/utilities.d.ts +8 -0
  27. package/build/transformer/transforms/utilities.js +73 -0
  28. package/build/types.d.ts +14 -4
  29. package/package.json +30 -3
  30. package/readme.md +276 -2
  31. package/src/constants.ts +1 -1
  32. package/src/index.ts +1 -1
  33. package/src/reactive/array.ts +49 -2
  34. package/src/reactive/index.ts +33 -57
  35. package/src/system.ts +14 -5
  36. package/src/transformer/detector.ts +65 -0
  37. package/src/transformer/index.ts +78 -0
  38. package/src/transformer/plugins/esbuild.ts +47 -0
  39. package/src/transformer/plugins/tsc.ts +8 -0
  40. package/src/transformer/plugins/vite.ts +39 -0
  41. package/src/transformer/transforms/auto-dispose.ts +191 -0
  42. package/src/transformer/transforms/reactive-array.ts +143 -0
  43. package/src/transformer/transforms/reactive-object.ts +253 -0
  44. package/src/transformer/transforms/reactive-primitives.ts +461 -0
  45. package/src/transformer/transforms/utilities.ts +119 -0
  46. package/src/types.ts +24 -5
  47. package/test/arrays.ts +146 -0
  48. package/test/effects.ts +168 -0
  49. package/test/index.ts +8 -0
  50. package/test/nested.ts +201 -0
  51. package/test/objects.ts +106 -0
  52. package/test/primitives.ts +171 -0
  53. package/test/vite.config.ts +40 -0
  54. package/build/reactive/object.d.ts +0 -7
  55. package/build/reactive/object.js +0 -79
  56. package/src/reactive/object.ts +0 -116
@@ -0,0 +1,164 @@
1
+ import { uid } from '@esportsplus/typescript/transformer';
2
+ import { addMissingImports, applyReplacements } from './utilities.js';
3
+ import ts from 'typescript';
4
+ const CLASS_NAME_REGEX = /class (\w+)/;
5
+ const EXTRA_IMPORTS = [
6
+ { module: '@esportsplus/reactivity/constants', specifier: 'REACTIVE_OBJECT' },
7
+ { module: '@esportsplus/reactivity/reactive/array', specifier: 'ReactiveArray' }
8
+ ];
9
+ function analyzeProperty(prop, sourceFile) {
10
+ if (!ts.isPropertyAssignment(prop)) {
11
+ return null;
12
+ }
13
+ let key;
14
+ if (ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)) {
15
+ key = prop.name.text;
16
+ }
17
+ else {
18
+ return null;
19
+ }
20
+ let value = prop.initializer, valueText = value.getText(sourceFile);
21
+ if (ts.isArrowFunction(value) || ts.isFunctionExpression(value)) {
22
+ return { key, type: 'computed', valueText };
23
+ }
24
+ if (ts.isArrayLiteralExpression(value)) {
25
+ return { key, type: 'array', valueText };
26
+ }
27
+ return { key, type: 'signal', valueText };
28
+ }
29
+ function buildClassCode(className, properties) {
30
+ let accessors = [], disposeStatements = [], fields = [];
31
+ fields.push(`[REACTIVE_OBJECT] = true;`);
32
+ for (let i = 0, n = properties.length; i < n; i++) {
33
+ let { key, type, valueText } = properties[i];
34
+ if (type === 'signal') {
35
+ let param = uid('v');
36
+ fields.push(`#${key} = signal(${valueText});`);
37
+ accessors.push(`get ${key}() { return read(this.#${key}); }`);
38
+ accessors.push(`set ${key}(${param}) { set(this.#${key}, ${param}); }`);
39
+ }
40
+ else if (type === 'array') {
41
+ let elements = valueText.slice(1, -1);
42
+ fields.push(`${key} = new ReactiveArray(${elements});`);
43
+ disposeStatements.push(`this.${key}.dispose();`);
44
+ }
45
+ else if (type === 'computed') {
46
+ fields.push(`#${key}: Computed<unknown> | null = null;`);
47
+ accessors.push(`get ${key}() { return read(this.#${key} ??= computed(${valueText})); }`);
48
+ disposeStatements.push(`if (this.#${key}) dispose(this.#${key});`);
49
+ }
50
+ }
51
+ let disposeBody = disposeStatements.length > 0 ? disposeStatements.join('\n') : '';
52
+ return `
53
+ class ${className} {
54
+ ${fields.join('\n')}
55
+ ${accessors.join('\n')}
56
+
57
+ dispose() {
58
+ ${disposeBody}
59
+ }
60
+ }
61
+ `;
62
+ }
63
+ function visit(ctx, node) {
64
+ if (ts.isImportDeclaration(node)) {
65
+ ctx.lastImportEnd = node.end;
66
+ if (ts.isStringLiteral(node.moduleSpecifier) &&
67
+ node.moduleSpecifier.text.includes('@esportsplus/reactivity')) {
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
+ }
80
+ if (ctx.hasReactiveImport &&
81
+ ts.isCallExpression(node) &&
82
+ ts.isIdentifier(node.expression) &&
83
+ node.expression.text === 'reactive') {
84
+ let arg = node.arguments[0];
85
+ if (arg && ts.isObjectLiteralExpression(arg)) {
86
+ let varName = null;
87
+ if (ts.isVariableDeclaration(node.parent) && ts.isIdentifier(node.parent.name)) {
88
+ varName = node.parent.name.text;
89
+ ctx.bindings.set(varName, 'object');
90
+ }
91
+ let needsImports = new Set(), properties = [];
92
+ needsImports.add('REACTIVE_OBJECT');
93
+ let props = arg.properties;
94
+ for (let i = 0, n = props.length; i < n; i++) {
95
+ let prop = props[i];
96
+ if (ts.isSpreadAssignment(prop)) {
97
+ ts.forEachChild(node, n => visit(ctx, n));
98
+ return;
99
+ }
100
+ let analyzed = analyzeProperty(prop, ctx.sourceFile);
101
+ if (!analyzed) {
102
+ ts.forEachChild(node, n => visit(ctx, n));
103
+ return;
104
+ }
105
+ properties.push(analyzed);
106
+ if (analyzed.type === 'signal') {
107
+ needsImports.add('read');
108
+ needsImports.add('set');
109
+ needsImports.add('signal');
110
+ }
111
+ else if (analyzed.type === 'array') {
112
+ needsImports.add('ReactiveArray');
113
+ if (varName) {
114
+ ctx.bindings.set(`${varName}.${analyzed.key}`, 'array');
115
+ }
116
+ }
117
+ else if (analyzed.type === 'computed') {
118
+ needsImports.add('computed');
119
+ needsImports.add('dispose');
120
+ needsImports.add('read');
121
+ }
122
+ }
123
+ needsImports.forEach(imp => ctx.allNeededImports.add(imp));
124
+ ctx.calls.push({
125
+ end: node.end,
126
+ generatedClass: buildClassCode(uid('ReactiveObject'), properties),
127
+ needsImports,
128
+ start: node.pos,
129
+ varName
130
+ });
131
+ }
132
+ }
133
+ ts.forEachChild(node, n => visit(ctx, n));
134
+ }
135
+ const transformReactiveObjects = (sourceFile, bindings) => {
136
+ let code = sourceFile.getFullText(), ctx = {
137
+ allNeededImports: new Set(),
138
+ bindings,
139
+ calls: [],
140
+ hasReactiveImport: false,
141
+ lastImportEnd: 0,
142
+ sourceFile
143
+ };
144
+ visit(ctx, sourceFile);
145
+ if (ctx.calls.length === 0) {
146
+ return code;
147
+ }
148
+ let replacements = [];
149
+ replacements.push({
150
+ end: ctx.lastImportEnd,
151
+ newText: code.substring(0, ctx.lastImportEnd) + '\n' + ctx.calls.map(c => c.generatedClass).join('\n') + '\n',
152
+ start: 0
153
+ });
154
+ for (let i = 0, n = ctx.calls.length; i < n; i++) {
155
+ let call = ctx.calls[i], classMatch = call.generatedClass.match(CLASS_NAME_REGEX);
156
+ replacements.push({
157
+ end: call.end,
158
+ newText: ` new ${classMatch ? classMatch[1] : 'ReactiveObject'}()`,
159
+ start: call.start
160
+ });
161
+ }
162
+ return addMissingImports(applyReplacements(code, replacements), ctx.allNeededImports, EXTRA_IMPORTS);
163
+ };
164
+ export { transformReactiveObjects };
@@ -0,0 +1,4 @@
1
+ import type { Bindings } from '../../types.js';
2
+ import ts from 'typescript';
3
+ declare const transformReactivePrimitives: (sourceFile: ts.SourceFile, bindings: Bindings) => string;
4
+ export { transformReactivePrimitives };
@@ -0,0 +1,335 @@
1
+ import { uid } from '@esportsplus/typescript/transformer';
2
+ import { addMissingImports, applyReplacements } from './utilities.js';
3
+ import ts from 'typescript';
4
+ function classifyReactiveArg(arg) {
5
+ if (ts.isArrowFunction(arg) || ts.isFunctionExpression(arg)) {
6
+ return 'computed';
7
+ }
8
+ if (ts.isObjectLiteralExpression(arg) || ts.isArrayLiteralExpression(arg)) {
9
+ return null;
10
+ }
11
+ return 'signal';
12
+ }
13
+ function findBinding(bindings, name, node) {
14
+ for (let i = 0, n = bindings.length; i < n; i++) {
15
+ let b = bindings[i];
16
+ if (b.name === name && isInScope(node, b)) {
17
+ return b;
18
+ }
19
+ }
20
+ return undefined;
21
+ }
22
+ function findEnclosingScope(node) {
23
+ let current = node.parent;
24
+ while (current) {
25
+ if (ts.isBlock(current) ||
26
+ ts.isSourceFile(current) ||
27
+ ts.isFunctionDeclaration(current) ||
28
+ ts.isFunctionExpression(current) ||
29
+ ts.isArrowFunction(current) ||
30
+ ts.isForStatement(current) ||
31
+ ts.isForInStatement(current) ||
32
+ ts.isForOfStatement(current)) {
33
+ return current;
34
+ }
35
+ current = current.parent;
36
+ }
37
+ return node.getSourceFile();
38
+ }
39
+ function getCompoundOperator(kind) {
40
+ if (kind === ts.SyntaxKind.PlusEqualsToken) {
41
+ return '+';
42
+ }
43
+ else if (kind === ts.SyntaxKind.MinusEqualsToken) {
44
+ return '-';
45
+ }
46
+ else if (kind === ts.SyntaxKind.AsteriskEqualsToken) {
47
+ return '*';
48
+ }
49
+ else if (kind === ts.SyntaxKind.SlashEqualsToken) {
50
+ return '/';
51
+ }
52
+ else if (kind === ts.SyntaxKind.PercentEqualsToken) {
53
+ return '%';
54
+ }
55
+ else if (kind === ts.SyntaxKind.AsteriskAsteriskEqualsToken) {
56
+ return '**';
57
+ }
58
+ else if (kind === ts.SyntaxKind.AmpersandEqualsToken) {
59
+ return '&';
60
+ }
61
+ else if (kind === ts.SyntaxKind.BarEqualsToken) {
62
+ return '|';
63
+ }
64
+ else if (kind === ts.SyntaxKind.CaretEqualsToken) {
65
+ return '^';
66
+ }
67
+ else if (kind === ts.SyntaxKind.LessThanLessThanEqualsToken) {
68
+ return '<<';
69
+ }
70
+ else if (kind === ts.SyntaxKind.GreaterThanGreaterThanEqualsToken) {
71
+ return '>>';
72
+ }
73
+ else if (kind === ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken) {
74
+ return '>>>';
75
+ }
76
+ else if (kind === ts.SyntaxKind.AmpersandAmpersandEqualsToken) {
77
+ return '&&';
78
+ }
79
+ else if (kind === ts.SyntaxKind.BarBarEqualsToken) {
80
+ return '||';
81
+ }
82
+ else if (kind === ts.SyntaxKind.QuestionQuestionEqualsToken) {
83
+ return '??';
84
+ }
85
+ else {
86
+ return '+';
87
+ }
88
+ }
89
+ function isInComputedRange(ranges, start, end) {
90
+ for (let i = 0, n = ranges.length; i < n; i++) {
91
+ let r = ranges[i];
92
+ if (start >= r.start && end <= r.end) {
93
+ return true;
94
+ }
95
+ }
96
+ return false;
97
+ }
98
+ function isInDeclarationInit(node) {
99
+ let parent = node.parent;
100
+ if (ts.isVariableDeclaration(parent) && parent.initializer === node) {
101
+ return true;
102
+ }
103
+ return false;
104
+ }
105
+ function isInScope(reference, binding) {
106
+ let current = reference;
107
+ while (current) {
108
+ if (current === binding.scope) {
109
+ return true;
110
+ }
111
+ current = current.parent;
112
+ }
113
+ return false;
114
+ }
115
+ function isReactiveReassignment(node) {
116
+ let parent = node.parent;
117
+ if (ts.isBinaryExpression(parent) &&
118
+ parent.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
119
+ parent.right === node &&
120
+ ts.isCallExpression(node) &&
121
+ ts.isIdentifier(node.expression) &&
122
+ node.expression.text === 'reactive') {
123
+ return true;
124
+ }
125
+ return false;
126
+ }
127
+ function isWriteContext(node) {
128
+ let parent = node.parent;
129
+ if (ts.isBinaryExpression(parent) && parent.left === node) {
130
+ let op = parent.operatorToken.kind;
131
+ if (op === ts.SyntaxKind.EqualsToken) {
132
+ return 'simple';
133
+ }
134
+ if (op >= ts.SyntaxKind.PlusEqualsToken && op <= ts.SyntaxKind.CaretEqualsToken) {
135
+ return 'compound';
136
+ }
137
+ if (op === ts.SyntaxKind.AmpersandAmpersandEqualsToken ||
138
+ op === ts.SyntaxKind.BarBarEqualsToken ||
139
+ op === ts.SyntaxKind.QuestionQuestionEqualsToken) {
140
+ return 'compound';
141
+ }
142
+ }
143
+ if (ts.isPostfixUnaryExpression(parent) || ts.isPrefixUnaryExpression(parent)) {
144
+ let op = parent.operator;
145
+ if (op === ts.SyntaxKind.PlusPlusToken || op === ts.SyntaxKind.MinusMinusToken) {
146
+ return 'increment';
147
+ }
148
+ }
149
+ return false;
150
+ }
151
+ function visit(ctx, node) {
152
+ if (ts.isImportDeclaration(node) &&
153
+ ts.isStringLiteral(node.moduleSpecifier) &&
154
+ node.moduleSpecifier.text.includes('@esportsplus/reactivity')) {
155
+ let clause = node.importClause;
156
+ if (clause?.namedBindings && ts.isNamedImports(clause.namedBindings)) {
157
+ for (let i = 0, n = clause.namedBindings.elements.length; i < n; i++) {
158
+ if (clause.namedBindings.elements[i].name.text === 'reactive') {
159
+ ctx.hasReactiveImport = true;
160
+ break;
161
+ }
162
+ }
163
+ }
164
+ }
165
+ if (ctx.hasReactiveImport &&
166
+ ts.isCallExpression(node) &&
167
+ ts.isIdentifier(node.expression) &&
168
+ node.expression.text === 'reactive' &&
169
+ node.arguments.length > 0) {
170
+ let arg = node.arguments[0], classification = classifyReactiveArg(arg);
171
+ if (classification) {
172
+ let varName = null;
173
+ if (ts.isVariableDeclaration(node.parent) && ts.isIdentifier(node.parent.name)) {
174
+ varName = node.parent.name.text;
175
+ }
176
+ else if (ts.isBinaryExpression(node.parent) &&
177
+ node.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
178
+ ts.isIdentifier(node.parent.left)) {
179
+ varName = node.parent.left.text;
180
+ }
181
+ if (varName) {
182
+ let scope = findEnclosingScope(node);
183
+ ctx.scopedBindings.push({ name: varName, scope, type: classification });
184
+ ctx.bindings.set(varName, classification);
185
+ }
186
+ if (classification === 'computed') {
187
+ ctx.computedArgRanges.push({
188
+ end: arg.end,
189
+ start: arg.getStart(ctx.sourceFile)
190
+ });
191
+ let argCtx = {
192
+ argStart: arg.getStart(ctx.sourceFile),
193
+ innerReplacements: [],
194
+ neededImports: ctx.neededImports,
195
+ scopedBindings: ctx.scopedBindings,
196
+ sourceFile: ctx.sourceFile
197
+ };
198
+ visitArg(argCtx, arg);
199
+ let argText = applyReplacements(arg.getText(ctx.sourceFile), argCtx.innerReplacements);
200
+ ctx.replacements.push({
201
+ end: node.end,
202
+ newText: `computed(${argText})`,
203
+ start: node.pos
204
+ });
205
+ ctx.neededImports.add('computed');
206
+ }
207
+ else {
208
+ let argText = arg.getText(ctx.sourceFile);
209
+ ctx.replacements.push({
210
+ end: node.end,
211
+ newText: `signal(${argText})`,
212
+ start: node.pos
213
+ });
214
+ ctx.neededImports.add('signal');
215
+ }
216
+ }
217
+ }
218
+ if (ts.isIdentifier(node) && !isInDeclarationInit(node.parent)) {
219
+ if (ts.isPropertyAccessExpression(node.parent) && node.parent.name === node) {
220
+ ts.forEachChild(node, n => visit(ctx, n));
221
+ return;
222
+ }
223
+ let nodeStart = node.getStart(ctx.sourceFile);
224
+ if (isInComputedRange(ctx.computedArgRanges, nodeStart, node.end)) {
225
+ ts.forEachChild(node, n => visit(ctx, n));
226
+ return;
227
+ }
228
+ let binding = findBinding(ctx.scopedBindings, node.text, node), name = node.text;
229
+ if (binding) {
230
+ if (!isReactiveReassignment(node.parent) &&
231
+ !(ts.isTypeOfExpression(node.parent) && node.parent.expression === node)) {
232
+ let writeCtx = isWriteContext(node);
233
+ if (writeCtx) {
234
+ if (binding.type !== 'computed') {
235
+ ctx.neededImports.add('set');
236
+ let parent = node.parent;
237
+ if (writeCtx === 'simple' && ts.isBinaryExpression(parent)) {
238
+ let valueText = parent.right.getText(ctx.sourceFile);
239
+ ctx.replacements.push({
240
+ end: parent.end,
241
+ newText: `set(${name}, ${valueText})`,
242
+ start: parent.pos
243
+ });
244
+ }
245
+ else if (writeCtx === 'compound' && ts.isBinaryExpression(parent)) {
246
+ let op = getCompoundOperator(parent.operatorToken.kind), valueText = parent.right.getText(ctx.sourceFile);
247
+ ctx.replacements.push({
248
+ end: parent.end,
249
+ newText: `set(${name}, ${name}.value ${op} ${valueText})`,
250
+ start: parent.pos
251
+ });
252
+ }
253
+ else if (writeCtx === 'increment') {
254
+ let isPrefix = ts.isPrefixUnaryExpression(parent), op = parent.operator, delta = op === ts.SyntaxKind.PlusPlusToken ? '+ 1' : '- 1';
255
+ if (ts.isExpressionStatement(parent.parent)) {
256
+ ctx.replacements.push({
257
+ end: parent.end,
258
+ newText: `set(${name}, ${name}.value ${delta})`,
259
+ start: parent.pos
260
+ });
261
+ }
262
+ else if (isPrefix) {
263
+ ctx.replacements.push({
264
+ end: parent.end,
265
+ newText: `(set(${name}, ${name}.value ${delta}), ${name}.value)`,
266
+ start: parent.pos
267
+ });
268
+ }
269
+ else {
270
+ let tmp = uid('tmp');
271
+ ctx.replacements.push({
272
+ end: parent.end,
273
+ newText: `((${tmp}) => (set(${name}, ${tmp} ${delta}), ${tmp}))(${name}.value)`,
274
+ start: parent.pos
275
+ });
276
+ }
277
+ }
278
+ }
279
+ }
280
+ else {
281
+ ctx.neededImports.add('read');
282
+ ctx.replacements.push({
283
+ end: node.end,
284
+ newText: `read(${name})`,
285
+ start: node.pos
286
+ });
287
+ }
288
+ }
289
+ }
290
+ }
291
+ ts.forEachChild(node, n => visit(ctx, n));
292
+ }
293
+ function visitArg(ctx, node) {
294
+ if (ts.isIdentifier(node)) {
295
+ if (ts.isPropertyAccessExpression(node.parent) && node.parent.name === node) {
296
+ ts.forEachChild(node, n => visitArg(ctx, n));
297
+ return;
298
+ }
299
+ if (ts.isCallExpression(node.parent) && node.parent.expression === node) {
300
+ ts.forEachChild(node, n => visitArg(ctx, n));
301
+ return;
302
+ }
303
+ let binding = findBinding(ctx.scopedBindings, node.text, node);
304
+ if (binding) {
305
+ ctx.neededImports.add('read');
306
+ ctx.innerReplacements.push({
307
+ end: node.end - ctx.argStart,
308
+ newText: `read(${node.text})`,
309
+ start: node.getStart(ctx.sourceFile) - ctx.argStart
310
+ });
311
+ }
312
+ }
313
+ ts.forEachChild(node, n => visitArg(ctx, n));
314
+ }
315
+ const transformReactivePrimitives = (sourceFile, bindings) => {
316
+ let code = sourceFile.getFullText(), ctx = {
317
+ bindings,
318
+ computedArgRanges: [],
319
+ hasReactiveImport: false,
320
+ neededImports: new Set(),
321
+ replacements: [],
322
+ scopedBindings: [],
323
+ sourceFile
324
+ };
325
+ visit(ctx, sourceFile);
326
+ if (ctx.replacements.length === 0) {
327
+ return code;
328
+ }
329
+ let result = applyReplacements(code, ctx.replacements);
330
+ if (ctx.neededImports.size > 0) {
331
+ result = addMissingImports(result, ctx.neededImports);
332
+ }
333
+ return result;
334
+ };
335
+ export { transformReactivePrimitives };
@@ -0,0 +1,8 @@
1
+ import { applyReplacements, type Replacement } from '@esportsplus/typescript/transformer';
2
+ type ExtraImport = {
3
+ module: string;
4
+ specifier: string;
5
+ };
6
+ declare const addMissingImports: (code: string, needed: Set<string>, extraImports?: ExtraImport[]) => string;
7
+ export { addMissingImports, applyReplacements };
8
+ export type { ExtraImport, Replacement };
@@ -0,0 +1,73 @@
1
+ import { applyReplacements } from '@esportsplus/typescript/transformer';
2
+ import ts from 'typescript';
3
+ function findReactivityImport(sourceFile) {
4
+ for (let i = 0, n = sourceFile.statements.length; i < n; i++) {
5
+ let stmt = sourceFile.statements[i];
6
+ if (ts.isImportDeclaration(stmt) &&
7
+ ts.isStringLiteral(stmt.moduleSpecifier) &&
8
+ stmt.moduleSpecifier.text === '@esportsplus/reactivity' &&
9
+ stmt.importClause?.namedBindings &&
10
+ ts.isNamedImports(stmt.importClause.namedBindings)) {
11
+ return stmt;
12
+ }
13
+ }
14
+ return null;
15
+ }
16
+ function getExistingSpecifiers(namedImports) {
17
+ let existing = new Set();
18
+ for (let i = 0, n = namedImports.elements.length; i < n; i++) {
19
+ let el = namedImports.elements[i], name = el.propertyName?.text ?? el.name.text;
20
+ existing.add(name);
21
+ }
22
+ return existing;
23
+ }
24
+ function getFirstImportPos(sourceFile) {
25
+ for (let i = 0, n = sourceFile.statements.length; i < n; i++) {
26
+ if (ts.isImportDeclaration(sourceFile.statements[i])) {
27
+ return sourceFile.statements[i].getStart(sourceFile);
28
+ }
29
+ }
30
+ return 0;
31
+ }
32
+ const addMissingImports = (code, needed, extraImports) => {
33
+ let sourceFile = ts.createSourceFile('temp.ts', code, ts.ScriptTarget.Latest, true), reactivityImport = findReactivityImport(sourceFile);
34
+ if (!reactivityImport) {
35
+ return code;
36
+ }
37
+ let extraSpecifiers = new Set(), namedImports = reactivityImport.importClause.namedBindings, existing = getExistingSpecifiers(namedImports), toAdd = [];
38
+ if (extraImports) {
39
+ for (let i = 0, n = extraImports.length; i < n; i++) {
40
+ extraSpecifiers.add(extraImports[i].specifier);
41
+ }
42
+ }
43
+ for (let imp of needed) {
44
+ if (!extraSpecifiers.has(imp) && !existing.has(imp)) {
45
+ toAdd.push(imp);
46
+ }
47
+ }
48
+ if (toAdd.length > 0) {
49
+ let combined = [];
50
+ for (let item of existing) {
51
+ combined.push(item);
52
+ }
53
+ for (let i = 0, n = toAdd.length; i < n; i++) {
54
+ combined.push(toAdd[i]);
55
+ }
56
+ combined.sort();
57
+ let newSpecifiers = `{ ${combined.join(', ')} }`, bindingsStart = namedImports.getStart(sourceFile), bindingsEnd = namedImports.getEnd();
58
+ code = code.substring(0, bindingsStart) + newSpecifiers + code.substring(bindingsEnd);
59
+ }
60
+ if (extraImports) {
61
+ let insertPos = getFirstImportPos(ts.createSourceFile('temp.ts', code, ts.ScriptTarget.Latest, true));
62
+ for (let i = 0, n = extraImports.length; i < n; i++) {
63
+ let extra = extraImports[i];
64
+ if (needed.has(extra.specifier) && !code.includes(extra.module)) {
65
+ code = code.substring(0, insertPos) +
66
+ `import { ${extra.specifier} } from '${extra.module}';\n` +
67
+ code.substring(insertPos);
68
+ }
69
+ }
70
+ }
71
+ return code;
72
+ };
73
+ export { addMissingImports, applyReplacements };
package/build/types.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { COMPUTED, SIGNAL, STATE_CHECK, STATE_DIRTY, STATE_IN_HEAP, STATE_NONE, STATE_RECOMPUTING } from './constants.js';
2
- import { ReactiveArray } from './reactive/array.js';
3
- import { ReactiveObject } from './reactive/object.js';
2
+ import { ReactiveArray, ReactiveObject } from './reactive/index.js';
3
+ import ts from 'typescript';
4
+ type BindingType = 'array' | 'computed' | 'object' | 'signal';
5
+ type Bindings = Map<string, BindingType>;
4
6
  interface Computed<T> {
5
7
  cleanup: VoidFunction | VoidFunction[] | null;
6
8
  deps: Link | null;
@@ -17,10 +19,10 @@ interface Computed<T> {
17
19
  }
18
20
  interface Link {
19
21
  dep: Signal<unknown> | Computed<unknown>;
20
- sub: Computed<unknown>;
21
22
  nextDep: Link | null;
22
23
  nextSub: Link | null;
23
24
  prevSub: Link | null;
25
+ sub: Computed<unknown>;
24
26
  version: number;
25
27
  }
26
28
  type Signal<T> = {
@@ -29,4 +31,12 @@ type Signal<T> = {
29
31
  type: typeof SIGNAL;
30
32
  value: T;
31
33
  };
32
- export type { Computed, Link, Signal, ReactiveArray, ReactiveObject };
34
+ interface TransformOptions {
35
+ autoDispose?: boolean;
36
+ }
37
+ interface TransformResult {
38
+ code: string;
39
+ sourceFile: ts.SourceFile;
40
+ transformed: boolean;
41
+ }
42
+ export type { BindingType, Bindings, Computed, Link, ReactiveArray, ReactiveObject, Signal, TransformOptions, TransformResult };
package/package.json CHANGED
@@ -1,10 +1,36 @@
1
1
  {
2
2
  "author": "ICJR",
3
3
  "dependencies": {
4
- "@esportsplus/utilities": "^0.25.0"
4
+ "@esportsplus/utilities": "^0.27.2"
5
5
  },
6
6
  "devDependencies": {
7
- "@esportsplus/typescript": "^0.9.2"
7
+ "@esportsplus/typescript": "^0.12.2",
8
+ "@types/node": "^25.0.3",
9
+ "esbuild": "^0.27.2",
10
+ "typescript": "^5.9.3",
11
+ "vite": "^7.3.0"
12
+ },
13
+ "exports": {
14
+ ".": {
15
+ "import": "./build/index.js",
16
+ "types": "./build/index.d.ts"
17
+ },
18
+ "./constants": {
19
+ "import": "./build/constants.js",
20
+ "types": "./build/constants.d.ts"
21
+ },
22
+ "./plugins/esbuild": {
23
+ "import": "./build/transformer/plugins/esbuild.js",
24
+ "types": "./build/transformer/plugins/esbuild.d.ts"
25
+ },
26
+ "./plugins/tsc": {
27
+ "import": "./build/transformer/plugins/tsc.js",
28
+ "types": "./build/transformer/plugins/tsc.d.ts"
29
+ },
30
+ "./plugins/vite": {
31
+ "import": "./build/transformer/plugins/vite.js",
32
+ "types": "./build/transformer/plugins/vite.d.ts"
33
+ }
8
34
  },
9
35
  "main": "build/index.js",
10
36
  "name": "@esportsplus/reactivity",
@@ -15,9 +41,10 @@
15
41
  },
16
42
  "type": "module",
17
43
  "types": "build/index.d.ts",
18
- "version": "0.22.3",
44
+ "version": "0.23.1",
19
45
  "scripts": {
20
46
  "build": "tsc && tsc-alias",
47
+ "build:test": "pnpm build && vite build --config test/vite.config.ts",
21
48
  "-": "-"
22
49
  }
23
50
  }