apify-schema-tools 3.0.0 → 3.2.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 (148) hide show
  1. package/.cspell/custom-dictionary.txt +1 -0
  2. package/.node-version +1 -1
  3. package/CHANGELOG.md +13 -1
  4. package/biome.json +24 -18
  5. package/check-samples.sh +4 -0
  6. package/dist/apify-schema-tools.js +16 -21
  7. package/dist/apify-schema-tools.js.map +1 -1
  8. package/dist/apify.d.ts +1 -1
  9. package/dist/apify.d.ts.map +1 -1
  10. package/dist/apify.js +22 -8
  11. package/dist/apify.js.map +1 -1
  12. package/dist/cli/check.d.ts +5 -0
  13. package/dist/cli/check.d.ts.map +1 -0
  14. package/dist/cli/check.js +86 -0
  15. package/dist/cli/check.js.map +1 -0
  16. package/dist/cli/init.d.ts +5 -0
  17. package/dist/cli/init.d.ts.map +1 -0
  18. package/dist/cli/init.js +92 -0
  19. package/dist/cli/init.js.map +1 -0
  20. package/dist/cli/sync.d.ts +5 -0
  21. package/dist/cli/sync.d.ts.map +1 -0
  22. package/dist/cli/sync.js +112 -0
  23. package/dist/cli/sync.js.map +1 -0
  24. package/dist/configuration.d.ts +16 -5
  25. package/dist/configuration.d.ts.map +1 -1
  26. package/dist/configuration.js +19 -7
  27. package/dist/configuration.js.map +1 -1
  28. package/dist/json-schema-conflicts.js +3 -3
  29. package/dist/json-schema-conflicts.js.map +1 -1
  30. package/dist/json-schemas.d.ts +1 -1
  31. package/dist/json-schemas.d.ts.map +1 -1
  32. package/dist/json-schemas.js +1 -1
  33. package/dist/json-schemas.js.map +1 -1
  34. package/dist/main.d.ts +4 -0
  35. package/dist/main.d.ts.map +1 -0
  36. package/dist/main.js +19 -0
  37. package/dist/main.js.map +1 -0
  38. package/dist/middle-schema/compare-schemas.d.ts +3 -0
  39. package/dist/middle-schema/compare-schemas.d.ts.map +1 -0
  40. package/dist/middle-schema/compare-schemas.js +90 -0
  41. package/dist/middle-schema/compare-schemas.js.map +1 -0
  42. package/dist/middle-schema/generate-typescript.d.ts +7 -0
  43. package/dist/middle-schema/generate-typescript.d.ts.map +1 -0
  44. package/dist/middle-schema/generate-typescript.js +70 -0
  45. package/dist/middle-schema/generate-typescript.js.map +1 -0
  46. package/dist/middle-schema/parse-json-schema.d.ts +4 -0
  47. package/dist/middle-schema/parse-json-schema.d.ts.map +1 -0
  48. package/dist/middle-schema/parse-json-schema.js +65 -0
  49. package/dist/middle-schema/parse-json-schema.js.map +1 -0
  50. package/dist/middle-schema/parse-typescript.d.ts +4 -0
  51. package/dist/middle-schema/parse-typescript.d.ts.map +1 -0
  52. package/dist/middle-schema/parse-typescript.js +199 -0
  53. package/dist/middle-schema/parse-typescript.js.map +1 -0
  54. package/dist/middle-schema/schema-types.d.ts +24 -0
  55. package/dist/middle-schema/schema-types.d.ts.map +1 -0
  56. package/dist/middle-schema/schema-types.js +14 -0
  57. package/dist/middle-schema/schema-types.js.map +1 -0
  58. package/dist/middle-schema/schema.d.ts +24 -0
  59. package/dist/middle-schema/schema.d.ts.map +1 -0
  60. package/dist/middle-schema/schema.js +14 -0
  61. package/dist/middle-schema/schema.js.map +1 -0
  62. package/dist/schema/entities/abstract-entity.d.ts +5 -0
  63. package/dist/schema/entities/abstract-entity.d.ts.map +1 -0
  64. package/dist/schema/entities/abstract-entity.js +3 -0
  65. package/dist/schema/entities/abstract-entity.js.map +1 -0
  66. package/dist/schema/entities/primitive-union.d.ts +12 -0
  67. package/dist/schema/entities/primitive-union.d.ts.map +1 -0
  68. package/dist/schema/entities/primitive-union.js +74 -0
  69. package/dist/schema/entities/primitive-union.js.map +1 -0
  70. package/dist/schema/entities/primitive.d.ts +15 -0
  71. package/dist/schema/entities/primitive.d.ts.map +1 -0
  72. package/dist/schema/entities/primitive.js +54 -0
  73. package/dist/schema/entities/primitive.js.map +1 -0
  74. package/dist/schema/parsers/json-schema.d.ts +4 -0
  75. package/dist/schema/parsers/json-schema.d.ts.map +1 -0
  76. package/dist/schema/parsers/json-schema.js +12 -0
  77. package/dist/schema/parsers/json-schema.js.map +1 -0
  78. package/dist/schema/parsers/typescript.d.ts +3 -0
  79. package/dist/schema/parsers/typescript.d.ts.map +1 -0
  80. package/dist/schema/parsers/typescript.js +24 -0
  81. package/dist/schema/parsers/typescript.js.map +1 -0
  82. package/dist/schemas/input.d.ts +840 -0
  83. package/dist/schemas/input.d.ts.map +1 -0
  84. package/dist/schemas/input.js +349 -0
  85. package/dist/schemas/input.js.map +1 -0
  86. package/dist/utils/filesystem.d.ts +8 -0
  87. package/dist/utils/filesystem.d.ts.map +1 -0
  88. package/dist/utils/filesystem.js +16 -0
  89. package/dist/utils/filesystem.js.map +1 -0
  90. package/dist/utils/json-schemas-interactive-conflict.d.ts +16 -0
  91. package/dist/utils/json-schemas-interactive-conflict.d.ts.map +1 -0
  92. package/dist/utils/json-schemas-interactive-conflict.js +165 -0
  93. package/dist/utils/json-schemas-interactive-conflict.js.map +1 -0
  94. package/dist/utils/json-schemas.d.ts +42 -0
  95. package/dist/utils/json-schemas.d.ts.map +1 -0
  96. package/dist/utils/json-schemas.js +162 -0
  97. package/dist/utils/json-schemas.js.map +1 -0
  98. package/dist/zod/schemas/input.d.ts +840 -0
  99. package/dist/zod/schemas/input.d.ts.map +1 -0
  100. package/dist/zod/schemas/input.js +393 -0
  101. package/dist/zod/schemas/input.js.map +1 -0
  102. package/package.json +12 -12
  103. package/samples/all-defaults/.actor/input_schema.json +32 -3
  104. package/samples/all-defaults/src-schemas/input.json +2 -1
  105. package/samples/deep-merged-schemas/.actor/input_schema.json +36 -3
  106. package/samples/merged-schemas/.actor/input_schema.json +27 -3
  107. package/samples/package-json-config/.actor/input_schema.json +32 -3
  108. package/samples/package-json-config-merged/.actor/actor.json +15 -0
  109. package/samples/package-json-config-merged/.actor/dataset_schema.json +32 -0
  110. package/samples/package-json-config-merged/.actor/input_schema.json +91 -0
  111. package/samples/package-json-config-merged/custom-add-schemas/add-input.json +21 -0
  112. package/samples/package-json-config-merged/custom-src-schemas/dataset-item.json +28 -0
  113. package/samples/package-json-config-merged/custom-src-schemas/input.json +89 -0
  114. package/samples/package-json-config-merged/package.json +19 -0
  115. package/samples/package-json-config-merged/src/custom-generated/dataset.ts +25 -0
  116. package/samples/package-json-config-merged/src/custom-generated/input-utils.ts +73 -0
  117. package/samples/package-json-config-merged/src/custom-generated/input.ts +49 -0
  118. package/src/apify.ts +24 -9
  119. package/src/cli/check.ts +114 -0
  120. package/src/cli/init.ts +125 -0
  121. package/src/cli/sync.ts +164 -0
  122. package/src/configuration.ts +27 -8
  123. package/src/main.ts +25 -0
  124. package/src/middle-schema/compare-schemas.ts +113 -0
  125. package/src/middle-schema/generate-typescript.ts +88 -0
  126. package/src/middle-schema/parse-json-schema.ts +104 -0
  127. package/src/middle-schema/parse-typescript.ts +239 -0
  128. package/src/middle-schema/schema-types.ts +40 -0
  129. package/src/{json-schema-conflicts.ts → utils/json-schemas-interactive-conflict.ts} +3 -3
  130. package/src/{json-schemas.ts → utils/json-schemas.ts} +1 -1
  131. package/test/apify.test.ts +413 -5
  132. package/test/cli/check.test.ts +1571 -0
  133. package/test/cli/init.test.ts +459 -0
  134. package/test/cli/sync.test.ts +341 -0
  135. package/test/common.ts +68 -0
  136. package/test/configuration.test.ts +59 -26
  137. package/test/middle-schema/compare-schemas.test.ts +585 -0
  138. package/test/middle-schema/generate-typescript.test.ts +191 -0
  139. package/test/middle-schema/parse-json-schema.test.ts +178 -0
  140. package/test/middle-schema/parse-typescript.test.ts +143 -0
  141. package/test/{json-schema-conflicts.test.ts → utils/json-schemas-interactive-conflict.test.ts} +3 -3
  142. package/test/{json-schemas.test.ts → utils/json-schemas.test.ts} +4 -4
  143. package/update-samples.sh +4 -0
  144. package/src/apify-schema-tools.ts +0 -431
  145. package/src/typescript.ts +0 -563
  146. package/test/apify-schema-tools.test.ts +0 -2216
  147. package/test/typescript.test.ts +0 -1079
  148. /package/src/{filesystem.ts → utils/filesystem.ts} +0 -0
package/src/typescript.ts DELETED
@@ -1,563 +0,0 @@
1
- import type { JSONSchema4 } from "json-schema";
2
- import { equals } from "ramda";
3
- import { readFile, writeFile } from "./filesystem.js";
4
- import { isArraySchema, isObjectSchema, type ObjectSchema } from "./json-schemas.js";
5
-
6
- const TYPESCRIPT_FILE_HEADER = `\
7
- /**
8
- * This file was automatically generated by apify-schema-tools.
9
- * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
10
- * and run apify-schema-tools' "sync" command to regenerate this file.
11
- */
12
-
13
- `;
14
-
15
- const TYPESCRIPT_ALLOWED_TYPES = ["string", "number", "boolean", "object", "null", "unknown"] as const;
16
-
17
- type AllowedType = (typeof TYPESCRIPT_ALLOWED_TYPES)[number];
18
-
19
- function isAllowedType(type: string): type is AllowedType {
20
- return TYPESCRIPT_ALLOWED_TYPES.includes(type as AllowedType);
21
- }
22
-
23
- const JSON_SCHEMA_TYPE_TO_TS_TYPE: Record<string, string> = {
24
- integer: "number",
25
- };
26
-
27
- export function writeTypeScriptFile(filePath: string, content: string): void {
28
- writeFile(filePath, TYPESCRIPT_FILE_HEADER + content);
29
- }
30
-
31
- export function readGeneratedTypeScriptFile(filePath: string): string {
32
- const content = readFile(filePath);
33
- return content.replace(TYPESCRIPT_FILE_HEADER, "");
34
- }
35
-
36
- interface TypeScriptBasicData {
37
- doc?: string;
38
- isRequired: boolean;
39
- isArray: boolean;
40
- }
41
-
42
- interface TypeScriptBasicType extends TypeScriptBasicData {
43
- type: string | string[];
44
- }
45
-
46
- function isTypeScriptBasicType(schema: TypeScriptSchema): schema is TypeScriptBasicType {
47
- return "type" in schema;
48
- }
49
-
50
- interface TypeScriptEnum extends TypeScriptBasicData {
51
- enum: unknown[];
52
- }
53
-
54
- function isTypeScriptEnum(schema: TypeScriptSchema): schema is TypeScriptEnum {
55
- return "enum" in schema;
56
- }
57
-
58
- export interface TypeScriptInterface extends TypeScriptBasicData {
59
- properties: Record<string, TypeScriptSchema>;
60
- additionalProperties?: TypeScriptSchema;
61
- }
62
-
63
- function isTypeScriptInterface(schema: TypeScriptSchema): schema is TypeScriptInterface {
64
- return "properties" in schema;
65
- }
66
-
67
- export type TypeScriptSchema = TypeScriptBasicType | TypeScriptEnum | TypeScriptInterface;
68
-
69
- function createTypeScriptInterface(
70
- schema: ObjectSchema,
71
- doc: string | undefined,
72
- isArray: boolean,
73
- isRequired: boolean,
74
- ): TypeScriptInterface {
75
- const requiredProperties = schema.required ?? [];
76
-
77
- const properties = Object.fromEntries(
78
- Object.entries(schema.properties ?? {}).map(([key, value]) => {
79
- return [key, jsonSchemaToTypeScriptSchema(value, false, requiredProperties.includes(key))];
80
- }),
81
- );
82
-
83
- let additionalProperties: TypeScriptSchema | undefined;
84
- if (schema.additionalProperties === true || schema.additionalProperties === undefined) {
85
- // If additionalProperties is true or undefined, we parse it as an "empty" schema
86
- additionalProperties = { doc: undefined, isArray: false, isRequired: true, type: "unknown" };
87
- } else if (schema.additionalProperties !== false) {
88
- additionalProperties = jsonSchemaToTypeScriptSchema(schema.additionalProperties, false, true);
89
- }
90
-
91
- return { doc, isArray, isRequired, properties, additionalProperties };
92
- }
93
-
94
- function createTypeScriptArraySchema(
95
- schema: JSONSchema4 & { type: "array" },
96
- doc: string | undefined,
97
- isRequired: boolean,
98
- ): TypeScriptSchema {
99
- if (Array.isArray(schema.items)) {
100
- throw new Error("Array schema with multiple items is not supported");
101
- }
102
- if (!schema.items) {
103
- return { doc, isArray: true, isRequired, type: "unknown" };
104
- }
105
- return jsonSchemaToTypeScriptSchema(schema.items, true, isRequired, doc);
106
- }
107
-
108
- function createTypeScriptBasicType(
109
- schema: JSONSchema4,
110
- doc: string | undefined,
111
- isArray: boolean,
112
- isRequired: boolean,
113
- ): TypeScriptBasicType {
114
- const types: AllowedType[] = [];
115
- for (const type of Array.isArray(schema.type) ? schema.type : [schema.type ?? "unknown"]) {
116
- const tsType = JSON_SCHEMA_TYPE_TO_TS_TYPE[type] ?? type;
117
- if (!isAllowedType(tsType)) {
118
- throw new Error(`Unsupported JSON schema type: ${type}`);
119
- }
120
- types.push(tsType);
121
- }
122
- if (schema.nullable) {
123
- types.push("null");
124
- }
125
- return {
126
- doc,
127
- isArray,
128
- isRequired,
129
- type: types.length === 1 ? types[0] : types,
130
- };
131
- }
132
-
133
- function jsonSchemaToTypeScriptSchema(
134
- schema: JSONSchema4,
135
- isArray: boolean,
136
- isRequired: boolean,
137
- inheritedDoc?: string,
138
- ): TypeScriptSchema {
139
- const doc = inheritedDoc ?? schema.description;
140
-
141
- if (isObjectSchema(schema)) {
142
- return createTypeScriptInterface(schema, doc, isArray, isRequired);
143
- }
144
-
145
- if (isArraySchema(schema)) {
146
- return createTypeScriptArraySchema(schema, doc, isRequired);
147
- }
148
-
149
- if (schema.enum) {
150
- return { doc, isArray, isRequired, enum: schema.enum };
151
- }
152
-
153
- return createTypeScriptBasicType(schema, doc, isArray, isRequired);
154
- }
155
-
156
- export function jsonSchemaToTypeScriptInterface(schema: ObjectSchema): TypeScriptInterface {
157
- return jsonSchemaToTypeScriptSchema(schema, false, true) as TypeScriptInterface;
158
- }
159
-
160
- function serializeInterfaceSchema(schema: TypeScriptInterface, indentLevel: number): string {
161
- const indent = "\t".repeat(indentLevel);
162
- const properties = Object.entries(schema.properties);
163
- if (properties.length === 0 && !schema.additionalProperties) {
164
- return "{}";
165
- }
166
- const innerIndentLevel = indentLevel + 1;
167
- const innerIndent = "\t".repeat(innerIndentLevel);
168
- let result = "{";
169
- for (const [key, value] of properties) {
170
- if (value.doc) {
171
- result += `\n${innerIndent}/**\n${innerIndent} * ${value.doc}\n${innerIndent} */`;
172
- }
173
- result += `\n${innerIndent}${key}${value.isRequired ? "" : "?"}: ${serializeTypeScriptSchema(value, innerIndentLevel)};`;
174
- }
175
- if (schema.additionalProperties) {
176
- result += `\n${innerIndent}[key: string]: ${serializeTypeScriptSchema(schema.additionalProperties, innerIndentLevel)};`;
177
- }
178
- result += `\n${indent}}`;
179
- return result;
180
- }
181
-
182
- function serializeEnumSchema(schema: TypeScriptEnum): string {
183
- const serializedEnum = JSON.stringify(schema.enum).replace(/\[|\]/g, "").replace(/,/g, " | ");
184
- return schema.isArray && schema.enum.length > 1 ? `(${serializedEnum})` : serializedEnum;
185
- }
186
-
187
- function serializeBasicTypeSchema(schema: TypeScriptBasicType): string {
188
- if (Array.isArray(schema.type)) {
189
- return schema.isArray ? `(${schema.type.join(" | ")})` : schema.type.join(" | ");
190
- }
191
- return schema.type;
192
- }
193
-
194
- export function serializeTypeScriptSchema(schema: TypeScriptSchema, indentLevel = 0): string {
195
- let result = "";
196
- if (isTypeScriptInterface(schema)) {
197
- result = serializeInterfaceSchema(schema, indentLevel);
198
- } else if (isTypeScriptEnum(schema)) {
199
- result = serializeEnumSchema(schema);
200
- } else if (isTypeScriptBasicType(schema)) {
201
- result = serializeBasicTypeSchema(schema);
202
- }
203
- if (schema.isArray) {
204
- result += "[]";
205
- }
206
- return result;
207
- }
208
-
209
- export function serializeTypeScriptInterface(name: string, schema: TypeScriptInterface): string {
210
- const serializedSchema = serializeTypeScriptSchema(schema);
211
- const docComment = schema.doc ? `/**\n * ${schema.doc}\n */\n` : "";
212
- return `${docComment}export interface ${name} ${serializedSchema}${schema.isRequired ? "" : " | undefined"}`;
213
- }
214
-
215
- const TYPESCRIPT_HEADER_REGEX = /\/\*\*[\s\S]*?\*\//;
216
-
217
- export function removeTypeScriptHeader(fileContent: string): string {
218
- // Match the first jsdoc and remove it
219
- const headerMatch = fileContent.match(TYPESCRIPT_HEADER_REGEX);
220
- if (!headerMatch || headerMatch.index === undefined) {
221
- throw new Error("No header found in the TypeScript file");
222
- }
223
- const headerEndIndex = headerMatch.index + headerMatch[0].length;
224
- return fileContent.slice(headerEndIndex).trim();
225
- }
226
-
227
- const WHITESPACE_REGEX = /\s/;
228
-
229
- function skipWhitespace(body: string, i: number): number {
230
- let cursor = i;
231
- while (cursor < body.length && WHITESPACE_REGEX.test(body[cursor])) {
232
- cursor++;
233
- }
234
- return cursor;
235
- }
236
-
237
- const TYPESCRIPT_DOC_DECORATIONS_REGEX = /^\s*\*\s?/;
238
- const TYPESCRIPT_DOC_PREFIX = "/**";
239
-
240
- function tryParseJsDoc(body: string, i: number): { doc: string | undefined; nextIndex: number } | null {
241
- if (body.slice(i, i + TYPESCRIPT_DOC_PREFIX.length) === TYPESCRIPT_DOC_PREFIX) {
242
- const docEnd = body.indexOf("*/", i);
243
- if (docEnd !== -1) {
244
- const docContent = body.slice(i + TYPESCRIPT_DOC_PREFIX.length, docEnd);
245
- const docLines = docContent.split("\n");
246
- const cleanDoc = docLines
247
- .map((line) => line.replace(TYPESCRIPT_DOC_DECORATIONS_REGEX, "").trim())
248
- .filter((line) => line.length > 0)
249
- .join(" ");
250
- return { doc: cleanDoc || undefined, nextIndex: docEnd + 2 };
251
- }
252
- }
253
- return null;
254
- }
255
-
256
- function extractTypeString(
257
- body: string,
258
- i: number,
259
- ): { typeStr: string; isArray: boolean; isObject: boolean; typeEnd: number } {
260
- let braceDepth = 0;
261
- let typeEnd = i;
262
-
263
- while (typeEnd < body.length) {
264
- const char = body[typeEnd];
265
- if (char === "{") {
266
- braceDepth++;
267
- } else if (char === "}") {
268
- braceDepth--;
269
- if (braceDepth < 0) {
270
- break;
271
- }
272
- } else if (char === ";" && braceDepth === 0) {
273
- break;
274
- }
275
- typeEnd++;
276
- }
277
-
278
- let typeStr = body.slice(i, typeEnd).trim();
279
- const isArray = typeStr.endsWith("[]");
280
- if (isArray) {
281
- typeStr = typeStr.slice(0, -2).trim();
282
- }
283
- const isObject = typeStr.startsWith("{") && typeStr.endsWith("}");
284
- if (isObject) {
285
- typeStr = typeStr.slice(1, -1).trim();
286
- }
287
- return { typeStr, isArray, isObject, typeEnd };
288
- }
289
-
290
- function parseTypeScriptValue(text: string) {
291
- try {
292
- return JSON.parse(text.trim());
293
- } catch {
294
- throw new Error(`Invalid TypeScript value: ${text}`);
295
- }
296
- }
297
-
298
- function extractSchemaFromText(
299
- text: string,
300
- currentDoc: string | undefined,
301
- isRequired: boolean,
302
- isArray: boolean,
303
- isObject: boolean,
304
- ): TypeScriptSchema {
305
- let schema: TypeScriptSchema;
306
-
307
- if (isObject) {
308
- const { properties: nestedProperties, additionalProperties: additionalNestedProperties } =
309
- parseInterfaceProperties(text);
310
- schema = {
311
- doc: currentDoc,
312
- isArray,
313
- isRequired,
314
- properties: nestedProperties,
315
- additionalProperties: additionalNestedProperties,
316
- };
317
- } else if (text.includes("|")) {
318
- const unionBody = isArray ? text.slice(1, -1) : text;
319
- const unionParts = unionBody.split("|").map((part) => part.trim());
320
-
321
- // Since we currently do not support complex unions, we check if all parts are allowed types
322
- if (unionParts.every((part) => isAllowedType(part))) {
323
- schema = {
324
- doc: currentDoc,
325
- isArray,
326
- isRequired,
327
- type: unionParts,
328
- };
329
- } else {
330
- schema = {
331
- doc: currentDoc,
332
- isArray,
333
- isRequired,
334
- enum: unionParts.map((value) => parseTypeScriptValue(value)),
335
- };
336
- }
337
- } else if (isAllowedType(text)) {
338
- schema = {
339
- doc: currentDoc,
340
- isArray,
341
- isRequired,
342
- type: text,
343
- };
344
- } else {
345
- schema = {
346
- doc: currentDoc,
347
- isArray,
348
- isRequired,
349
- enum: [parseTypeScriptValue(text)],
350
- };
351
- }
352
-
353
- return schema;
354
- }
355
-
356
- const TYPESCRIPT_ADDITIONAL_PROPERTIES_REGEX = /^\s*\[\s*key\s*:\s*string\s*\]\s*:\s*/;
357
- const TYPESCRIPT_PROPERTY_REGEX = /^(\w+)(\?)?:\s*/;
358
-
359
- function tryParseProperty(
360
- body: string,
361
- i: number,
362
- currentDoc: string | undefined,
363
- ): { propName?: string; schema: TypeScriptSchema; nextIndex: number } | null {
364
- let cursor = i;
365
-
366
- let propName: string | undefined;
367
- let isRequired: boolean;
368
-
369
- const bodySlice = body.slice(cursor);
370
- const additionalPropertiesMatch = bodySlice.match(TYPESCRIPT_ADDITIONAL_PROPERTIES_REGEX);
371
- if (additionalPropertiesMatch) {
372
- cursor += additionalPropertiesMatch[0].length;
373
- isRequired = true;
374
- } else {
375
- const propMatch = bodySlice.match(TYPESCRIPT_PROPERTY_REGEX);
376
- if (!propMatch) {
377
- return null;
378
- }
379
-
380
- propName = propMatch[1];
381
- isRequired = !propMatch[2];
382
- cursor += propMatch[0].length;
383
- }
384
-
385
- const { typeStr, isArray, isObject, typeEnd } = extractTypeString(body, cursor);
386
- const schema = extractSchemaFromText(typeStr, currentDoc, isRequired, isArray, isObject);
387
-
388
- let nextIndex = typeEnd;
389
- if (body[nextIndex] === ";") {
390
- nextIndex++;
391
- }
392
-
393
- return { propName, schema, nextIndex };
394
- }
395
-
396
- function parseInterfaceProperties(body: string): {
397
- properties: Record<string, TypeScriptSchema>;
398
- additionalProperties?: TypeScriptSchema;
399
- } {
400
- const properties: Record<string, TypeScriptSchema> = {};
401
- let additionalProperties: TypeScriptSchema | undefined;
402
- let i = 0;
403
- let currentDoc: string | undefined;
404
-
405
- while (i < body.length) {
406
- i = skipWhitespace(body, i);
407
-
408
- const docResult = tryParseJsDoc(body, i);
409
- if (docResult) {
410
- currentDoc = docResult.doc;
411
- i = docResult.nextIndex;
412
- continue;
413
- }
414
-
415
- const propResult = tryParseProperty(body, i, currentDoc);
416
- if (propResult) {
417
- if (propResult.propName) {
418
- properties[propResult.propName] = propResult.schema;
419
- } else {
420
- additionalProperties = propResult.schema;
421
- }
422
- currentDoc = undefined;
423
- i = propResult.nextIndex;
424
- continue;
425
- }
426
-
427
- i++;
428
- }
429
-
430
- return { properties, additionalProperties };
431
- }
432
-
433
- const TYPESCRIPT_JSDOC_REGEX = /\/\*\*\s*\n\s*\*\s*(.*?)\s*\n\s*\*\//;
434
- const TYPESCRIPT_INTERFACE_REGEX = /export interface (\w+) \{([\s\S]*)\}$/;
435
-
436
- export function parseTypeScriptInterface(text: string): TypeScriptInterface {
437
- // Extract JSDoc comment
438
- const docMatch = text.match(TYPESCRIPT_JSDOC_REGEX);
439
- const doc = docMatch ? docMatch[1] : undefined;
440
-
441
- // Extract interface declaration
442
- const interfaceMatch = text.match(TYPESCRIPT_INTERFACE_REGEX);
443
- if (!interfaceMatch) {
444
- throw new Error("Invalid TypeScript interface format");
445
- }
446
-
447
- const body = interfaceMatch[2];
448
- const { properties, additionalProperties } = parseInterfaceProperties(body);
449
-
450
- return { doc, isArray: false, isRequired: true, properties, additionalProperties };
451
- }
452
-
453
- function compareInterfaceProperty(
454
- key: string,
455
- propA: TypeScriptInterface,
456
- propB: TypeScriptSchema,
457
- ignoreDocs?: boolean,
458
- ): boolean {
459
- if (!isTypeScriptInterface(propB)) {
460
- console.error(`Property "${key}" is an interface in one schema but not in the other.`);
461
- return false;
462
- }
463
- if (!compareTypescriptInterfaces(propA, propB, ignoreDocs)) {
464
- console.error(`Property "${key}" interfaces do not match.`);
465
- return false;
466
- }
467
- return true;
468
- }
469
-
470
- function compareEnumProperty(key: string, propA: TypeScriptSchema, propB: TypeScriptSchema): boolean {
471
- if (!isTypeScriptEnum(propB)) {
472
- console.error(`Property "${key}" is an enum in one schema but not in the other.`);
473
- return false;
474
- }
475
- if (JSON.stringify((propA as TypeScriptEnum).enum) !== JSON.stringify((propB as TypeScriptEnum).enum)) {
476
- console.error(
477
- `Property "${key}" enums do not match: ${JSON.stringify((propA as TypeScriptEnum).enum)} vs ${JSON.stringify((propB as TypeScriptEnum).enum)}`,
478
- );
479
- return false;
480
- }
481
- return true;
482
- }
483
-
484
- function compareBasicTypeProperty(key: string, propA: TypeScriptSchema, propB: TypeScriptSchema): boolean {
485
- if (!isTypeScriptBasicType(propB)) {
486
- console.error(`Property "${key}" is a basic type in one schema but not in the other.`);
487
- return false;
488
- }
489
- if (!equals((propA as TypeScriptBasicType).type, (propB as TypeScriptBasicType).type)) {
490
- console.error(
491
- `Property "${key}" types do not match: ${JSON.stringify((propA as TypeScriptBasicType).type)} vs ${JSON.stringify((propB as TypeScriptBasicType).type)}`,
492
- );
493
- return false;
494
- }
495
- return true;
496
- }
497
-
498
- function compareProperty(key: string, propA: TypeScriptSchema, propB: TypeScriptSchema, ignoreDocs?: boolean): boolean {
499
- if (propA.isArray !== propB.isArray) {
500
- console.error(`Property "${key}" has different array status: ${propA.isArray} vs ${propB.isArray}`);
501
- return false;
502
- }
503
-
504
- if (propA.isRequired !== propB.isRequired) {
505
- console.error(`Property "${key}" has different required status: ${propA.isRequired} vs ${propB.isRequired}`);
506
- return false;
507
- }
508
-
509
- if (!ignoreDocs && propA.doc !== propB.doc) {
510
- console.error(`Property "${key}" has different documentation: "${propA.doc}" vs "${propB.doc}"`);
511
- return false;
512
- }
513
-
514
- if (isTypeScriptInterface(propA)) {
515
- return compareInterfaceProperty(key, propA, propB, ignoreDocs);
516
- }
517
- if (isTypeScriptEnum(propA)) {
518
- return compareEnumProperty(key, propA, propB);
519
- }
520
- return compareBasicTypeProperty(key, propA, propB);
521
- }
522
-
523
- function compareAdditionalProperties(a: TypeScriptInterface, b: TypeScriptInterface, ignoreDocs?: boolean): boolean {
524
- if (!(a.additionalProperties || b.additionalProperties)) {
525
- return true; // Both have no additional properties
526
- }
527
- if (!(a.additionalProperties && b.additionalProperties)) {
528
- console.error(`One interface has additionalProperties defined, but the other does not.
529
- If you generated an interface from a JSON schema, pay attention that "additionalProperties" is set to an empty schema by default.`);
530
- return false;
531
- }
532
- return compareProperty("[key: string]", a.additionalProperties, b.additionalProperties, ignoreDocs);
533
- }
534
-
535
- export function compareTypescriptInterfaces(
536
- a: TypeScriptInterface,
537
- b: TypeScriptInterface,
538
- ignoreDocs?: boolean,
539
- ): boolean {
540
- if (Object.keys(a.properties).length !== Object.keys(b.properties).length) {
541
- console.error("Interfaces have different number of properties.");
542
- return false;
543
- }
544
-
545
- for (const key in a.properties) {
546
- if (Object.hasOwn(a.properties, key)) {
547
- if (!(key in b.properties)) {
548
- return false;
549
- }
550
- const propA = a.properties[key];
551
- const propB = b.properties[key];
552
- if (!compareProperty(key, propA, propB, ignoreDocs)) {
553
- return false;
554
- }
555
- }
556
- }
557
-
558
- if (!compareAdditionalProperties(a, b, ignoreDocs)) {
559
- return false;
560
- }
561
-
562
- return true;
563
- }