ts-runtime-validation 1.6.15 → 1.7.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 (79) hide show
  1. package/.claude/settings.local.json +9 -0
  2. package/CONTRIBUTING.md +430 -0
  3. package/README.md +444 -64
  4. package/dist/ICommandOptions.js +3 -0
  5. package/dist/ICommandOptions.js.map +1 -0
  6. package/dist/SchemaGenerator.integration.test.js +323 -0
  7. package/dist/SchemaGenerator.integration.test.js.map +1 -0
  8. package/dist/SchemaGenerator.js +120 -0
  9. package/dist/SchemaGenerator.js.map +1 -0
  10. package/dist/SchemaGenerator.test.js +132 -0
  11. package/dist/SchemaGenerator.test.js.map +1 -0
  12. package/dist/errors/index.js +95 -0
  13. package/dist/errors/index.js.map +1 -0
  14. package/dist/errors/index.test.js +232 -0
  15. package/dist/errors/index.test.js.map +1 -0
  16. package/dist/getPosixPath.js +13 -0
  17. package/dist/getPosixPath.js.map +1 -0
  18. package/dist/index.js +27 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/lib.js.map +1 -0
  21. package/dist/services/CodeGenerator.js +305 -0
  22. package/dist/services/CodeGenerator.js.map +1 -0
  23. package/dist/services/FileDiscovery.js +121 -0
  24. package/dist/services/FileDiscovery.js.map +1 -0
  25. package/dist/services/FileDiscovery.test.js +184 -0
  26. package/dist/services/FileDiscovery.test.js.map +1 -0
  27. package/dist/services/SchemaProcessor.js +182 -0
  28. package/dist/services/SchemaProcessor.js.map +1 -0
  29. package/dist/services/SchemaProcessor.test.js +395 -0
  30. package/dist/services/SchemaProcessor.test.js.map +1 -0
  31. package/dist/services/SchemaWriter.js +76 -0
  32. package/dist/services/SchemaWriter.js.map +1 -0
  33. package/dist/services/SchemaWriter.test.js +255 -0
  34. package/dist/services/SchemaWriter.test.js.map +1 -0
  35. package/dist/test/basic-scenario/types.jsonschema.js +3 -0
  36. package/dist/test/basic-scenario/types.jsonschema.js.map +1 -0
  37. package/dist/test/duplicate-symbols-different-implementation/IBaseType.js +3 -0
  38. package/dist/test/duplicate-symbols-different-implementation/IBaseType.js.map +1 -0
  39. package/dist/test/duplicate-symbols-different-implementation/IBaseTypeDefinitionReplicated.js +3 -0
  40. package/dist/test/duplicate-symbols-different-implementation/IBaseTypeDefinitionReplicated.js.map +1 -0
  41. package/dist/test/duplicate-symbols-different-implementation/IBasicTypesA.jsonschema.js +3 -0
  42. package/dist/test/duplicate-symbols-different-implementation/IBasicTypesA.jsonschema.js.map +1 -0
  43. package/dist/test/duplicate-symbols-different-implementation/IBasicTypesB.jsonschema.js +3 -0
  44. package/dist/test/duplicate-symbols-different-implementation/IBasicTypesB.jsonschema.js.map +1 -0
  45. package/dist/test/duplicate-symbols-identitcal-implementation/IBaseType.js +3 -0
  46. package/dist/test/duplicate-symbols-identitcal-implementation/IBaseType.js.map +1 -0
  47. package/dist/test/duplicate-symbols-identitcal-implementation/IBaseTypeDefinitionReplicated.js +3 -0
  48. package/dist/test/duplicate-symbols-identitcal-implementation/IBaseTypeDefinitionReplicated.js.map +1 -0
  49. package/dist/test/duplicate-symbols-identitcal-implementation/IBasicTypesA.jsonschema.js +3 -0
  50. package/dist/test/duplicate-symbols-identitcal-implementation/IBasicTypesA.jsonschema.js.map +1 -0
  51. package/dist/test/duplicate-symbols-identitcal-implementation/IBasicTypesB.jsonschema.js +3 -0
  52. package/dist/test/duplicate-symbols-identitcal-implementation/IBasicTypesB.jsonschema.js.map +1 -0
  53. package/dist/test/output/duplicate-symbols-identitcal-implementation/ValidationType.js +3 -0
  54. package/dist/test/output/duplicate-symbols-identitcal-implementation/ValidationType.js.map +1 -0
  55. package/dist/test/output/duplicate-symbols-identitcal-implementation/isValidSchema.js +49 -0
  56. package/dist/test/output/duplicate-symbols-identitcal-implementation/isValidSchema.js.map +1 -0
  57. package/dist/utils/ProgressReporter.js +67 -0
  58. package/dist/utils/ProgressReporter.js.map +1 -0
  59. package/dist/utils/ProgressReporter.test.js +267 -0
  60. package/dist/utils/ProgressReporter.test.js.map +1 -0
  61. package/dist/writeLine.js +12 -0
  62. package/dist/writeLine.js.map +1 -0
  63. package/package.json +2 -2
  64. package/src/ICommandOptions.ts +7 -0
  65. package/src/SchemaGenerator.integration.test.ts +411 -0
  66. package/src/SchemaGenerator.test.ts +7 -0
  67. package/src/SchemaGenerator.ts +112 -298
  68. package/src/errors/index.test.ts +319 -0
  69. package/src/errors/index.ts +92 -0
  70. package/src/index.ts +7 -0
  71. package/src/services/CodeGenerator.ts +352 -0
  72. package/src/services/FileDiscovery.test.ts +216 -0
  73. package/src/services/FileDiscovery.ts +137 -0
  74. package/src/services/SchemaProcessor.test.ts +464 -0
  75. package/src/services/SchemaProcessor.ts +173 -0
  76. package/src/services/SchemaWriter.test.ts +304 -0
  77. package/src/services/SchemaWriter.ts +75 -0
  78. package/src/utils/ProgressReporter.test.ts +357 -0
  79. package/src/utils/ProgressReporter.ts +76 -0
@@ -1,316 +1,130 @@
1
- import { fdir } from "fdir";
2
1
  import { ICommandOptions } from "./ICommandOptions";
3
- import fs from "fs";
4
- import picomatch from "picomatch";
5
2
  import path from "path";
6
- import {
7
- Project,
8
- IndentationText,
9
- NewLineKind,
10
- QuoteKind,
11
- StructureKind,
12
- VariableDeclarationKind,
13
- CodeBlockWriter,
14
- ProjectOptions,
15
- SourceFileCreateOptions,
16
- } from "ts-morph";
17
- import * as tsj from "ts-json-schema-generator";
18
- import { Config, Schema } from "ts-json-schema-generator";
19
- import assert from "assert";
20
- import { writeLine } from "./writeLine";
21
- import { getPosixPath } from "./getPosixPath";
22
-
23
- const defaultTsMorphProjectSettings: ProjectOptions = {
24
- manipulationSettings: {
25
- indentationText: IndentationText.FourSpaces,
26
- newLineKind: NewLineKind.LineFeed,
27
- quoteKind: QuoteKind.Double,
28
- usePrefixAndSuffixTextForRename: false,
29
- useTrailingCommas: true,
30
- },
31
- };
32
-
33
- const defaultCreateFileOptions: SourceFileCreateOptions = {
34
- overwrite: true,
35
- };
3
+ import { FileDiscovery } from "./services/FileDiscovery";
4
+ import { SchemaProcessor } from "./services/SchemaProcessor";
5
+ import { CodeGenerator } from "./services/CodeGenerator";
6
+ import { SchemaWriter } from "./services/SchemaWriter";
7
+ import { ProgressReporter } from "./utils/ProgressReporter";
8
+ import { formatError, isKnownError } from "./errors";
36
9
 
37
10
  const validationSchemaFileName = "validation.schema.json";
38
11
  const schemaDefinitionFileName = "SchemaDefinition.ts";
39
12
  const validationInterfacesFile = "ValidationType.ts";
13
+ const isValidSchemaFileName = "isValidSchema.ts";
40
14
 
41
15
  export class SchemaGenerator {
42
16
  private outputPath = path.join(this.options.rootPath, this.options.output);
43
- private jsonSchemaOutputFile = path.join(this.options.rootPath, this.options.output, validationSchemaFileName);
44
- private tsSchemaDefinitionOutputFile = path.join(this.options.rootPath, this.options.output, schemaDefinitionFileName);
45
- private validationTypesOutputFile = path.join(this.options.rootPath, this.options.output, validationInterfacesFile);
46
- private isValidSchemaOutputFile = path.join(this.options.rootPath, this.options.output, "isValidSchema.ts");
47
-
48
- public constructor(private options: ICommandOptions) {}
49
-
50
- public Generate = async () => {
51
- const { helpers, glob } = this.options;
52
- const fileList = await this.getMatchingFiles();
53
-
54
- console.log(`Found ${fileList.length} schema file(s)`);
55
- if (fileList.length === 0) {
56
- writeLine(`Aborting - no files found with glob: ${glob}`);
57
- return;
58
- }
59
- const fileSchemas = await this.getJsonSchemasForFiles(fileList);
60
-
61
- if (fileSchemas.size === 0) {
62
- writeLine(`Aborting - no types found: ${glob}`);
63
- return;
64
- }
65
- this.writeSchemaMapToValidationSchema(fileSchemas);
66
- if (helpers === false) {
67
- writeLine("Skipping helper file generation");
68
- return;
69
- }
70
- await this.writeSchemaMapToValidationTypes(fileSchemas);
71
- this.writeValidatorFunction();
72
- writeLine("Writing validation types file");
73
- this.writeValidationTypes(fileSchemas);
74
- };
75
-
76
- private getMatchingFiles = async () => {
77
- const { glob, rootPath } = this.options;
78
- const api = new fdir({
79
- includeBasePath: true,
80
- includeDirs: false,
81
- filters: [
82
- (path) => {
83
- return picomatch.isMatch(path, glob, { contains: true });
84
- },
85
- ],
86
- }).crawl(rootPath);
87
- return api.withPromise();
88
- };
89
-
90
- private getJsonSchemasForFiles = async (filesList: Array<string>) => {
91
- const { additionalProperties, tsconfigPath } = this.options;
92
- const schemaMap = new Map<string, Schema>();
93
- const tsconfig = tsconfigPath.length > 0 ? tsconfigPath : undefined;
94
- filesList.forEach((file, index) => {
95
- writeLine(`\rProcessing file ${index + 1} of ${filesList.length}: ${file}`);
96
- const config: Config = {
97
- path: file,
98
- type: "*",
99
- additionalProperties,
100
- encodeRefs: false,
101
- sortProps: true,
102
- ...(tsconfig !== null ? { tsconfig } : {}),
103
- };
104
-
105
- const schemaGenerator = tsj.createGenerator(config);
106
- const fileSchemas = schemaGenerator.createSchema(config.type);
107
- schemaMap.set(file, fileSchemas);
108
- });
109
- return schemaMap;
110
- };
111
-
112
- private getSchemaVersion = (schemaMap: Map<string, Schema>) => {
113
- const firstEntry = schemaMap.values().next().value;
114
- return firstEntry?.["$schema"] ?? "";
115
- };
116
-
117
- private ensureOutputPathExists = () => {
118
- if (!fs.existsSync(this.outputPath)) {
119
- fs.mkdirSync(this.outputPath, { recursive: true });
120
- }
121
- };
122
-
123
- private writeSchemaMapToValidationSchema = (schemaMap: Map<string, Schema>) => {
124
- const definitions: { [id: string]: Schema } = {};
125
- schemaMap.forEach((fileSchema) => {
126
- const defs = fileSchema.definitions ?? {};
127
-
128
- Object.keys(defs).forEach((key) => {
129
- if (definitions[key] !== undefined) {
130
- try {
131
- assert.deepEqual(definitions[key], defs[key]);
132
- } catch (e) {
133
- console.error(
134
- `Duplicate symbol: ${key} found with varying definitions.\nDefinition 1:\n${JSON.stringify(
135
- definitions[key],
136
- null,
137
- 4
138
- )}\nDefinition 2:\n${JSON.stringify(defs[key], null, 4)}`
139
- );
140
- throw e;
141
- }
142
- }
143
- const schema = defs[key] as Schema;
144
- definitions[key] = schema;
145
- });
146
- });
147
- const outputBuffer: Schema = {
148
- $schema: this.getSchemaVersion(schemaMap),
149
- definitions,
150
- };
151
-
152
- this.ensureOutputPathExists();
153
- fs.writeFileSync(this.jsonSchemaOutputFile, JSON.stringify(outputBuffer, null, 4));
154
- };
155
-
156
- private writeSchemaMapToValidationTypes = async (schemaMap: Map<string, Schema>) => {
157
- const project = new Project(defaultTsMorphProjectSettings);
158
- const readerProject = new Project(defaultTsMorphProjectSettings);
159
-
160
- const symbols: Array<string> = [];
161
-
162
- const importMap = new Map<string, Array<string>>();
163
- schemaMap.forEach((schema, filePath) => {
164
- const dir = path.dirname(filePath);
165
- const fileWithoutExtension = path.parse(filePath).name;
166
- const relativeFilePath = path.relative(this.outputPath, dir);
167
- const relativeImportPath = `${relativeFilePath}/${fileWithoutExtension}`;
168
- const defs = schema.definitions ?? {};
169
-
170
- const readerSourceFile = readerProject.addSourceFileAtPath(filePath);
171
-
172
- Object.keys(defs).forEach((symbol) => {
173
- const typeAlias = readerSourceFile.getTypeAlias(symbol);
174
- const typeInterface = readerSourceFile.getInterface(symbol);
175
- const hasTypeOrInterface = (typeAlias ?? typeInterface) !== undefined;
176
- if (hasTypeOrInterface) {
177
- const namedImports = importMap.get(relativeImportPath) ?? [];
178
- namedImports.push(symbol);
179
- importMap.set(relativeImportPath, namedImports);
180
- symbols.push(symbol);
181
- }
182
- });
183
- });
184
-
185
- const sourceFile = project.createSourceFile(this.tsSchemaDefinitionOutputFile, {}, defaultCreateFileOptions);
186
-
187
- importMap.forEach((namedImports, importPath) => {
188
- sourceFile.addImportDeclaration({ namedImports, moduleSpecifier: getPosixPath(importPath) });
17
+ private jsonSchemaOutputFile = path.join(this.outputPath, validationSchemaFileName);
18
+ private tsSchemaDefinitionOutputFile = path.join(this.outputPath, schemaDefinitionFileName);
19
+ private validationTypesOutputFile = path.join(this.outputPath, validationInterfacesFile);
20
+ private isValidSchemaOutputFile = path.join(this.outputPath, isValidSchemaFileName);
21
+
22
+ private fileDiscovery: FileDiscovery;
23
+ private schemaProcessor: SchemaProcessor;
24
+ private codeGenerator: CodeGenerator;
25
+ private schemaWriter: SchemaWriter;
26
+ private progressReporter: ProgressReporter;
27
+
28
+ public constructor(private options: ICommandOptions) {
29
+ this.fileDiscovery = new FileDiscovery({
30
+ glob: options.glob,
31
+ rootPath: options.rootPath,
32
+ cacheEnabled: options.cache || false,
33
+ cachePath: path.join(options.rootPath, ".ts-runtime-validation-cache")
189
34
  });
190
-
191
- sourceFile.addVariableStatement({
192
- isExported: true,
193
- declarationKind: VariableDeclarationKind.Const,
194
- declarations: [
195
- {
196
- name: "schemas",
197
- type: "Record<keyof ISchema, string>",
198
- initializer: (writer: CodeBlockWriter) => {
199
- writer.writeLine(`{`);
200
- symbols.forEach((symbol) => {
201
- writer.writeLine(`["#/definitions/${symbol}"] : "${symbol}",`);
202
- }),
203
- writer.writeLine(`}`);
204
- },
205
- },
206
- ],
35
+
36
+ this.schemaProcessor = new SchemaProcessor({
37
+ additionalProperties: options.additionalProperties,
38
+ tsconfigPath: options.tsconfigPath || undefined,
39
+ parallel: options.parallel !== false,
40
+ verbose: options.verbose || false
207
41
  });
208
-
209
- sourceFile.addInterface({
210
- kind: StructureKind.Interface,
211
- name: "ISchema",
212
- isExported: true,
213
- properties: symbols.map((symbol) => {
214
- return { name: `readonly ["#/definitions/${symbol}"]`, type: symbol };
215
- }),
42
+
43
+ this.codeGenerator = new CodeGenerator({
44
+ outputPath: this.outputPath,
45
+ minify: options.minify || false,
46
+ treeShaking: options.treeShaking || false,
47
+ lazyLoad: options.lazyLoad || false
216
48
  });
217
-
218
- await project.save();
219
- };
220
-
221
- private writeValidatorFunction = async () => {
222
- const project = new Project(defaultTsMorphProjectSettings);
223
- const sourceFile = project.createSourceFile(this.isValidSchemaOutputFile, {}, defaultCreateFileOptions);
224
- sourceFile.addImportDeclaration({ namespaceImport: "schema", moduleSpecifier: `./${validationSchemaFileName}` });
225
- sourceFile.addImportDeclaration({ defaultImport: "Ajv", moduleSpecifier: "ajv" });
226
- sourceFile.addImportDeclaration({
227
- namedImports: ["ISchema", "schemas"],
228
- moduleSpecifier: `./${path.parse(schemaDefinitionFileName).name}`,
49
+
50
+ this.schemaWriter = new SchemaWriter({
51
+ outputPath: this.outputPath,
52
+ minify: options.minify || false
229
53
  });
230
- sourceFile.addVariableStatement({
231
- isExported: true,
232
- declarationKind: VariableDeclarationKind.Const,
233
- declarations: [
234
- {
235
- name: "validator",
236
- initializer: (writer: CodeBlockWriter) => {
237
- writer.writeLine(`new Ajv({ allErrors: true });`);
238
- writer.writeLine(`validator.compile(schema)`);
239
- },
240
- },
241
- ],
54
+
55
+ this.progressReporter = new ProgressReporter({
56
+ enabled: options.progress || false,
57
+ showBar: true
242
58
  });
59
+ }
243
60
 
244
- sourceFile.addVariableStatement({
245
- declarationKind: VariableDeclarationKind.Const,
246
- isExported: true,
247
- declarations: [
248
- {
249
- name: "isValidSchema",
250
- initializer: (writer: CodeBlockWriter) => {
251
- writer.writeLine(`<T extends keyof typeof schemas>(data: unknown, schemaKeyRef: T): data is ISchema[T] => {`);
252
- writer.writeLine(`validator.validate(schemaKeyRef as string, data);`);
253
- writer.writeLine(`return Boolean(validator.errors) === false;`);
254
- writer.writeLine(`}`);
255
- },
256
- },
257
- ],
258
- });
259
- await project.save();
61
+ public Generate = async () => {
62
+ try {
63
+ this.progressReporter.start("Starting schema generation...");
64
+
65
+ const { helpers } = this.options;
66
+
67
+ // Discover files
68
+ this.progressReporter.update(0, "Discovering files...");
69
+ const files = await this.fileDiscovery.discoverFiles();
70
+
71
+ if (this.options.verbose) {
72
+ console.log(`Found ${files.length} schema file(s)`);
73
+ files.forEach(file => console.log(` - ${file.path}`));
74
+ }
75
+
76
+ // Process schemas
77
+ this.progressReporter.update(1, "Processing TypeScript files...");
78
+ this.progressReporter.options.total = files.length + 4; // files + 4 generation steps
79
+
80
+ const schemaMap = await this.schemaProcessor.processFiles(files);
81
+
82
+ if (schemaMap.size === 0) {
83
+ console.log("No types found to generate schemas for");
84
+ return;
85
+ }
86
+
87
+ // Validate schema compatibility
88
+ this.progressReporter.update(files.length + 1, "Validating schema compatibility...");
89
+ this.schemaProcessor.validateSchemaCompatibility(schemaMap);
90
+
91
+ // Merge and write JSON schema
92
+ this.progressReporter.update(files.length + 2, "Writing JSON schema...");
93
+ const mergedSchema = this.schemaProcessor.mergeSchemas(schemaMap);
94
+ await this.schemaWriter.writeJsonSchema(mergedSchema, this.jsonSchemaOutputFile);
95
+
96
+ if (helpers === false) {
97
+ this.progressReporter.complete("Schema generation completed (helpers skipped)");
98
+ return;
99
+ }
100
+
101
+ // Generate TypeScript helpers
102
+ this.progressReporter.update(files.length + 3, "Generating TypeScript helpers...");
103
+ await Promise.all([
104
+ this.codeGenerator.generateSchemaDefinition(schemaMap, this.tsSchemaDefinitionOutputFile),
105
+ this.codeGenerator.generateValidatorFunction(this.isValidSchemaOutputFile),
106
+ this.codeGenerator.generateValidationTypes(schemaMap, this.validationTypesOutputFile)
107
+ ]);
108
+
109
+ this.progressReporter.complete("Schema generation completed successfully");
110
+
111
+ } catch (error) {
112
+ const formattedError = formatError(error, this.options.verbose || false);
113
+ console.error(`Schema generation failed: ${formattedError}`);
114
+
115
+ if (!isKnownError(error) && this.options.verbose) {
116
+ console.error(error);
117
+ }
118
+
119
+ throw error;
120
+ }
260
121
  };
261
122
 
262
- private writeValidationTypes = async (schemaMap: Map<string, Schema>) => {
263
- const project = new Project(defaultTsMorphProjectSettings);
264
- const readerProject = new Project(defaultTsMorphProjectSettings);
265
-
266
- const symbols: Array<string> = [];
267
-
268
- const importMap = new Map<string, Array<string>>();
269
- schemaMap.forEach((schema, filePath) => {
270
- const dir = path.dirname(filePath);
271
- const fileWithoutExtension = path.parse(filePath).name;
272
- const relativeFilePath = path.relative(this.outputPath, dir);
273
- const relativeImportPath = `${relativeFilePath}/${fileWithoutExtension}`;
274
- const defs = schema.definitions ?? {};
275
-
276
- const readerSourceFile = readerProject.addSourceFileAtPath(filePath);
277
-
278
- Object.keys(defs).forEach((symbol) => {
279
- const typeAlias = readerSourceFile.getTypeAlias(symbol);
280
- const typeInterface = readerSourceFile.getInterface(symbol);
281
- const hasTypeOrInterface = (typeAlias ?? typeInterface) !== undefined;
282
- if (hasTypeOrInterface) {
283
- const namedImports = importMap.get(relativeImportPath) ?? [];
284
- namedImports.push(symbol);
285
- importMap.set(relativeImportPath, namedImports);
286
- symbols.push(symbol);
287
- }
288
- });
289
- });
290
-
291
- const sourceFile = project.createSourceFile(this.validationTypesOutputFile, {}, defaultCreateFileOptions);
292
-
293
- importMap.forEach((namedImports, importPath) => {
294
- const declaration = sourceFile.addImportDeclaration({ moduleSpecifier: getPosixPath(importPath) });
295
- namedImports.forEach((namedImport) => {
296
- const name = namedImport.valueOf();
297
- const alias = `_${name}`;
298
- declaration.addNamedImport({ name, alias });
299
- });
300
- });
301
- const namespace = sourceFile.addModule({
302
- name: "ValidationType",
303
- isExported: true,
304
- });
305
-
306
- importMap.forEach((namedImports) => {
307
- namedImports.forEach((namedImport) => {
308
- const name = namedImport.valueOf();
309
- const alias = `_${name}`;
310
- namespace.addTypeAlias({ name, type: alias, isExported: true });
311
- });
312
- });
313
-
314
- await project.save();
315
- };
123
+ public clearCache(): void {
124
+ this.fileDiscovery.clearCache();
125
+ }
126
+
127
+ public async cleanOutput(): Promise<void> {
128
+ await this.schemaWriter.cleanOutputDirectory();
129
+ }
316
130
  }