ts-runtime-validation 1.6.16 → 1.8.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.
Files changed (83) hide show
  1. package/CONTRIBUTING.md +430 -0
  2. package/README.md +505 -62
  3. package/dist/ICommandOptions.js +3 -0
  4. package/dist/ICommandOptions.js.map +1 -0
  5. package/dist/SchemaGenerator.deterministic-extended.test.js +420 -0
  6. package/dist/SchemaGenerator.deterministic-extended.test.js.map +1 -0
  7. package/dist/SchemaGenerator.deterministic.test.js +251 -0
  8. package/dist/SchemaGenerator.deterministic.test.js.map +1 -0
  9. package/dist/SchemaGenerator.integration.test.js +323 -0
  10. package/dist/SchemaGenerator.integration.test.js.map +1 -0
  11. package/dist/SchemaGenerator.js +120 -0
  12. package/dist/SchemaGenerator.js.map +1 -0
  13. package/dist/SchemaGenerator.test.js +226 -0
  14. package/dist/SchemaGenerator.test.js.map +1 -0
  15. package/dist/cli.test.js +155 -0
  16. package/dist/cli.test.js.map +1 -0
  17. package/dist/errors/index.js +95 -0
  18. package/dist/errors/index.js.map +1 -0
  19. package/dist/errors/index.test.js +232 -0
  20. package/dist/errors/index.test.js.map +1 -0
  21. package/dist/getPosixPath.js +13 -0
  22. package/dist/getPosixPath.js.map +1 -0
  23. package/dist/index.js +27 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/lib.js.map +1 -0
  26. package/dist/services/CodeGenerator.js +321 -0
  27. package/dist/services/CodeGenerator.js.map +1 -0
  28. package/dist/services/FileDiscovery.js +123 -0
  29. package/dist/services/FileDiscovery.js.map +1 -0
  30. package/dist/services/FileDiscovery.test.js +184 -0
  31. package/dist/services/FileDiscovery.test.js.map +1 -0
  32. package/dist/services/SchemaProcessor.js +198 -0
  33. package/dist/services/SchemaProcessor.js.map +1 -0
  34. package/dist/services/SchemaProcessor.test.js +455 -0
  35. package/dist/services/SchemaProcessor.test.js.map +1 -0
  36. package/dist/services/SchemaWriter.js +76 -0
  37. package/dist/services/SchemaWriter.js.map +1 -0
  38. package/dist/services/SchemaWriter.test.js +255 -0
  39. package/dist/services/SchemaWriter.test.js.map +1 -0
  40. package/dist/test/basic-scenario/types.jsonschema.js +3 -0
  41. package/dist/test/basic-scenario/types.jsonschema.js.map +1 -0
  42. package/dist/test/duplicate-symbols-different-implementation/IBaseType.js +3 -0
  43. package/dist/test/duplicate-symbols-different-implementation/IBaseType.js.map +1 -0
  44. package/dist/test/duplicate-symbols-different-implementation/IBaseTypeDefinitionReplicated.js +3 -0
  45. package/dist/test/duplicate-symbols-different-implementation/IBaseTypeDefinitionReplicated.js.map +1 -0
  46. package/dist/test/duplicate-symbols-different-implementation/IBasicTypesA.jsonschema.js +3 -0
  47. package/dist/test/duplicate-symbols-different-implementation/IBasicTypesA.jsonschema.js.map +1 -0
  48. package/dist/test/duplicate-symbols-different-implementation/IBasicTypesB.jsonschema.js +3 -0
  49. package/dist/test/duplicate-symbols-different-implementation/IBasicTypesB.jsonschema.js.map +1 -0
  50. package/dist/test/duplicate-symbols-identitcal-implementation/IBaseType.js +3 -0
  51. package/dist/test/duplicate-symbols-identitcal-implementation/IBaseType.js.map +1 -0
  52. package/dist/test/duplicate-symbols-identitcal-implementation/IBaseTypeDefinitionReplicated.js +3 -0
  53. package/dist/test/duplicate-symbols-identitcal-implementation/IBaseTypeDefinitionReplicated.js.map +1 -0
  54. package/dist/test/duplicate-symbols-identitcal-implementation/IBasicTypesA.jsonschema.js +3 -0
  55. package/dist/test/duplicate-symbols-identitcal-implementation/IBasicTypesA.jsonschema.js.map +1 -0
  56. package/dist/test/duplicate-symbols-identitcal-implementation/IBasicTypesB.jsonschema.js +3 -0
  57. package/dist/test/duplicate-symbols-identitcal-implementation/IBasicTypesB.jsonschema.js.map +1 -0
  58. package/dist/utils/ProgressReporter.js +67 -0
  59. package/dist/utils/ProgressReporter.js.map +1 -0
  60. package/dist/utils/ProgressReporter.test.js +267 -0
  61. package/dist/utils/ProgressReporter.test.js.map +1 -0
  62. package/dist/writeLine.js +12 -0
  63. package/dist/writeLine.js.map +1 -0
  64. package/package.json +2 -2
  65. package/src/ICommandOptions.ts +7 -0
  66. package/src/SchemaGenerator.deterministic-extended.test.ts +429 -0
  67. package/src/SchemaGenerator.deterministic.test.ts +276 -0
  68. package/src/SchemaGenerator.integration.test.ts +411 -0
  69. package/src/SchemaGenerator.test.ts +118 -0
  70. package/src/SchemaGenerator.ts +112 -298
  71. package/src/cli.test.ts +130 -0
  72. package/src/errors/index.test.ts +319 -0
  73. package/src/errors/index.ts +92 -0
  74. package/src/index.ts +8 -1
  75. package/src/services/CodeGenerator.ts +370 -0
  76. package/src/services/FileDiscovery.test.ts +216 -0
  77. package/src/services/FileDiscovery.ts +140 -0
  78. package/src/services/SchemaProcessor.test.ts +536 -0
  79. package/src/services/SchemaProcessor.ts +194 -0
  80. package/src/services/SchemaWriter.test.ts +304 -0
  81. package/src/services/SchemaWriter.ts +75 -0
  82. package/src/utils/ProgressReporter.test.ts +357 -0
  83. package/src/utils/ProgressReporter.ts +76 -0
@@ -0,0 +1,370 @@
1
+ import {
2
+ Project,
3
+ IndentationText,
4
+ NewLineKind,
5
+ QuoteKind,
6
+ StructureKind,
7
+ VariableDeclarationKind,
8
+ CodeBlockWriter,
9
+ ProjectOptions,
10
+ SourceFileCreateOptions,
11
+ } from "ts-morph";
12
+ import { Schema } from "ts-json-schema-generator";
13
+ import path from "path";
14
+ import { getPosixPath } from "../getPosixPath";
15
+ import { CodeGenerationError } from "../errors";
16
+
17
+ export interface CodeGeneratorOptions {
18
+ outputPath: string;
19
+ minify?: boolean;
20
+ treeShaking?: boolean;
21
+ lazyLoad?: boolean;
22
+ }
23
+
24
+ interface TypeInfo {
25
+ symbol: string;
26
+ importPath: string;
27
+ isInterface: boolean;
28
+ }
29
+
30
+ const defaultTsMorphProjectSettings: ProjectOptions = {
31
+ manipulationSettings: {
32
+ indentationText: IndentationText.FourSpaces,
33
+ newLineKind: NewLineKind.LineFeed,
34
+ quoteKind: QuoteKind.Double,
35
+ usePrefixAndSuffixTextForRename: false,
36
+ useTrailingCommas: true,
37
+ },
38
+ };
39
+
40
+ const defaultCreateFileOptions: SourceFileCreateOptions = {
41
+ overwrite: true,
42
+ };
43
+
44
+ export class CodeGenerator {
45
+ private project: Project;
46
+
47
+ constructor(private options: CodeGeneratorOptions) {
48
+ this.project = new Project(defaultTsMorphProjectSettings);
49
+ }
50
+
51
+ public async generateSchemaDefinition(
52
+ schemaMap: Map<string, Schema>,
53
+ outputFile: string
54
+ ): Promise<void> {
55
+ try {
56
+ const typeInfos = this.extractTypeInfo(schemaMap);
57
+ await this.writeSchemaDefinitionFile(typeInfos, outputFile);
58
+ } catch (error) {
59
+ throw new CodeGenerationError(
60
+ `Failed to generate schema definition: ${error instanceof Error ? error.message : String(error)}`
61
+ );
62
+ }
63
+ }
64
+
65
+ public async generateValidatorFunction(outputFile: string): Promise<void> {
66
+ try {
67
+ const sourceFile = this.project.createSourceFile(outputFile, {}, defaultCreateFileOptions);
68
+
69
+ if (this.options.lazyLoad) {
70
+ this.generateLazyValidator(sourceFile);
71
+ } else {
72
+ this.generateStandardValidator(sourceFile);
73
+ }
74
+
75
+ await this.project.save();
76
+ } catch (error) {
77
+ throw new CodeGenerationError(
78
+ `Failed to generate validator function: ${error instanceof Error ? error.message : String(error)}`
79
+ );
80
+ }
81
+ }
82
+
83
+ public async generateValidationTypes(
84
+ schemaMap: Map<string, Schema>,
85
+ outputFile: string
86
+ ): Promise<void> {
87
+ try {
88
+ const typeInfos = this.extractTypeInfo(schemaMap);
89
+ await this.writeValidationTypesFile(typeInfos, outputFile);
90
+ } catch (error) {
91
+ throw new CodeGenerationError(
92
+ `Failed to generate validation types: ${error instanceof Error ? error.message : String(error)}`
93
+ );
94
+ }
95
+ }
96
+
97
+ private extractTypeInfo(schemaMap: Map<string, Schema>): TypeInfo[] {
98
+ const readerProject = new Project(defaultTsMorphProjectSettings);
99
+ const typeInfos: TypeInfo[] = [];
100
+
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) {
105
+ const dir = path.dirname(filePath);
106
+ const fileWithoutExtension = path.parse(filePath).name;
107
+ const relativeFilePath = path.relative(this.options.outputPath, dir);
108
+ const relativeImportPath = `${relativeFilePath}/${fileWithoutExtension}`;
109
+ const defs = schema.definitions ?? {};
110
+
111
+ const readerSourceFile = readerProject.addSourceFileAtPath(filePath);
112
+
113
+ // Sort definition keys alphabetically
114
+ Object.keys(defs).sort().forEach((symbol) => {
115
+ const typeAlias = readerSourceFile.getTypeAlias(symbol);
116
+ const typeInterface = readerSourceFile.getInterface(symbol);
117
+
118
+ if (typeAlias || typeInterface) {
119
+ typeInfos.push({
120
+ symbol,
121
+ importPath: getPosixPath(relativeImportPath),
122
+ isInterface: !!typeInterface
123
+ });
124
+ }
125
+ });
126
+ }
127
+
128
+ // Sort typeInfos alphabetically by symbol
129
+ return typeInfos.sort((a, b) => a.symbol.localeCompare(b.symbol));
130
+ }
131
+
132
+ private async writeSchemaDefinitionFile(
133
+ typeInfos: TypeInfo[],
134
+ outputFile: string
135
+ ): Promise<void> {
136
+ const sourceFile = this.project.createSourceFile(outputFile, {}, defaultCreateFileOptions);
137
+
138
+ if (this.options.treeShaking) {
139
+ this.generateTreeShakingImports(sourceFile, typeInfos);
140
+ } else {
141
+ this.generateStandardImports(sourceFile, typeInfos);
142
+ }
143
+
144
+ sourceFile.addVariableStatement({
145
+ isExported: true,
146
+ declarationKind: VariableDeclarationKind.Const,
147
+ declarations: [
148
+ {
149
+ name: "schemas",
150
+ type: "Record<keyof ISchema, string>",
151
+ initializer: (writer: CodeBlockWriter) => {
152
+ writer.writeLine(`{`);
153
+ // Sort by symbol for consistent output
154
+ [...typeInfos].sort((a, b) => a.symbol.localeCompare(b.symbol)).forEach(({ symbol }) => {
155
+ writer.writeLine(`["#/definitions/${symbol}"] : "${symbol}",`);
156
+ });
157
+ writer.writeLine(`}`);
158
+ },
159
+ },
160
+ ],
161
+ });
162
+
163
+ sourceFile.addInterface({
164
+ kind: StructureKind.Interface,
165
+ name: "ISchema",
166
+ isExported: true,
167
+ properties: [...typeInfos].sort((a, b) => a.symbol.localeCompare(b.symbol)).map(({ symbol }) => ({
168
+ name: `readonly ["#/definitions/${symbol}"]`,
169
+ type: symbol
170
+ })),
171
+ });
172
+
173
+ await this.project.save();
174
+ }
175
+
176
+ private generateTreeShakingImports(sourceFile: any, typeInfos: TypeInfo[]): void {
177
+ const importMap = new Map<string, string[]>();
178
+
179
+ typeInfos.forEach(({ symbol, importPath }) => {
180
+ const existing = importMap.get(importPath) || [];
181
+ existing.push(symbol);
182
+ importMap.set(importPath, existing);
183
+ });
184
+
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();
189
+ sourceFile.addImportDeclaration({
190
+ namedImports: symbols,
191
+ moduleSpecifier: importPath
192
+ });
193
+ });
194
+ }
195
+
196
+ private generateStandardImports(sourceFile: any, typeInfos: TypeInfo[]): void {
197
+ const importMap = new Map<string, string[]>();
198
+
199
+ typeInfos.forEach(({ symbol, importPath }) => {
200
+ const existing = importMap.get(importPath) || [];
201
+ existing.push(symbol);
202
+ importMap.set(importPath, existing);
203
+ });
204
+
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();
209
+ sourceFile.addImportDeclaration({
210
+ namedImports: symbols,
211
+ moduleSpecifier: importPath
212
+ });
213
+ });
214
+ }
215
+
216
+ private generateLazyValidator(sourceFile: any): void {
217
+ sourceFile.addImportDeclaration({
218
+ namespaceImport: "schema",
219
+ moduleSpecifier: "./validation.schema.json"
220
+ });
221
+ sourceFile.addImportDeclaration({
222
+ namedImports: ["ISchema", "schemas"],
223
+ moduleSpecifier: "./SchemaDefinition"
224
+ });
225
+
226
+ sourceFile.addVariableStatement({
227
+ isExported: true,
228
+ declarationKind: VariableDeclarationKind.Let,
229
+ declarations: [
230
+ {
231
+ name: "validator",
232
+ type: "any",
233
+ initializer: "null"
234
+ }
235
+ ]
236
+ });
237
+
238
+ sourceFile.addFunction({
239
+ name: "getValidator",
240
+ isExported: false,
241
+ statements: (writer: CodeBlockWriter) => {
242
+ writer.writeLine(`if (!validator) {`);
243
+ writer.writeLine(` const Ajv = require("ajv");`);
244
+ writer.writeLine(` validator = new Ajv({ allErrors: true });`);
245
+ writer.writeLine(` validator.compile(schema);`);
246
+ writer.writeLine(`}`);
247
+ writer.writeLine(`return validator;`);
248
+ }
249
+ });
250
+
251
+ sourceFile.addVariableStatement({
252
+ declarationKind: VariableDeclarationKind.Const,
253
+ isExported: true,
254
+ declarations: [
255
+ {
256
+ name: "isValidSchema",
257
+ initializer: (writer: CodeBlockWriter) => {
258
+ writer.writeLine(`<T extends keyof typeof schemas>(data: unknown, schemaKeyRef: T): data is ISchema[T] => {`);
259
+ writer.writeLine(`const v = getValidator();`);
260
+ writer.writeLine(`v.validate(schemaKeyRef as string, data);`);
261
+ writer.writeLine(`return Boolean(v.errors) === false;`);
262
+ writer.writeLine(`}`);
263
+ },
264
+ },
265
+ ],
266
+ });
267
+ }
268
+
269
+ private generateStandardValidator(sourceFile: any): void {
270
+ sourceFile.addImportDeclaration({
271
+ namespaceImport: "schema",
272
+ moduleSpecifier: "./validation.schema.json"
273
+ });
274
+ sourceFile.addImportDeclaration({
275
+ defaultImport: "Ajv",
276
+ moduleSpecifier: "ajv"
277
+ });
278
+ sourceFile.addImportDeclaration({
279
+ namedImports: ["ISchema", "schemas"],
280
+ moduleSpecifier: "./SchemaDefinition"
281
+ });
282
+
283
+ sourceFile.addVariableStatement({
284
+ isExported: true,
285
+ declarationKind: VariableDeclarationKind.Const,
286
+ declarations: [
287
+ {
288
+ name: "validator",
289
+ initializer: (writer: CodeBlockWriter) => {
290
+ writer.writeLine(`new Ajv({ allErrors: true });`);
291
+ writer.writeLine(`validator.compile(schema)`);
292
+ },
293
+ },
294
+ ],
295
+ });
296
+
297
+ sourceFile.addVariableStatement({
298
+ declarationKind: VariableDeclarationKind.Const,
299
+ isExported: true,
300
+ declarations: [
301
+ {
302
+ name: "isValidSchema",
303
+ initializer: (writer: CodeBlockWriter) => {
304
+ writer.writeLine(`<T extends keyof typeof schemas>(data: unknown, schemaKeyRef: T): data is ISchema[T] => {`);
305
+ writer.writeLine(`validator.validate(schemaKeyRef as string, data);`);
306
+ writer.writeLine(`return Boolean(validator.errors) === false;`);
307
+ writer.writeLine(`}`);
308
+ },
309
+ },
310
+ ],
311
+ });
312
+ }
313
+
314
+ private async writeValidationTypesFile(
315
+ typeInfos: TypeInfo[],
316
+ outputFile: string
317
+ ): Promise<void> {
318
+ const sourceFile = this.project.createSourceFile(outputFile, {}, defaultCreateFileOptions);
319
+
320
+ const importMap = new Map<string, string[]>();
321
+ // Sort typeInfos for consistent processing
322
+ const sortedTypeInfos = [...typeInfos].sort((a, b) => a.symbol.localeCompare(b.symbol));
323
+
324
+ sortedTypeInfos.forEach(({ symbol, importPath }) => {
325
+ const existing = importMap.get(importPath) || [];
326
+ existing.push(symbol);
327
+ importMap.set(importPath, existing);
328
+ });
329
+
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();
334
+ const declaration = sourceFile.addImportDeclaration({
335
+ moduleSpecifier: importPath
336
+ });
337
+ symbols.forEach((symbol) => {
338
+ declaration.addNamedImport({
339
+ name: symbol,
340
+ alias: `_${symbol}`
341
+ });
342
+ });
343
+ });
344
+
345
+ if (this.options.treeShaking) {
346
+ sortedTypeInfos.forEach(({ symbol }) => {
347
+ sourceFile.addTypeAlias({
348
+ name: symbol,
349
+ type: `_${symbol}`,
350
+ isExported: true
351
+ });
352
+ });
353
+ } else {
354
+ const namespace = sourceFile.addModule({
355
+ name: "ValidationType",
356
+ isExported: true,
357
+ });
358
+
359
+ sortedTypeInfos.forEach(({ symbol }) => {
360
+ namespace.addTypeAlias({
361
+ name: symbol,
362
+ type: `_${symbol}`,
363
+ isExported: true
364
+ });
365
+ });
366
+ }
367
+
368
+ await this.project.save();
369
+ }
370
+ }
@@ -0,0 +1,216 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { FileDiscovery } from "./FileDiscovery";
4
+ import { FileDiscoveryError } from "../errors";
5
+
6
+ const testDir = path.resolve(__dirname, "../../.test-tmp/file-discovery");
7
+ const cacheDir = path.resolve(testDir, ".cache");
8
+
9
+ const createTestFile = async (filePath: string, content: string = "test content") => {
10
+ const fullPath = path.resolve(testDir, filePath);
11
+ await fs.promises.mkdir(path.dirname(fullPath), { recursive: true });
12
+ await fs.promises.writeFile(fullPath, content);
13
+ return fullPath;
14
+ };
15
+
16
+ const cleanup = async () => {
17
+ if (fs.existsSync(testDir)) {
18
+ await fs.promises.rm(testDir, { recursive: true, force: true });
19
+ }
20
+ };
21
+
22
+ beforeEach(cleanup);
23
+ afterAll(cleanup);
24
+
25
+ describe("FileDiscovery", () => {
26
+ describe("discoverFiles", () => {
27
+ it("should find files matching glob pattern", async () => {
28
+ await createTestFile("types/user.jsonschema.ts", "export interface User {}");
29
+ await createTestFile("types/product.jsonschema.ts", "export interface Product {}");
30
+ await createTestFile("types/order.ts", "export interface Order {}");
31
+
32
+ const discovery = new FileDiscovery({
33
+ glob: "*.jsonschema.ts",
34
+ rootPath: testDir
35
+ });
36
+
37
+ const files = await discovery.discoverFiles();
38
+
39
+ expect(files).toHaveLength(2);
40
+ expect(files.map(f => path.basename(f.path))).toEqual(
41
+ expect.arrayContaining(["user.jsonschema.ts", "product.jsonschema.ts"])
42
+ );
43
+ });
44
+
45
+ it("should handle nested directory patterns", async () => {
46
+ await createTestFile("api/v1/user.jsonschema.ts", "export interface User {}");
47
+ await createTestFile("api/v2/product.jsonschema.ts", "export interface Product {}");
48
+ await createTestFile("utils/helper.ts", "export const helper = () => {}");
49
+
50
+ const discovery = new FileDiscovery({
51
+ glob: "**/*.jsonschema.ts",
52
+ rootPath: testDir
53
+ });
54
+
55
+ const files = await discovery.discoverFiles();
56
+
57
+ expect(files).toHaveLength(2);
58
+ expect(files.every(f => f.path.includes(".jsonschema.ts"))).toBe(true);
59
+ });
60
+
61
+ it("should throw error when no files found", async () => {
62
+ const discovery = new FileDiscovery({
63
+ glob: "*.jsonschema.ts",
64
+ rootPath: testDir
65
+ });
66
+
67
+ await expect(discovery.discoverFiles()).rejects.toThrow(FileDiscoveryError);
68
+ });
69
+
70
+ it("should throw error for invalid directory", async () => {
71
+ const discovery = new FileDiscovery({
72
+ glob: "*.jsonschema.ts",
73
+ rootPath: "/nonexistent/path"
74
+ });
75
+
76
+ await expect(discovery.discoverFiles()).rejects.toThrow(FileDiscoveryError);
77
+ });
78
+ });
79
+
80
+ describe("caching", () => {
81
+ it("should generate file hashes when caching enabled", async () => {
82
+ await createTestFile("user.jsonschema.ts", "export interface User {}");
83
+
84
+ const discovery = new FileDiscovery({
85
+ glob: "*.jsonschema.ts",
86
+ rootPath: testDir,
87
+ cacheEnabled: true,
88
+ cachePath: cacheDir
89
+ });
90
+
91
+ const files = await discovery.discoverFiles();
92
+
93
+ expect(files).toHaveLength(1);
94
+ expect(files[0].hash).toBeDefined();
95
+ expect(files[0].lastModified).toBeDefined();
96
+ expect(typeof files[0].hash).toBe("string");
97
+ });
98
+
99
+ it("should save and load cache correctly", async () => {
100
+ await createTestFile("user.jsonschema.ts", "export interface User {}");
101
+
102
+ const discovery = new FileDiscovery({
103
+ glob: "*.jsonschema.ts",
104
+ rootPath: testDir,
105
+ cacheEnabled: true,
106
+ cachePath: cacheDir
107
+ });
108
+
109
+ // First run - should create cache
110
+ const files1 = await discovery.discoverFiles();
111
+ expect(fs.existsSync(path.join(cacheDir, "file-hashes.json"))).toBe(true);
112
+
113
+ // Second run - should load from cache
114
+ const discovery2 = new FileDiscovery({
115
+ glob: "*.jsonschema.ts",
116
+ rootPath: testDir,
117
+ cacheEnabled: true,
118
+ cachePath: cacheDir
119
+ });
120
+
121
+ const files2 = await discovery2.discoverFiles();
122
+ expect(files1[0].hash).toBe(files2[0].hash);
123
+ });
124
+
125
+ it("should detect file changes", async () => {
126
+ const filePath = await createTestFile("user.jsonschema.ts", "export interface User {}");
127
+
128
+ const discovery = new FileDiscovery({
129
+ glob: "*.jsonschema.ts",
130
+ rootPath: testDir,
131
+ cacheEnabled: true,
132
+ cachePath: cacheDir
133
+ });
134
+
135
+ const files1 = await discovery.discoverFiles();
136
+ const originalHash = files1[0].hash!;
137
+
138
+ // Modify file
139
+ await fs.promises.writeFile(filePath, "export interface User { name: string; }");
140
+
141
+ const files2 = await discovery.discoverFiles();
142
+ const newHash = files2[0].hash!;
143
+
144
+ expect(discovery.hasFileChanged(filePath, originalHash)).toBe(true);
145
+ expect(originalHash).not.toBe(newHash);
146
+ });
147
+
148
+ it("should clear cache correctly", async () => {
149
+ await createTestFile("user.jsonschema.ts", "export interface User {}");
150
+
151
+ const discovery = new FileDiscovery({
152
+ glob: "*.jsonschema.ts",
153
+ rootPath: testDir,
154
+ cacheEnabled: true,
155
+ cachePath: cacheDir
156
+ });
157
+
158
+ await discovery.discoverFiles();
159
+ expect(fs.existsSync(path.join(cacheDir, "file-hashes.json"))).toBe(true);
160
+
161
+ discovery.clearCache();
162
+ expect(fs.existsSync(path.join(cacheDir, "file-hashes.json"))).toBe(false);
163
+ });
164
+
165
+ it("should handle corrupted cache gracefully", async () => {
166
+ await createTestFile("user.jsonschema.ts", "export interface User {}");
167
+
168
+ // Create corrupted cache file
169
+ await fs.promises.mkdir(cacheDir, { recursive: true });
170
+ await fs.promises.writeFile(path.join(cacheDir, "file-hashes.json"), "invalid json");
171
+
172
+ const discovery = new FileDiscovery({
173
+ glob: "*.jsonschema.ts",
174
+ rootPath: testDir,
175
+ cacheEnabled: true,
176
+ cachePath: cacheDir
177
+ });
178
+
179
+ // Should not throw error and should work normally
180
+ const files = await discovery.discoverFiles();
181
+ expect(files).toHaveLength(1);
182
+ });
183
+ });
184
+
185
+ describe("file patterns", () => {
186
+ it("should support multiple extensions", async () => {
187
+ await createTestFile("user.jsonschema.ts", "export interface User {}");
188
+ await createTestFile("product.jsonschema.tsx", "export interface Product {}");
189
+ await createTestFile("order.types.ts", "export interface Order {}");
190
+
191
+ const discovery = new FileDiscovery({
192
+ glob: "*.{jsonschema.ts,jsonschema.tsx}",
193
+ rootPath: testDir
194
+ });
195
+
196
+ const files = await discovery.discoverFiles();
197
+ expect(files).toHaveLength(2);
198
+ });
199
+
200
+ it("should exclude files not matching pattern", async () => {
201
+ await createTestFile("user.jsonschema.ts", "export interface User {}");
202
+ await createTestFile("user.test.ts", "test file");
203
+ await createTestFile("user.spec.ts", "spec file");
204
+ await createTestFile("README.md", "readme");
205
+
206
+ const discovery = new FileDiscovery({
207
+ glob: "*.jsonschema.ts",
208
+ rootPath: testDir
209
+ });
210
+
211
+ const files = await discovery.discoverFiles();
212
+ expect(files).toHaveLength(1);
213
+ expect(files[0].path).toContain("user.jsonschema.ts");
214
+ });
215
+ });
216
+ });