@player-lang/functional-dsl-generator 0.0.2-next.0
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.
- package/dist/cjs/index.cjs +2146 -0
- package/dist/cjs/index.cjs.map +1 -0
- package/dist/index.legacy-esm.js +2075 -0
- package/dist/index.mjs +2075 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +38 -0
- package/src/__tests__/__snapshots__/generator.test.ts.snap +886 -0
- package/src/__tests__/builder-class-generator.test.ts +627 -0
- package/src/__tests__/cli.test.ts +685 -0
- package/src/__tests__/default-value-generator.test.ts +365 -0
- package/src/__tests__/generator.test.ts +2860 -0
- package/src/__tests__/import-generator.test.ts +444 -0
- package/src/__tests__/path-utils.test.ts +174 -0
- package/src/__tests__/type-collector.test.ts +674 -0
- package/src/__tests__/type-transformer.test.ts +934 -0
- package/src/__tests__/utils.test.ts +597 -0
- package/src/builder-class-generator.ts +254 -0
- package/src/cli.ts +285 -0
- package/src/default-value-generator.ts +307 -0
- package/src/generator.ts +257 -0
- package/src/import-generator.ts +331 -0
- package/src/index.ts +38 -0
- package/src/path-utils.ts +155 -0
- package/src/ts-morph-type-finder.ts +319 -0
- package/src/type-categorizer.ts +131 -0
- package/src/type-collector.ts +296 -0
- package/src/type-resolver.ts +266 -0
- package/src/type-transformer.ts +487 -0
- package/src/utils.ts +762 -0
- package/types/builder-class-generator.d.ts +56 -0
- package/types/cli.d.ts +6 -0
- package/types/default-value-generator.d.ts +74 -0
- package/types/generator.d.ts +102 -0
- package/types/import-generator.d.ts +77 -0
- package/types/index.d.ts +12 -0
- package/types/path-utils.d.ts +65 -0
- package/types/ts-morph-type-finder.d.ts +73 -0
- package/types/type-categorizer.d.ts +46 -0
- package/types/type-collector.d.ts +62 -0
- package/types/type-resolver.d.ts +49 -0
- package/types/type-transformer.d.ts +74 -0
- package/types/utils.d.ts +205 -0
|
@@ -0,0 +1,2860 @@
|
|
|
1
|
+
import { describe, test, expect, vi } from "vitest";
|
|
2
|
+
import { setupTestEnv } from "@player-lang/test-utils";
|
|
3
|
+
import { TsConverter } from "@xlr-lib/xlr-converters";
|
|
4
|
+
import type { NamedType, ObjectType } from "@xlr-lib/xlr";
|
|
5
|
+
import { generateFunctionalBuilder, type GeneratorConfig } from "../generator";
|
|
6
|
+
import type { TypeRegistry } from "../utils";
|
|
7
|
+
import { FunctionalBuilderBase } from "@player-lang/functional-dsl";
|
|
8
|
+
|
|
9
|
+
/** Custom primitives that should be treated as refs rather than resolved */
|
|
10
|
+
const CUSTOM_PRIMITIVES = ["Asset", "AssetWrapper", "Binding", "Expression"];
|
|
11
|
+
|
|
12
|
+
vi.setConfig({
|
|
13
|
+
testTimeout: 2 * 60 * 1000,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Converts TypeScript source code to XLR types
|
|
18
|
+
*/
|
|
19
|
+
function convertTsToXLR(
|
|
20
|
+
sourceCode: string,
|
|
21
|
+
customPrimitives = CUSTOM_PRIMITIVES,
|
|
22
|
+
) {
|
|
23
|
+
const { sf, tc } = setupTestEnv(sourceCode);
|
|
24
|
+
const converter = new TsConverter(tc, customPrimitives);
|
|
25
|
+
return converter.convertSourceFile(sf).data.types;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("FunctionalBuilderGenerator", () => {
|
|
29
|
+
describe("Basic Types", () => {
|
|
30
|
+
test("generates builder for simple asset with string property", () => {
|
|
31
|
+
const source = `
|
|
32
|
+
interface Asset<T extends string> {
|
|
33
|
+
id: string;
|
|
34
|
+
type: T;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface TextAsset extends Asset<"text"> {
|
|
38
|
+
value: string;
|
|
39
|
+
}
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
const types = convertTsToXLR(source);
|
|
43
|
+
const textAsset = types.find(
|
|
44
|
+
(t) => t.name === "TextAsset",
|
|
45
|
+
) as NamedType<ObjectType>;
|
|
46
|
+
expect(textAsset).toBeDefined();
|
|
47
|
+
|
|
48
|
+
const code = generateFunctionalBuilder(textAsset);
|
|
49
|
+
expect(code).toMatchSnapshot();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("generates builder for asset with optional properties", () => {
|
|
53
|
+
const source = `
|
|
54
|
+
interface Asset<T extends string> {
|
|
55
|
+
id: string;
|
|
56
|
+
type: T;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface InputAsset extends Asset<"input"> {
|
|
60
|
+
binding: string;
|
|
61
|
+
label?: string;
|
|
62
|
+
placeholder?: string;
|
|
63
|
+
}
|
|
64
|
+
`;
|
|
65
|
+
|
|
66
|
+
const types = convertTsToXLR(source);
|
|
67
|
+
const inputAsset = types.find(
|
|
68
|
+
(t) => t.name === "InputAsset",
|
|
69
|
+
) as NamedType<ObjectType>;
|
|
70
|
+
expect(inputAsset).toBeDefined();
|
|
71
|
+
|
|
72
|
+
const code = generateFunctionalBuilder(inputAsset);
|
|
73
|
+
expect(code).toMatchSnapshot();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("generates builder for asset with number and boolean properties", () => {
|
|
77
|
+
const source = `
|
|
78
|
+
interface Asset<T extends string> {
|
|
79
|
+
id: string;
|
|
80
|
+
type: T;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface CounterAsset extends Asset<"counter"> {
|
|
84
|
+
value: number;
|
|
85
|
+
min?: number;
|
|
86
|
+
max?: number;
|
|
87
|
+
enabled?: boolean;
|
|
88
|
+
}
|
|
89
|
+
`;
|
|
90
|
+
|
|
91
|
+
const types = convertTsToXLR(source);
|
|
92
|
+
const counterAsset = types.find(
|
|
93
|
+
(t) => t.name === "CounterAsset",
|
|
94
|
+
) as NamedType<ObjectType>;
|
|
95
|
+
expect(counterAsset).toBeDefined();
|
|
96
|
+
|
|
97
|
+
const code = generateFunctionalBuilder(counterAsset);
|
|
98
|
+
expect(code).toMatchSnapshot();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("Asset Wrapper (Slot) Types", () => {
|
|
103
|
+
test("generates builder for asset with AssetWrapper slots", () => {
|
|
104
|
+
const source = `
|
|
105
|
+
interface Asset<T extends string = string> {
|
|
106
|
+
id: string;
|
|
107
|
+
type: T;
|
|
108
|
+
}
|
|
109
|
+
type AssetWrapper<T extends Asset = Asset> = { asset: T };
|
|
110
|
+
|
|
111
|
+
export interface InfoAsset extends Asset<"info"> {
|
|
112
|
+
title?: AssetWrapper<Asset>;
|
|
113
|
+
subtitle?: AssetWrapper<Asset>;
|
|
114
|
+
primaryInfo: Array<AssetWrapper<Asset>>;
|
|
115
|
+
}
|
|
116
|
+
`;
|
|
117
|
+
|
|
118
|
+
const types = convertTsToXLR(source);
|
|
119
|
+
const infoAsset = types.find(
|
|
120
|
+
(t) => t.name === "InfoAsset",
|
|
121
|
+
) as NamedType<ObjectType>;
|
|
122
|
+
expect(infoAsset).toBeDefined();
|
|
123
|
+
|
|
124
|
+
const code = generateFunctionalBuilder(infoAsset);
|
|
125
|
+
expect(code).toMatchSnapshot();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("generates builder for asset with array of AssetWrapper slots", () => {
|
|
129
|
+
const source = `
|
|
130
|
+
interface Asset<T extends string = string> {
|
|
131
|
+
id: string;
|
|
132
|
+
type: T;
|
|
133
|
+
}
|
|
134
|
+
type AssetWrapper<T extends Asset = Asset> = { asset: T };
|
|
135
|
+
|
|
136
|
+
export interface CollectionAsset extends Asset<"collection"> {
|
|
137
|
+
label?: AssetWrapper<Asset>;
|
|
138
|
+
values?: Array<AssetWrapper<Asset>>;
|
|
139
|
+
}
|
|
140
|
+
`;
|
|
141
|
+
|
|
142
|
+
const types = convertTsToXLR(source);
|
|
143
|
+
const collectionAsset = types.find(
|
|
144
|
+
(t) => t.name === "CollectionAsset",
|
|
145
|
+
) as NamedType<ObjectType>;
|
|
146
|
+
expect(collectionAsset).toBeDefined();
|
|
147
|
+
|
|
148
|
+
const code = generateFunctionalBuilder(collectionAsset);
|
|
149
|
+
expect(code).toMatchSnapshot();
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe("Binding and Expression Types", () => {
|
|
154
|
+
test("generates builder for asset with Binding property", () => {
|
|
155
|
+
const source = `
|
|
156
|
+
interface Asset<T extends string = string> {
|
|
157
|
+
id: string;
|
|
158
|
+
type: T;
|
|
159
|
+
}
|
|
160
|
+
type Binding = string;
|
|
161
|
+
|
|
162
|
+
export interface InputAsset extends Asset<"input"> {
|
|
163
|
+
binding: Binding;
|
|
164
|
+
label?: string;
|
|
165
|
+
}
|
|
166
|
+
`;
|
|
167
|
+
|
|
168
|
+
const types = convertTsToXLR(source);
|
|
169
|
+
const inputAsset = types.find(
|
|
170
|
+
(t) => t.name === "InputAsset",
|
|
171
|
+
) as NamedType<ObjectType>;
|
|
172
|
+
expect(inputAsset).toBeDefined();
|
|
173
|
+
|
|
174
|
+
const code = generateFunctionalBuilder(inputAsset);
|
|
175
|
+
expect(code).toMatchSnapshot();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("generates builder for asset with Expression property", () => {
|
|
179
|
+
const source = `
|
|
180
|
+
interface Asset<T extends string = string> {
|
|
181
|
+
id: string;
|
|
182
|
+
type: T;
|
|
183
|
+
}
|
|
184
|
+
type Expression = string;
|
|
185
|
+
|
|
186
|
+
export interface ActionAsset extends Asset<"action"> {
|
|
187
|
+
value?: string;
|
|
188
|
+
exp?: Expression;
|
|
189
|
+
}
|
|
190
|
+
`;
|
|
191
|
+
|
|
192
|
+
const types = convertTsToXLR(source);
|
|
193
|
+
const actionAsset = types.find(
|
|
194
|
+
(t) => t.name === "ActionAsset",
|
|
195
|
+
) as NamedType<ObjectType>;
|
|
196
|
+
expect(actionAsset).toBeDefined();
|
|
197
|
+
|
|
198
|
+
const code = generateFunctionalBuilder(actionAsset);
|
|
199
|
+
expect(code).toMatchSnapshot();
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe("Nested Object Types", () => {
|
|
204
|
+
test("generates builder for asset with nested object property", () => {
|
|
205
|
+
const source = `
|
|
206
|
+
interface Asset<T extends string = string> {
|
|
207
|
+
id: string;
|
|
208
|
+
type: T;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export interface ActionAsset extends Asset<"action"> {
|
|
212
|
+
value?: string;
|
|
213
|
+
confirmation?: {
|
|
214
|
+
message: string;
|
|
215
|
+
affirmativeLabel: string;
|
|
216
|
+
negativeLabel?: string;
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
`;
|
|
220
|
+
|
|
221
|
+
const types = convertTsToXLR(source);
|
|
222
|
+
const actionAsset = types.find(
|
|
223
|
+
(t) => t.name === "ActionAsset",
|
|
224
|
+
) as NamedType<ObjectType>;
|
|
225
|
+
expect(actionAsset).toBeDefined();
|
|
226
|
+
|
|
227
|
+
const code = generateFunctionalBuilder(actionAsset);
|
|
228
|
+
expect(code).toMatchSnapshot();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("generates builder for asset with named nested type", () => {
|
|
232
|
+
const source = `
|
|
233
|
+
interface Asset<T extends string = string> {
|
|
234
|
+
id: string;
|
|
235
|
+
type: T;
|
|
236
|
+
}
|
|
237
|
+
type AssetWrapper<T extends Asset = Asset> = { asset: T };
|
|
238
|
+
|
|
239
|
+
export interface ActionMetaData {
|
|
240
|
+
beacon?: string | Record<string, unknown>;
|
|
241
|
+
role?: "primary" | "secondary" | "link";
|
|
242
|
+
skipValidation?: boolean;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export interface ActionAsset extends Asset<"action"> {
|
|
246
|
+
value?: string;
|
|
247
|
+
label?: AssetWrapper<Asset>;
|
|
248
|
+
metaData?: ActionMetaData;
|
|
249
|
+
}
|
|
250
|
+
`;
|
|
251
|
+
|
|
252
|
+
const types = convertTsToXLR(source);
|
|
253
|
+
const actionAsset = types.find(
|
|
254
|
+
(t) => t.name === "ActionAsset",
|
|
255
|
+
) as NamedType<ObjectType>;
|
|
256
|
+
expect(actionAsset).toBeDefined();
|
|
257
|
+
|
|
258
|
+
const code = generateFunctionalBuilder(actionAsset);
|
|
259
|
+
expect(code).toMatchSnapshot();
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
describe("Union Types", () => {
|
|
264
|
+
test("generates builder for asset with union property", () => {
|
|
265
|
+
const source = `
|
|
266
|
+
interface Asset<T extends string = string> {
|
|
267
|
+
id: string;
|
|
268
|
+
type: T;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export interface ActionAsset extends Asset<"action"> {
|
|
272
|
+
size?: "small" | "medium" | "large";
|
|
273
|
+
}
|
|
274
|
+
`;
|
|
275
|
+
|
|
276
|
+
const types = convertTsToXLR(source);
|
|
277
|
+
const actionAsset = types.find(
|
|
278
|
+
(t) => t.name === "ActionAsset",
|
|
279
|
+
) as NamedType<ObjectType>;
|
|
280
|
+
expect(actionAsset).toBeDefined();
|
|
281
|
+
|
|
282
|
+
const code = generateFunctionalBuilder(actionAsset);
|
|
283
|
+
expect(code).toMatchSnapshot();
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test("generates builder for asset with discriminated union modifiers", () => {
|
|
287
|
+
const source = `
|
|
288
|
+
interface Asset<T extends string = string> {
|
|
289
|
+
id: string;
|
|
290
|
+
type: T;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
interface CalloutModifier {
|
|
294
|
+
type: "callout";
|
|
295
|
+
value: "support" | "legal";
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
interface TagModifier {
|
|
299
|
+
type: "tag";
|
|
300
|
+
value: "block";
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export interface CollectionAsset extends Asset<"collection"> {
|
|
304
|
+
modifiers?: Array<CalloutModifier | TagModifier>;
|
|
305
|
+
}
|
|
306
|
+
`;
|
|
307
|
+
|
|
308
|
+
const types = convertTsToXLR(source);
|
|
309
|
+
const collectionAsset = types.find(
|
|
310
|
+
(t) => t.name === "CollectionAsset",
|
|
311
|
+
) as NamedType<ObjectType>;
|
|
312
|
+
expect(collectionAsset).toBeDefined();
|
|
313
|
+
|
|
314
|
+
const code = generateFunctionalBuilder(collectionAsset);
|
|
315
|
+
expect(code).toMatchSnapshot();
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
describe("Array Types", () => {
|
|
320
|
+
test("generates builder for asset with array of primitives", () => {
|
|
321
|
+
const source = `
|
|
322
|
+
interface Asset<T extends string = string> {
|
|
323
|
+
id: string;
|
|
324
|
+
type: T;
|
|
325
|
+
}
|
|
326
|
+
type Binding = string;
|
|
327
|
+
|
|
328
|
+
export interface ActionAsset extends Asset<"action"> {
|
|
329
|
+
validate?: Array<Binding> | Binding;
|
|
330
|
+
}
|
|
331
|
+
`;
|
|
332
|
+
|
|
333
|
+
const types = convertTsToXLR(source);
|
|
334
|
+
const actionAsset = types.find(
|
|
335
|
+
(t) => t.name === "ActionAsset",
|
|
336
|
+
) as NamedType<ObjectType>;
|
|
337
|
+
expect(actionAsset).toBeDefined();
|
|
338
|
+
|
|
339
|
+
const code = generateFunctionalBuilder(actionAsset);
|
|
340
|
+
expect(code).toMatchSnapshot();
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test("generates builder for asset with array of complex objects", () => {
|
|
344
|
+
const source = `
|
|
345
|
+
interface Asset<T extends string = string> {
|
|
346
|
+
id: string;
|
|
347
|
+
type: T;
|
|
348
|
+
}
|
|
349
|
+
type AssetWrapper<T extends Asset = Asset> = { asset: T };
|
|
350
|
+
|
|
351
|
+
export interface ChoiceItem {
|
|
352
|
+
id: string;
|
|
353
|
+
label?: AssetWrapper<Asset>;
|
|
354
|
+
value?: string | number | boolean | null;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export interface ChoiceAsset extends Asset<"choice"> {
|
|
358
|
+
binding: string;
|
|
359
|
+
choices?: Array<ChoiceItem>;
|
|
360
|
+
}
|
|
361
|
+
`;
|
|
362
|
+
|
|
363
|
+
const types = convertTsToXLR(source);
|
|
364
|
+
const choiceAsset = types.find(
|
|
365
|
+
(t) => t.name === "ChoiceAsset",
|
|
366
|
+
) as NamedType<ObjectType>;
|
|
367
|
+
expect(choiceAsset).toBeDefined();
|
|
368
|
+
|
|
369
|
+
const code = generateFunctionalBuilder(choiceAsset);
|
|
370
|
+
expect(code).toMatchSnapshot();
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
test("generates builder for non-Asset type with complex properties", () => {
|
|
374
|
+
const source = `
|
|
375
|
+
interface Asset<T extends string = string> {
|
|
376
|
+
id: string;
|
|
377
|
+
type: T;
|
|
378
|
+
}
|
|
379
|
+
type AssetWrapper<T extends Asset = Asset> = { asset: T };
|
|
380
|
+
|
|
381
|
+
export interface ChoiceItem {
|
|
382
|
+
id: string;
|
|
383
|
+
label?: AssetWrapper<Asset>;
|
|
384
|
+
value?: string | number | boolean | null;
|
|
385
|
+
}
|
|
386
|
+
`;
|
|
387
|
+
|
|
388
|
+
const types = convertTsToXLR(source);
|
|
389
|
+
const choiceItem = types.find(
|
|
390
|
+
(t) => t.name === "ChoiceItem",
|
|
391
|
+
) as NamedType<ObjectType>;
|
|
392
|
+
expect(choiceItem).toBeDefined();
|
|
393
|
+
|
|
394
|
+
const code = generateFunctionalBuilder(choiceItem);
|
|
395
|
+
expect(code).toMatchSnapshot();
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
describe("Generic Types", () => {
|
|
400
|
+
test("generates builder for asset with generic parameter", () => {
|
|
401
|
+
const source = `
|
|
402
|
+
interface Asset<T extends string = string> {
|
|
403
|
+
id: string;
|
|
404
|
+
type: T;
|
|
405
|
+
}
|
|
406
|
+
type AssetWrapper<T extends Asset = Asset> = { asset: T };
|
|
407
|
+
|
|
408
|
+
export interface InputAsset<AnyTextAsset extends Asset = Asset> extends Asset<"input"> {
|
|
409
|
+
binding: string;
|
|
410
|
+
label?: AssetWrapper<AnyTextAsset>;
|
|
411
|
+
note?: AssetWrapper<AnyTextAsset>;
|
|
412
|
+
}
|
|
413
|
+
`;
|
|
414
|
+
|
|
415
|
+
const types = convertTsToXLR(source);
|
|
416
|
+
const inputAsset = types.find(
|
|
417
|
+
(t) => t.name === "InputAsset",
|
|
418
|
+
) as NamedType<ObjectType>;
|
|
419
|
+
expect(inputAsset).toBeDefined();
|
|
420
|
+
|
|
421
|
+
const code = generateFunctionalBuilder(inputAsset);
|
|
422
|
+
expect(code).toMatchSnapshot();
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
test("string properties accept TaggedTemplateValue for binding support", () => {
|
|
427
|
+
const source = `
|
|
428
|
+
interface Asset<T extends string = string> {
|
|
429
|
+
id: string;
|
|
430
|
+
type: T;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export interface TextAsset extends Asset<"text"> {
|
|
434
|
+
value: string;
|
|
435
|
+
}
|
|
436
|
+
`;
|
|
437
|
+
|
|
438
|
+
const types = convertTsToXLR(source);
|
|
439
|
+
const textAsset = types.find(
|
|
440
|
+
(t) => t.name === "TextAsset",
|
|
441
|
+
) as NamedType<ObjectType>;
|
|
442
|
+
const code = generateFunctionalBuilder(textAsset);
|
|
443
|
+
|
|
444
|
+
// New generator adds TaggedTemplateValue support - OLD generator did NOT
|
|
445
|
+
expect(code).toContain(
|
|
446
|
+
"withValue(value: string | TaggedTemplateValue<string>)",
|
|
447
|
+
);
|
|
448
|
+
// Verify we don't just have plain string
|
|
449
|
+
expect(code).not.toMatch(/withValue\(value: string\):/);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
test("AssetWrapper slots accept Asset | FunctionalBuilder instead of AssetWrapper", () => {
|
|
453
|
+
const source = `
|
|
454
|
+
interface Asset<T extends string = string> {
|
|
455
|
+
id: string;
|
|
456
|
+
type: T;
|
|
457
|
+
}
|
|
458
|
+
type AssetWrapper<T extends Asset = Asset> = { asset: T };
|
|
459
|
+
|
|
460
|
+
export interface ActionAsset extends Asset<"action"> {
|
|
461
|
+
label?: AssetWrapper<Asset>;
|
|
462
|
+
}
|
|
463
|
+
`;
|
|
464
|
+
|
|
465
|
+
const types = convertTsToXLR(source);
|
|
466
|
+
const actionAsset = types.find(
|
|
467
|
+
(t) => t.name === "ActionAsset",
|
|
468
|
+
) as NamedType<ObjectType>;
|
|
469
|
+
const code = generateFunctionalBuilder(actionAsset);
|
|
470
|
+
|
|
471
|
+
// New generator uses Asset | FunctionalBuilder<Asset> - OLD generator incorrectly used AssetWrapper
|
|
472
|
+
expect(code).toContain(
|
|
473
|
+
"Asset | FunctionalBuilder<Asset, BaseBuildContext>",
|
|
474
|
+
);
|
|
475
|
+
// Verify we don't use AssetWrapper (which was the bug)
|
|
476
|
+
expect(code).not.toContain("FunctionalBuilder<AssetWrapper");
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
test("Binding properties accept TaggedTemplateValue", () => {
|
|
480
|
+
const source = `
|
|
481
|
+
interface Asset<T extends string = string> {
|
|
482
|
+
id: string;
|
|
483
|
+
type: T;
|
|
484
|
+
}
|
|
485
|
+
type Binding = string;
|
|
486
|
+
|
|
487
|
+
export interface InputAsset extends Asset<"input"> {
|
|
488
|
+
binding: Binding;
|
|
489
|
+
}
|
|
490
|
+
`;
|
|
491
|
+
|
|
492
|
+
const types = convertTsToXLR(source);
|
|
493
|
+
const inputAsset = types.find(
|
|
494
|
+
(t) => t.name === "InputAsset",
|
|
495
|
+
) as NamedType<ObjectType>;
|
|
496
|
+
const code = generateFunctionalBuilder(inputAsset);
|
|
497
|
+
|
|
498
|
+
// Bindings should accept TaggedTemplateValue
|
|
499
|
+
expect(code).toContain(
|
|
500
|
+
"withBinding(value: string | TaggedTemplateValue<string>)",
|
|
501
|
+
);
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
test("Expression properties accept TaggedTemplateValue", () => {
|
|
505
|
+
const source = `
|
|
506
|
+
interface Asset<T extends string = string> {
|
|
507
|
+
id: string;
|
|
508
|
+
type: T;
|
|
509
|
+
}
|
|
510
|
+
type Expression = string;
|
|
511
|
+
|
|
512
|
+
export interface ActionAsset extends Asset<"action"> {
|
|
513
|
+
exp?: Expression;
|
|
514
|
+
}
|
|
515
|
+
`;
|
|
516
|
+
|
|
517
|
+
const types = convertTsToXLR(source);
|
|
518
|
+
const actionAsset = types.find(
|
|
519
|
+
(t) => t.name === "ActionAsset",
|
|
520
|
+
) as NamedType<ObjectType>;
|
|
521
|
+
const code = generateFunctionalBuilder(actionAsset);
|
|
522
|
+
|
|
523
|
+
// Expressions should accept TaggedTemplateValue
|
|
524
|
+
expect(code).toContain(
|
|
525
|
+
"withExp(value: string | TaggedTemplateValue<string>)",
|
|
526
|
+
);
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
describe("Integration with Complex Type Patterns", () => {
|
|
531
|
+
test("generates builder for TextAsset", () => {
|
|
532
|
+
const source = `
|
|
533
|
+
interface Asset<T extends string = string> {
|
|
534
|
+
id: string;
|
|
535
|
+
type: T;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
export interface TextAsset extends Asset<"text"> {
|
|
539
|
+
value: string;
|
|
540
|
+
}
|
|
541
|
+
`;
|
|
542
|
+
|
|
543
|
+
const types = convertTsToXLR(source);
|
|
544
|
+
const textAsset = types.find(
|
|
545
|
+
(t) => t.name === "TextAsset",
|
|
546
|
+
) as NamedType<ObjectType>;
|
|
547
|
+
|
|
548
|
+
const code = generateFunctionalBuilder(textAsset);
|
|
549
|
+
|
|
550
|
+
// Verify it has the expected parts
|
|
551
|
+
expect(code).toContain("TextAssetBuilder");
|
|
552
|
+
expect(code).toContain("withValue");
|
|
553
|
+
expect(code).toContain("string | TaggedTemplateValue<string>");
|
|
554
|
+
// Smart defaults now include required primitive fields
|
|
555
|
+
expect(code).toContain(
|
|
556
|
+
'defaults: Record<string, unknown> = {"type":"text","id":"","value":""}',
|
|
557
|
+
);
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
test("generates builder for InfoAsset", () => {
|
|
561
|
+
const source = `
|
|
562
|
+
interface Asset<T extends string = string> {
|
|
563
|
+
id: string;
|
|
564
|
+
type: T;
|
|
565
|
+
}
|
|
566
|
+
type AssetWrapper<T extends Asset = Asset> = { asset: T };
|
|
567
|
+
|
|
568
|
+
export interface InfoAsset extends Asset<"info"> {
|
|
569
|
+
primaryInfo: Array<AssetWrapper<Asset>>;
|
|
570
|
+
title?: AssetWrapper<Asset>;
|
|
571
|
+
subtitle?: AssetWrapper<Asset>;
|
|
572
|
+
}
|
|
573
|
+
`;
|
|
574
|
+
|
|
575
|
+
const types = convertTsToXLR(source);
|
|
576
|
+
const infoAsset = types.find(
|
|
577
|
+
(t) => t.name === "InfoAsset",
|
|
578
|
+
) as NamedType<ObjectType>;
|
|
579
|
+
|
|
580
|
+
const code = generateFunctionalBuilder(infoAsset);
|
|
581
|
+
|
|
582
|
+
// Verify it has the expected parts
|
|
583
|
+
expect(code).toContain("InfoAssetBuilder");
|
|
584
|
+
expect(code).toContain("withPrimaryInfo");
|
|
585
|
+
expect(code).toContain("withTitle");
|
|
586
|
+
expect(code).toContain("withSubtitle");
|
|
587
|
+
expect(code).toContain(
|
|
588
|
+
"Asset | FunctionalBuilder<Asset, BaseBuildContext>",
|
|
589
|
+
);
|
|
590
|
+
expect(code).toContain("__arrayProperties__");
|
|
591
|
+
expect(code).toContain('"primaryInfo"');
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
test("generates builder for InputAsset", () => {
|
|
595
|
+
const source = `
|
|
596
|
+
interface Asset<T extends string = string> {
|
|
597
|
+
id: string;
|
|
598
|
+
type: T;
|
|
599
|
+
}
|
|
600
|
+
type AssetWrapper<T extends Asset = Asset> = { asset: T };
|
|
601
|
+
|
|
602
|
+
export interface InputAsset extends Asset<"input"> {
|
|
603
|
+
binding: string;
|
|
604
|
+
label: AssetWrapper<Asset>;
|
|
605
|
+
placeholder?: string;
|
|
606
|
+
}
|
|
607
|
+
`;
|
|
608
|
+
|
|
609
|
+
const types = convertTsToXLR(source);
|
|
610
|
+
const inputAsset = types.find(
|
|
611
|
+
(t) => t.name === "InputAsset",
|
|
612
|
+
) as NamedType<ObjectType>;
|
|
613
|
+
|
|
614
|
+
const code = generateFunctionalBuilder(inputAsset);
|
|
615
|
+
|
|
616
|
+
// Verify it has the expected parts
|
|
617
|
+
expect(code).toContain("InputAssetBuilder");
|
|
618
|
+
expect(code).toContain("withBinding");
|
|
619
|
+
expect(code).toContain("withLabel");
|
|
620
|
+
expect(code).toContain("withPlaceholder");
|
|
621
|
+
expect(code).toContain(
|
|
622
|
+
"Asset | FunctionalBuilder<Asset, BaseBuildContext>",
|
|
623
|
+
);
|
|
624
|
+
});
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
describe("__arrayProperties__ generation", () => {
|
|
628
|
+
test("generates __arrayProperties__ for types with array properties", () => {
|
|
629
|
+
const source = `
|
|
630
|
+
interface Asset<T extends string = string> {
|
|
631
|
+
id: string;
|
|
632
|
+
type: T;
|
|
633
|
+
}
|
|
634
|
+
type AssetWrapper<T extends Asset = Asset> = { asset: T };
|
|
635
|
+
|
|
636
|
+
export interface CollectionAsset extends Asset<"collection"> {
|
|
637
|
+
values: Array<AssetWrapper<Asset>>;
|
|
638
|
+
actions?: Array<AssetWrapper<Asset>>;
|
|
639
|
+
label?: AssetWrapper<Asset>;
|
|
640
|
+
modifiers?: Array<{ type: string }>;
|
|
641
|
+
}
|
|
642
|
+
`;
|
|
643
|
+
|
|
644
|
+
const types = convertTsToXLR(source);
|
|
645
|
+
const collectionAsset = types.find(
|
|
646
|
+
(t) => t.name === "CollectionAsset",
|
|
647
|
+
) as NamedType<ObjectType>;
|
|
648
|
+
|
|
649
|
+
const code = generateFunctionalBuilder(collectionAsset);
|
|
650
|
+
|
|
651
|
+
// Must have __arrayProperties__ static property
|
|
652
|
+
expect(code).toContain("__arrayProperties__");
|
|
653
|
+
expect(code).toContain("ReadonlySet<string>");
|
|
654
|
+
|
|
655
|
+
// Must include the array properties
|
|
656
|
+
expect(code).toContain('"values"');
|
|
657
|
+
expect(code).toContain('"actions"');
|
|
658
|
+
expect(code).toContain('"modifiers"');
|
|
659
|
+
|
|
660
|
+
// Non-array properties should NOT be in __arrayProperties__
|
|
661
|
+
expect(code).not.toMatch(/__arrayProperties__.*"label"/);
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
test("does not generate __arrayProperties__ when no array properties exist", () => {
|
|
665
|
+
const source = `
|
|
666
|
+
interface Asset<T extends string = string> {
|
|
667
|
+
id: string;
|
|
668
|
+
type: T;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
export interface TextAsset extends Asset<"text"> {
|
|
672
|
+
value: string;
|
|
673
|
+
optional?: string;
|
|
674
|
+
}
|
|
675
|
+
`;
|
|
676
|
+
|
|
677
|
+
const types = convertTsToXLR(source);
|
|
678
|
+
const textAsset = types.find(
|
|
679
|
+
(t) => t.name === "TextAsset",
|
|
680
|
+
) as NamedType<ObjectType>;
|
|
681
|
+
|
|
682
|
+
const code = generateFunctionalBuilder(textAsset);
|
|
683
|
+
|
|
684
|
+
// Should NOT have __arrayProperties__ since there are no arrays
|
|
685
|
+
expect(code).not.toContain("__arrayProperties__");
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
test("array detection works with union types containing arrays", () => {
|
|
689
|
+
const source = `
|
|
690
|
+
interface Asset<T extends string = string> {
|
|
691
|
+
id: string;
|
|
692
|
+
type: T;
|
|
693
|
+
}
|
|
694
|
+
type Binding = string;
|
|
695
|
+
|
|
696
|
+
export interface ActionAsset extends Asset<"action"> {
|
|
697
|
+
value?: string;
|
|
698
|
+
validate?: Array<Binding> | Binding;
|
|
699
|
+
}
|
|
700
|
+
`;
|
|
701
|
+
|
|
702
|
+
const types = convertTsToXLR(source);
|
|
703
|
+
const actionAsset = types.find(
|
|
704
|
+
(t) => t.name === "ActionAsset",
|
|
705
|
+
) as NamedType<ObjectType>;
|
|
706
|
+
|
|
707
|
+
const code = generateFunctionalBuilder(actionAsset);
|
|
708
|
+
|
|
709
|
+
// validate is Array | string, the Array variant should still be detected
|
|
710
|
+
expect(code).toContain("__arrayProperties__");
|
|
711
|
+
expect(code).toContain('"validate"');
|
|
712
|
+
});
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
describe("Generator configuration", () => {
|
|
716
|
+
test("supports custom functional import path", () => {
|
|
717
|
+
const source = `
|
|
718
|
+
interface Asset<T extends string = string> {
|
|
719
|
+
id: string;
|
|
720
|
+
type: T;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
export interface TextAsset extends Asset<"text"> {
|
|
724
|
+
value: string;
|
|
725
|
+
}
|
|
726
|
+
`;
|
|
727
|
+
|
|
728
|
+
const types = convertTsToXLR(source);
|
|
729
|
+
const textAsset = types.find(
|
|
730
|
+
(t) => t.name === "TextAsset",
|
|
731
|
+
) as NamedType<ObjectType>;
|
|
732
|
+
|
|
733
|
+
const config: GeneratorConfig = {
|
|
734
|
+
functionalImportPath: "../../../gen/common.js",
|
|
735
|
+
};
|
|
736
|
+
|
|
737
|
+
const code = generateFunctionalBuilder(textAsset, config);
|
|
738
|
+
|
|
739
|
+
expect(code).toContain('from "../../../gen/common.js"');
|
|
740
|
+
expect(code).not.toContain('from "@player-lang/functional-dsl"');
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
test("supports custom types import path", () => {
|
|
744
|
+
const source = `
|
|
745
|
+
interface Asset<T extends string = string> {
|
|
746
|
+
id: string;
|
|
747
|
+
type: T;
|
|
748
|
+
}
|
|
749
|
+
type AssetWrapper<T extends Asset = Asset> = { asset: T };
|
|
750
|
+
|
|
751
|
+
export interface ActionAsset extends Asset<"action"> {
|
|
752
|
+
label?: AssetWrapper<Asset>;
|
|
753
|
+
}
|
|
754
|
+
`;
|
|
755
|
+
|
|
756
|
+
const types = convertTsToXLR(source);
|
|
757
|
+
const actionAsset = types.find(
|
|
758
|
+
(t) => t.name === "ActionAsset",
|
|
759
|
+
) as NamedType<ObjectType>;
|
|
760
|
+
|
|
761
|
+
const config: GeneratorConfig = {
|
|
762
|
+
typesImportPath: "../custom-types.js",
|
|
763
|
+
};
|
|
764
|
+
|
|
765
|
+
const code = generateFunctionalBuilder(actionAsset, config);
|
|
766
|
+
|
|
767
|
+
expect(code).toContain('from "../custom-types.js"');
|
|
768
|
+
expect(code).not.toContain('from "@player-ui/types"');
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
test("supports custom type import path generator", () => {
|
|
772
|
+
const source = `
|
|
773
|
+
interface Asset<T extends string = string> {
|
|
774
|
+
id: string;
|
|
775
|
+
type: T;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
export interface TextAsset extends Asset<"text"> {
|
|
779
|
+
value: string;
|
|
780
|
+
}
|
|
781
|
+
`;
|
|
782
|
+
|
|
783
|
+
const types = convertTsToXLR(source);
|
|
784
|
+
const textAsset = types.find(
|
|
785
|
+
(t) => t.name === "TextAsset",
|
|
786
|
+
) as NamedType<ObjectType>;
|
|
787
|
+
|
|
788
|
+
const config: GeneratorConfig = {
|
|
789
|
+
typeImportPathGenerator: (typeName) =>
|
|
790
|
+
`../types/${typeName.toLowerCase()}.js`,
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
const code = generateFunctionalBuilder(textAsset, config);
|
|
794
|
+
|
|
795
|
+
expect(code).toContain('from "../types/textasset.js"');
|
|
796
|
+
});
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
describe("End-to-end: TypeScript → XLR → Builder → Built Object", () => {
|
|
800
|
+
/**
|
|
801
|
+
* This e2e test validates the entire pipeline:
|
|
802
|
+
* 1. TypeScript interface definition
|
|
803
|
+
* 2. Conversion to XLR via TsConverter
|
|
804
|
+
* 3. Builder generation via FunctionalBuilderGenerator
|
|
805
|
+
* 4. Builder execution to produce an object
|
|
806
|
+
* 5. Verification that the object matches the original interface structure
|
|
807
|
+
*/
|
|
808
|
+
test("generates working builder for simple asset", () => {
|
|
809
|
+
// Step 1: Define TypeScript interface
|
|
810
|
+
const source = `
|
|
811
|
+
interface Asset<T extends string = string> {
|
|
812
|
+
id: string;
|
|
813
|
+
type: T;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
export interface TextAsset extends Asset<"text"> {
|
|
817
|
+
value: string;
|
|
818
|
+
}
|
|
819
|
+
`;
|
|
820
|
+
|
|
821
|
+
// Step 2: Convert to XLR
|
|
822
|
+
const types = convertTsToXLR(source);
|
|
823
|
+
const textAsset = types.find(
|
|
824
|
+
(t) => t.name === "TextAsset",
|
|
825
|
+
) as NamedType<ObjectType>;
|
|
826
|
+
expect(textAsset).toBeDefined();
|
|
827
|
+
|
|
828
|
+
// Step 3: Generate builder code
|
|
829
|
+
const code = generateFunctionalBuilder(textAsset);
|
|
830
|
+
|
|
831
|
+
// Step 4: Verify the generated code structure
|
|
832
|
+
// The generated builder should have defaults for id, type, and required primitive fields
|
|
833
|
+
expect(code).toContain(
|
|
834
|
+
'defaults: Record<string, unknown> = {"type":"text","id":"","value":""}',
|
|
835
|
+
);
|
|
836
|
+
// Should have withValue method that accepts TaggedTemplateValue
|
|
837
|
+
expect(code).toContain(
|
|
838
|
+
"withValue(value: string | TaggedTemplateValue<string>)",
|
|
839
|
+
);
|
|
840
|
+
// Should have proper build method
|
|
841
|
+
expect(code).toContain("build(context?: BaseBuildContext): TextAsset");
|
|
842
|
+
|
|
843
|
+
// Step 5: Verify the built object structure (by parsing the generated code)
|
|
844
|
+
// The defaults object shows what fields the builder produces
|
|
845
|
+
const defaultsMatch = code.match(
|
|
846
|
+
/defaults: Record<string, unknown> = ({[^}]+})/,
|
|
847
|
+
);
|
|
848
|
+
expect(defaultsMatch).toBeTruthy();
|
|
849
|
+
const defaults = JSON.parse(defaultsMatch![1]);
|
|
850
|
+
// Smart defaults now include required primitive fields
|
|
851
|
+
expect(defaults).toEqual({ type: "text", id: "", value: "" });
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
test("generates working builder for asset with slots", () => {
|
|
855
|
+
// Step 1: Define TypeScript interface with AssetWrapper slots
|
|
856
|
+
const source = `
|
|
857
|
+
interface Asset<T extends string = string> {
|
|
858
|
+
id: string;
|
|
859
|
+
type: T;
|
|
860
|
+
}
|
|
861
|
+
type AssetWrapper<T extends Asset = Asset> = { asset: T };
|
|
862
|
+
|
|
863
|
+
export interface ActionAsset extends Asset<"action"> {
|
|
864
|
+
value?: string;
|
|
865
|
+
label?: AssetWrapper<Asset>;
|
|
866
|
+
}
|
|
867
|
+
`;
|
|
868
|
+
|
|
869
|
+
// Step 2: Convert to XLR
|
|
870
|
+
const types = convertTsToXLR(source);
|
|
871
|
+
const actionAsset = types.find(
|
|
872
|
+
(t) => t.name === "ActionAsset",
|
|
873
|
+
) as NamedType<ObjectType>;
|
|
874
|
+
expect(actionAsset).toBeDefined();
|
|
875
|
+
|
|
876
|
+
// Step 3: Generate builder code
|
|
877
|
+
const code = generateFunctionalBuilder(actionAsset);
|
|
878
|
+
|
|
879
|
+
// Step 4: Verify slot handling
|
|
880
|
+
// The label slot should accept Asset | FunctionalBuilder, not AssetWrapper
|
|
881
|
+
expect(code).toContain(
|
|
882
|
+
"withLabel(value: Asset | FunctionalBuilder<Asset, BaseBuildContext>)",
|
|
883
|
+
);
|
|
884
|
+
// Should NOT have AssetWrapper in the parameter type
|
|
885
|
+
expect(code).not.toContain("FunctionalBuilder<AssetWrapper");
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
test("generates working builder for asset with array properties", () => {
|
|
889
|
+
// Step 1: Define TypeScript interface with array properties
|
|
890
|
+
const source = `
|
|
891
|
+
interface Asset<T extends string = string> {
|
|
892
|
+
id: string;
|
|
893
|
+
type: T;
|
|
894
|
+
}
|
|
895
|
+
type AssetWrapper<T extends Asset = Asset> = { asset: T };
|
|
896
|
+
|
|
897
|
+
export interface CollectionAsset extends Asset<"collection"> {
|
|
898
|
+
values?: Array<AssetWrapper<Asset>>;
|
|
899
|
+
actions?: Array<AssetWrapper<Asset>>;
|
|
900
|
+
label?: AssetWrapper<Asset>;
|
|
901
|
+
}
|
|
902
|
+
`;
|
|
903
|
+
|
|
904
|
+
// Step 2: Convert to XLR
|
|
905
|
+
const types = convertTsToXLR(source);
|
|
906
|
+
const collectionAsset = types.find(
|
|
907
|
+
(t) => t.name === "CollectionAsset",
|
|
908
|
+
) as NamedType<ObjectType>;
|
|
909
|
+
expect(collectionAsset).toBeDefined();
|
|
910
|
+
|
|
911
|
+
// Step 3: Generate builder code
|
|
912
|
+
const code = generateFunctionalBuilder(collectionAsset);
|
|
913
|
+
|
|
914
|
+
// Step 4: Verify __arrayProperties__ is generated correctly
|
|
915
|
+
expect(code).toContain("__arrayProperties__");
|
|
916
|
+
expect(code).toContain('"values"');
|
|
917
|
+
expect(code).toContain('"actions"');
|
|
918
|
+
|
|
919
|
+
// Step 5: Verify array properties are correctly identified
|
|
920
|
+
// The __arrayProperties__ line should include values and actions, but not label
|
|
921
|
+
const arrayPropsMatch = code.match(
|
|
922
|
+
/__arrayProperties__.*new Set\(\[([^\]]+)\]\)/,
|
|
923
|
+
);
|
|
924
|
+
expect(arrayPropsMatch).toBeTruthy();
|
|
925
|
+
const arrayPropsContent = arrayPropsMatch![1];
|
|
926
|
+
expect(arrayPropsContent).toContain('"values"');
|
|
927
|
+
expect(arrayPropsContent).toContain('"actions"');
|
|
928
|
+
expect(arrayPropsContent).not.toContain('"label"');
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
test("generates working builder for asset with nested objects", () => {
|
|
932
|
+
// Step 1: Define TypeScript interface with nested object
|
|
933
|
+
const source = `
|
|
934
|
+
interface Asset<T extends string = string> {
|
|
935
|
+
id: string;
|
|
936
|
+
type: T;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
export interface ActionAsset extends Asset<"action"> {
|
|
940
|
+
value?: string;
|
|
941
|
+
confirmation?: {
|
|
942
|
+
message: string;
|
|
943
|
+
affirmativeLabel: string;
|
|
944
|
+
negativeLabel?: string;
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
`;
|
|
948
|
+
|
|
949
|
+
// Step 2: Convert to XLR
|
|
950
|
+
const types = convertTsToXLR(source);
|
|
951
|
+
const actionAsset = types.find(
|
|
952
|
+
(t) => t.name === "ActionAsset",
|
|
953
|
+
) as NamedType<ObjectType>;
|
|
954
|
+
expect(actionAsset).toBeDefined();
|
|
955
|
+
|
|
956
|
+
// Step 3: Generate builder code
|
|
957
|
+
const code = generateFunctionalBuilder(actionAsset);
|
|
958
|
+
|
|
959
|
+
// Step 4: Verify nested object handling
|
|
960
|
+
// The confirmation property should have inline type with TaggedTemplateValue support
|
|
961
|
+
expect(code).toContain("withConfirmation");
|
|
962
|
+
expect(code).toContain("message: string | TaggedTemplateValue<string>");
|
|
963
|
+
expect(code).toContain(
|
|
964
|
+
"affirmativeLabel: string | TaggedTemplateValue<string>",
|
|
965
|
+
);
|
|
966
|
+
expect(code).toContain(
|
|
967
|
+
"negativeLabel?: string | TaggedTemplateValue<string>",
|
|
968
|
+
);
|
|
969
|
+
|
|
970
|
+
// Step 5: Verify complex nested objects accept both raw objects AND FunctionalBuilder
|
|
971
|
+
// Complex nested objects (3+ properties) should accept FunctionalBuilder alternative
|
|
972
|
+
expect(code).toContain("FunctionalBuilder<{");
|
|
973
|
+
expect(code).toContain("}, BaseBuildContext>");
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
test("generates working builder for generic asset", () => {
|
|
977
|
+
// Step 1: Define TypeScript interface with generic parameter
|
|
978
|
+
const source = `
|
|
979
|
+
interface Asset<T extends string = string> {
|
|
980
|
+
id: string;
|
|
981
|
+
type: T;
|
|
982
|
+
}
|
|
983
|
+
type AssetWrapper<T extends Asset = Asset> = { asset: T };
|
|
984
|
+
|
|
985
|
+
export interface InputAsset<AnyTextAsset extends Asset = Asset> extends Asset<"input"> {
|
|
986
|
+
binding: string;
|
|
987
|
+
label?: AssetWrapper<AnyTextAsset>;
|
|
988
|
+
}
|
|
989
|
+
`;
|
|
990
|
+
|
|
991
|
+
// Step 2: Convert to XLR
|
|
992
|
+
const types = convertTsToXLR(source);
|
|
993
|
+
const inputAsset = types.find(
|
|
994
|
+
(t) => t.name === "InputAsset",
|
|
995
|
+
) as NamedType<ObjectType>;
|
|
996
|
+
expect(inputAsset).toBeDefined();
|
|
997
|
+
|
|
998
|
+
// Step 3: Generate builder code
|
|
999
|
+
const code = generateFunctionalBuilder(inputAsset);
|
|
1000
|
+
|
|
1001
|
+
// Step 4: Verify generic parameter handling
|
|
1002
|
+
expect(code).toContain("InputAssetBuilder<AnyTextAsset extends Asset");
|
|
1003
|
+
expect(code).toContain("export function input<AnyTextAsset extends Asset");
|
|
1004
|
+
});
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
describe("Edge Cases", () => {
|
|
1008
|
+
test("handles special types: null, undefined, any, unknown, never, void", () => {
|
|
1009
|
+
const source = `
|
|
1010
|
+
interface Asset<T extends string = string> {
|
|
1011
|
+
id: string;
|
|
1012
|
+
type: T;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
export interface SpecialTypesAsset extends Asset<"special"> {
|
|
1016
|
+
nullValue: null;
|
|
1017
|
+
undefinedValue: undefined;
|
|
1018
|
+
anyValue: any;
|
|
1019
|
+
unknownValue: unknown;
|
|
1020
|
+
neverValue: never;
|
|
1021
|
+
voidValue: void;
|
|
1022
|
+
}
|
|
1023
|
+
`;
|
|
1024
|
+
|
|
1025
|
+
const types = convertTsToXLR(source);
|
|
1026
|
+
const specialAsset = types.find(
|
|
1027
|
+
(t) => t.name === "SpecialTypesAsset",
|
|
1028
|
+
) as NamedType<ObjectType>;
|
|
1029
|
+
expect(specialAsset).toBeDefined();
|
|
1030
|
+
|
|
1031
|
+
const code = generateFunctionalBuilder(specialAsset);
|
|
1032
|
+
|
|
1033
|
+
expect(code).toContain("withNullValue(value: null)");
|
|
1034
|
+
expect(code).toContain("withUndefinedValue(value: undefined)");
|
|
1035
|
+
expect(code).toContain("withAnyValue(value: any)");
|
|
1036
|
+
expect(code).toContain("withUnknownValue(value: unknown)");
|
|
1037
|
+
expect(code).toContain("withNeverValue(value: never)");
|
|
1038
|
+
expect(code).toContain("withVoidValue(value: void)");
|
|
1039
|
+
});
|
|
1040
|
+
|
|
1041
|
+
test("handles intersection types (AndType)", () => {
|
|
1042
|
+
const source = `
|
|
1043
|
+
interface Asset<T extends string = string> {
|
|
1044
|
+
id: string;
|
|
1045
|
+
type: T;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
interface BaseProps {
|
|
1049
|
+
name: string;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
interface ExtendedProps {
|
|
1053
|
+
description: string;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
export interface IntersectionAsset extends Asset<"intersection"> {
|
|
1057
|
+
combined: BaseProps & ExtendedProps;
|
|
1058
|
+
}
|
|
1059
|
+
`;
|
|
1060
|
+
|
|
1061
|
+
const types = convertTsToXLR(source);
|
|
1062
|
+
const asset = types.find(
|
|
1063
|
+
(t) => t.name === "IntersectionAsset",
|
|
1064
|
+
) as NamedType<ObjectType>;
|
|
1065
|
+
expect(asset).toBeDefined();
|
|
1066
|
+
|
|
1067
|
+
const code = generateFunctionalBuilder(asset);
|
|
1068
|
+
|
|
1069
|
+
// Should handle intersection type
|
|
1070
|
+
expect(code).toContain("withCombined");
|
|
1071
|
+
});
|
|
1072
|
+
|
|
1073
|
+
test("handles union types containing arrays for __arrayProperties__", () => {
|
|
1074
|
+
const source = `
|
|
1075
|
+
interface Asset<T extends string = string> {
|
|
1076
|
+
id: string;
|
|
1077
|
+
type: T;
|
|
1078
|
+
}
|
|
1079
|
+
type Binding = string;
|
|
1080
|
+
|
|
1081
|
+
export interface ActionAsset extends Asset<"action"> {
|
|
1082
|
+
value?: string;
|
|
1083
|
+
/** Can be single binding or array of bindings */
|
|
1084
|
+
validate?: Array<Binding> | Binding;
|
|
1085
|
+
}
|
|
1086
|
+
`;
|
|
1087
|
+
|
|
1088
|
+
const types = convertTsToXLR(source);
|
|
1089
|
+
const actionAsset = types.find(
|
|
1090
|
+
(t) => t.name === "ActionAsset",
|
|
1091
|
+
) as NamedType<ObjectType>;
|
|
1092
|
+
expect(actionAsset).toBeDefined();
|
|
1093
|
+
|
|
1094
|
+
const code = generateFunctionalBuilder(actionAsset);
|
|
1095
|
+
|
|
1096
|
+
// The validate property is Array<Binding> | Binding - should be in __arrayProperties__
|
|
1097
|
+
expect(code).toContain("__arrayProperties__");
|
|
1098
|
+
expect(code).toContain('"validate"');
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
test("handles Record types", () => {
|
|
1102
|
+
const source = `
|
|
1103
|
+
interface Asset<T extends string = string> {
|
|
1104
|
+
id: string;
|
|
1105
|
+
type: T;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
export interface DataAsset extends Asset<"data"> {
|
|
1109
|
+
metadata: Record<string, unknown>;
|
|
1110
|
+
counts: Record<string, number>;
|
|
1111
|
+
}
|
|
1112
|
+
`;
|
|
1113
|
+
|
|
1114
|
+
const types = convertTsToXLR(source);
|
|
1115
|
+
const dataAsset = types.find(
|
|
1116
|
+
(t) => t.name === "DataAsset",
|
|
1117
|
+
) as NamedType<ObjectType>;
|
|
1118
|
+
expect(dataAsset).toBeDefined();
|
|
1119
|
+
|
|
1120
|
+
const code = generateFunctionalBuilder(dataAsset);
|
|
1121
|
+
|
|
1122
|
+
expect(code).toContain("withMetadata(value: Record<string, unknown>)");
|
|
1123
|
+
expect(code).toContain("withCounts(value: Record<string, number");
|
|
1124
|
+
});
|
|
1125
|
+
|
|
1126
|
+
test("handles deeply nested union types", () => {
|
|
1127
|
+
const source = `
|
|
1128
|
+
interface Asset<T extends string = string> {
|
|
1129
|
+
id: string;
|
|
1130
|
+
type: T;
|
|
1131
|
+
}
|
|
1132
|
+
type AssetWrapper<T extends Asset = Asset> = { asset: T };
|
|
1133
|
+
|
|
1134
|
+
export interface ComplexAsset extends Asset<"complex"> {
|
|
1135
|
+
/** Single asset, array of assets, or nothing */
|
|
1136
|
+
content?: AssetWrapper<Asset> | Array<AssetWrapper<Asset>>;
|
|
1137
|
+
}
|
|
1138
|
+
`;
|
|
1139
|
+
|
|
1140
|
+
const types = convertTsToXLR(source);
|
|
1141
|
+
const complexAsset = types.find(
|
|
1142
|
+
(t) => t.name === "ComplexAsset",
|
|
1143
|
+
) as NamedType<ObjectType>;
|
|
1144
|
+
expect(complexAsset).toBeDefined();
|
|
1145
|
+
|
|
1146
|
+
const code = generateFunctionalBuilder(complexAsset);
|
|
1147
|
+
|
|
1148
|
+
// Should handle the union and include in __arrayProperties__
|
|
1149
|
+
expect(code).toContain("withContent");
|
|
1150
|
+
expect(code).toContain("__arrayProperties__");
|
|
1151
|
+
expect(code).toContain('"content"');
|
|
1152
|
+
});
|
|
1153
|
+
|
|
1154
|
+
test("handles literal union types", () => {
|
|
1155
|
+
const source = `
|
|
1156
|
+
interface Asset<T extends string = string> {
|
|
1157
|
+
id: string;
|
|
1158
|
+
type: T;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
export interface SizeAsset extends Asset<"sized"> {
|
|
1162
|
+
size: "small" | "medium" | "large";
|
|
1163
|
+
alignment?: "left" | "center" | "right";
|
|
1164
|
+
}
|
|
1165
|
+
`;
|
|
1166
|
+
|
|
1167
|
+
const types = convertTsToXLR(source);
|
|
1168
|
+
const sizeAsset = types.find(
|
|
1169
|
+
(t) => t.name === "SizeAsset",
|
|
1170
|
+
) as NamedType<ObjectType>;
|
|
1171
|
+
expect(sizeAsset).toBeDefined();
|
|
1172
|
+
|
|
1173
|
+
const code = generateFunctionalBuilder(sizeAsset);
|
|
1174
|
+
|
|
1175
|
+
// Should preserve literal union types
|
|
1176
|
+
expect(code).toContain('"small"');
|
|
1177
|
+
expect(code).toContain('"medium"');
|
|
1178
|
+
expect(code).toContain('"large"');
|
|
1179
|
+
});
|
|
1180
|
+
});
|
|
1181
|
+
|
|
1182
|
+
describe("Nested Objects Accept Raw or Builder", () => {
|
|
1183
|
+
test("complex nested objects accept raw object OR FunctionalBuilder", () => {
|
|
1184
|
+
const source = `
|
|
1185
|
+
interface Asset<T extends string = string> {
|
|
1186
|
+
id: string;
|
|
1187
|
+
type: T;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
export interface ActionAsset extends Asset<"action"> {
|
|
1191
|
+
confirmation?: {
|
|
1192
|
+
message: string;
|
|
1193
|
+
affirmativeLabel: string;
|
|
1194
|
+
negativeLabel?: string;
|
|
1195
|
+
extra?: string;
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
`;
|
|
1199
|
+
|
|
1200
|
+
const types = convertTsToXLR(source);
|
|
1201
|
+
const actionAsset = types.find(
|
|
1202
|
+
(t) => t.name === "ActionAsset",
|
|
1203
|
+
) as NamedType<ObjectType>;
|
|
1204
|
+
const code = generateFunctionalBuilder(actionAsset);
|
|
1205
|
+
|
|
1206
|
+
// Should accept both raw inline object AND FunctionalBuilder
|
|
1207
|
+
expect(code).toContain("withConfirmation(value: {");
|
|
1208
|
+
expect(code).toContain("| FunctionalBuilder<{");
|
|
1209
|
+
expect(code).toContain("}, BaseBuildContext>");
|
|
1210
|
+
});
|
|
1211
|
+
|
|
1212
|
+
test("any nested object accepts raw object OR FunctionalBuilder", () => {
|
|
1213
|
+
const source = `
|
|
1214
|
+
interface Asset<T extends string = string> {
|
|
1215
|
+
id: string;
|
|
1216
|
+
type: T;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
export interface ActionAsset extends Asset<"action"> {
|
|
1220
|
+
simple?: {
|
|
1221
|
+
a: string;
|
|
1222
|
+
b: string;
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
`;
|
|
1226
|
+
|
|
1227
|
+
const types = convertTsToXLR(source);
|
|
1228
|
+
const actionAsset = types.find(
|
|
1229
|
+
(t) => t.name === "ActionAsset",
|
|
1230
|
+
) as NamedType<ObjectType>;
|
|
1231
|
+
const code = generateFunctionalBuilder(actionAsset);
|
|
1232
|
+
|
|
1233
|
+
// Any nested object should accept either raw object or FunctionalBuilder
|
|
1234
|
+
expect(code).toContain("withSimple(value: {");
|
|
1235
|
+
expect(code).toContain("| FunctionalBuilder<{");
|
|
1236
|
+
});
|
|
1237
|
+
|
|
1238
|
+
test("named nested types accept raw object OR FunctionalBuilder", () => {
|
|
1239
|
+
const source = `
|
|
1240
|
+
interface Asset<T extends string = string> {
|
|
1241
|
+
id: string;
|
|
1242
|
+
type: T;
|
|
1243
|
+
}
|
|
1244
|
+
type AssetWrapper<T extends Asset = Asset> = { asset: T };
|
|
1245
|
+
|
|
1246
|
+
export interface Metadata {
|
|
1247
|
+
beacon?: string;
|
|
1248
|
+
role?: "primary" | "secondary";
|
|
1249
|
+
skipValidation?: boolean;
|
|
1250
|
+
extra?: string;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
export interface ActionAsset extends Asset<"action"> {
|
|
1254
|
+
value?: string;
|
|
1255
|
+
label?: AssetWrapper<Asset>;
|
|
1256
|
+
metaData?: Metadata;
|
|
1257
|
+
}
|
|
1258
|
+
`;
|
|
1259
|
+
|
|
1260
|
+
const types = convertTsToXLR(source);
|
|
1261
|
+
const actionAsset = types.find(
|
|
1262
|
+
(t) => t.name === "ActionAsset",
|
|
1263
|
+
) as NamedType<ObjectType>;
|
|
1264
|
+
const code = generateFunctionalBuilder(actionAsset);
|
|
1265
|
+
|
|
1266
|
+
// Named complex types should accept the type, FunctionalBuilder, or FunctionalPartial
|
|
1267
|
+
expect(code).toContain(
|
|
1268
|
+
"withMetaData(value: Metadata | FunctionalBuilder<Metadata, BaseBuildContext> | FunctionalPartial<Metadata, BaseBuildContext>)",
|
|
1269
|
+
);
|
|
1270
|
+
});
|
|
1271
|
+
});
|
|
1272
|
+
|
|
1273
|
+
describe("Bug Fixes", () => {
|
|
1274
|
+
describe("Issue #1: Malformed Generic Type Parameters", () => {
|
|
1275
|
+
test("handles simple generic parameters", () => {
|
|
1276
|
+
const source = `
|
|
1277
|
+
interface Asset<T extends string = string> {
|
|
1278
|
+
id: string;
|
|
1279
|
+
type: T;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
export interface SimpleGeneric<T extends string = string, U extends number = number> extends Asset<"test"> {
|
|
1283
|
+
prop1?: T;
|
|
1284
|
+
prop2?: U;
|
|
1285
|
+
}
|
|
1286
|
+
`;
|
|
1287
|
+
|
|
1288
|
+
const types = convertTsToXLR(source);
|
|
1289
|
+
const asset = types.find(
|
|
1290
|
+
(t) => t.name === "SimpleGeneric",
|
|
1291
|
+
) as NamedType<ObjectType>;
|
|
1292
|
+
const code = generateFunctionalBuilder(asset);
|
|
1293
|
+
|
|
1294
|
+
expect(code).toContain("SimpleGenericBuilder<T extends string");
|
|
1295
|
+
expect(code).toContain("<T, U>");
|
|
1296
|
+
// Should not have malformed syntax
|
|
1297
|
+
expect(code).not.toContain("<T, U,");
|
|
1298
|
+
expect(code).not.toContain("<T, U, BaseBuildContext>");
|
|
1299
|
+
});
|
|
1300
|
+
|
|
1301
|
+
test("handles nested generic parameters with commas", () => {
|
|
1302
|
+
const source = `
|
|
1303
|
+
interface Asset<T extends string = string> {
|
|
1304
|
+
id: string;
|
|
1305
|
+
type: T;
|
|
1306
|
+
}
|
|
1307
|
+
type AssetWrapper<T extends Asset = Asset> = { asset: T };
|
|
1308
|
+
|
|
1309
|
+
interface ListItemNoHelp<AnyAsset extends Asset = Asset> extends AssetWrapper<AnyAsset> {}
|
|
1310
|
+
interface ListItem<AnyAsset extends Asset = Asset> extends ListItemNoHelp<AnyAsset> {
|
|
1311
|
+
help?: { id: string; };
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
export interface ListAsset<
|
|
1315
|
+
AnyAsset extends Asset = Asset,
|
|
1316
|
+
ItemType extends ListItemNoHelp<AnyAsset> = ListItem<AnyAsset>
|
|
1317
|
+
> extends Asset<"list"> {
|
|
1318
|
+
values?: Array<ItemType>;
|
|
1319
|
+
}
|
|
1320
|
+
`;
|
|
1321
|
+
|
|
1322
|
+
const types = convertTsToXLR(source);
|
|
1323
|
+
const listAsset = types.find(
|
|
1324
|
+
(t) => t.name === "ListAsset",
|
|
1325
|
+
) as NamedType<ObjectType>;
|
|
1326
|
+
const code = generateFunctionalBuilder(listAsset);
|
|
1327
|
+
|
|
1328
|
+
// Should produce <AnyAsset, ItemType>, not something malformed
|
|
1329
|
+
expect(code).toContain("ListAssetBuilder<AnyAsset extends Asset");
|
|
1330
|
+
expect(code).toContain("<AnyAsset, ItemType>");
|
|
1331
|
+
// Should not have extra generic parameter in the class type usage
|
|
1332
|
+
// The bug was producing things like <AnyAsset, ItemType, BaseBuildContext> as if
|
|
1333
|
+
// BaseBuildContext was a third type parameter
|
|
1334
|
+
expect(code).not.toMatch(
|
|
1335
|
+
/ListAssetBuilder<AnyAsset,\s*ItemType,\s*BaseBuildContext>/,
|
|
1336
|
+
);
|
|
1337
|
+
// The method return type should just be <AnyAsset, ItemType>, not malformed
|
|
1338
|
+
expect(code).toMatch(/:\s*ListAssetBuilder<AnyAsset,\s*ItemType>/);
|
|
1339
|
+
});
|
|
1340
|
+
});
|
|
1341
|
+
|
|
1342
|
+
describe("Issue #2: Quoted Property Names", () => {
|
|
1343
|
+
test("generates valid method names for quoted properties", () => {
|
|
1344
|
+
const source = `
|
|
1345
|
+
interface Asset<T extends string = string> {
|
|
1346
|
+
id: string;
|
|
1347
|
+
type: T;
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
export interface ImageAsset extends Asset<"image"> {
|
|
1351
|
+
value?: string;
|
|
1352
|
+
"mime-type"?: string;
|
|
1353
|
+
'single-quoted'?: number;
|
|
1354
|
+
}
|
|
1355
|
+
`;
|
|
1356
|
+
|
|
1357
|
+
const types = convertTsToXLR(source);
|
|
1358
|
+
const imageAsset = types.find(
|
|
1359
|
+
(t) => t.name === "ImageAsset",
|
|
1360
|
+
) as NamedType<ObjectType>;
|
|
1361
|
+
const code = generateFunctionalBuilder(imageAsset);
|
|
1362
|
+
|
|
1363
|
+
// Should generate valid method names without quotes
|
|
1364
|
+
expect(code).toContain("withMimeType");
|
|
1365
|
+
expect(code).toContain("withSingleQuoted");
|
|
1366
|
+
// Should NOT have quotes in method names
|
|
1367
|
+
expect(code).not.toContain("with'");
|
|
1368
|
+
expect(code).not.toContain('with"');
|
|
1369
|
+
// The set() call should also have clean property names
|
|
1370
|
+
expect(code).toContain('this.set("mime-type"');
|
|
1371
|
+
expect(code).toContain('this.set("single-quoted"');
|
|
1372
|
+
});
|
|
1373
|
+
});
|
|
1374
|
+
|
|
1375
|
+
describe("Issue #3: Missing Type Imports", () => {
|
|
1376
|
+
test("imports types referenced in generic constraints and defaults", () => {
|
|
1377
|
+
const source = `
|
|
1378
|
+
interface Asset<T extends string = string> {
|
|
1379
|
+
id: string;
|
|
1380
|
+
type: T;
|
|
1381
|
+
}
|
|
1382
|
+
type AssetWrapper<T extends Asset = Asset> = { asset: T };
|
|
1383
|
+
|
|
1384
|
+
export interface ListItemNoHelp<AnyAsset extends Asset = Asset>
|
|
1385
|
+
extends AssetWrapper<AnyAsset> {}
|
|
1386
|
+
|
|
1387
|
+
export interface ListItem<AnyAsset extends Asset = Asset>
|
|
1388
|
+
extends ListItemNoHelp<AnyAsset> {
|
|
1389
|
+
help?: { id: string; };
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
export interface ListAsset<
|
|
1393
|
+
AnyAsset extends Asset = Asset,
|
|
1394
|
+
ItemType extends ListItemNoHelp<AnyAsset> = ListItem<AnyAsset>
|
|
1395
|
+
> extends Asset<"list"> {
|
|
1396
|
+
values?: Array<ItemType>;
|
|
1397
|
+
}
|
|
1398
|
+
`;
|
|
1399
|
+
|
|
1400
|
+
const types = convertTsToXLR(source);
|
|
1401
|
+
const listAsset = types.find(
|
|
1402
|
+
(t) => t.name === "ListAsset",
|
|
1403
|
+
) as NamedType<ObjectType>;
|
|
1404
|
+
const code = generateFunctionalBuilder(listAsset);
|
|
1405
|
+
|
|
1406
|
+
// Should import types from generic constraints/defaults
|
|
1407
|
+
// The import statement should include these types
|
|
1408
|
+
expect(code).toMatch(/import type \{[^}]*ListItemNoHelp[^}]*\}/);
|
|
1409
|
+
expect(code).toMatch(/import type \{[^}]*ListItem[^}]*\}/);
|
|
1410
|
+
});
|
|
1411
|
+
|
|
1412
|
+
test("imports nested generic argument types", () => {
|
|
1413
|
+
const source = `
|
|
1414
|
+
interface Asset<T extends string = string> {
|
|
1415
|
+
id: string;
|
|
1416
|
+
type: T;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
export interface Wrapper<T> {
|
|
1420
|
+
value: T;
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
export interface Nested<T> {
|
|
1424
|
+
inner: T;
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
export interface ComplexGeneric<
|
|
1428
|
+
T extends Wrapper<Nested<string>> = Wrapper<Nested<string>>
|
|
1429
|
+
> extends Asset<"complex"> {
|
|
1430
|
+
prop?: T;
|
|
1431
|
+
}
|
|
1432
|
+
`;
|
|
1433
|
+
|
|
1434
|
+
const types = convertTsToXLR(source);
|
|
1435
|
+
const asset = types.find(
|
|
1436
|
+
(t) => t.name === "ComplexGeneric",
|
|
1437
|
+
) as NamedType<ObjectType>;
|
|
1438
|
+
const code = generateFunctionalBuilder(asset);
|
|
1439
|
+
|
|
1440
|
+
// Should import nested types from generic arguments
|
|
1441
|
+
expect(code).toMatch(/import type \{[^}]*Wrapper[^}]*\}/);
|
|
1442
|
+
expect(code).toMatch(/import type \{[^}]*Nested[^}]*\}/);
|
|
1443
|
+
});
|
|
1444
|
+
});
|
|
1445
|
+
|
|
1446
|
+
describe("Issue #4: Built-in TypeScript Types Should Not Be Imported", () => {
|
|
1447
|
+
test("does not import Map type from source file", () => {
|
|
1448
|
+
const source = `
|
|
1449
|
+
interface Asset<T extends string = string> {
|
|
1450
|
+
id: string;
|
|
1451
|
+
type: T;
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
export interface DataAsset extends Asset<"data"> {
|
|
1455
|
+
errorsAndWarnings?: Map<string, Array<string>>;
|
|
1456
|
+
}
|
|
1457
|
+
`;
|
|
1458
|
+
|
|
1459
|
+
const types = convertTsToXLR(source);
|
|
1460
|
+
const asset = types.find(
|
|
1461
|
+
(t) => t.name === "DataAsset",
|
|
1462
|
+
) as NamedType<ObjectType>;
|
|
1463
|
+
const code = generateFunctionalBuilder(asset);
|
|
1464
|
+
|
|
1465
|
+
// Map should NOT appear in imports - it's a built-in
|
|
1466
|
+
expect(code).not.toMatch(/import type \{[^}]*\bMap\b[^}]*\}/);
|
|
1467
|
+
// Should still generate the method correctly
|
|
1468
|
+
expect(code).toContain("withErrorsAndWarnings");
|
|
1469
|
+
});
|
|
1470
|
+
|
|
1471
|
+
test("does not import Set type from source file", () => {
|
|
1472
|
+
const source = `
|
|
1473
|
+
interface Asset<T extends string = string> {
|
|
1474
|
+
id: string;
|
|
1475
|
+
type: T;
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
export interface CollectionAsset extends Asset<"collection"> {
|
|
1479
|
+
uniqueIds?: Set<string>;
|
|
1480
|
+
}
|
|
1481
|
+
`;
|
|
1482
|
+
|
|
1483
|
+
const types = convertTsToXLR(source);
|
|
1484
|
+
const asset = types.find(
|
|
1485
|
+
(t) => t.name === "CollectionAsset",
|
|
1486
|
+
) as NamedType<ObjectType>;
|
|
1487
|
+
const code = generateFunctionalBuilder(asset);
|
|
1488
|
+
|
|
1489
|
+
// Set should NOT appear in imports - it's a built-in
|
|
1490
|
+
expect(code).not.toMatch(/import type \{[^}]*\bSet\b[^}]*\}/);
|
|
1491
|
+
// Should still generate the method correctly
|
|
1492
|
+
expect(code).toContain("withUniqueIds");
|
|
1493
|
+
});
|
|
1494
|
+
|
|
1495
|
+
test("does not import WeakMap or WeakSet types", () => {
|
|
1496
|
+
const source = `
|
|
1497
|
+
interface Asset<T extends string = string> {
|
|
1498
|
+
id: string;
|
|
1499
|
+
type: T;
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
export interface CacheAsset extends Asset<"cache"> {
|
|
1503
|
+
weakCache?: WeakMap<object, string>;
|
|
1504
|
+
weakSet?: WeakSet<object>;
|
|
1505
|
+
}
|
|
1506
|
+
`;
|
|
1507
|
+
|
|
1508
|
+
const types = convertTsToXLR(source);
|
|
1509
|
+
const asset = types.find(
|
|
1510
|
+
(t) => t.name === "CacheAsset",
|
|
1511
|
+
) as NamedType<ObjectType>;
|
|
1512
|
+
const code = generateFunctionalBuilder(asset);
|
|
1513
|
+
|
|
1514
|
+
// WeakMap and WeakSet should NOT appear in imports
|
|
1515
|
+
expect(code).not.toMatch(/import type \{[^}]*\bWeakMap\b[^}]*\}/);
|
|
1516
|
+
expect(code).not.toMatch(/import type \{[^}]*\bWeakSet\b[^}]*\}/);
|
|
1517
|
+
});
|
|
1518
|
+
|
|
1519
|
+
test("handles multiple built-in types in single property", () => {
|
|
1520
|
+
const source = `
|
|
1521
|
+
interface Asset<T extends string = string> {
|
|
1522
|
+
id: string;
|
|
1523
|
+
type: T;
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
export interface ComplexAsset extends Asset<"complex"> {
|
|
1527
|
+
data?: Map<string, Set<number>>;
|
|
1528
|
+
metadata?: Record<string, Array<Promise<string>>>;
|
|
1529
|
+
}
|
|
1530
|
+
`;
|
|
1531
|
+
|
|
1532
|
+
const types = convertTsToXLR(source);
|
|
1533
|
+
const asset = types.find(
|
|
1534
|
+
(t) => t.name === "ComplexAsset",
|
|
1535
|
+
) as NamedType<ObjectType>;
|
|
1536
|
+
const code = generateFunctionalBuilder(asset);
|
|
1537
|
+
|
|
1538
|
+
// None of the built-in types should appear in imports
|
|
1539
|
+
expect(code).not.toMatch(/import type \{[^}]*\bMap\b[^}]*\}/);
|
|
1540
|
+
expect(code).not.toMatch(/import type \{[^}]*\bSet\b[^}]*\}/);
|
|
1541
|
+
expect(code).not.toMatch(/import type \{[^}]*\bRecord\b[^}]*\}/);
|
|
1542
|
+
expect(code).not.toMatch(/import type \{[^}]*\bArray\b[^}]*\}/);
|
|
1543
|
+
expect(code).not.toMatch(/import type \{[^}]*\bPromise\b[^}]*\}/);
|
|
1544
|
+
// Should still generate the methods correctly
|
|
1545
|
+
expect(code).toContain("withData");
|
|
1546
|
+
expect(code).toContain("withMetadata");
|
|
1547
|
+
});
|
|
1548
|
+
|
|
1549
|
+
test("does not import Date, Error, or RegExp types", () => {
|
|
1550
|
+
const source = `
|
|
1551
|
+
interface Asset<T extends string = string> {
|
|
1552
|
+
id: string;
|
|
1553
|
+
type: T;
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
export interface EventAsset extends Asset<"event"> {
|
|
1557
|
+
timestamp?: Date;
|
|
1558
|
+
error?: Error;
|
|
1559
|
+
pattern?: RegExp;
|
|
1560
|
+
}
|
|
1561
|
+
`;
|
|
1562
|
+
|
|
1563
|
+
const types = convertTsToXLR(source);
|
|
1564
|
+
const asset = types.find(
|
|
1565
|
+
(t) => t.name === "EventAsset",
|
|
1566
|
+
) as NamedType<ObjectType>;
|
|
1567
|
+
const code = generateFunctionalBuilder(asset);
|
|
1568
|
+
|
|
1569
|
+
// Date, Error, RegExp should NOT appear in imports
|
|
1570
|
+
expect(code).not.toMatch(/import type \{[^}]*\bDate\b[^}]*\}/);
|
|
1571
|
+
expect(code).not.toMatch(/import type \{[^}]*\bError\b[^}]*\}/);
|
|
1572
|
+
expect(code).not.toMatch(/import type \{[^}]*\bRegExp\b[^}]*\}/);
|
|
1573
|
+
});
|
|
1574
|
+
|
|
1575
|
+
test("imports custom types used as generic arguments of built-in types", () => {
|
|
1576
|
+
const source = `
|
|
1577
|
+
interface Asset<T extends string = string> {
|
|
1578
|
+
id: string;
|
|
1579
|
+
type: T;
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
export interface CustomData {
|
|
1583
|
+
name: string;
|
|
1584
|
+
value: number;
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
export interface CustomKey {
|
|
1588
|
+
id: string;
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
export interface DataAsset extends Asset<"data"> {
|
|
1592
|
+
/** Map with custom value type - should import CustomData but not Map */
|
|
1593
|
+
dataMap?: Map<string, CustomData>;
|
|
1594
|
+
/** Set with custom type - should import CustomKey but not Set */
|
|
1595
|
+
keySet?: Set<CustomKey>;
|
|
1596
|
+
/** Array with custom type - should import CustomData but not Array */
|
|
1597
|
+
items?: Array<CustomData>;
|
|
1598
|
+
}
|
|
1599
|
+
`;
|
|
1600
|
+
|
|
1601
|
+
const types = convertTsToXLR(source);
|
|
1602
|
+
const asset = types.find(
|
|
1603
|
+
(t) => t.name === "DataAsset",
|
|
1604
|
+
) as NamedType<ObjectType>;
|
|
1605
|
+
const code = generateFunctionalBuilder(asset);
|
|
1606
|
+
|
|
1607
|
+
// Built-in types should NOT appear in imports
|
|
1608
|
+
expect(code).not.toMatch(/import type \{[^}]*\bMap\b[^}]*\}/);
|
|
1609
|
+
expect(code).not.toMatch(/import type \{[^}]*\bSet\b[^}]*\}/);
|
|
1610
|
+
expect(code).not.toMatch(/import type \{[^}]*\bArray\b[^}]*\}/);
|
|
1611
|
+
|
|
1612
|
+
// Custom types SHOULD appear in imports
|
|
1613
|
+
expect(code).toMatch(/import type \{[^}]*\bCustomData\b[^}]*\}/);
|
|
1614
|
+
expect(code).toMatch(/import type \{[^}]*\bCustomKey\b[^}]*\}/);
|
|
1615
|
+
|
|
1616
|
+
// Should still generate the methods correctly
|
|
1617
|
+
expect(code).toContain("withDataMap");
|
|
1618
|
+
expect(code).toContain("withKeySet");
|
|
1619
|
+
expect(code).toContain("withItems");
|
|
1620
|
+
});
|
|
1621
|
+
});
|
|
1622
|
+
|
|
1623
|
+
describe("Issue #5: External Package Imports", () => {
|
|
1624
|
+
test("imports types from external packages using externalTypes config", () => {
|
|
1625
|
+
const source = `
|
|
1626
|
+
interface Asset<T extends string = string> {
|
|
1627
|
+
id: string;
|
|
1628
|
+
type: T;
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
export interface ExternalData {
|
|
1632
|
+
name: string;
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
export interface MyAsset extends Asset<"my"> {
|
|
1636
|
+
data?: ExternalData;
|
|
1637
|
+
}
|
|
1638
|
+
`;
|
|
1639
|
+
|
|
1640
|
+
const types = convertTsToXLR(source);
|
|
1641
|
+
const asset = types.find(
|
|
1642
|
+
(t) => t.name === "MyAsset",
|
|
1643
|
+
) as NamedType<ObjectType>;
|
|
1644
|
+
|
|
1645
|
+
// Configure ExternalData as coming from an external package
|
|
1646
|
+
const externalTypes = new Map<string, string>();
|
|
1647
|
+
externalTypes.set("ExternalData", "@player-lang/types");
|
|
1648
|
+
|
|
1649
|
+
const code = generateFunctionalBuilder(asset, { externalTypes });
|
|
1650
|
+
|
|
1651
|
+
// ExternalData should be imported from the package
|
|
1652
|
+
expect(code).toContain(
|
|
1653
|
+
'import type { ExternalData } from "@player-lang/types"',
|
|
1654
|
+
);
|
|
1655
|
+
// Should still generate the method correctly
|
|
1656
|
+
expect(code).toContain("withData");
|
|
1657
|
+
});
|
|
1658
|
+
|
|
1659
|
+
test("groups multiple types from same external package", () => {
|
|
1660
|
+
const source = `
|
|
1661
|
+
interface Asset<T extends string = string> {
|
|
1662
|
+
id: string;
|
|
1663
|
+
type: T;
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
export interface TypeA {
|
|
1667
|
+
value: string;
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
export interface TypeB {
|
|
1671
|
+
count: number;
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
export interface MyAsset extends Asset<"my"> {
|
|
1675
|
+
a?: TypeA;
|
|
1676
|
+
b?: TypeB;
|
|
1677
|
+
}
|
|
1678
|
+
`;
|
|
1679
|
+
|
|
1680
|
+
const types = convertTsToXLR(source);
|
|
1681
|
+
const asset = types.find(
|
|
1682
|
+
(t) => t.name === "MyAsset",
|
|
1683
|
+
) as NamedType<ObjectType>;
|
|
1684
|
+
|
|
1685
|
+
// Configure both types as coming from the same external package
|
|
1686
|
+
const externalTypes = new Map<string, string>();
|
|
1687
|
+
externalTypes.set("TypeA", "@external/types");
|
|
1688
|
+
externalTypes.set("TypeB", "@external/types");
|
|
1689
|
+
|
|
1690
|
+
const code = generateFunctionalBuilder(asset, { externalTypes });
|
|
1691
|
+
|
|
1692
|
+
// Both types should be imported from the same package in one statement
|
|
1693
|
+
expect(code).toMatch(
|
|
1694
|
+
/import type \{[^}]*TypeA[^}]*TypeB[^}]*\} from "@external\/types"/,
|
|
1695
|
+
);
|
|
1696
|
+
});
|
|
1697
|
+
|
|
1698
|
+
test("external types take precedence over sameFileTypes", () => {
|
|
1699
|
+
const source = `
|
|
1700
|
+
interface Asset<T extends string = string> {
|
|
1701
|
+
id: string;
|
|
1702
|
+
type: T;
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
export interface SharedType {
|
|
1706
|
+
value: string;
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
export interface MyAsset extends Asset<"my"> {
|
|
1710
|
+
shared?: SharedType;
|
|
1711
|
+
}
|
|
1712
|
+
`;
|
|
1713
|
+
|
|
1714
|
+
const types = convertTsToXLR(source);
|
|
1715
|
+
const asset = types.find(
|
|
1716
|
+
(t) => t.name === "MyAsset",
|
|
1717
|
+
) as NamedType<ObjectType>;
|
|
1718
|
+
|
|
1719
|
+
// Configure SharedType as external (even though it would be in sameFileTypes)
|
|
1720
|
+
const sameFileTypes = new Set<string>(["SharedType"]);
|
|
1721
|
+
const externalTypes = new Map<string, string>();
|
|
1722
|
+
externalTypes.set("SharedType", "@external/shared");
|
|
1723
|
+
|
|
1724
|
+
const code = generateFunctionalBuilder(asset, {
|
|
1725
|
+
sameFileTypes,
|
|
1726
|
+
externalTypes,
|
|
1727
|
+
});
|
|
1728
|
+
|
|
1729
|
+
// SharedType should be imported from external package, not from same file
|
|
1730
|
+
expect(code).toContain(
|
|
1731
|
+
'import type { SharedType } from "@external/shared"',
|
|
1732
|
+
);
|
|
1733
|
+
// Should NOT be in the main type import
|
|
1734
|
+
expect(code).not.toMatch(
|
|
1735
|
+
/import type \{[^}]*MyAsset[^}]*SharedType[^}]*\} from/,
|
|
1736
|
+
);
|
|
1737
|
+
});
|
|
1738
|
+
});
|
|
1739
|
+
|
|
1740
|
+
describe("Issue #6: Generic Type Parameters Should Not Be Imported", () => {
|
|
1741
|
+
test("does not import generic parameters from referenced type constraints", () => {
|
|
1742
|
+
const source = `
|
|
1743
|
+
interface Asset<T extends string = string> {
|
|
1744
|
+
id: string;
|
|
1745
|
+
type: T;
|
|
1746
|
+
}
|
|
1747
|
+
type AssetWrapper<T extends Asset = Asset> = { asset: T };
|
|
1748
|
+
|
|
1749
|
+
export interface Bar<AnyAsset extends Asset = Asset> {
|
|
1750
|
+
label: AssetWrapper<AnyAsset>;
|
|
1751
|
+
info: AssetWrapper<AnyAsset>;
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
export interface DataVisualizationAsset<
|
|
1755
|
+
MetadataType = DataBarsMetaData,
|
|
1756
|
+
BarType extends Bar = SingleBar
|
|
1757
|
+
> extends Asset<"dataViz"> {
|
|
1758
|
+
metaData: MetadataType;
|
|
1759
|
+
data: BarType[];
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
export interface DataBarsMetaData {
|
|
1763
|
+
title: string;
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
export interface SingleBar extends Bar {
|
|
1767
|
+
value: number;
|
|
1768
|
+
}
|
|
1769
|
+
`;
|
|
1770
|
+
|
|
1771
|
+
const types = convertTsToXLR(source);
|
|
1772
|
+
const asset = types.find(
|
|
1773
|
+
(t) => t.name === "DataVisualizationAsset",
|
|
1774
|
+
) as NamedType<ObjectType>;
|
|
1775
|
+
const code = generateFunctionalBuilder(asset);
|
|
1776
|
+
|
|
1777
|
+
// Should NOT try to import AnyAsset (it's a generic param of Bar, not a concrete type)
|
|
1778
|
+
expect(code).not.toMatch(/import type \{[^}]*\bAnyAsset\b[^}]*\}/);
|
|
1779
|
+
|
|
1780
|
+
// Should import actual types like Bar, SingleBar, DataBarsMetaData
|
|
1781
|
+
expect(code).toMatch(/import type \{[^}]*\bBar\b[^}]*\}/);
|
|
1782
|
+
expect(code).toMatch(/import type \{[^}]*\bSingleBar\b[^}]*\}/);
|
|
1783
|
+
expect(code).toMatch(/import type \{[^}]*\bDataBarsMetaData\b[^}]*\}/);
|
|
1784
|
+
});
|
|
1785
|
+
|
|
1786
|
+
test("does not import generic parameters from nested generic constraints", () => {
|
|
1787
|
+
const source = `
|
|
1788
|
+
interface Asset<T extends string = string> {
|
|
1789
|
+
id: string;
|
|
1790
|
+
type: T;
|
|
1791
|
+
}
|
|
1792
|
+
type AssetWrapper<T extends Asset = Asset> = { asset: T };
|
|
1793
|
+
|
|
1794
|
+
export interface ListItemNoHelp<AnyAsset extends Asset = Asset>
|
|
1795
|
+
extends AssetWrapper<AnyAsset> {}
|
|
1796
|
+
|
|
1797
|
+
export interface ListItem<AnyAsset extends Asset = Asset>
|
|
1798
|
+
extends ListItemNoHelp<AnyAsset> {
|
|
1799
|
+
help?: { id: string; };
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
export interface ListAsset<
|
|
1803
|
+
AnyAsset extends Asset = Asset,
|
|
1804
|
+
ItemType extends ListItemNoHelp = ListItem<AnyAsset>
|
|
1805
|
+
> extends Asset<"list"> {
|
|
1806
|
+
values?: Array<ItemType>;
|
|
1807
|
+
}
|
|
1808
|
+
`;
|
|
1809
|
+
|
|
1810
|
+
const types = convertTsToXLR(source);
|
|
1811
|
+
const listAsset = types.find(
|
|
1812
|
+
(t) => t.name === "ListAsset",
|
|
1813
|
+
) as NamedType<ObjectType>;
|
|
1814
|
+
const code = generateFunctionalBuilder(listAsset);
|
|
1815
|
+
|
|
1816
|
+
// Should NOT try to import AnyAsset (it's a generic param)
|
|
1817
|
+
expect(code).not.toMatch(/import type \{[^}]*\bAnyAsset\b[^}]*\}/);
|
|
1818
|
+
|
|
1819
|
+
// Should import actual types
|
|
1820
|
+
expect(code).toMatch(/import type \{[^}]*\bListItemNoHelp\b[^}]*\}/);
|
|
1821
|
+
expect(code).toMatch(/import type \{[^}]*\bListItem\b[^}]*\}/);
|
|
1822
|
+
});
|
|
1823
|
+
});
|
|
1824
|
+
|
|
1825
|
+
describe("Issue #7: Generic Constraints Should Not Include FunctionalBuilder", () => {
|
|
1826
|
+
test("generates raw type names in generic constraints", () => {
|
|
1827
|
+
const source = `
|
|
1828
|
+
interface Asset<T extends string = string> {
|
|
1829
|
+
id: string;
|
|
1830
|
+
type: T;
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
export interface Bar<AnyAsset extends Asset = Asset> {
|
|
1834
|
+
label: string;
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
export interface DataVisualizationAsset<
|
|
1838
|
+
BarType extends Bar = SingleBar
|
|
1839
|
+
> extends Asset<"dataViz"> {
|
|
1840
|
+
data: BarType[];
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
export interface SingleBar extends Bar {
|
|
1844
|
+
value: number;
|
|
1845
|
+
}
|
|
1846
|
+
`;
|
|
1847
|
+
|
|
1848
|
+
const types = convertTsToXLR(source);
|
|
1849
|
+
const asset = types.find(
|
|
1850
|
+
(t) => t.name === "DataVisualizationAsset",
|
|
1851
|
+
) as NamedType<ObjectType>;
|
|
1852
|
+
const code = generateFunctionalBuilder(asset);
|
|
1853
|
+
|
|
1854
|
+
// Constraint should be "extends Bar", NOT "extends Bar | FunctionalBuilder<Bar>"
|
|
1855
|
+
expect(code).toContain("BarType extends Bar");
|
|
1856
|
+
expect(code).not.toContain("BarType extends Bar | FunctionalBuilder");
|
|
1857
|
+
|
|
1858
|
+
// Default should be "= SingleBar", NOT "= SingleBar | FunctionalBuilder<SingleBar>"
|
|
1859
|
+
expect(code).toContain("= SingleBar");
|
|
1860
|
+
expect(code).not.toContain("= SingleBar | FunctionalBuilder");
|
|
1861
|
+
});
|
|
1862
|
+
|
|
1863
|
+
test("generates multiple generic constraints without FunctionalBuilder", () => {
|
|
1864
|
+
const source = `
|
|
1865
|
+
interface Asset<T extends string = string> {
|
|
1866
|
+
id: string;
|
|
1867
|
+
type: T;
|
|
1868
|
+
}
|
|
1869
|
+
type AssetWrapper<T extends Asset = Asset> = { asset: T };
|
|
1870
|
+
|
|
1871
|
+
export interface ListItemNoHelp<AnyAsset extends Asset = Asset>
|
|
1872
|
+
extends AssetWrapper<AnyAsset> {}
|
|
1873
|
+
|
|
1874
|
+
export interface ListItem<AnyAsset extends Asset = Asset>
|
|
1875
|
+
extends ListItemNoHelp<AnyAsset> {
|
|
1876
|
+
help?: { id: string; };
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
export interface ListAsset<
|
|
1880
|
+
AnyAsset extends Asset = Asset,
|
|
1881
|
+
ItemType extends ListItemNoHelp = ListItem
|
|
1882
|
+
> extends Asset<"list"> {
|
|
1883
|
+
values?: Array<ItemType>;
|
|
1884
|
+
}
|
|
1885
|
+
`;
|
|
1886
|
+
|
|
1887
|
+
const types = convertTsToXLR(source);
|
|
1888
|
+
const listAsset = types.find(
|
|
1889
|
+
(t) => t.name === "ListAsset",
|
|
1890
|
+
) as NamedType<ObjectType>;
|
|
1891
|
+
const code = generateFunctionalBuilder(listAsset);
|
|
1892
|
+
|
|
1893
|
+
// Both constraints should be raw types without FunctionalBuilder
|
|
1894
|
+
expect(code).toContain("AnyAsset extends Asset");
|
|
1895
|
+
expect(code).not.toContain("AnyAsset extends Asset | FunctionalBuilder");
|
|
1896
|
+
expect(code).toContain("ItemType extends ListItemNoHelp");
|
|
1897
|
+
expect(code).not.toContain(
|
|
1898
|
+
"ItemType extends ListItemNoHelp | FunctionalBuilder",
|
|
1899
|
+
);
|
|
1900
|
+
|
|
1901
|
+
// Defaults should also be raw types
|
|
1902
|
+
expect(code).toContain("= Asset");
|
|
1903
|
+
expect(code).toContain("= ListItem");
|
|
1904
|
+
expect(code).not.toMatch(/=\s*Asset\s*\|\s*FunctionalBuilder/);
|
|
1905
|
+
expect(code).not.toMatch(/=\s*ListItem\s*\|\s*FunctionalBuilder/);
|
|
1906
|
+
});
|
|
1907
|
+
});
|
|
1908
|
+
|
|
1909
|
+
describe("Issue #8: Embedded Type Arguments Should Be Preserved When Available", () => {
|
|
1910
|
+
test("preserves embedded type arguments in extends ref string", () => {
|
|
1911
|
+
const source = `
|
|
1912
|
+
interface Asset<T extends string = string> {
|
|
1913
|
+
id: string;
|
|
1914
|
+
type: T;
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
export interface LinkModifier extends Asset<"link"> {
|
|
1918
|
+
url: string;
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
export interface FormattingAsset extends Asset<"format"> {
|
|
1922
|
+
style: string;
|
|
1923
|
+
}
|
|
1924
|
+
`;
|
|
1925
|
+
|
|
1926
|
+
const types = convertTsToXLR(source);
|
|
1927
|
+
const linkAsset = types.find(
|
|
1928
|
+
(t) => t.name === "LinkModifier",
|
|
1929
|
+
) as NamedType<ObjectType>;
|
|
1930
|
+
const code = generateFunctionalBuilder(linkAsset);
|
|
1931
|
+
|
|
1932
|
+
// Should have the correct asset type default
|
|
1933
|
+
expect(code).toContain('"type":"link"');
|
|
1934
|
+
});
|
|
1935
|
+
|
|
1936
|
+
test("handles union types with different modifiers", () => {
|
|
1937
|
+
const source = `
|
|
1938
|
+
interface Asset<T extends string = string> {
|
|
1939
|
+
id: string;
|
|
1940
|
+
type: T;
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
export interface LinkModifier {
|
|
1944
|
+
type: 'link';
|
|
1945
|
+
url: string;
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
export interface FormatModifier {
|
|
1949
|
+
type: 'format';
|
|
1950
|
+
style: string;
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
export interface TextAsset extends Asset<"text"> {
|
|
1954
|
+
value: string;
|
|
1955
|
+
modifiers?: Array<LinkModifier | FormatModifier>;
|
|
1956
|
+
}
|
|
1957
|
+
`;
|
|
1958
|
+
|
|
1959
|
+
const types = convertTsToXLR(source);
|
|
1960
|
+
const asset = types.find(
|
|
1961
|
+
(t) => t.name === "TextAsset",
|
|
1962
|
+
) as NamedType<ObjectType>;
|
|
1963
|
+
const code = generateFunctionalBuilder(asset);
|
|
1964
|
+
|
|
1965
|
+
// Should generate method for modifiers
|
|
1966
|
+
expect(code).toContain("withModifiers");
|
|
1967
|
+
|
|
1968
|
+
// Should handle both modifier types in the union
|
|
1969
|
+
expect(code).toContain("LinkModifier");
|
|
1970
|
+
expect(code).toContain("FormatModifier");
|
|
1971
|
+
});
|
|
1972
|
+
});
|
|
1973
|
+
});
|
|
1974
|
+
|
|
1975
|
+
describe("Namespaced Types", () => {
|
|
1976
|
+
test("preserves full qualified name for namespaced types in constraints", () => {
|
|
1977
|
+
const source = `
|
|
1978
|
+
interface Asset<T extends string = string> {
|
|
1979
|
+
id: string;
|
|
1980
|
+
type: T;
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
namespace Validation {
|
|
1984
|
+
export interface CrossfieldReference {
|
|
1985
|
+
field: string;
|
|
1986
|
+
condition: string;
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
export interface InputAsset extends Asset<"input"> {
|
|
1991
|
+
binding: string;
|
|
1992
|
+
validation?: Validation.CrossfieldReference;
|
|
1993
|
+
}
|
|
1994
|
+
`;
|
|
1995
|
+
|
|
1996
|
+
const types = convertTsToXLR(source);
|
|
1997
|
+
const inputAsset = types.find(
|
|
1998
|
+
(t) => t.name === "InputAsset",
|
|
1999
|
+
) as NamedType<ObjectType>;
|
|
2000
|
+
const code = generateFunctionalBuilder(inputAsset);
|
|
2001
|
+
|
|
2002
|
+
// Should preserve the full qualified name
|
|
2003
|
+
expect(code).toContain("Validation.CrossfieldReference");
|
|
2004
|
+
});
|
|
2005
|
+
|
|
2006
|
+
test("handles namespaced types in generic arguments", () => {
|
|
2007
|
+
const source = `
|
|
2008
|
+
interface Asset<T extends string = string> {
|
|
2009
|
+
id: string;
|
|
2010
|
+
type: T;
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
namespace Config {
|
|
2014
|
+
export interface Settings {
|
|
2015
|
+
enabled: boolean;
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
export interface AppAsset extends Asset<"app"> {
|
|
2020
|
+
settings?: Config.Settings;
|
|
2021
|
+
allSettings?: Array<Config.Settings>;
|
|
2022
|
+
}
|
|
2023
|
+
`;
|
|
2024
|
+
|
|
2025
|
+
const types = convertTsToXLR(source);
|
|
2026
|
+
const appAsset = types.find(
|
|
2027
|
+
(t) => t.name === "AppAsset",
|
|
2028
|
+
) as NamedType<ObjectType>;
|
|
2029
|
+
const code = generateFunctionalBuilder(appAsset);
|
|
2030
|
+
|
|
2031
|
+
// Should preserve the namespace in type references
|
|
2032
|
+
expect(code).toContain("Config.Settings");
|
|
2033
|
+
});
|
|
2034
|
+
|
|
2035
|
+
test("imports namespace for namespaced types", () => {
|
|
2036
|
+
const source = `
|
|
2037
|
+
interface Asset<T extends string = string> {
|
|
2038
|
+
id: string;
|
|
2039
|
+
type: T;
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
namespace Types {
|
|
2043
|
+
export interface Data {
|
|
2044
|
+
value: string;
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
export interface DataAsset extends Asset<"data"> {
|
|
2049
|
+
data?: Types.Data;
|
|
2050
|
+
}
|
|
2051
|
+
`;
|
|
2052
|
+
|
|
2053
|
+
const types = convertTsToXLR(source);
|
|
2054
|
+
const dataAsset = types.find(
|
|
2055
|
+
(t) => t.name === "DataAsset",
|
|
2056
|
+
) as NamedType<ObjectType>;
|
|
2057
|
+
|
|
2058
|
+
// Configure Types namespace as external
|
|
2059
|
+
const externalTypes = new Map<string, string>();
|
|
2060
|
+
externalTypes.set("Types", "@custom/types");
|
|
2061
|
+
|
|
2062
|
+
const code = generateFunctionalBuilder(dataAsset, { externalTypes });
|
|
2063
|
+
|
|
2064
|
+
// Should import the namespace
|
|
2065
|
+
expect(code).toContain('import type { Types } from "@custom/types"');
|
|
2066
|
+
// Should use the full qualified name in the code
|
|
2067
|
+
expect(code).toContain("Types.Data");
|
|
2068
|
+
});
|
|
2069
|
+
});
|
|
2070
|
+
|
|
2071
|
+
describe("FunctionalPartial Support for Nested Builder Objects", () => {
|
|
2072
|
+
test("generates FunctionalPartial union for nested object properties with AssetWrapper", () => {
|
|
2073
|
+
const source = `
|
|
2074
|
+
interface Asset<T extends string = string> {
|
|
2075
|
+
id: string;
|
|
2076
|
+
type: T;
|
|
2077
|
+
}
|
|
2078
|
+
type AssetWrapper<T extends Asset = Asset> = { asset: T };
|
|
2079
|
+
|
|
2080
|
+
export interface Foo {
|
|
2081
|
+
bar: AssetWrapper<Asset>;
|
|
2082
|
+
buz: AssetWrapper<Asset>;
|
|
2083
|
+
lol: string;
|
|
2084
|
+
ok: boolean;
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
export interface ContainerAsset extends Asset<"container"> {
|
|
2088
|
+
foo: Foo;
|
|
2089
|
+
}
|
|
2090
|
+
`;
|
|
2091
|
+
|
|
2092
|
+
const types = convertTsToXLR(source);
|
|
2093
|
+
const containerAsset = types.find(
|
|
2094
|
+
(t) => t.name === "ContainerAsset",
|
|
2095
|
+
) as NamedType<ObjectType>;
|
|
2096
|
+
expect(containerAsset).toBeDefined();
|
|
2097
|
+
|
|
2098
|
+
const code = generateFunctionalBuilder(containerAsset);
|
|
2099
|
+
|
|
2100
|
+
// The foo property should accept Foo, FunctionalBuilder<Foo>, or FunctionalPartial<Foo>
|
|
2101
|
+
expect(code).toContain("FunctionalPartial<Foo, BaseBuildContext>");
|
|
2102
|
+
// Verify the full signature
|
|
2103
|
+
expect(code).toContain(
|
|
2104
|
+
"withFoo(value: Foo | FunctionalBuilder<Foo, BaseBuildContext> | FunctionalPartial<Foo, BaseBuildContext>)",
|
|
2105
|
+
);
|
|
2106
|
+
});
|
|
2107
|
+
|
|
2108
|
+
test("generates FunctionalPartial for deeply nested object structures", () => {
|
|
2109
|
+
const source = `
|
|
2110
|
+
interface Asset<T extends string = string> {
|
|
2111
|
+
id: string;
|
|
2112
|
+
type: T;
|
|
2113
|
+
}
|
|
2114
|
+
type AssetWrapper<T extends Asset = Asset> = { asset: T };
|
|
2115
|
+
|
|
2116
|
+
export interface DeepNested {
|
|
2117
|
+
level1: {
|
|
2118
|
+
level2: {
|
|
2119
|
+
content: AssetWrapper<Asset>;
|
|
2120
|
+
value: string;
|
|
2121
|
+
};
|
|
2122
|
+
};
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
export interface DeepAsset extends Asset<"deep"> {
|
|
2126
|
+
nested: DeepNested;
|
|
2127
|
+
}
|
|
2128
|
+
`;
|
|
2129
|
+
|
|
2130
|
+
const types = convertTsToXLR(source);
|
|
2131
|
+
const deepAsset = types.find(
|
|
2132
|
+
(t) => t.name === "DeepAsset",
|
|
2133
|
+
) as NamedType<ObjectType>;
|
|
2134
|
+
expect(deepAsset).toBeDefined();
|
|
2135
|
+
|
|
2136
|
+
const code = generateFunctionalBuilder(deepAsset);
|
|
2137
|
+
|
|
2138
|
+
// The nested property should accept DeepNested, FunctionalBuilder<DeepNested>, or FunctionalPartial<DeepNested>
|
|
2139
|
+
expect(code).toContain("FunctionalPartial<DeepNested, BaseBuildContext>");
|
|
2140
|
+
});
|
|
2141
|
+
|
|
2142
|
+
test("generates FunctionalPartial for array element types", () => {
|
|
2143
|
+
const source = `
|
|
2144
|
+
interface Asset<T extends string = string> {
|
|
2145
|
+
id: string;
|
|
2146
|
+
type: T;
|
|
2147
|
+
}
|
|
2148
|
+
type AssetWrapper<T extends Asset = Asset> = { asset: T };
|
|
2149
|
+
|
|
2150
|
+
export interface ListItem {
|
|
2151
|
+
label: AssetWrapper<Asset>;
|
|
2152
|
+
value: string;
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
export interface ListAsset extends Asset<"list"> {
|
|
2156
|
+
items: Array<ListItem>;
|
|
2157
|
+
}
|
|
2158
|
+
`;
|
|
2159
|
+
|
|
2160
|
+
const types = convertTsToXLR(source);
|
|
2161
|
+
const listAsset = types.find(
|
|
2162
|
+
(t) => t.name === "ListAsset",
|
|
2163
|
+
) as NamedType<ObjectType>;
|
|
2164
|
+
expect(listAsset).toBeDefined();
|
|
2165
|
+
|
|
2166
|
+
const code = generateFunctionalBuilder(listAsset);
|
|
2167
|
+
|
|
2168
|
+
// Array elements should also support FunctionalPartial
|
|
2169
|
+
expect(code).toContain(
|
|
2170
|
+
"Array<ListItem | FunctionalBuilder<ListItem, BaseBuildContext> | FunctionalPartial<ListItem, BaseBuildContext>>",
|
|
2171
|
+
);
|
|
2172
|
+
});
|
|
2173
|
+
|
|
2174
|
+
test("generates FunctionalPartial for union types containing objects", () => {
|
|
2175
|
+
const source = `
|
|
2176
|
+
interface Asset<T extends string = string> {
|
|
2177
|
+
id: string;
|
|
2178
|
+
type: T;
|
|
2179
|
+
}
|
|
2180
|
+
type AssetWrapper<T extends Asset = Asset> = { asset: T };
|
|
2181
|
+
|
|
2182
|
+
export interface TypeA {
|
|
2183
|
+
slot: AssetWrapper<Asset>;
|
|
2184
|
+
kind: "a";
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
export interface TypeB {
|
|
2188
|
+
slot: AssetWrapper<Asset>;
|
|
2189
|
+
kind: "b";
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
export interface UnionAsset extends Asset<"union"> {
|
|
2193
|
+
content: TypeA | TypeB;
|
|
2194
|
+
}
|
|
2195
|
+
`;
|
|
2196
|
+
|
|
2197
|
+
const types = convertTsToXLR(source);
|
|
2198
|
+
const unionAsset = types.find(
|
|
2199
|
+
(t) => t.name === "UnionAsset",
|
|
2200
|
+
) as NamedType<ObjectType>;
|
|
2201
|
+
expect(unionAsset).toBeDefined();
|
|
2202
|
+
|
|
2203
|
+
const code = generateFunctionalBuilder(unionAsset);
|
|
2204
|
+
|
|
2205
|
+
// Both TypeA and TypeB should support FunctionalPartial in the union
|
|
2206
|
+
expect(code).toContain("FunctionalPartial<TypeA, BaseBuildContext>");
|
|
2207
|
+
expect(code).toContain("FunctionalPartial<TypeB, BaseBuildContext>");
|
|
2208
|
+
});
|
|
2209
|
+
});
|
|
2210
|
+
|
|
2211
|
+
describe("Generic Type Arguments Preservation", () => {
|
|
2212
|
+
test("preserves string literal generic arguments in type names", () => {
|
|
2213
|
+
const source = `
|
|
2214
|
+
interface Asset<T extends string = string> {
|
|
2215
|
+
id: string;
|
|
2216
|
+
type: T;
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
export interface SimpleModifier<T extends string> {
|
|
2220
|
+
type: T;
|
|
2221
|
+
value: string;
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
export interface TextAsset extends Asset<"text"> {
|
|
2225
|
+
value: string;
|
|
2226
|
+
modifiers?: Array<SimpleModifier<"format"> | SimpleModifier<"link">>;
|
|
2227
|
+
}
|
|
2228
|
+
`;
|
|
2229
|
+
|
|
2230
|
+
const types = convertTsToXLR(source);
|
|
2231
|
+
const textAsset = types.find(
|
|
2232
|
+
(t) => t.name === "TextAsset",
|
|
2233
|
+
) as NamedType<ObjectType>;
|
|
2234
|
+
const code = generateFunctionalBuilder(textAsset);
|
|
2235
|
+
|
|
2236
|
+
// Should preserve the generic arguments in the type
|
|
2237
|
+
expect(code).toContain('SimpleModifier<"format">');
|
|
2238
|
+
expect(code).toContain('SimpleModifier<"link">');
|
|
2239
|
+
});
|
|
2240
|
+
});
|
|
2241
|
+
|
|
2242
|
+
describe("Import Handling for Asset", () => {
|
|
2243
|
+
test("Asset is imported exactly once when used in AssetWrapper", () => {
|
|
2244
|
+
const source = `
|
|
2245
|
+
interface Asset<T extends string = string> {
|
|
2246
|
+
id: string;
|
|
2247
|
+
type: T;
|
|
2248
|
+
}
|
|
2249
|
+
type AssetWrapper<T extends Asset = Asset> = { asset: T };
|
|
2250
|
+
|
|
2251
|
+
export interface ActionAsset extends Asset<"action"> {
|
|
2252
|
+
label?: AssetWrapper<Asset>;
|
|
2253
|
+
icon?: AssetWrapper<Asset>;
|
|
2254
|
+
}
|
|
2255
|
+
`;
|
|
2256
|
+
|
|
2257
|
+
const types = convertTsToXLR(source);
|
|
2258
|
+
const actionAsset = types.find(
|
|
2259
|
+
(t) => t.name === "ActionAsset",
|
|
2260
|
+
) as NamedType<ObjectType>;
|
|
2261
|
+
const code = generateFunctionalBuilder(actionAsset);
|
|
2262
|
+
|
|
2263
|
+
// Asset should be imported exactly once from @player-ui/types
|
|
2264
|
+
const assetImportMatches = code.match(
|
|
2265
|
+
/import type \{ Asset \} from "@player-ui\/types"/g,
|
|
2266
|
+
);
|
|
2267
|
+
expect(assetImportMatches?.length).toBe(1);
|
|
2268
|
+
|
|
2269
|
+
// Asset should NOT be in the main type import
|
|
2270
|
+
expect(code).not.toMatch(
|
|
2271
|
+
/import type \{[^}]*ActionAsset[^}]*Asset[^}]*\} from/,
|
|
2272
|
+
);
|
|
2273
|
+
});
|
|
2274
|
+
|
|
2275
|
+
test("generic parameters of the current type are not imported", () => {
|
|
2276
|
+
const source = `
|
|
2277
|
+
interface Asset<T extends string = string> {
|
|
2278
|
+
id: string;
|
|
2279
|
+
type: T;
|
|
2280
|
+
}
|
|
2281
|
+
type AssetWrapper<T extends Asset = Asset> = { asset: T };
|
|
2282
|
+
|
|
2283
|
+
export interface InputAsset<AnyTextAsset extends Asset = Asset> extends Asset<"input"> {
|
|
2284
|
+
binding: string;
|
|
2285
|
+
label?: AssetWrapper<AnyTextAsset>;
|
|
2286
|
+
}
|
|
2287
|
+
`;
|
|
2288
|
+
|
|
2289
|
+
const types = convertTsToXLR(source);
|
|
2290
|
+
const inputAsset = types.find(
|
|
2291
|
+
(t) => t.name === "InputAsset",
|
|
2292
|
+
) as NamedType<ObjectType>;
|
|
2293
|
+
const code = generateFunctionalBuilder(inputAsset);
|
|
2294
|
+
|
|
2295
|
+
// AnyTextAsset should NOT be imported - it's a generic parameter of InputAsset
|
|
2296
|
+
expect(code).not.toMatch(/import type \{[^}]*AnyTextAsset[^}]*\}/);
|
|
2297
|
+
});
|
|
2298
|
+
});
|
|
2299
|
+
|
|
2300
|
+
describe("Types Extending AssetWrapper", () => {
|
|
2301
|
+
test("generates correct type signature and __assetWrapperPaths__ for type extending AssetWrapper", () => {
|
|
2302
|
+
const source = `
|
|
2303
|
+
interface Asset<T extends string = string> {
|
|
2304
|
+
id: string;
|
|
2305
|
+
type: T;
|
|
2306
|
+
}
|
|
2307
|
+
type AssetWrapper<T extends Asset = Asset> = { asset: T };
|
|
2308
|
+
|
|
2309
|
+
export interface Header extends AssetWrapper<Asset> {
|
|
2310
|
+
title: string;
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2313
|
+
export interface TableAsset extends Asset<"table"> {
|
|
2314
|
+
headers: Array<Header>;
|
|
2315
|
+
}
|
|
2316
|
+
`;
|
|
2317
|
+
|
|
2318
|
+
const types = convertTsToXLR(source);
|
|
2319
|
+
const headerType = types.find(
|
|
2320
|
+
(t) => t.name === "Header",
|
|
2321
|
+
) as NamedType<ObjectType>;
|
|
2322
|
+
const tableAsset = types.find(
|
|
2323
|
+
(t) => t.name === "TableAsset",
|
|
2324
|
+
) as NamedType<ObjectType>;
|
|
2325
|
+
expect(headerType).toBeDefined();
|
|
2326
|
+
expect(tableAsset).toBeDefined();
|
|
2327
|
+
|
|
2328
|
+
const typeRegistry: TypeRegistry = new Map([["Header", headerType]]);
|
|
2329
|
+
|
|
2330
|
+
const code = generateFunctionalBuilder(tableAsset, { typeRegistry });
|
|
2331
|
+
|
|
2332
|
+
// Should include Asset | FunctionalBuilder<Asset> for the inner asset type
|
|
2333
|
+
expect(code).toContain(
|
|
2334
|
+
"Asset | FunctionalBuilder<Asset, BaseBuildContext>",
|
|
2335
|
+
);
|
|
2336
|
+
// Should include Header type in the union
|
|
2337
|
+
expect(code).toContain("Header");
|
|
2338
|
+
// Should have __assetWrapperPaths__ that includes "headers"
|
|
2339
|
+
expect(code).toContain("__assetWrapperPaths__");
|
|
2340
|
+
expect(code).toContain('"headers"');
|
|
2341
|
+
});
|
|
2342
|
+
|
|
2343
|
+
test("generates correct type for single property extending AssetWrapper", () => {
|
|
2344
|
+
const source = `
|
|
2345
|
+
interface Asset<T extends string = string> {
|
|
2346
|
+
id: string;
|
|
2347
|
+
type: T;
|
|
2348
|
+
}
|
|
2349
|
+
type AssetWrapper<T extends Asset = Asset> = { asset: T };
|
|
2350
|
+
|
|
2351
|
+
export interface Header extends AssetWrapper<Asset> {
|
|
2352
|
+
title: string;
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
export interface CardAsset extends Asset<"card"> {
|
|
2356
|
+
header?: Header;
|
|
2357
|
+
}
|
|
2358
|
+
`;
|
|
2359
|
+
|
|
2360
|
+
const types = convertTsToXLR(source);
|
|
2361
|
+
const headerType = types.find(
|
|
2362
|
+
(t) => t.name === "Header",
|
|
2363
|
+
) as NamedType<ObjectType>;
|
|
2364
|
+
const cardAsset = types.find(
|
|
2365
|
+
(t) => t.name === "CardAsset",
|
|
2366
|
+
) as NamedType<ObjectType>;
|
|
2367
|
+
expect(headerType).toBeDefined();
|
|
2368
|
+
expect(cardAsset).toBeDefined();
|
|
2369
|
+
|
|
2370
|
+
const typeRegistry: TypeRegistry = new Map([["Header", headerType]]);
|
|
2371
|
+
|
|
2372
|
+
const code = generateFunctionalBuilder(cardAsset, { typeRegistry });
|
|
2373
|
+
|
|
2374
|
+
// Should accept Asset, FunctionalBuilder<Asset>, Header, FunctionalBuilder<Header>, or FunctionalPartial<Header>
|
|
2375
|
+
expect(code).toContain(
|
|
2376
|
+
"Asset | FunctionalBuilder<Asset, BaseBuildContext>",
|
|
2377
|
+
);
|
|
2378
|
+
expect(code).toContain(
|
|
2379
|
+
"Header | FunctionalBuilder<Header, BaseBuildContext>",
|
|
2380
|
+
);
|
|
2381
|
+
expect(code).toContain("FunctionalPartial<Header, BaseBuildContext>");
|
|
2382
|
+
// Should have __assetWrapperPaths__
|
|
2383
|
+
expect(code).toContain("__assetWrapperPaths__");
|
|
2384
|
+
expect(code).toContain('"header"');
|
|
2385
|
+
});
|
|
2386
|
+
|
|
2387
|
+
test("handles transitive AssetWrapper extension", () => {
|
|
2388
|
+
const source = `
|
|
2389
|
+
interface Asset<T extends string = string> {
|
|
2390
|
+
id: string;
|
|
2391
|
+
type: T;
|
|
2392
|
+
}
|
|
2393
|
+
type AssetWrapper<T extends Asset = Asset> = { asset: T };
|
|
2394
|
+
|
|
2395
|
+
export interface ListItemBase extends AssetWrapper<Asset> {}
|
|
2396
|
+
|
|
2397
|
+
export interface ListItem extends ListItemBase {
|
|
2398
|
+
help?: string;
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
export interface ListAsset extends Asset<"list"> {
|
|
2402
|
+
items: Array<ListItem>;
|
|
2403
|
+
}
|
|
2404
|
+
`;
|
|
2405
|
+
|
|
2406
|
+
const types = convertTsToXLR(source);
|
|
2407
|
+
const listItemBaseType = types.find(
|
|
2408
|
+
(t) => t.name === "ListItemBase",
|
|
2409
|
+
) as NamedType<ObjectType>;
|
|
2410
|
+
const listItemType = types.find(
|
|
2411
|
+
(t) => t.name === "ListItem",
|
|
2412
|
+
) as NamedType<ObjectType>;
|
|
2413
|
+
const listAsset = types.find(
|
|
2414
|
+
(t) => t.name === "ListAsset",
|
|
2415
|
+
) as NamedType<ObjectType>;
|
|
2416
|
+
expect(listItemBaseType).toBeDefined();
|
|
2417
|
+
expect(listItemType).toBeDefined();
|
|
2418
|
+
expect(listAsset).toBeDefined();
|
|
2419
|
+
|
|
2420
|
+
const typeRegistry: TypeRegistry = new Map([
|
|
2421
|
+
["ListItemBase", listItemBaseType],
|
|
2422
|
+
["ListItem", listItemType],
|
|
2423
|
+
]);
|
|
2424
|
+
|
|
2425
|
+
const code = generateFunctionalBuilder(listAsset, { typeRegistry });
|
|
2426
|
+
|
|
2427
|
+
// Should detect ListItem as extending AssetWrapper (transitively)
|
|
2428
|
+
expect(code).toContain("__assetWrapperPaths__");
|
|
2429
|
+
expect(code).toContain('"items"');
|
|
2430
|
+
// Should include Asset in the type signature
|
|
2431
|
+
expect(code).toContain(
|
|
2432
|
+
"Asset | FunctionalBuilder<Asset, BaseBuildContext>",
|
|
2433
|
+
);
|
|
2434
|
+
});
|
|
2435
|
+
|
|
2436
|
+
test("generates correct inner type from specific AssetWrapper generic argument", () => {
|
|
2437
|
+
const source = `
|
|
2438
|
+
interface Asset<T extends string = string> {
|
|
2439
|
+
id: string;
|
|
2440
|
+
type: T;
|
|
2441
|
+
}
|
|
2442
|
+
type AssetWrapper<T extends Asset = Asset> = { asset: T };
|
|
2443
|
+
|
|
2444
|
+
export interface ImageAsset extends Asset<"image"> {
|
|
2445
|
+
src: string;
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
export interface ImageSlot extends AssetWrapper<ImageAsset> {
|
|
2449
|
+
alt?: string;
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2452
|
+
export interface GalleryAsset extends Asset<"gallery"> {
|
|
2453
|
+
images: Array<ImageSlot>;
|
|
2454
|
+
}
|
|
2455
|
+
`;
|
|
2456
|
+
|
|
2457
|
+
const types = convertTsToXLR(source);
|
|
2458
|
+
const imageAssetType = types.find(
|
|
2459
|
+
(t) => t.name === "ImageAsset",
|
|
2460
|
+
) as NamedType<ObjectType>;
|
|
2461
|
+
const imageSlotType = types.find(
|
|
2462
|
+
(t) => t.name === "ImageSlot",
|
|
2463
|
+
) as NamedType<ObjectType>;
|
|
2464
|
+
const galleryAsset = types.find(
|
|
2465
|
+
(t) => t.name === "GalleryAsset",
|
|
2466
|
+
) as NamedType<ObjectType>;
|
|
2467
|
+
expect(imageSlotType).toBeDefined();
|
|
2468
|
+
expect(galleryAsset).toBeDefined();
|
|
2469
|
+
|
|
2470
|
+
const typeRegistry: TypeRegistry = new Map([
|
|
2471
|
+
["ImageSlot", imageSlotType],
|
|
2472
|
+
["ImageAsset", imageAssetType],
|
|
2473
|
+
]);
|
|
2474
|
+
|
|
2475
|
+
const code = generateFunctionalBuilder(galleryAsset, { typeRegistry });
|
|
2476
|
+
|
|
2477
|
+
// Should use ImageAsset as the inner type (not generic Asset)
|
|
2478
|
+
expect(code).toContain(
|
|
2479
|
+
"ImageAsset | FunctionalBuilder<ImageAsset, BaseBuildContext>",
|
|
2480
|
+
);
|
|
2481
|
+
// Should also include ImageSlot in the union
|
|
2482
|
+
expect(code).toContain("ImageSlot");
|
|
2483
|
+
// Should have __assetWrapperPaths__
|
|
2484
|
+
expect(code).toContain("__assetWrapperPaths__");
|
|
2485
|
+
expect(code).toContain('"images"');
|
|
2486
|
+
});
|
|
2487
|
+
});
|
|
2488
|
+
|
|
2489
|
+
describe("Runtime: Table Composition with Nested Builders", () => {
|
|
2490
|
+
// Inline builder classes matching generated patterns to validate
|
|
2491
|
+
// that the functional builder runtime correctly handles:
|
|
2492
|
+
// 1. AssetWrapper auto-wrapping (search, filters, actions)
|
|
2493
|
+
// 2. Types extending AssetWrapper (Header, TableColumn)
|
|
2494
|
+
// 3. Nested builders (StaticFilter inside Header inside Table)
|
|
2495
|
+
// 4. Array of extending types (Array<Header>, Array<TableColumn>)
|
|
2496
|
+
|
|
2497
|
+
class TextBuilder extends FunctionalBuilderBase<any> {
|
|
2498
|
+
static defaults = { type: "text", id: "" };
|
|
2499
|
+
withValue(v: any) {
|
|
2500
|
+
return this.set("value", v);
|
|
2501
|
+
}
|
|
2502
|
+
build(ctx?: any) {
|
|
2503
|
+
return this.buildWithDefaults(TextBuilder.defaults, ctx);
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
const text = (initial?: any) => new TextBuilder(initial);
|
|
2507
|
+
|
|
2508
|
+
class ActionBuilder extends FunctionalBuilderBase<any> {
|
|
2509
|
+
static defaults = { type: "action", id: "" };
|
|
2510
|
+
withLabel(v: any) {
|
|
2511
|
+
return this.set("label", v);
|
|
2512
|
+
}
|
|
2513
|
+
build(ctx?: any) {
|
|
2514
|
+
return this.buildWithDefaults(ActionBuilder.defaults, ctx);
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
const action = (initial?: any) => new ActionBuilder(initial);
|
|
2518
|
+
|
|
2519
|
+
class StaticFilterBuilder extends FunctionalBuilderBase<any> {
|
|
2520
|
+
static defaults = {};
|
|
2521
|
+
static __assetWrapperPaths__ = [["label"], ["value"]];
|
|
2522
|
+
withLabel(v: any) {
|
|
2523
|
+
return this.set("label", v);
|
|
2524
|
+
}
|
|
2525
|
+
withValue(v: any) {
|
|
2526
|
+
return this.set("value", v);
|
|
2527
|
+
}
|
|
2528
|
+
withComparator(v: any) {
|
|
2529
|
+
return this.set("comparator", v);
|
|
2530
|
+
}
|
|
2531
|
+
build(ctx?: any) {
|
|
2532
|
+
return this.buildWithDefaults(StaticFilterBuilder.defaults, ctx);
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
const staticFilter = (initial?: any) => new StaticFilterBuilder(initial);
|
|
2536
|
+
|
|
2537
|
+
class TableColumnBuilder extends FunctionalBuilderBase<any> {
|
|
2538
|
+
static defaults = { id: "" };
|
|
2539
|
+
withComparator(v: any) {
|
|
2540
|
+
return this.set("comparator", v);
|
|
2541
|
+
}
|
|
2542
|
+
build(ctx?: any) {
|
|
2543
|
+
return this.buildWithDefaults(TableColumnBuilder.defaults, ctx);
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
const tableColumn = (initial?: any) => new TableColumnBuilder(initial);
|
|
2547
|
+
|
|
2548
|
+
class HeaderBuilder extends FunctionalBuilderBase<any> {
|
|
2549
|
+
static defaults = { id: "", significance: "optional" };
|
|
2550
|
+
static __assetWrapperPaths__: string[][] = [
|
|
2551
|
+
["staticFilters", "label"],
|
|
2552
|
+
["staticFilters", "value"],
|
|
2553
|
+
];
|
|
2554
|
+
withStaticFilters(v: any) {
|
|
2555
|
+
return this.set("staticFilters", v);
|
|
2556
|
+
}
|
|
2557
|
+
withSortable(v: any) {
|
|
2558
|
+
return this.set("sortable", v);
|
|
2559
|
+
}
|
|
2560
|
+
withSignificance(v: any) {
|
|
2561
|
+
return this.set("significance", v);
|
|
2562
|
+
}
|
|
2563
|
+
build(ctx?: any) {
|
|
2564
|
+
return this.buildWithDefaults(HeaderBuilder.defaults, ctx);
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
const header = (initial?: any) => new HeaderBuilder(initial);
|
|
2568
|
+
|
|
2569
|
+
class RowBuilder extends FunctionalBuilderBase<any> {
|
|
2570
|
+
static defaults = {};
|
|
2571
|
+
static __assetWrapperPaths__ = [["values"], ["actions"]];
|
|
2572
|
+
withContext(v: any) {
|
|
2573
|
+
return this.set("context", v);
|
|
2574
|
+
}
|
|
2575
|
+
withValues(v: any) {
|
|
2576
|
+
return this.set("values", v);
|
|
2577
|
+
}
|
|
2578
|
+
withActions(v: any) {
|
|
2579
|
+
return this.set("actions", v);
|
|
2580
|
+
}
|
|
2581
|
+
build(ctx?: any) {
|
|
2582
|
+
return this.buildWithDefaults(RowBuilder.defaults, ctx);
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
const row = (initial?: any) => new RowBuilder(initial);
|
|
2586
|
+
|
|
2587
|
+
class TableAssetBuilder extends FunctionalBuilderBase<any> {
|
|
2588
|
+
static defaults = { type: "table", id: "" };
|
|
2589
|
+
static __assetWrapperPaths__ = [
|
|
2590
|
+
["search"],
|
|
2591
|
+
["filters"],
|
|
2592
|
+
["headers", "values"],
|
|
2593
|
+
["headers", "values", "staticFilters", "label"],
|
|
2594
|
+
["headers", "values", "staticFilters", "value"],
|
|
2595
|
+
["actions"],
|
|
2596
|
+
["values", "values"],
|
|
2597
|
+
["values", "actions"],
|
|
2598
|
+
];
|
|
2599
|
+
withSearch(v: any) {
|
|
2600
|
+
return this.set("search", v);
|
|
2601
|
+
}
|
|
2602
|
+
withFilters(v: any) {
|
|
2603
|
+
return this.set("filters", v);
|
|
2604
|
+
}
|
|
2605
|
+
withHeaders(v: any) {
|
|
2606
|
+
return this.set("headers", v);
|
|
2607
|
+
}
|
|
2608
|
+
withValues(v: any) {
|
|
2609
|
+
return this.set("values", v);
|
|
2610
|
+
}
|
|
2611
|
+
withActions(v: any) {
|
|
2612
|
+
return this.set("actions", v);
|
|
2613
|
+
}
|
|
2614
|
+
withPlaceholder(v: any) {
|
|
2615
|
+
return this.set("placeholder", v);
|
|
2616
|
+
}
|
|
2617
|
+
withAutomationId(v: any) {
|
|
2618
|
+
return this.set("automationId", v);
|
|
2619
|
+
}
|
|
2620
|
+
build(ctx?: any) {
|
|
2621
|
+
return this.buildWithDefaults(TableAssetBuilder.defaults, ctx);
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
const tableAsset = (initial?: any) => new TableAssetBuilder(initial);
|
|
2625
|
+
|
|
2626
|
+
test("composes table with nested builders and auto-wraps AssetWrapper properties", () => {
|
|
2627
|
+
const result = tableAsset()
|
|
2628
|
+
.withSearch(text().withValue("Search..."))
|
|
2629
|
+
.withFilters([text().withValue("Filter 1")])
|
|
2630
|
+
.withHeaders({
|
|
2631
|
+
values: [
|
|
2632
|
+
header()
|
|
2633
|
+
.withStaticFilters([
|
|
2634
|
+
staticFilter()
|
|
2635
|
+
.withLabel(text().withValue("Active"))
|
|
2636
|
+
.withValue(text().withValue("Yes"))
|
|
2637
|
+
.withComparator("active"),
|
|
2638
|
+
])
|
|
2639
|
+
.withSortable(true),
|
|
2640
|
+
header().withSignificance("optional"),
|
|
2641
|
+
],
|
|
2642
|
+
})
|
|
2643
|
+
.withValues([
|
|
2644
|
+
row()
|
|
2645
|
+
.withContext({ state: "complete" })
|
|
2646
|
+
.withValues([
|
|
2647
|
+
tableColumn().withComparator("name"),
|
|
2648
|
+
tableColumn().withComparator("status"),
|
|
2649
|
+
])
|
|
2650
|
+
.withActions([action().withLabel("Edit")]),
|
|
2651
|
+
])
|
|
2652
|
+
.withActions([action().withLabel("Add Row")])
|
|
2653
|
+
.withPlaceholder("No data available")
|
|
2654
|
+
.withAutomationId("main-table")
|
|
2655
|
+
.build();
|
|
2656
|
+
|
|
2657
|
+
// Top-level structure
|
|
2658
|
+
expect(result.type).toBe("table");
|
|
2659
|
+
expect(result.placeholder).toBe("No data available");
|
|
2660
|
+
expect(result.automationId).toBe("main-table");
|
|
2661
|
+
|
|
2662
|
+
// Direct AssetWrapper: search is auto-wrapped
|
|
2663
|
+
expect(result.search.asset).toBeDefined();
|
|
2664
|
+
expect(result.search.asset.type).toBe("text");
|
|
2665
|
+
expect(result.search.asset.value).toBe("Search...");
|
|
2666
|
+
|
|
2667
|
+
// Array<AssetWrapper>: filters are auto-wrapped
|
|
2668
|
+
expect(result.filters).toHaveLength(1);
|
|
2669
|
+
expect(result.filters[0].asset.type).toBe("text");
|
|
2670
|
+
expect(result.filters[0].asset.value).toBe("Filter 1");
|
|
2671
|
+
|
|
2672
|
+
// Table-level actions are auto-wrapped
|
|
2673
|
+
expect(result.actions).toHaveLength(1);
|
|
2674
|
+
expect(result.actions[0].asset.type).toBe("action");
|
|
2675
|
+
expect(result.actions[0].asset.label).toBe("Add Row");
|
|
2676
|
+
|
|
2677
|
+
// Headers with nested Header builders
|
|
2678
|
+
expect(result.headers.values).toHaveLength(2);
|
|
2679
|
+
const header1 = result.headers.values[0];
|
|
2680
|
+
expect(header1.sortable).toBe(true);
|
|
2681
|
+
expect(header1.staticFilters).toHaveLength(1);
|
|
2682
|
+
|
|
2683
|
+
// StaticFilter.label/value auto-wrapped within the nested builder
|
|
2684
|
+
const sf = header1.staticFilters[0];
|
|
2685
|
+
expect(sf.comparator).toBe("active");
|
|
2686
|
+
expect(sf.label.asset).toBeDefined();
|
|
2687
|
+
expect(sf.label.asset.value).toBe("Active");
|
|
2688
|
+
expect(sf.value.asset).toBeDefined();
|
|
2689
|
+
expect(sf.value.asset.value).toBe("Yes");
|
|
2690
|
+
|
|
2691
|
+
// Rows with nested TableColumn builders
|
|
2692
|
+
expect(result.values).toHaveLength(1);
|
|
2693
|
+
const row1 = result.values[0];
|
|
2694
|
+
expect(row1.context).toEqual({ state: "complete" });
|
|
2695
|
+
expect(row1.values).toHaveLength(2);
|
|
2696
|
+
expect(row1.values[0].comparator).toBe("name");
|
|
2697
|
+
expect(row1.values[1].comparator).toBe("status");
|
|
2698
|
+
|
|
2699
|
+
// Row-level actions are auto-wrapped
|
|
2700
|
+
expect(row1.actions).toHaveLength(1);
|
|
2701
|
+
expect(row1.actions[0].asset.type).toBe("action");
|
|
2702
|
+
expect(row1.actions[0].asset.label).toBe("Edit");
|
|
2703
|
+
});
|
|
2704
|
+
|
|
2705
|
+
test("generated __assetWrapperPaths__ match for Table with extends-AssetWrapper types", () => {
|
|
2706
|
+
const source = `
|
|
2707
|
+
interface Asset<T extends string = string> {
|
|
2708
|
+
id: string;
|
|
2709
|
+
type: T;
|
|
2710
|
+
}
|
|
2711
|
+
type AssetWrapper<T extends Asset = Asset> = { asset: T };
|
|
2712
|
+
type Binding = string;
|
|
2713
|
+
|
|
2714
|
+
export interface StaticFilter<AnyAsset extends Asset = Asset> {
|
|
2715
|
+
label: AssetWrapper<AnyAsset>;
|
|
2716
|
+
value: AssetWrapper<AnyAsset>;
|
|
2717
|
+
comparator: string;
|
|
2718
|
+
}
|
|
2719
|
+
|
|
2720
|
+
export interface Header<AnyAsset extends Asset = Asset>
|
|
2721
|
+
extends AssetWrapper<AnyAsset> {
|
|
2722
|
+
staticFilters?: Array<StaticFilter<AnyAsset>>;
|
|
2723
|
+
sortable?: boolean;
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
export interface TableColumn<AnyAsset extends Asset = Asset>
|
|
2727
|
+
extends AssetWrapper<AnyAsset> {
|
|
2728
|
+
comparator?: string;
|
|
2729
|
+
}
|
|
2730
|
+
|
|
2731
|
+
export interface Row<AnyAsset extends Asset = Asset> {
|
|
2732
|
+
values: Array<TableColumn<AnyAsset>>;
|
|
2733
|
+
actions?: Array<AssetWrapper<AnyAsset>>;
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2736
|
+
export interface TableAsset<AnyAsset extends Asset = Asset>
|
|
2737
|
+
extends Asset<'table'> {
|
|
2738
|
+
search?: AssetWrapper<AnyAsset>;
|
|
2739
|
+
filters?: Array<AssetWrapper<AnyAsset>>;
|
|
2740
|
+
headers?: { values?: Array<Header<AnyAsset>> };
|
|
2741
|
+
values?: Array<Row<AnyAsset>>;
|
|
2742
|
+
actions?: Array<AssetWrapper<AnyAsset>>;
|
|
2743
|
+
}
|
|
2744
|
+
`;
|
|
2745
|
+
|
|
2746
|
+
const types = convertTsToXLR(source);
|
|
2747
|
+
const findType = (name: string) =>
|
|
2748
|
+
types.find((t) => t.name === name) as NamedType<ObjectType>;
|
|
2749
|
+
|
|
2750
|
+
const headerType = findType("Header");
|
|
2751
|
+
const tableColumnType = findType("TableColumn");
|
|
2752
|
+
const rowType = findType("Row");
|
|
2753
|
+
const staticFilterType = findType("StaticFilter");
|
|
2754
|
+
const tableAssetType = findType("TableAsset");
|
|
2755
|
+
|
|
2756
|
+
const typeRegistry: TypeRegistry = new Map(
|
|
2757
|
+
[headerType, tableColumnType, rowType, staticFilterType]
|
|
2758
|
+
.filter(Boolean)
|
|
2759
|
+
.map((t) => [t.name, t]),
|
|
2760
|
+
);
|
|
2761
|
+
const config: GeneratorConfig = { typeRegistry };
|
|
2762
|
+
|
|
2763
|
+
const tableCode = generateFunctionalBuilder(tableAssetType, config);
|
|
2764
|
+
|
|
2765
|
+
// Direct AssetWrapper paths
|
|
2766
|
+
expect(tableCode).toContain('"search"');
|
|
2767
|
+
expect(tableCode).toContain('"filters"');
|
|
2768
|
+
expect(tableCode).toContain('"actions"');
|
|
2769
|
+
|
|
2770
|
+
// Nested paths through extends-AssetWrapper types
|
|
2771
|
+
// headers.values -> Header extends AssetWrapper
|
|
2772
|
+
expect(tableCode).toMatch(/"headers".*"values"/);
|
|
2773
|
+
// headers.values -> staticFilters.label/value (via array recursion)
|
|
2774
|
+
expect(tableCode).toMatch(/"headers".*"values".*"staticFilters".*"label"/);
|
|
2775
|
+
expect(tableCode).toMatch(/"headers".*"values".*"staticFilters".*"value"/);
|
|
2776
|
+
|
|
2777
|
+
// values.values -> Row.values = Array<TableColumn extends AssetWrapper>
|
|
2778
|
+
expect(tableCode).toMatch(/"values".*"values"/);
|
|
2779
|
+
// values.actions -> Row.actions = Array<AssetWrapper>
|
|
2780
|
+
expect(tableCode).toMatch(/"values".*"actions"/);
|
|
2781
|
+
|
|
2782
|
+
// Type signatures: Header accepted with Asset union
|
|
2783
|
+
expect(tableCode).toContain(
|
|
2784
|
+
"Asset | FunctionalBuilder<Asset, BaseBuildContext>",
|
|
2785
|
+
);
|
|
2786
|
+
expect(tableCode).toContain("Header");
|
|
2787
|
+
});
|
|
2788
|
+
});
|
|
2789
|
+
|
|
2790
|
+
describe("Issue #9: Generic Parameter Leak from Base Type", () => {
|
|
2791
|
+
test("non-generic type extending generic base does not import generic params", () => {
|
|
2792
|
+
const source = `
|
|
2793
|
+
interface Asset<T extends string = string> {
|
|
2794
|
+
id: string;
|
|
2795
|
+
type: T;
|
|
2796
|
+
}
|
|
2797
|
+
type AssetWrapper<T extends Asset = Asset> = { asset: T };
|
|
2798
|
+
|
|
2799
|
+
export interface FileInputAssetBase<AnyAsset extends Asset = Asset> extends Asset<'fileInput'> {
|
|
2800
|
+
label?: AssetWrapper<AnyAsset>;
|
|
2801
|
+
}
|
|
2802
|
+
|
|
2803
|
+
export interface FileInputAsset extends FileInputAssetBase {
|
|
2804
|
+
uploadTrigger: string;
|
|
2805
|
+
}
|
|
2806
|
+
`;
|
|
2807
|
+
|
|
2808
|
+
const types = convertTsToXLR(source);
|
|
2809
|
+
const base = types.find(
|
|
2810
|
+
(t) => t.name === "FileInputAssetBase",
|
|
2811
|
+
) as NamedType<ObjectType>;
|
|
2812
|
+
const asset = types.find(
|
|
2813
|
+
(t) => t.name === "FileInputAsset",
|
|
2814
|
+
) as NamedType<ObjectType>;
|
|
2815
|
+
|
|
2816
|
+
const typeRegistry: TypeRegistry = new Map([["FileInputAssetBase", base]]);
|
|
2817
|
+
const code = generateFunctionalBuilder(asset, { typeRegistry });
|
|
2818
|
+
|
|
2819
|
+
// AnyAsset is a generic param of the base type, not a concrete type to import
|
|
2820
|
+
expect(code).not.toContain("AnyAsset");
|
|
2821
|
+
// The AssetWrapper<AnyAsset> property should resolve to Asset (the default)
|
|
2822
|
+
expect(code).toContain(
|
|
2823
|
+
"Asset | FunctionalBuilder<Asset, BaseBuildContext>",
|
|
2824
|
+
);
|
|
2825
|
+
});
|
|
2826
|
+
|
|
2827
|
+
test("non-generic type extending generic base with multiple params", () => {
|
|
2828
|
+
const source = `
|
|
2829
|
+
interface Asset<T extends string = string> {
|
|
2830
|
+
id: string;
|
|
2831
|
+
type: T;
|
|
2832
|
+
}
|
|
2833
|
+
type AssetWrapper<T extends Asset = Asset> = { asset: T };
|
|
2834
|
+
|
|
2835
|
+
export interface MultiGenericBase<AnyLabelAsset extends Asset = Asset, AnyIconAsset extends Asset = Asset> extends Asset<'multi'> {
|
|
2836
|
+
label?: AssetWrapper<AnyLabelAsset>;
|
|
2837
|
+
icon?: AssetWrapper<AnyIconAsset>;
|
|
2838
|
+
}
|
|
2839
|
+
|
|
2840
|
+
export interface ConcreteMulti extends MultiGenericBase {
|
|
2841
|
+
extra: string;
|
|
2842
|
+
}
|
|
2843
|
+
`;
|
|
2844
|
+
|
|
2845
|
+
const types = convertTsToXLR(source);
|
|
2846
|
+
const base = types.find(
|
|
2847
|
+
(t) => t.name === "MultiGenericBase",
|
|
2848
|
+
) as NamedType<ObjectType>;
|
|
2849
|
+
const asset = types.find(
|
|
2850
|
+
(t) => t.name === "ConcreteMulti",
|
|
2851
|
+
) as NamedType<ObjectType>;
|
|
2852
|
+
|
|
2853
|
+
const typeRegistry: TypeRegistry = new Map([["MultiGenericBase", base]]);
|
|
2854
|
+
const code = generateFunctionalBuilder(asset, { typeRegistry });
|
|
2855
|
+
|
|
2856
|
+
// Neither generic param should be imported as a concrete type
|
|
2857
|
+
expect(code).not.toContain("AnyLabelAsset");
|
|
2858
|
+
expect(code).not.toContain("AnyIconAsset");
|
|
2859
|
+
});
|
|
2860
|
+
});
|