@esportsplus/reactivity 0.29.13 → 0.29.14

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
- import type { ReplacementIntent } from '@esportsplus/typescript/compiler';
1
+ import { type ReplacementIntent } from '@esportsplus/typescript/compiler';
2
2
  import { ts } from '@esportsplus/typescript';
3
3
  import type { Bindings } from './types.js';
4
4
  type ObjectTransformResult = {
@@ -1,3 +1,4 @@
1
+ import { code } from '@esportsplus/typescript/compiler';
1
2
  import { ts } from '@esportsplus/typescript';
2
3
  import { uid } from '@esportsplus/typescript/compiler';
3
4
  import { NAMESPACE, TYPES } from './constants.js';
@@ -28,12 +29,21 @@ function analyzeProperty(prop, sourceFile) {
28
29
  }
29
30
  return { isStatic, key, type: TYPES.Array, valueText };
30
31
  }
31
- return { isStatic: isStaticValue(value), key, type: TYPES.Signal, valueText };
32
+ return {
33
+ isStatic: isStaticValue(value),
34
+ key,
35
+ type: TYPES.Signal,
36
+ valueText
37
+ };
32
38
  }
33
- function buildClassCode(className, properties) {
34
- let accessors = [], body = [], fields = [], generics = [], parameters = [], setters = 0;
39
+ function buildClassCode(classname, properties, typehint) {
40
+ let accessors = [], body = [], constraint = [], fields = [], generics = [], parameters = [], setters = 0;
35
41
  for (let i = 0, n = properties.length; i < n; i++) {
36
- let { isStatic, key, type, valueText } = properties[i], generic = `T${parameters.length}`, parameter = `_p${parameters.length}`;
42
+ let { isStatic, key, type, valueText } = properties[i], generic = typehint ? `T['${key}']` : `T${parameters.length}`, parameter = `_p${parameters.length}`;
43
+ if (typehint && type === TYPES.Signal) {
44
+ constraint.push(`'${key}'?: unknown`);
45
+ isStatic = false;
46
+ }
37
47
  if (type === TYPES.Signal) {
38
48
  let value = `_v${setters++}`;
39
49
  if (isStatic) {
@@ -58,11 +68,16 @@ function buildClassCode(className, properties) {
58
68
  `);
59
69
  body.push(`this.#${key} = this[${NAMESPACE}.SIGNAL](${parameter});`);
60
70
  fields.push(`#${key};`);
61
- generics.push(generic);
71
+ if (!typehint) {
72
+ generics.push(generic);
73
+ }
62
74
  parameters.push(`${parameter}: ${generic}`);
63
75
  }
64
76
  }
65
77
  else if (type === TYPES.Array) {
78
+ if (typehint) {
79
+ constraint.push(`'${key}'?: unknown[]`);
80
+ }
66
81
  accessors.push(`
67
82
  get ${key}() {
68
83
  return this.#${key};
@@ -70,10 +85,15 @@ function buildClassCode(className, properties) {
70
85
  `);
71
86
  body.push(`this.#${key} = this[${NAMESPACE}.REACTIVE_ARRAY](${parameter});`);
72
87
  fields.push(`#${key};`);
73
- generics.push(`${generic} extends unknown[]`);
88
+ if (!typehint) {
89
+ generics.push(`${generic} extends unknown[]`);
90
+ }
74
91
  parameters.push(`${parameter}: ${generic}`);
75
92
  }
76
93
  else if (type === TYPES.Computed) {
94
+ if (typehint) {
95
+ constraint.push(`'${key}'?: unknown`);
96
+ }
77
97
  accessors.push(`
78
98
  get ${key}() {
79
99
  return ${NAMESPACE}.read(this.#${key});
@@ -81,34 +101,35 @@ function buildClassCode(className, properties) {
81
101
  `);
82
102
  body.push(`this.#${key} = this[${NAMESPACE}.COMPUTED](${parameter});`);
83
103
  fields.push(`#${key};`);
84
- generics.push(`${generic} extends ${NAMESPACE}.Computed<ReturnType<${generic}>>['fn']`);
104
+ if (!typehint) {
105
+ generics.push(`${generic} extends ${NAMESPACE}.Computed<ReturnType<${generic}>>['fn']`);
106
+ }
85
107
  parameters.push(`${parameter}: ${generic}`);
86
108
  }
87
109
  }
88
- return `
89
- class ${className}${generics.length > 0 ? `<${generics.join(', ')}>` : ''} extends ${NAMESPACE}.ReactiveObject<any> {
110
+ return code `
111
+ class ${classname}${typehint
112
+ ? `<T extends { ${constraint.join(', ')} }>`
113
+ : generics.length && `<${generics.join(', ')}>`} extends ${NAMESPACE}.ReactiveObject<any> {
90
114
  ${fields.join('\n')}
115
+
91
116
  constructor(${parameters.join(', ')}) {
92
117
  super(null);
93
118
  ${body.join('\n')}
94
119
  }
120
+
95
121
  ${accessors.join('\n')}
96
122
  }
97
123
  `;
98
124
  }
99
125
  function isStaticValue(node) {
100
- if (ts.isNumericLiteral(node) ||
126
+ return ts.isNumericLiteral(node) ||
101
127
  ts.isStringLiteral(node) ||
102
128
  node.kind === ts.SyntaxKind.TrueKeyword ||
103
129
  node.kind === ts.SyntaxKind.FalseKeyword ||
104
130
  node.kind === ts.SyntaxKind.NullKeyword ||
105
- node.kind === ts.SyntaxKind.UndefinedKeyword) {
106
- return true;
107
- }
108
- if (ts.isPrefixUnaryExpression(node) && ts.isNumericLiteral(node.operand)) {
109
- return true;
110
- }
111
- return false;
131
+ node.kind === ts.SyntaxKind.UndefinedKeyword ||
132
+ (ts.isPrefixUnaryExpression(node) && ts.isNumericLiteral(node.operand));
112
133
  }
113
134
  function visit(ctx, node) {
114
135
  if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'reactive') {
@@ -136,9 +157,10 @@ function visit(ctx, node) {
136
157
  }
137
158
  }
138
159
  ctx.calls.push({
139
- className: uid('ReactiveObject'),
160
+ classname: uid('ReactiveObject'),
140
161
  node,
141
162
  properties,
163
+ typehint: node.typeArguments?.[0]?.getText(ctx.sourceFile) ?? null,
142
164
  varname
143
165
  });
144
166
  }
@@ -154,13 +176,18 @@ export default (sourceFile, bindings) => {
154
176
  visit(ctx, sourceFile);
155
177
  let prepend = [], replacements = [];
156
178
  for (let i = 0, n = ctx.calls.length; i < n; i++) {
157
- let call = ctx.calls[i];
158
- prepend.push(buildClassCode(call.className, call.properties));
179
+ let call = ctx.calls[i], typehint = call.typehint;
180
+ prepend.push(buildClassCode(call.classname, call.properties, typehint));
159
181
  replacements.push({
160
- generate: () => ` new ${call.className}(${call.properties
161
- .filter(({ isStatic, type }) => !isStatic || type === TYPES.Computed)
162
- .map(p => p.valueText)
163
- .join(', ')})`,
182
+ generate: () => {
183
+ let args = call.properties
184
+ .filter(({ isStatic, type }) => typehint || !isStatic || type === TYPES.Computed)
185
+ .map(p => p.valueText)
186
+ .join(', ');
187
+ return typehint
188
+ ? ` new ${call.classname}<${typehint}>(${args})`
189
+ : ` new ${call.classname}(${args})`;
190
+ },
164
191
  node: call.node,
165
192
  });
166
193
  }
package/package.json CHANGED
@@ -35,7 +35,7 @@
35
35
  },
36
36
  "type": "module",
37
37
  "types": "build/index.d.ts",
38
- "version": "0.29.13",
38
+ "version": "0.29.14",
39
39
  "scripts": {
40
40
  "build": "tsc",
41
41
  "build:test": "pnpm build && vite build --config test/vite.config.ts",
@@ -1,4 +1,4 @@
1
- import type { ReplacementIntent } from '@esportsplus/typescript/compiler';
1
+ import { code, type ReplacementIntent } from '@esportsplus/typescript/compiler';
2
2
  import { ts } from '@esportsplus/typescript';
3
3
  import { uid } from '@esportsplus/typescript/compiler';
4
4
  import { NAMESPACE, TYPES } from './constants';
@@ -12,10 +12,16 @@ interface AnalyzedProperty {
12
12
  valueText: string;
13
13
  }
14
14
 
15
+ type ObjectTransformResult = {
16
+ prepend: string[];
17
+ replacements: ReplacementIntent[];
18
+ }
19
+
15
20
  interface ReactiveObjectCall {
16
- className: string;
21
+ classname: string;
17
22
  node: ts.CallExpression;
18
23
  properties: AnalyzedProperty[];
24
+ typehint: string | null;
19
25
  varname: string | null;
20
26
  }
21
27
 
@@ -65,12 +71,18 @@ function analyzeProperty(prop: ts.ObjectLiteralElementLike, sourceFile: ts.Sourc
65
71
  return { isStatic, key, type: TYPES.Array, valueText };
66
72
  }
67
73
 
68
- return { isStatic: isStaticValue(value), key, type: TYPES.Signal, valueText };
74
+ return {
75
+ isStatic: isStaticValue(value),
76
+ key,
77
+ type: TYPES.Signal,
78
+ valueText
79
+ };
69
80
  }
70
81
 
71
- function buildClassCode(className: string, properties: AnalyzedProperty[]): string {
82
+ function buildClassCode(classname: string, properties: AnalyzedProperty[], typehint: string | null): string {
72
83
  let accessors: string[] = [],
73
84
  body: string[] = [],
85
+ constraint: string[] = [],
74
86
  fields: string[] = [],
75
87
  generics: string[] = [],
76
88
  parameters: string[] = [],
@@ -78,9 +90,15 @@ function buildClassCode(className: string, properties: AnalyzedProperty[]): stri
78
90
 
79
91
  for (let i = 0, n = properties.length; i < n; i++) {
80
92
  let { isStatic, key, type, valueText } = properties[i],
81
- generic = `T${parameters.length}`,
93
+ generic = typehint ? `T['${key}']` : `T${parameters.length}`,
82
94
  parameter = `_p${parameters.length}`;
83
95
 
96
+ // When typehint is present, treat signal properties as non-static to preserve types
97
+ if (typehint && type === TYPES.Signal) {
98
+ constraint.push(`'${key}'?: unknown`);
99
+ isStatic = false;
100
+ }
101
+
84
102
  if (type === TYPES.Signal) {
85
103
  let value = `_v${setters++}`;
86
104
 
@@ -106,11 +124,19 @@ function buildClassCode(className: string, properties: AnalyzedProperty[]): stri
106
124
  `);
107
125
  body.push(`this.#${key} = this[${NAMESPACE}.SIGNAL](${parameter});`);
108
126
  fields.push(`#${key};`);
109
- generics.push(generic);
127
+
128
+ if (!typehint) {
129
+ generics.push(generic);
130
+ }
131
+
110
132
  parameters.push(`${parameter}: ${generic}`);
111
133
  }
112
134
  }
113
135
  else if (type === TYPES.Array) {
136
+ if (typehint) {
137
+ constraint.push(`'${key}'?: unknown[]`);
138
+ }
139
+
114
140
  accessors.push(`
115
141
  get ${key}() {
116
142
  return this.#${key};
@@ -118,10 +144,18 @@ function buildClassCode(className: string, properties: AnalyzedProperty[]): stri
118
144
  `);
119
145
  body.push(`this.#${key} = this[${NAMESPACE}.REACTIVE_ARRAY](${parameter});`);
120
146
  fields.push(`#${key};`);
121
- generics.push(`${generic} extends unknown[]`);
147
+
148
+ if (!typehint) {
149
+ generics.push(`${generic} extends unknown[]`);
150
+ }
151
+
122
152
  parameters.push(`${parameter}: ${generic}`);
123
153
  }
124
154
  else if (type === TYPES.Computed) {
155
+ if (typehint) {
156
+ constraint.push(`'${key}'?: unknown`);
157
+ }
158
+
125
159
  accessors.push(`
126
160
  get ${key}() {
127
161
  return ${NAMESPACE}.read(this.#${key});
@@ -129,40 +163,41 @@ function buildClassCode(className: string, properties: AnalyzedProperty[]): stri
129
163
  `);
130
164
  body.push(`this.#${key} = this[${NAMESPACE}.COMPUTED](${parameter});`);
131
165
  fields.push(`#${key};`);
132
- generics.push(`${generic} extends ${NAMESPACE}.Computed<ReturnType<${generic}>>['fn']`);
166
+
167
+ if (!typehint) {
168
+ generics.push(`${generic} extends ${NAMESPACE}.Computed<ReturnType<${generic}>>['fn']`);
169
+ }
170
+
133
171
  parameters.push(`${parameter}: ${generic}`);
134
172
  }
135
173
  }
136
174
 
137
- return `
138
- class ${className}${generics.length > 0 ? `<${generics.join(', ')}>` : ''} extends ${NAMESPACE}.ReactiveObject<any> {
175
+ return code`
176
+ class ${classname}${
177
+ typehint
178
+ ? `<T extends { ${constraint.join(', ')} }>`
179
+ : generics.length && `<${generics.join(', ')}>`
180
+ } extends ${NAMESPACE}.ReactiveObject<any> {
139
181
  ${fields.join('\n')}
182
+
140
183
  constructor(${parameters.join(', ')}) {
141
184
  super(null);
142
185
  ${body.join('\n')}
143
186
  }
187
+
144
188
  ${accessors.join('\n')}
145
189
  }
146
190
  `;
147
191
  }
148
192
 
149
193
  function isStaticValue(node: ts.Node): boolean {
150
- if (
151
- ts.isNumericLiteral(node) ||
194
+ return ts.isNumericLiteral(node) ||
152
195
  ts.isStringLiteral(node) ||
153
196
  node.kind === ts.SyntaxKind.TrueKeyword ||
154
197
  node.kind === ts.SyntaxKind.FalseKeyword ||
155
198
  node.kind === ts.SyntaxKind.NullKeyword ||
156
- node.kind === ts.SyntaxKind.UndefinedKeyword
157
- ) {
158
- return true;
159
- }
160
-
161
- if (ts.isPrefixUnaryExpression(node) && ts.isNumericLiteral(node.operand)) {
162
- return true;
163
- }
164
-
165
- return false;
199
+ node.kind === ts.SyntaxKind.UndefinedKeyword ||
200
+ (ts.isPrefixUnaryExpression(node) && ts.isNumericLiteral(node.operand));
166
201
  }
167
202
 
168
203
  function visit(ctx: VisitContext, node: ts.Node): void {
@@ -202,9 +237,10 @@ function visit(ctx: VisitContext, node: ts.Node): void {
202
237
  }
203
238
 
204
239
  ctx.calls.push({
205
- className: uid('ReactiveObject'),
240
+ classname: uid('ReactiveObject'),
206
241
  node,
207
242
  properties,
243
+ typehint: node.typeArguments?.[0]?.getText(ctx.sourceFile) ?? null,
208
244
  varname
209
245
  });
210
246
  }
@@ -214,12 +250,6 @@ function visit(ctx: VisitContext, node: ts.Node): void {
214
250
  }
215
251
 
216
252
 
217
- type ObjectTransformResult = {
218
- prepend: string[];
219
- replacements: ReplacementIntent[];
220
- };
221
-
222
-
223
253
  export default (sourceFile: ts.SourceFile, bindings: Bindings): ObjectTransformResult => {
224
254
  let ctx: VisitContext = {
225
255
  bindings,
@@ -233,16 +263,21 @@ export default (sourceFile: ts.SourceFile, bindings: Bindings): ObjectTransformR
233
263
  replacements: ReplacementIntent[] = [];
234
264
 
235
265
  for (let i = 0, n = ctx.calls.length; i < n; i++) {
236
- let call = ctx.calls[i];
266
+ let call = ctx.calls[i],
267
+ typehint = call.typehint;
237
268
 
238
- prepend.push(buildClassCode(call.className, call.properties));
269
+ prepend.push(buildClassCode(call.classname, call.properties, typehint));
239
270
  replacements.push({
240
- generate: () => ` new ${call.className}(${
241
- call.properties
242
- .filter(({ isStatic, type }) => !isStatic || type === TYPES.Computed)
271
+ generate: () => {
272
+ let args = call.properties
273
+ .filter(({ isStatic, type }) => typehint || !isStatic || type === TYPES.Computed)
243
274
  .map(p => p.valueText)
244
- .join(', ')
245
- })`,
275
+ .join(', ');
276
+
277
+ return typehint
278
+ ? ` new ${call.classname}<${typehint}>(${args})`
279
+ : ` new ${call.classname}(${args})`;
280
+ },
246
281
  node: call.node,
247
282
  });
248
283
  }