@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,685 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, vi } from "vitest";
|
|
2
|
+
import { join, normalize } from "path";
|
|
3
|
+
import type { NamedType, ObjectType } from "@xlr-lib/xlr";
|
|
4
|
+
|
|
5
|
+
// Mock the modules before importing the cli functions
|
|
6
|
+
vi.mock("fs", async () => {
|
|
7
|
+
const actual = await vi.importActual<typeof import("fs")>("fs");
|
|
8
|
+
return {
|
|
9
|
+
...actual,
|
|
10
|
+
readFileSync: vi.fn(),
|
|
11
|
+
writeFileSync: vi.fn(),
|
|
12
|
+
mkdirSync: vi.fn(),
|
|
13
|
+
existsSync: vi.fn(),
|
|
14
|
+
accessSync: vi.fn(),
|
|
15
|
+
constants: actual.constants,
|
|
16
|
+
};
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
vi.mock("../generator", () => ({
|
|
20
|
+
generateFunctionalBuilder: vi.fn(() => "// generated code"),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
vi.mock("../ts-morph-type-finder", () => ({
|
|
24
|
+
TsMorphTypeDefinitionFinder: vi.fn().mockImplementation(() => ({
|
|
25
|
+
findTypeSourceFile: vi.fn(),
|
|
26
|
+
dispose: vi.fn(),
|
|
27
|
+
})),
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
// Import mocked functions after mocking
|
|
31
|
+
import { readFileSync, mkdirSync, existsSync, accessSync, constants } from "fs";
|
|
32
|
+
import { generateFunctionalBuilder } from "../generator";
|
|
33
|
+
import { TsMorphTypeDefinitionFinder } from "../ts-morph-type-finder";
|
|
34
|
+
import { isNodeModulesPath, extractPackageNameFromPath } from "../path-utils";
|
|
35
|
+
|
|
36
|
+
// Helper functions that mirror the CLI implementation for testing
|
|
37
|
+
function toKebabCase(name: string): string {
|
|
38
|
+
return name
|
|
39
|
+
.replace(/([A-Z])/g, "-$1")
|
|
40
|
+
.toLowerCase()
|
|
41
|
+
.replace(/^-/, "");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface Manifest {
|
|
45
|
+
version: number;
|
|
46
|
+
capabilities: {
|
|
47
|
+
Assets?: string[];
|
|
48
|
+
Views?: string[];
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe("CLI - Argument Parsing", () => {
|
|
53
|
+
// Testing argument parsing logic
|
|
54
|
+
|
|
55
|
+
test("parses -i flag for input", () => {
|
|
56
|
+
const args = ["-i", "/path/to/input"];
|
|
57
|
+
let input = "";
|
|
58
|
+
let output = "./dist";
|
|
59
|
+
|
|
60
|
+
for (let i = 0; i < args.length; i++) {
|
|
61
|
+
const arg = args[i];
|
|
62
|
+
if (arg === "-i" || arg === "--input") {
|
|
63
|
+
input = args[++i] || "";
|
|
64
|
+
} else if (arg === "-o" || arg === "--output") {
|
|
65
|
+
output = args[++i] || "./dist";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
expect(input).toBe("/path/to/input");
|
|
70
|
+
expect(output).toBe("./dist");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("parses --input flag for input", () => {
|
|
74
|
+
const args = ["--input", "/path/to/input"];
|
|
75
|
+
let input = "";
|
|
76
|
+
let output = "./dist";
|
|
77
|
+
|
|
78
|
+
for (let i = 0; i < args.length; i++) {
|
|
79
|
+
const arg = args[i];
|
|
80
|
+
if (arg === "-i" || arg === "--input") {
|
|
81
|
+
input = args[++i] || "";
|
|
82
|
+
} else if (arg === "-o" || arg === "--output") {
|
|
83
|
+
output = args[++i] || "./dist";
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
expect(input).toBe("/path/to/input");
|
|
88
|
+
expect(output).toBe("./dist");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("parses -o flag for output", () => {
|
|
92
|
+
const args = ["-i", "/input", "-o", "/custom/output"];
|
|
93
|
+
let input = "";
|
|
94
|
+
let output = "./dist";
|
|
95
|
+
|
|
96
|
+
for (let i = 0; i < args.length; i++) {
|
|
97
|
+
const arg = args[i];
|
|
98
|
+
if (arg === "-i" || arg === "--input") {
|
|
99
|
+
input = args[++i] || "";
|
|
100
|
+
} else if (arg === "-o" || arg === "--output") {
|
|
101
|
+
output = args[++i] || "./dist";
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
expect(input).toBe("/input");
|
|
106
|
+
expect(output).toBe("/custom/output");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("parses --output flag for output", () => {
|
|
110
|
+
const args = ["-i", "/input", "--output", "/custom/output"];
|
|
111
|
+
let output = "./dist";
|
|
112
|
+
|
|
113
|
+
for (let i = 0; i < args.length; i++) {
|
|
114
|
+
const arg = args[i];
|
|
115
|
+
if (arg === "-i" || arg === "--input") {
|
|
116
|
+
i++; // skip input value
|
|
117
|
+
} else if (arg === "-o" || arg === "--output") {
|
|
118
|
+
output = args[++i] || "./dist";
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
expect(output).toBe("/custom/output");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("uses default output ./dist when not specified", () => {
|
|
126
|
+
const args = ["-i", "/input"];
|
|
127
|
+
let output = "./dist";
|
|
128
|
+
|
|
129
|
+
for (let i = 0; i < args.length; i++) {
|
|
130
|
+
const arg = args[i];
|
|
131
|
+
if (arg === "-i" || arg === "--input") {
|
|
132
|
+
i++; // skip input value
|
|
133
|
+
} else if (arg === "-o" || arg === "--output") {
|
|
134
|
+
output = args[++i] || "./dist";
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
expect(output).toBe("./dist");
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe("CLI - Manifest Loading", () => {
|
|
143
|
+
beforeEach(() => {
|
|
144
|
+
vi.clearAllMocks();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("loads valid manifest.json", () => {
|
|
148
|
+
const manifest: Manifest = {
|
|
149
|
+
version: 1,
|
|
150
|
+
capabilities: {
|
|
151
|
+
Assets: ["TextAsset", "ActionAsset"],
|
|
152
|
+
Views: ["InfoView"],
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
157
|
+
vi.mocked(readFileSync).mockReturnValue(JSON.stringify(manifest));
|
|
158
|
+
|
|
159
|
+
// Simulate loadManifest function
|
|
160
|
+
function loadManifest(inputDir: string): Manifest {
|
|
161
|
+
const path = join(inputDir, "manifest.json");
|
|
162
|
+
if (!existsSync(path)) {
|
|
163
|
+
throw new Error(`Manifest not found: ${path}`);
|
|
164
|
+
}
|
|
165
|
+
const content = readFileSync(path, "utf-8") as string;
|
|
166
|
+
return JSON.parse(content) as Manifest;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const result = loadManifest("/test/input");
|
|
170
|
+
|
|
171
|
+
expect(result.version).toBe(1);
|
|
172
|
+
expect(result.capabilities.Assets).toEqual(["TextAsset", "ActionAsset"]);
|
|
173
|
+
expect(result.capabilities.Views).toEqual(["InfoView"]);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("throws when manifest.json not found", () => {
|
|
177
|
+
vi.mocked(existsSync).mockReturnValue(false);
|
|
178
|
+
|
|
179
|
+
function loadManifest(inputDir: string): Manifest {
|
|
180
|
+
const manifestPath = join(inputDir, "manifest.json");
|
|
181
|
+
if (!existsSync(manifestPath)) {
|
|
182
|
+
throw new Error(`Manifest not found: ${manifestPath}`);
|
|
183
|
+
}
|
|
184
|
+
const content = readFileSync(manifestPath, "utf-8") as string;
|
|
185
|
+
return JSON.parse(content) as Manifest;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
expect(() => loadManifest("/test/input")).toThrow(/Manifest not found/);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("throws when manifest.json is invalid JSON", () => {
|
|
192
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
193
|
+
vi.mocked(readFileSync).mockReturnValue("{ invalid json }");
|
|
194
|
+
|
|
195
|
+
function loadManifest(inputDir: string): Manifest {
|
|
196
|
+
const manifestPath = join(inputDir, "manifest.json");
|
|
197
|
+
if (!existsSync(manifestPath)) {
|
|
198
|
+
throw new Error(`Manifest not found: ${manifestPath}`);
|
|
199
|
+
}
|
|
200
|
+
const content = readFileSync(manifestPath, "utf-8") as string;
|
|
201
|
+
return JSON.parse(content) as Manifest;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
expect(() => loadManifest("/test/input")).toThrow();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("handles manifest with empty capabilities", () => {
|
|
208
|
+
const manifest: Manifest = {
|
|
209
|
+
version: 1,
|
|
210
|
+
capabilities: {},
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
214
|
+
vi.mocked(readFileSync).mockReturnValue(JSON.stringify(manifest));
|
|
215
|
+
|
|
216
|
+
function loadManifest(inputDir: string): Manifest {
|
|
217
|
+
const manifestPath = join(inputDir, "manifest.json");
|
|
218
|
+
if (!existsSync(manifestPath)) {
|
|
219
|
+
throw new Error(`Manifest not found: ${manifestPath}`);
|
|
220
|
+
}
|
|
221
|
+
const content = readFileSync(manifestPath, "utf-8") as string;
|
|
222
|
+
return JSON.parse(content) as Manifest;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const result = loadManifest("/test/input");
|
|
226
|
+
const { Assets = [], Views = [] } = result.capabilities;
|
|
227
|
+
|
|
228
|
+
expect(Assets).toEqual([]);
|
|
229
|
+
expect(Views).toEqual([]);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("handles manifest with partial capabilities", () => {
|
|
233
|
+
const manifest: Manifest = {
|
|
234
|
+
version: 1,
|
|
235
|
+
capabilities: {
|
|
236
|
+
Assets: ["TextAsset"],
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
241
|
+
vi.mocked(readFileSync).mockReturnValue(JSON.stringify(manifest));
|
|
242
|
+
|
|
243
|
+
function loadManifest(inputDir: string): Manifest {
|
|
244
|
+
const manifestPath = join(inputDir, "manifest.json");
|
|
245
|
+
if (!existsSync(manifestPath)) {
|
|
246
|
+
throw new Error(`Manifest not found: ${manifestPath}`);
|
|
247
|
+
}
|
|
248
|
+
const content = readFileSync(manifestPath, "utf-8") as string;
|
|
249
|
+
return JSON.parse(content) as Manifest;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const result = loadManifest("/test/input");
|
|
253
|
+
const { Assets = [], Views = [] } = result.capabilities;
|
|
254
|
+
|
|
255
|
+
expect(Assets).toEqual(["TextAsset"]);
|
|
256
|
+
expect(Views).toEqual([]);
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
describe("CLI - XLR Type Loading", () => {
|
|
261
|
+
beforeEach(() => {
|
|
262
|
+
vi.clearAllMocks();
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("loads valid XLR type JSON", () => {
|
|
266
|
+
const xlrType: NamedType<ObjectType> = {
|
|
267
|
+
name: "TextAsset",
|
|
268
|
+
type: "object",
|
|
269
|
+
properties: {
|
|
270
|
+
id: { required: true, node: { type: "string" } },
|
|
271
|
+
type: { required: true, node: { type: "string", const: "text" } },
|
|
272
|
+
value: { required: true, node: { type: "string" } },
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
277
|
+
vi.mocked(readFileSync).mockReturnValue(JSON.stringify(xlrType));
|
|
278
|
+
|
|
279
|
+
function loadXlrType(
|
|
280
|
+
inputDir: string,
|
|
281
|
+
typeName: string,
|
|
282
|
+
): NamedType<ObjectType> {
|
|
283
|
+
const typePath = join(inputDir, `${typeName}.json`);
|
|
284
|
+
if (!existsSync(typePath)) {
|
|
285
|
+
throw new Error(`XLR type file not found: ${typePath}`);
|
|
286
|
+
}
|
|
287
|
+
const content = readFileSync(typePath, "utf-8") as string;
|
|
288
|
+
return JSON.parse(content) as NamedType<ObjectType>;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const result = loadXlrType("/test/input", "TextAsset");
|
|
292
|
+
|
|
293
|
+
expect(result.name).toBe("TextAsset");
|
|
294
|
+
expect(result.type).toBe("object");
|
|
295
|
+
expect(result.properties?.value).toBeDefined();
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test("throws when type file not found", () => {
|
|
299
|
+
vi.mocked(existsSync).mockReturnValue(false);
|
|
300
|
+
|
|
301
|
+
function loadXlrType(
|
|
302
|
+
inputDir: string,
|
|
303
|
+
typeName: string,
|
|
304
|
+
): NamedType<ObjectType> {
|
|
305
|
+
const typePath = join(inputDir, `${typeName}.json`);
|
|
306
|
+
if (!existsSync(typePath)) {
|
|
307
|
+
throw new Error(`XLR type file not found: ${typePath}`);
|
|
308
|
+
}
|
|
309
|
+
const content = readFileSync(typePath, "utf-8") as string;
|
|
310
|
+
return JSON.parse(content) as NamedType<ObjectType>;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
expect(() => loadXlrType("/test/input", "MissingType")).toThrow(
|
|
314
|
+
/XLR type file not found/,
|
|
315
|
+
);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test("throws when type file is invalid JSON", () => {
|
|
319
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
320
|
+
vi.mocked(readFileSync).mockReturnValue("not valid json");
|
|
321
|
+
|
|
322
|
+
function loadXlrType(
|
|
323
|
+
inputDir: string,
|
|
324
|
+
typeName: string,
|
|
325
|
+
): NamedType<ObjectType> {
|
|
326
|
+
const typePath = join(inputDir, `${typeName}.json`);
|
|
327
|
+
if (!existsSync(typePath)) {
|
|
328
|
+
throw new Error(`XLR type file not found: ${typePath}`);
|
|
329
|
+
}
|
|
330
|
+
const content = readFileSync(typePath, "utf-8") as string;
|
|
331
|
+
return JSON.parse(content) as NamedType<ObjectType>;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
expect(() => loadXlrType("/test/input", "InvalidType")).toThrow();
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
describe("CLI - Output Directory", () => {
|
|
339
|
+
beforeEach(() => {
|
|
340
|
+
vi.clearAllMocks();
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test("creates directory if does not exist", () => {
|
|
344
|
+
vi.mocked(existsSync).mockReturnValue(false);
|
|
345
|
+
vi.mocked(mkdirSync).mockReturnValue(undefined);
|
|
346
|
+
vi.mocked(accessSync).mockReturnValue(undefined);
|
|
347
|
+
|
|
348
|
+
function validateOutputDirectory(outputDir: string): void {
|
|
349
|
+
if (!existsSync(outputDir)) {
|
|
350
|
+
try {
|
|
351
|
+
mkdirSync(outputDir, { recursive: true });
|
|
352
|
+
} catch (error) {
|
|
353
|
+
throw new Error(
|
|
354
|
+
`Cannot create output directory "${outputDir}": ${error instanceof Error ? error.message : String(error)}`,
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
try {
|
|
360
|
+
accessSync(outputDir, constants.W_OK);
|
|
361
|
+
} catch {
|
|
362
|
+
throw new Error(
|
|
363
|
+
`Output directory "${outputDir}" is not writable. Check permissions.`,
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
expect(() => validateOutputDirectory("/test/output")).not.toThrow();
|
|
369
|
+
expect(mkdirSync).toHaveBeenCalledWith("/test/output", { recursive: true });
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
test("succeeds when directory exists and is writable", () => {
|
|
373
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
374
|
+
vi.mocked(accessSync).mockReturnValue(undefined);
|
|
375
|
+
|
|
376
|
+
function validateOutputDirectory(outputDir: string): void {
|
|
377
|
+
if (!existsSync(outputDir)) {
|
|
378
|
+
try {
|
|
379
|
+
mkdirSync(outputDir, { recursive: true });
|
|
380
|
+
} catch (error) {
|
|
381
|
+
throw new Error(
|
|
382
|
+
`Cannot create output directory "${outputDir}": ${error instanceof Error ? error.message : String(error)}`,
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
try {
|
|
388
|
+
accessSync(outputDir, constants.W_OK);
|
|
389
|
+
} catch {
|
|
390
|
+
throw new Error(
|
|
391
|
+
`Output directory "${outputDir}" is not writable. Check permissions.`,
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
expect(() => validateOutputDirectory("/test/output")).not.toThrow();
|
|
397
|
+
expect(mkdirSync).not.toHaveBeenCalled();
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
test("throws when cannot create directory (permission denied)", () => {
|
|
401
|
+
vi.mocked(existsSync).mockReturnValue(false);
|
|
402
|
+
vi.mocked(mkdirSync).mockImplementation(() => {
|
|
403
|
+
throw new Error("EACCES: permission denied");
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
function validateOutputDirectory(outputDir: string): void {
|
|
407
|
+
if (!existsSync(outputDir)) {
|
|
408
|
+
try {
|
|
409
|
+
mkdirSync(outputDir, { recursive: true });
|
|
410
|
+
} catch (error) {
|
|
411
|
+
throw new Error(
|
|
412
|
+
`Cannot create output directory "${outputDir}": ${error instanceof Error ? error.message : String(error)}`,
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
try {
|
|
418
|
+
accessSync(outputDir, constants.W_OK);
|
|
419
|
+
} catch {
|
|
420
|
+
throw new Error(
|
|
421
|
+
`Output directory "${outputDir}" is not writable. Check permissions.`,
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
expect(() => validateOutputDirectory("/test/output")).toThrow(
|
|
427
|
+
/Cannot create output directory/,
|
|
428
|
+
);
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
describe("CLI - File Writing", () => {
|
|
433
|
+
test("converts PascalCase to kebab-case filename", () => {
|
|
434
|
+
expect(toKebabCase("TextAsset")).toBe("text-asset");
|
|
435
|
+
expect(toKebabCase("ActionAsset")).toBe("action-asset");
|
|
436
|
+
expect(toKebabCase("CollectionAsset")).toBe("collection-asset");
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
test("handles consecutive capitals", () => {
|
|
440
|
+
expect(toKebabCase("XMLParser")).toBe("x-m-l-parser");
|
|
441
|
+
expect(toKebabCase("HTTPServer")).toBe("h-t-t-p-server");
|
|
442
|
+
expect(toKebabCase("JSONData")).toBe("j-s-o-n-data");
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
test("handles single word", () => {
|
|
446
|
+
expect(toKebabCase("Asset")).toBe("asset");
|
|
447
|
+
expect(toKebabCase("Text")).toBe("text");
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
test("handles already lowercase", () => {
|
|
451
|
+
expect(toKebabCase("asset")).toBe("asset");
|
|
452
|
+
expect(toKebabCase("text")).toBe("text");
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
describe("CLI - Generator Config", () => {
|
|
457
|
+
test("returns minimal config when no source file", () => {
|
|
458
|
+
const xlrType: NamedType<ObjectType> = {
|
|
459
|
+
name: "TextAsset",
|
|
460
|
+
type: "object",
|
|
461
|
+
properties: {},
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
// When source is undefined or doesn't exist
|
|
465
|
+
vi.mocked(existsSync).mockReturnValue(false);
|
|
466
|
+
|
|
467
|
+
interface GeneratorConfig {
|
|
468
|
+
typeImportPathGenerator?: (refTypeName: string) => string;
|
|
469
|
+
externalTypes?: Map<string, string>;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function buildGeneratorConfig(
|
|
473
|
+
type: NamedType<ObjectType>,
|
|
474
|
+
): GeneratorConfig {
|
|
475
|
+
const source = type.source;
|
|
476
|
+
|
|
477
|
+
if (!source || !existsSync(source)) {
|
|
478
|
+
return {};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return {};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const config = buildGeneratorConfig(xlrType);
|
|
485
|
+
|
|
486
|
+
expect(config).toEqual({});
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
describe("CLI - Path Utils Integration", () => {
|
|
491
|
+
test("resolves node_modules imports to package names", () => {
|
|
492
|
+
const nodePath = normalize(
|
|
493
|
+
"/project/node_modules/@player-lang/types/dist/index.d.ts",
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
expect(isNodeModulesPath(nodePath)).toBe(true);
|
|
497
|
+
expect(extractPackageNameFromPath(nodePath)).toBe("@player-lang/types");
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
test("handles standard npm package path", () => {
|
|
501
|
+
const nodePath = normalize("/project/node_modules/lodash/index.d.ts");
|
|
502
|
+
|
|
503
|
+
expect(isNodeModulesPath(nodePath)).toBe(true);
|
|
504
|
+
expect(extractPackageNameFromPath(nodePath)).toBe("lodash");
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
test("handles local file path", () => {
|
|
508
|
+
const localPath = normalize("/project/src/types/text.ts");
|
|
509
|
+
|
|
510
|
+
expect(isNodeModulesPath(localPath)).toBe(false);
|
|
511
|
+
expect(extractPackageNameFromPath(localPath)).toBe(null);
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
test("handles pnpm nested node_modules structure", () => {
|
|
515
|
+
const pnpmPath = normalize(
|
|
516
|
+
"/project/node_modules/.pnpm/@player-lang+types@1.0.0/node_modules/@player-lang/types/index.d.ts",
|
|
517
|
+
);
|
|
518
|
+
|
|
519
|
+
expect(isNodeModulesPath(pnpmPath)).toBe(true);
|
|
520
|
+
expect(extractPackageNameFromPath(pnpmPath)).toBe("@player-lang/types");
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
test("handles scoped packages", () => {
|
|
524
|
+
const scopedPath = normalize(
|
|
525
|
+
"/project/node_modules/@org/package/index.d.ts",
|
|
526
|
+
);
|
|
527
|
+
|
|
528
|
+
expect(isNodeModulesPath(scopedPath)).toBe(true);
|
|
529
|
+
expect(extractPackageNameFromPath(scopedPath)).toBe("@org/package");
|
|
530
|
+
});
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
describe("CLI - Main Orchestration", () => {
|
|
534
|
+
beforeEach(() => {
|
|
535
|
+
vi.clearAllMocks();
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
test("processes all types and generates builders", () => {
|
|
539
|
+
const manifest: Manifest = {
|
|
540
|
+
version: 1,
|
|
541
|
+
capabilities: {
|
|
542
|
+
Assets: ["TextAsset", "ActionAsset"],
|
|
543
|
+
},
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
const textXlr: NamedType<ObjectType> = {
|
|
547
|
+
name: "TextAsset",
|
|
548
|
+
type: "object",
|
|
549
|
+
properties: {
|
|
550
|
+
value: { required: true, node: { type: "string" } },
|
|
551
|
+
},
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
const actionXlr: NamedType<ObjectType> = {
|
|
555
|
+
name: "ActionAsset",
|
|
556
|
+
type: "object",
|
|
557
|
+
properties: {
|
|
558
|
+
value: { required: false, node: { type: "string" } },
|
|
559
|
+
},
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
563
|
+
vi.mocked(accessSync).mockReturnValue(undefined);
|
|
564
|
+
vi.mocked(readFileSync).mockImplementation((path) => {
|
|
565
|
+
const pathStr = String(path);
|
|
566
|
+
if (pathStr.includes("manifest.json")) {
|
|
567
|
+
return JSON.stringify(manifest);
|
|
568
|
+
}
|
|
569
|
+
if (pathStr.includes("TextAsset.json")) {
|
|
570
|
+
return JSON.stringify(textXlr);
|
|
571
|
+
}
|
|
572
|
+
if (pathStr.includes("ActionAsset.json")) {
|
|
573
|
+
return JSON.stringify(actionXlr);
|
|
574
|
+
}
|
|
575
|
+
return "";
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
vi.mocked(generateFunctionalBuilder).mockReturnValue("// generated code");
|
|
579
|
+
|
|
580
|
+
// Simulate processing all types
|
|
581
|
+
const { Assets = [], Views = [] } = manifest.capabilities;
|
|
582
|
+
const allTypes = [...Assets, ...Views];
|
|
583
|
+
|
|
584
|
+
let succeeded = 0;
|
|
585
|
+
let failed = 0;
|
|
586
|
+
|
|
587
|
+
for (const typeName of allTypes) {
|
|
588
|
+
try {
|
|
589
|
+
const typePath = join("/input", `${typeName}.json`);
|
|
590
|
+
const content = readFileSync(typePath, "utf-8") as string;
|
|
591
|
+
const xlrType = JSON.parse(content) as NamedType<ObjectType>;
|
|
592
|
+
|
|
593
|
+
generateFunctionalBuilder(xlrType);
|
|
594
|
+
succeeded++;
|
|
595
|
+
} catch {
|
|
596
|
+
failed++;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
expect(succeeded).toBe(2);
|
|
601
|
+
expect(failed).toBe(0);
|
|
602
|
+
expect(generateFunctionalBuilder).toHaveBeenCalledTimes(2);
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
test("reports success/failure counts", () => {
|
|
606
|
+
const manifest: Manifest = {
|
|
607
|
+
version: 1,
|
|
608
|
+
capabilities: {
|
|
609
|
+
Assets: ["TextAsset", "MissingAsset"],
|
|
610
|
+
},
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
vi.mocked(existsSync).mockImplementation((path) => {
|
|
614
|
+
const pathStr = String(path);
|
|
615
|
+
if (pathStr.includes("MissingAsset")) {
|
|
616
|
+
return false;
|
|
617
|
+
}
|
|
618
|
+
return true;
|
|
619
|
+
});
|
|
620
|
+
vi.mocked(accessSync).mockReturnValue(undefined);
|
|
621
|
+
vi.mocked(readFileSync).mockImplementation((path) => {
|
|
622
|
+
const pathStr = String(path);
|
|
623
|
+
if (pathStr.includes("manifest.json")) {
|
|
624
|
+
return JSON.stringify(manifest);
|
|
625
|
+
}
|
|
626
|
+
if (pathStr.includes("TextAsset.json")) {
|
|
627
|
+
return JSON.stringify({
|
|
628
|
+
name: "TextAsset",
|
|
629
|
+
type: "object",
|
|
630
|
+
properties: {},
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
throw new Error("File not found");
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
vi.mocked(generateFunctionalBuilder).mockReturnValue("// generated code");
|
|
637
|
+
|
|
638
|
+
const { Assets = [], Views = [] } = manifest.capabilities;
|
|
639
|
+
const allTypes = [...Assets, ...Views];
|
|
640
|
+
|
|
641
|
+
let succeeded = 0;
|
|
642
|
+
let failed = 0;
|
|
643
|
+
|
|
644
|
+
for (const typeName of allTypes) {
|
|
645
|
+
try {
|
|
646
|
+
const typePath = join("/input", `${typeName}.json`);
|
|
647
|
+
if (!existsSync(typePath)) {
|
|
648
|
+
throw new Error(`XLR type file not found: ${typePath}`);
|
|
649
|
+
}
|
|
650
|
+
const content = readFileSync(typePath, "utf-8") as string;
|
|
651
|
+
const xlrType = JSON.parse(content) as NamedType<ObjectType>;
|
|
652
|
+
|
|
653
|
+
generateFunctionalBuilder(xlrType);
|
|
654
|
+
succeeded++;
|
|
655
|
+
} catch {
|
|
656
|
+
failed++;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
expect(succeeded).toBe(1);
|
|
661
|
+
expect(failed).toBe(1);
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
test("disposes TsMorphTypeDefinitionFinder on completion", () => {
|
|
665
|
+
const mockDispose = vi.fn();
|
|
666
|
+
const MockFinder = vi.mocked(TsMorphTypeDefinitionFinder);
|
|
667
|
+
MockFinder.mockImplementation(
|
|
668
|
+
() =>
|
|
669
|
+
({
|
|
670
|
+
findTypeSourceFile: vi.fn(),
|
|
671
|
+
dispose: mockDispose,
|
|
672
|
+
}) as unknown as InstanceType<typeof TsMorphTypeDefinitionFinder>,
|
|
673
|
+
);
|
|
674
|
+
|
|
675
|
+
const finder = new TsMorphTypeDefinitionFinder();
|
|
676
|
+
|
|
677
|
+
try {
|
|
678
|
+
// Simulate processing
|
|
679
|
+
} finally {
|
|
680
|
+
finder.dispose();
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
expect(mockDispose).toHaveBeenCalled();
|
|
684
|
+
});
|
|
685
|
+
});
|