ts-runtime-validation 1.7.0 → 1.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +143 -0
- package/README.md +93 -30
- package/dist/SchemaGenerator.deterministic-extended.test.js +420 -0
- package/dist/SchemaGenerator.deterministic-extended.test.js.map +1 -0
- package/dist/SchemaGenerator.deterministic.test.js +251 -0
- package/dist/SchemaGenerator.deterministic.test.js.map +1 -0
- package/dist/SchemaGenerator.integration.test.js +3 -3
- package/dist/SchemaGenerator.integration.test.js.map +1 -1
- package/dist/SchemaGenerator.test.js +94 -0
- package/dist/SchemaGenerator.test.js.map +1 -1
- package/dist/cli.test.js +155 -0
- package/dist/cli.test.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/services/CodeGenerator.js +29 -13
- package/dist/services/CodeGenerator.js.map +1 -1
- package/dist/services/FileDiscovery.js +4 -2
- package/dist/services/FileDiscovery.js.map +1 -1
- package/dist/services/FileDiscovery.test.js +1 -1
- package/dist/services/FileDiscovery.test.js.map +1 -1
- package/dist/services/SchemaProcessor.js +27 -11
- package/dist/services/SchemaProcessor.js.map +1 -1
- package/dist/services/SchemaProcessor.test.js +61 -1
- package/dist/services/SchemaProcessor.test.js.map +1 -1
- package/dist/services/SchemaWriter.test.js +1 -1
- package/dist/services/SchemaWriter.test.js.map +1 -1
- package/package.json +10 -9
- package/src/SchemaGenerator.deterministic-extended.test.ts +429 -0
- package/src/SchemaGenerator.deterministic.test.ts +276 -0
- package/src/SchemaGenerator.integration.test.ts +3 -3
- package/src/SchemaGenerator.test.ts +111 -0
- package/src/cli.test.ts +130 -0
- package/src/index.ts +1 -1
- package/src/services/CodeGenerator.ts +30 -12
- package/src/services/FileDiscovery.test.ts +1 -1
- package/src/services/FileDiscovery.ts +5 -2
- package/src/services/SchemaProcessor.test.ts +73 -1
- package/src/services/SchemaProcessor.ts +30 -9
- package/src/services/SchemaWriter.test.ts +1 -1
- package/.claude/settings.local.json +0 -9
- package/dist/test/output/duplicate-symbols-identitcal-implementation/ValidationType.js +0 -3
- package/dist/test/output/duplicate-symbols-identitcal-implementation/ValidationType.js.map +0 -1
- package/dist/test/output/duplicate-symbols-identitcal-implementation/isValidSchema.js +0 -49
- package/dist/test/output/duplicate-symbols-identitcal-implementation/isValidSchema.js.map +0 -1
|
@@ -3,7 +3,7 @@ import path from "path";
|
|
|
3
3
|
import { SchemaGenerator } from "./SchemaGenerator";
|
|
4
4
|
import { ICommandOptions } from "./ICommandOptions";
|
|
5
5
|
|
|
6
|
-
const testDir = path.resolve(__dirname, "
|
|
6
|
+
const testDir = path.resolve(__dirname, "../.test-tmp/integration");
|
|
7
7
|
|
|
8
8
|
const createTestFile = async (filePath: string, content: string) => {
|
|
9
9
|
const fullPath = path.resolve(testDir, filePath);
|
|
@@ -396,8 +396,8 @@ describe("SchemaGenerator Integration Tests", () => {
|
|
|
396
396
|
const endTime = Date.now();
|
|
397
397
|
const duration = endTime - startTime;
|
|
398
398
|
|
|
399
|
-
// Should complete reasonably quickly (less than
|
|
400
|
-
expect(duration).toBeLessThan(
|
|
399
|
+
// Should complete reasonably quickly (less than 20 seconds)
|
|
400
|
+
expect(duration).toBeLessThan(20000);
|
|
401
401
|
|
|
402
402
|
// Verify all files were processed
|
|
403
403
|
const schemaFile = path.join(testDir, "output", "validation.schema.json");
|
|
@@ -122,4 +122,115 @@ describe("SchemaGenerator", () => {
|
|
|
122
122
|
};
|
|
123
123
|
expect(result).toStrictEqual(expected);
|
|
124
124
|
});
|
|
125
|
+
|
|
126
|
+
test("it should sort definitions alphabetically in the generated schema", async () => {
|
|
127
|
+
// Create test directory and files
|
|
128
|
+
const testDir = path.resolve(__dirname, "./test/alphabetical-sorting");
|
|
129
|
+
|
|
130
|
+
// Ensure test directory exists
|
|
131
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
132
|
+
|
|
133
|
+
// Create test file with unsorted type definitions
|
|
134
|
+
const testFilePath = path.join(testDir, "types.jsonschema.ts");
|
|
135
|
+
fs.writeFileSync(testFilePath, `
|
|
136
|
+
export interface ZebraType {
|
|
137
|
+
id: string;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface AppleType {
|
|
141
|
+
name: string;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface MiddleType {
|
|
145
|
+
value: number;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface BananaType {
|
|
149
|
+
flag: boolean;
|
|
150
|
+
}
|
|
151
|
+
`);
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
const options = getGeneratorConfig("alphabetical-sorting");
|
|
155
|
+
const generator = new SchemaGenerator(options);
|
|
156
|
+
await generator.Generate();
|
|
157
|
+
|
|
158
|
+
const rawFile = fs.readFileSync(getOutputSchemaPath("alphabetical-sorting")).toString();
|
|
159
|
+
const result = JSON.parse(rawFile);
|
|
160
|
+
|
|
161
|
+
// Check that definitions are sorted alphabetically
|
|
162
|
+
const definitionKeys = Object.keys(result.definitions);
|
|
163
|
+
const sortedKeys = [...definitionKeys].sort();
|
|
164
|
+
|
|
165
|
+
expect(definitionKeys).toEqual(sortedKeys);
|
|
166
|
+
expect(definitionKeys).toEqual(['AppleType', 'BananaType', 'MiddleType', 'ZebraType']);
|
|
167
|
+
} finally {
|
|
168
|
+
// Clean up test directory
|
|
169
|
+
if (fs.existsSync(testDir)) {
|
|
170
|
+
fs.rmSync(testDir, { recursive: true });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("it should sort properties alphabetically in generated TypeScript helpers", async () => {
|
|
176
|
+
// Create test directory and files
|
|
177
|
+
const testDir = path.resolve(__dirname, "./test/alphabetical-helpers");
|
|
178
|
+
const outputDir = path.resolve(__dirname, "./test/output/alphabetical-helpers");
|
|
179
|
+
|
|
180
|
+
// Ensure test directory exists
|
|
181
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
182
|
+
|
|
183
|
+
// Create test file with unsorted type definitions
|
|
184
|
+
const testFilePath = path.join(testDir, "types.jsonschema.ts");
|
|
185
|
+
fs.writeFileSync(testFilePath, `
|
|
186
|
+
export type ZebraType = string;
|
|
187
|
+
export type AppleType = number;
|
|
188
|
+
export type MiddleType = boolean;
|
|
189
|
+
export type BananaType = object;
|
|
190
|
+
`);
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
const options = {
|
|
194
|
+
...getGeneratorConfig("alphabetical-helpers"),
|
|
195
|
+
helpers: true
|
|
196
|
+
};
|
|
197
|
+
const generator = new SchemaGenerator(options);
|
|
198
|
+
await generator.Generate();
|
|
199
|
+
|
|
200
|
+
// Check SchemaDefinition.ts for sorted imports and properties
|
|
201
|
+
const schemaDefPath = path.resolve(outputDir, "SchemaDefinition.ts");
|
|
202
|
+
if (fs.existsSync(schemaDefPath)) {
|
|
203
|
+
const schemaDefContent = fs.readFileSync(schemaDefPath, 'utf-8');
|
|
204
|
+
|
|
205
|
+
// Extract type names from the schemas object (looking only at the schemas constant)
|
|
206
|
+
const schemasMatch = schemaDefContent.match(/export const schemas[^{]*{([\s\S]*?)}/);
|
|
207
|
+
let schemaKeys: string[] = [];
|
|
208
|
+
if (schemasMatch) {
|
|
209
|
+
const schemaLines = schemasMatch[1].split('\n').filter(line => line.includes('#/definitions/'));
|
|
210
|
+
schemaKeys = schemaLines.map(line => {
|
|
211
|
+
const match = line.match(/\["#\/definitions\/([^"]+)"\]/);
|
|
212
|
+
return match ? match[1] : null;
|
|
213
|
+
}).filter(Boolean) as string[];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const sortedSchemaKeys = [...schemaKeys].sort();
|
|
217
|
+
|
|
218
|
+
expect(schemaKeys).toEqual(sortedSchemaKeys);
|
|
219
|
+
expect(schemaKeys).toEqual(['AppleType', 'BananaType', 'MiddleType', 'ZebraType']);
|
|
220
|
+
|
|
221
|
+
// Check that imports are sorted
|
|
222
|
+
const importMatch = schemaDefContent.match(/import\s+{\s*([^}]+)\s*}/);
|
|
223
|
+
if (importMatch) {
|
|
224
|
+
const imports = importMatch[1].split(',').map(s => s.trim());
|
|
225
|
+
const sortedImports = [...imports].sort();
|
|
226
|
+
expect(imports).toEqual(sortedImports);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
} finally {
|
|
230
|
+
// Clean up test directory
|
|
231
|
+
if (fs.existsSync(testDir)) {
|
|
232
|
+
fs.rmSync(testDir, { recursive: true });
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
});
|
|
125
236
|
});
|
package/src/cli.test.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import * as fs from "fs";
|
|
4
|
+
|
|
5
|
+
describe("CLI Arguments", () => {
|
|
6
|
+
const cliPath = path.join(__dirname, "..", "dist", "index.js");
|
|
7
|
+
const testOutputPath = path.join(__dirname, "../.test-tmp/cli-output");
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
if (fs.existsSync(testOutputPath)) {
|
|
11
|
+
fs.rmSync(testOutputPath, { recursive: true, force: true });
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
if (fs.existsSync(testOutputPath)) {
|
|
17
|
+
fs.rmSync(testOutputPath, { recursive: true, force: true });
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const runCLI = (args: string[]): Promise<{ code: number; stdout: string; stderr: string }> => {
|
|
22
|
+
return new Promise((resolve) => {
|
|
23
|
+
const proc = spawn("node", [cliPath, ...args]);
|
|
24
|
+
let stdout = "";
|
|
25
|
+
let stderr = "";
|
|
26
|
+
|
|
27
|
+
proc.stdout.on("data", (data) => {
|
|
28
|
+
stdout += data.toString();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
proc.stderr.on("data", (data) => {
|
|
32
|
+
stderr += data.toString();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
proc.on("close", (code) => {
|
|
36
|
+
resolve({ code: code || 0, stdout, stderr });
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
it("should accept glob pattern with --glob parameter", async () => {
|
|
42
|
+
const result = await runCLI([
|
|
43
|
+
"--glob", "test/basic-scenario/*.jsonschema.ts",
|
|
44
|
+
"--rootPath", path.join(__dirname),
|
|
45
|
+
"--output", "../.test-tmp/cli-output"
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
expect(result.code).toBe(0);
|
|
49
|
+
expect(fs.existsSync(testOutputPath)).toBe(true);
|
|
50
|
+
|
|
51
|
+
// Check that files were generated
|
|
52
|
+
const generatedFiles = fs.readdirSync(testOutputPath);
|
|
53
|
+
expect(generatedFiles).toContain("validation.schema.json");
|
|
54
|
+
expect(generatedFiles).toContain("SchemaDefinition.ts");
|
|
55
|
+
expect(generatedFiles).toContain("ValidationType.ts");
|
|
56
|
+
expect(generatedFiles).toContain("isValidSchema.ts");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should use default glob pattern when --glob is not provided", async () => {
|
|
60
|
+
// Create test files matching the default pattern
|
|
61
|
+
const testDir = path.join(__dirname, "test-default-glob");
|
|
62
|
+
const testFile1 = path.join(testDir, "test.jsonschema.ts");
|
|
63
|
+
const testFile2 = path.join(testDir, "test2.jsonschema.tsx");
|
|
64
|
+
|
|
65
|
+
if (!fs.existsSync(testDir)) {
|
|
66
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
fs.writeFileSync(testFile1, "export interface TestInterface { test: string; }");
|
|
70
|
+
fs.writeFileSync(testFile2, "export interface TestInterface2 { test2: number; }");
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const result = await runCLI([
|
|
74
|
+
"--rootPath", testDir,
|
|
75
|
+
"--output", path.join("..", "../.test-tmp/cli-output")
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
expect(result.code).toBe(0);
|
|
79
|
+
expect(fs.existsSync(testOutputPath)).toBe(true);
|
|
80
|
+
} finally {
|
|
81
|
+
// Clean up test files and directory
|
|
82
|
+
if (fs.existsSync(testDir)) {
|
|
83
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("should accept all supported CLI options", async () => {
|
|
89
|
+
const result = await runCLI([
|
|
90
|
+
"--glob", "test/basic-scenario/*.jsonschema.ts",
|
|
91
|
+
"--rootPath", path.join(__dirname),
|
|
92
|
+
"--output", "../.test-tmp/cli-output",
|
|
93
|
+
"--additionalProperties",
|
|
94
|
+
"--verbose",
|
|
95
|
+
"--progress",
|
|
96
|
+
"--minify",
|
|
97
|
+
"--cache",
|
|
98
|
+
"--tree-shaking",
|
|
99
|
+
"--lazy-load"
|
|
100
|
+
]);
|
|
101
|
+
|
|
102
|
+
expect(result.code).toBe(0);
|
|
103
|
+
// With verbose flag, we should see output
|
|
104
|
+
expect(result.stdout).toContain("Found");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("should handle parallel processing flags correctly", async () => {
|
|
108
|
+
// Test with parallel disabled
|
|
109
|
+
const result = await runCLI([
|
|
110
|
+
"--glob", "test/basic-scenario/*.jsonschema.ts",
|
|
111
|
+
"--rootPath", path.join(__dirname),
|
|
112
|
+
"--output", "../.test-tmp/cli-output",
|
|
113
|
+
"--no-parallel"
|
|
114
|
+
]);
|
|
115
|
+
|
|
116
|
+
expect(result.code).toBe(0);
|
|
117
|
+
expect(fs.existsSync(testOutputPath)).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("should error gracefully when no matching files are found", async () => {
|
|
121
|
+
const result = await runCLI([
|
|
122
|
+
"--glob", "nonexistent/*.jsonschema.ts",
|
|
123
|
+
"--rootPath", path.join(__dirname),
|
|
124
|
+
"--output", "../.test-tmp/cli-output"
|
|
125
|
+
]);
|
|
126
|
+
|
|
127
|
+
expect(result.code).not.toBe(0);
|
|
128
|
+
expect(result.stderr).toContain("No files found");
|
|
129
|
+
});
|
|
130
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -11,7 +11,7 @@ const defaultOutputFolder = "./.ts-runtime-validation";
|
|
|
11
11
|
const defaultTsconfig = "";
|
|
12
12
|
|
|
13
13
|
program.option(
|
|
14
|
-
"--glob",
|
|
14
|
+
"--glob <glob>",
|
|
15
15
|
`Glob file path of typescript files to generate ts-interface -> json-schema validations - default: ${defaultGlobPattern}`,
|
|
16
16
|
defaultGlobPattern
|
|
17
17
|
);
|
|
@@ -98,7 +98,10 @@ export class CodeGenerator {
|
|
|
98
98
|
const readerProject = new Project(defaultTsMorphProjectSettings);
|
|
99
99
|
const typeInfos: TypeInfo[] = [];
|
|
100
100
|
|
|
101
|
-
|
|
101
|
+
// Sort schema map entries by file path for consistent processing order
|
|
102
|
+
const sortedEntries = [...schemaMap.entries()].sort(([a], [b]) => a.localeCompare(b));
|
|
103
|
+
|
|
104
|
+
for (const [filePath, schema] of sortedEntries) {
|
|
102
105
|
const dir = path.dirname(filePath);
|
|
103
106
|
const fileWithoutExtension = path.parse(filePath).name;
|
|
104
107
|
const relativeFilePath = path.relative(this.options.outputPath, dir);
|
|
@@ -107,7 +110,8 @@ export class CodeGenerator {
|
|
|
107
110
|
|
|
108
111
|
const readerSourceFile = readerProject.addSourceFileAtPath(filePath);
|
|
109
112
|
|
|
110
|
-
|
|
113
|
+
// Sort definition keys alphabetically
|
|
114
|
+
Object.keys(defs).sort().forEach((symbol) => {
|
|
111
115
|
const typeAlias = readerSourceFile.getTypeAlias(symbol);
|
|
112
116
|
const typeInterface = readerSourceFile.getInterface(symbol);
|
|
113
117
|
|
|
@@ -119,9 +123,10 @@ export class CodeGenerator {
|
|
|
119
123
|
});
|
|
120
124
|
}
|
|
121
125
|
});
|
|
122
|
-
}
|
|
126
|
+
}
|
|
123
127
|
|
|
124
|
-
|
|
128
|
+
// Sort typeInfos alphabetically by symbol
|
|
129
|
+
return typeInfos.sort((a, b) => a.symbol.localeCompare(b.symbol));
|
|
125
130
|
}
|
|
126
131
|
|
|
127
132
|
private async writeSchemaDefinitionFile(
|
|
@@ -145,7 +150,8 @@ export class CodeGenerator {
|
|
|
145
150
|
type: "Record<keyof ISchema, string>",
|
|
146
151
|
initializer: (writer: CodeBlockWriter) => {
|
|
147
152
|
writer.writeLine(`{`);
|
|
148
|
-
|
|
153
|
+
// Sort by symbol for consistent output
|
|
154
|
+
[...typeInfos].sort((a, b) => a.symbol.localeCompare(b.symbol)).forEach(({ symbol }) => {
|
|
149
155
|
writer.writeLine(`["#/definitions/${symbol}"] : "${symbol}",`);
|
|
150
156
|
});
|
|
151
157
|
writer.writeLine(`}`);
|
|
@@ -158,7 +164,7 @@ export class CodeGenerator {
|
|
|
158
164
|
kind: StructureKind.Interface,
|
|
159
165
|
name: "ISchema",
|
|
160
166
|
isExported: true,
|
|
161
|
-
properties: typeInfos.map(({ symbol }) => ({
|
|
167
|
+
properties: [...typeInfos].sort((a, b) => a.symbol.localeCompare(b.symbol)).map(({ symbol }) => ({
|
|
162
168
|
name: `readonly ["#/definitions/${symbol}"]`,
|
|
163
169
|
type: symbol
|
|
164
170
|
})),
|
|
@@ -176,7 +182,10 @@ export class CodeGenerator {
|
|
|
176
182
|
importMap.set(importPath, existing);
|
|
177
183
|
});
|
|
178
184
|
|
|
179
|
-
|
|
185
|
+
// Sort import paths and symbols alphabetically
|
|
186
|
+
const sortedImportPaths = Array.from(importMap.keys()).sort();
|
|
187
|
+
sortedImportPaths.forEach((importPath) => {
|
|
188
|
+
const symbols = importMap.get(importPath)!.sort();
|
|
180
189
|
sourceFile.addImportDeclaration({
|
|
181
190
|
namedImports: symbols,
|
|
182
191
|
moduleSpecifier: importPath
|
|
@@ -193,7 +202,10 @@ export class CodeGenerator {
|
|
|
193
202
|
importMap.set(importPath, existing);
|
|
194
203
|
});
|
|
195
204
|
|
|
196
|
-
|
|
205
|
+
// Sort import paths and symbols alphabetically
|
|
206
|
+
const sortedImportPaths = Array.from(importMap.keys()).sort();
|
|
207
|
+
sortedImportPaths.forEach((importPath) => {
|
|
208
|
+
const symbols = importMap.get(importPath)!.sort();
|
|
197
209
|
sourceFile.addImportDeclaration({
|
|
198
210
|
namedImports: symbols,
|
|
199
211
|
moduleSpecifier: importPath
|
|
@@ -306,13 +318,19 @@ export class CodeGenerator {
|
|
|
306
318
|
const sourceFile = this.project.createSourceFile(outputFile, {}, defaultCreateFileOptions);
|
|
307
319
|
|
|
308
320
|
const importMap = new Map<string, string[]>();
|
|
309
|
-
|
|
321
|
+
// Sort typeInfos for consistent processing
|
|
322
|
+
const sortedTypeInfos = [...typeInfos].sort((a, b) => a.symbol.localeCompare(b.symbol));
|
|
323
|
+
|
|
324
|
+
sortedTypeInfos.forEach(({ symbol, importPath }) => {
|
|
310
325
|
const existing = importMap.get(importPath) || [];
|
|
311
326
|
existing.push(symbol);
|
|
312
327
|
importMap.set(importPath, existing);
|
|
313
328
|
});
|
|
314
329
|
|
|
315
|
-
|
|
330
|
+
// Sort import paths and symbols alphabetically
|
|
331
|
+
const sortedImportPaths = Array.from(importMap.keys()).sort();
|
|
332
|
+
sortedImportPaths.forEach((importPath) => {
|
|
333
|
+
const symbols = importMap.get(importPath)!.sort();
|
|
316
334
|
const declaration = sourceFile.addImportDeclaration({
|
|
317
335
|
moduleSpecifier: importPath
|
|
318
336
|
});
|
|
@@ -325,7 +343,7 @@ export class CodeGenerator {
|
|
|
325
343
|
});
|
|
326
344
|
|
|
327
345
|
if (this.options.treeShaking) {
|
|
328
|
-
|
|
346
|
+
sortedTypeInfos.forEach(({ symbol }) => {
|
|
329
347
|
sourceFile.addTypeAlias({
|
|
330
348
|
name: symbol,
|
|
331
349
|
type: `_${symbol}`,
|
|
@@ -338,7 +356,7 @@ export class CodeGenerator {
|
|
|
338
356
|
isExported: true,
|
|
339
357
|
});
|
|
340
358
|
|
|
341
|
-
|
|
359
|
+
sortedTypeInfos.forEach(({ symbol }) => {
|
|
342
360
|
namespace.addTypeAlias({
|
|
343
361
|
name: symbol,
|
|
344
362
|
type: `_${symbol}`,
|
|
@@ -3,7 +3,7 @@ import path from "path";
|
|
|
3
3
|
import { FileDiscovery } from "./FileDiscovery";
|
|
4
4
|
import { FileDiscoveryError } from "../errors";
|
|
5
5
|
|
|
6
|
-
const testDir = path.resolve(__dirname, "
|
|
6
|
+
const testDir = path.resolve(__dirname, "../../.test-tmp/file-discovery");
|
|
7
7
|
const cacheDir = path.resolve(testDir, ".cache");
|
|
8
8
|
|
|
9
9
|
const createTestFile = async (filePath: string, content: string = "test content") => {
|
|
@@ -54,9 +54,12 @@ export class FileDiscovery {
|
|
|
54
54
|
);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
// Sort files alphabetically to ensure consistent ordering
|
|
58
|
+
const sortedFiles = [...files].sort();
|
|
59
|
+
|
|
57
60
|
return this.options.cacheEnabled
|
|
58
|
-
? await this.enrichWithCacheInfo(
|
|
59
|
-
:
|
|
61
|
+
? await this.enrichWithCacheInfo(sortedFiles)
|
|
62
|
+
: sortedFiles.map(path => ({ path }));
|
|
60
63
|
} catch (error) {
|
|
61
64
|
if (error instanceof FileDiscoveryError) {
|
|
62
65
|
throw error;
|
|
@@ -4,7 +4,7 @@ import { SchemaProcessor } from "./SchemaProcessor";
|
|
|
4
4
|
import { FileInfo } from "./FileDiscovery";
|
|
5
5
|
import { DuplicateSymbolError } from "../errors";
|
|
6
6
|
|
|
7
|
-
const testDir = path.resolve(__dirname, "
|
|
7
|
+
const testDir = path.resolve(__dirname, "../../.test-tmp/schema-processor");
|
|
8
8
|
|
|
9
9
|
const createTestFile = async (filePath: string, content: string) => {
|
|
10
10
|
const fullPath = path.resolve(testDir, filePath);
|
|
@@ -340,6 +340,78 @@ describe("SchemaProcessor", () => {
|
|
|
340
340
|
expect(mergedSchema.$schema).toBe("http://json-schema.org/draft-07/schema#");
|
|
341
341
|
expect(mergedSchema.definitions).toEqual({});
|
|
342
342
|
});
|
|
343
|
+
|
|
344
|
+
it("should sort definitions alphabetically in merged schema", async () => {
|
|
345
|
+
const file1 = await createTestFile("types1.jsonschema.ts", `
|
|
346
|
+
export interface ZebraType {
|
|
347
|
+
id: string;
|
|
348
|
+
}
|
|
349
|
+
export interface AppleType {
|
|
350
|
+
name: string;
|
|
351
|
+
}
|
|
352
|
+
`);
|
|
353
|
+
|
|
354
|
+
const file2 = await createTestFile("types2.jsonschema.ts", `
|
|
355
|
+
export interface MiddleType {
|
|
356
|
+
value: number;
|
|
357
|
+
}
|
|
358
|
+
export interface BananaType {
|
|
359
|
+
flag: boolean;
|
|
360
|
+
}
|
|
361
|
+
`);
|
|
362
|
+
|
|
363
|
+
const processor = new SchemaProcessor({
|
|
364
|
+
additionalProperties: false,
|
|
365
|
+
parallel: false
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
const files: FileInfo[] = [
|
|
369
|
+
{ path: file1 },
|
|
370
|
+
{ path: file2 }
|
|
371
|
+
];
|
|
372
|
+
|
|
373
|
+
const schemaMap = await processor.processFiles(files);
|
|
374
|
+
const mergedSchema = processor.mergeSchemas(schemaMap);
|
|
375
|
+
|
|
376
|
+
const definitionKeys = Object.keys(mergedSchema.definitions || {});
|
|
377
|
+
const sortedKeys = [...definitionKeys].sort();
|
|
378
|
+
|
|
379
|
+
expect(definitionKeys).toEqual(sortedKeys);
|
|
380
|
+
expect(definitionKeys).toEqual(['AppleType', 'BananaType', 'MiddleType', 'ZebraType']);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it("should maintain alphabetical order for definitions with numbers and special characters", async () => {
|
|
384
|
+
const file1 = await createTestFile("special.jsonschema.ts", `
|
|
385
|
+
export interface Type1 {
|
|
386
|
+
id: string;
|
|
387
|
+
}
|
|
388
|
+
export interface TypeA {
|
|
389
|
+
name: string;
|
|
390
|
+
}
|
|
391
|
+
export interface Type10 {
|
|
392
|
+
value: number;
|
|
393
|
+
}
|
|
394
|
+
export interface Type2 {
|
|
395
|
+
flag: boolean;
|
|
396
|
+
}
|
|
397
|
+
`);
|
|
398
|
+
|
|
399
|
+
const processor = new SchemaProcessor({
|
|
400
|
+
additionalProperties: false,
|
|
401
|
+
parallel: false
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
const files: FileInfo[] = [{ path: file1 }];
|
|
405
|
+
const schemaMap = await processor.processFiles(files);
|
|
406
|
+
const mergedSchema = processor.mergeSchemas(schemaMap);
|
|
407
|
+
|
|
408
|
+
const definitionKeys = Object.keys(mergedSchema.definitions || {});
|
|
409
|
+
const sortedKeys = [...definitionKeys].sort();
|
|
410
|
+
|
|
411
|
+
expect(definitionKeys).toEqual(sortedKeys);
|
|
412
|
+
// Natural alphabetical order: numbers before letters
|
|
413
|
+
expect(definitionKeys).toEqual(['Type1', 'Type10', 'Type2', 'TypeA']);
|
|
414
|
+
});
|
|
343
415
|
});
|
|
344
416
|
|
|
345
417
|
describe("error handling", () => {
|
|
@@ -27,9 +27,12 @@ export class SchemaProcessor {
|
|
|
27
27
|
console.log(`Processing ${files.length} files...`);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
// Sort files by path to ensure consistent processing order
|
|
31
|
+
const sortedFiles = [...files].sort((a, b) => a.path.localeCompare(b.path));
|
|
32
|
+
|
|
30
33
|
const results = parallel
|
|
31
|
-
? await this.processInParallel(
|
|
32
|
-
: await this.processSequentially(
|
|
34
|
+
? await this.processInParallel(sortedFiles)
|
|
35
|
+
: await this.processSequentially(sortedFiles);
|
|
33
36
|
|
|
34
37
|
return this.consolidateSchemas(results);
|
|
35
38
|
}
|
|
@@ -38,6 +41,7 @@ export class SchemaProcessor {
|
|
|
38
41
|
const promises = files.map(file => this.processFile(file));
|
|
39
42
|
const results = await Promise.allSettled(promises);
|
|
40
43
|
|
|
44
|
+
// Map results back to original file order to maintain consistency
|
|
41
45
|
return results.map((result, index) => {
|
|
42
46
|
if (result.status === 'fulfilled') {
|
|
43
47
|
return result.value;
|
|
@@ -106,7 +110,10 @@ export class SchemaProcessor {
|
|
|
106
110
|
const schemaMap = new Map<string, Schema>();
|
|
107
111
|
const errors: Error[] = [];
|
|
108
112
|
|
|
109
|
-
|
|
113
|
+
// Sort results by file path to ensure consistent order
|
|
114
|
+
const sortedResults = [...results].sort((a, b) => a.file.localeCompare(b.file));
|
|
115
|
+
|
|
116
|
+
for (const result of sortedResults) {
|
|
110
117
|
if (result.error) {
|
|
111
118
|
errors.push(result.error);
|
|
112
119
|
continue;
|
|
@@ -128,10 +135,14 @@ export class SchemaProcessor {
|
|
|
128
135
|
public validateSchemaCompatibility(schemaMap: Map<string, Schema>): void {
|
|
129
136
|
const definitions: { [id: string]: any } = {};
|
|
130
137
|
|
|
131
|
-
|
|
138
|
+
// Sort by file path for consistent processing order
|
|
139
|
+
const sortedEntries = [...schemaMap.entries()].sort(([a], [b]) => a.localeCompare(b));
|
|
140
|
+
|
|
141
|
+
for (const [filePath, fileSchema] of sortedEntries) {
|
|
132
142
|
const defs = fileSchema.definitions ?? {};
|
|
133
143
|
|
|
134
|
-
|
|
144
|
+
// Sort definition keys for consistent processing
|
|
145
|
+
Object.keys(defs).sort().forEach((key) => {
|
|
135
146
|
if (definitions[key] !== undefined) {
|
|
136
147
|
try {
|
|
137
148
|
assert.deepEqual(definitions[key], defs[key]);
|
|
@@ -147,27 +158,37 @@ export class SchemaProcessor {
|
|
|
147
158
|
}
|
|
148
159
|
definitions[key] = defs[key];
|
|
149
160
|
});
|
|
150
|
-
}
|
|
161
|
+
}
|
|
151
162
|
}
|
|
152
163
|
|
|
153
164
|
public mergeSchemas(schemaMap: Map<string, Schema>): Schema {
|
|
154
165
|
const definitions: { [id: string]: Schema } = {};
|
|
155
166
|
let schemaVersion = "";
|
|
156
167
|
|
|
157
|
-
|
|
168
|
+
// Sort by file path for consistent processing order
|
|
169
|
+
const sortedEntries = [...schemaMap.entries()].sort(([a], [b]) => a.localeCompare(b));
|
|
170
|
+
|
|
171
|
+
for (const [, fileSchema] of sortedEntries) {
|
|
158
172
|
if (!schemaVersion && fileSchema["$schema"]) {
|
|
159
173
|
schemaVersion = fileSchema["$schema"];
|
|
160
174
|
}
|
|
161
175
|
|
|
162
176
|
const defs = fileSchema.definitions ?? {};
|
|
163
|
-
|
|
177
|
+
// Sort definition keys for consistent processing
|
|
178
|
+
Object.keys(defs).sort().forEach((key) => {
|
|
164
179
|
definitions[key] = defs[key] as Schema;
|
|
165
180
|
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Sort definitions alphabetically
|
|
184
|
+
const sortedDefinitions: { [id: string]: Schema } = {};
|
|
185
|
+
Object.keys(definitions).sort().forEach(key => {
|
|
186
|
+
sortedDefinitions[key] = definitions[key];
|
|
166
187
|
});
|
|
167
188
|
|
|
168
189
|
return {
|
|
169
190
|
$schema: schemaVersion || "http://json-schema.org/draft-07/schema#",
|
|
170
|
-
definitions,
|
|
191
|
+
definitions: sortedDefinitions,
|
|
171
192
|
};
|
|
172
193
|
}
|
|
173
194
|
}
|
|
@@ -3,7 +3,7 @@ import path from "path";
|
|
|
3
3
|
import { SchemaWriter } from "./SchemaWriter";
|
|
4
4
|
import { CodeGenerationError } from "../errors";
|
|
5
5
|
|
|
6
|
-
const testDir = path.resolve(__dirname, "
|
|
6
|
+
const testDir = path.resolve(__dirname, "../../.test-tmp/schema-writer");
|
|
7
7
|
|
|
8
8
|
const cleanup = async () => {
|
|
9
9
|
if (fs.existsSync(testDir)) {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ValidationType.js","sourceRoot":"","sources":["../../../../src/test/output/duplicate-symbols-identitcal-implementation/ValidationType.ts"],"names":[],"mappings":""}
|