@gabrielbryk/json-schema-to-zod 2.10.1 → 2.11.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.
Files changed (138) hide show
  1. package/AGENTS.md +44 -0
  2. package/CHANGELOG.md +38 -0
  3. package/README.md +6 -33
  4. package/check-types-lift.sh +23 -0
  5. package/check-types.sh +20 -0
  6. package/dist/{esm/cli.js → cli.js} +0 -6
  7. package/dist/{esm/core → core}/analyzeSchema.js +4 -5
  8. package/dist/core/emitZod.js +263 -0
  9. package/dist/{esm/generators → generators}/generateBundle.js +26 -13
  10. package/dist/{esm/index.js → index.js} +6 -0
  11. package/dist/jsonSchemaToZod.js +17 -0
  12. package/dist/parsers/parseAllOf.js +125 -0
  13. package/dist/parsers/parseAnyOf.js +28 -0
  14. package/dist/{esm/parsers → parsers}/parseArray.js +27 -11
  15. package/dist/parsers/parseBoolean.js +4 -0
  16. package/dist/parsers/parseConst.js +22 -0
  17. package/dist/parsers/parseEnum.js +35 -0
  18. package/dist/{esm/parsers → parsers}/parseIfThenElse.js +10 -6
  19. package/dist/parsers/parseMultipleType.js +10 -0
  20. package/dist/parsers/parseNot.js +14 -0
  21. package/dist/parsers/parseNull.js +4 -0
  22. package/dist/parsers/parseNullable.js +12 -0
  23. package/dist/{esm/parsers → parsers}/parseNumber.js +4 -1
  24. package/dist/{esm/parsers → parsers}/parseObject.js +200 -37
  25. package/dist/parsers/parseOneOf.js +365 -0
  26. package/dist/{esm/parsers → parsers}/parseSchema.js +55 -117
  27. package/dist/parsers/parseSimpleDiscriminatedOneOf.js +24 -0
  28. package/dist/{esm/parsers → parsers}/parseString.js +29 -18
  29. package/dist/types/Types.d.ts +32 -4
  30. package/dist/types/core/analyzeSchema.d.ts +3 -2
  31. package/dist/types/generators/generateBundle.d.ts +0 -2
  32. package/dist/types/index.d.ts +6 -0
  33. package/dist/types/parsers/parseAllOf.d.ts +2 -2
  34. package/dist/types/parsers/parseAnyOf.d.ts +2 -2
  35. package/dist/types/parsers/parseArray.d.ts +2 -2
  36. package/dist/types/parsers/parseBoolean.d.ts +2 -1
  37. package/dist/types/parsers/parseConst.d.ts +2 -2
  38. package/dist/types/parsers/parseDefault.d.ts +2 -2
  39. package/dist/types/parsers/parseEnum.d.ts +2 -2
  40. package/dist/types/parsers/parseIfThenElse.d.ts +2 -2
  41. package/dist/types/parsers/parseMultipleType.d.ts +2 -2
  42. package/dist/types/parsers/parseNot.d.ts +2 -2
  43. package/dist/types/parsers/parseNull.d.ts +2 -1
  44. package/dist/types/parsers/parseNullable.d.ts +2 -2
  45. package/dist/types/parsers/parseNumber.d.ts +2 -2
  46. package/dist/types/parsers/parseObject.d.ts +2 -2
  47. package/dist/types/parsers/parseOneOf.d.ts +2 -2
  48. package/dist/types/parsers/parseSchema.d.ts +2 -2
  49. package/dist/types/parsers/parseSimpleDiscriminatedOneOf.d.ts +2 -2
  50. package/dist/types/parsers/parseString.d.ts +2 -2
  51. package/dist/types/utils/anyOrUnknown.d.ts +5 -4
  52. package/dist/types/utils/esmEmitter.d.ts +29 -0
  53. package/dist/types/utils/extractInlineObject.d.ts +15 -0
  54. package/dist/types/utils/liftInlineObjects.d.ts +21 -0
  55. package/dist/types/utils/namingService.d.ts +21 -0
  56. package/dist/types/utils/resolveRef.d.ts +7 -0
  57. package/dist/types/utils/schemaRepresentation.d.ts +71 -0
  58. package/dist/utils/anyOrUnknown.js +13 -0
  59. package/dist/{esm/utils → utils}/buildRefRegistry.js +4 -0
  60. package/dist/utils/esmEmitter.js +87 -0
  61. package/dist/utils/extractInlineObject.js +119 -0
  62. package/dist/utils/liftInlineObjects.js +476 -0
  63. package/dist/utils/namingService.js +58 -0
  64. package/dist/utils/resolveRef.js +92 -0
  65. package/dist/utils/schemaRepresentation.js +569 -0
  66. package/docs/IMPROVEMENT-PLAN.md +243 -0
  67. package/docs/ZOD-V4-RECURSIVE-TYPE-LIMITATIONS.md +292 -0
  68. package/docs/proposals/bundle-refactor.md +1 -1
  69. package/docs/proposals/discriminated-union-with-default.md +248 -0
  70. package/docs/proposals/inline-object-lifting.md +77 -0
  71. package/eslint.config.js +4 -2
  72. package/jest.config.mjs +19 -0
  73. package/package.json +17 -20
  74. package/scripts/generateWorkflowSchema.ts +0 -1
  75. package/dist/cjs/Types.js +0 -2
  76. package/dist/cjs/cli.js +0 -70
  77. package/dist/cjs/core/analyzeSchema.js +0 -62
  78. package/dist/cjs/core/emitZod.js +0 -157
  79. package/dist/cjs/generators/generateBundle.js +0 -510
  80. package/dist/cjs/index.js +0 -50
  81. package/dist/cjs/jsonSchemaToZod.js +0 -10
  82. package/dist/cjs/package.json +0 -1
  83. package/dist/cjs/parsers/parseAllOf.js +0 -46
  84. package/dist/cjs/parsers/parseAnyOf.js +0 -18
  85. package/dist/cjs/parsers/parseArray.js +0 -90
  86. package/dist/cjs/parsers/parseBoolean.js +0 -5
  87. package/dist/cjs/parsers/parseConst.js +0 -7
  88. package/dist/cjs/parsers/parseDefault.js +0 -8
  89. package/dist/cjs/parsers/parseEnum.js +0 -21
  90. package/dist/cjs/parsers/parseIfThenElse.js +0 -35
  91. package/dist/cjs/parsers/parseMultipleType.js +0 -10
  92. package/dist/cjs/parsers/parseNot.js +0 -12
  93. package/dist/cjs/parsers/parseNull.js +0 -5
  94. package/dist/cjs/parsers/parseNullable.js +0 -12
  95. package/dist/cjs/parsers/parseNumber.js +0 -116
  96. package/dist/cjs/parsers/parseObject.js +0 -318
  97. package/dist/cjs/parsers/parseOneOf.js +0 -53
  98. package/dist/cjs/parsers/parseSchema.js +0 -419
  99. package/dist/cjs/parsers/parseSimpleDiscriminatedOneOf.js +0 -21
  100. package/dist/cjs/parsers/parseString.js +0 -317
  101. package/dist/cjs/utils/anyOrUnknown.js +0 -14
  102. package/dist/cjs/utils/buildRefRegistry.js +0 -56
  103. package/dist/cjs/utils/cliTools.js +0 -108
  104. package/dist/cjs/utils/cycles.js +0 -113
  105. package/dist/cjs/utils/half.js +0 -7
  106. package/dist/cjs/utils/jsdocs.js +0 -20
  107. package/dist/cjs/utils/omit.js +0 -11
  108. package/dist/cjs/utils/resolveUri.js +0 -16
  109. package/dist/cjs/utils/withMessage.js +0 -21
  110. package/dist/cjs/zodToJsonSchema.js +0 -89
  111. package/dist/esm/core/emitZod.js +0 -153
  112. package/dist/esm/jsonSchemaToZod.js +0 -6
  113. package/dist/esm/package.json +0 -1
  114. package/dist/esm/parsers/parseAllOf.js +0 -43
  115. package/dist/esm/parsers/parseAnyOf.js +0 -14
  116. package/dist/esm/parsers/parseBoolean.js +0 -1
  117. package/dist/esm/parsers/parseConst.js +0 -3
  118. package/dist/esm/parsers/parseEnum.js +0 -17
  119. package/dist/esm/parsers/parseMultipleType.js +0 -6
  120. package/dist/esm/parsers/parseNot.js +0 -8
  121. package/dist/esm/parsers/parseNull.js +0 -1
  122. package/dist/esm/parsers/parseNullable.js +0 -8
  123. package/dist/esm/parsers/parseOneOf.js +0 -49
  124. package/dist/esm/parsers/parseSimpleDiscriminatedOneOf.js +0 -17
  125. package/dist/esm/utils/anyOrUnknown.js +0 -10
  126. package/jest.config.cjs +0 -4
  127. package/postcjs.cjs +0 -1
  128. package/postesm.cjs +0 -1
  129. /package/dist/{esm/Types.js → Types.js} +0 -0
  130. /package/dist/{esm/parsers → parsers}/parseDefault.js +0 -0
  131. /package/dist/{esm/utils → utils}/cliTools.js +0 -0
  132. /package/dist/{esm/utils → utils}/cycles.js +0 -0
  133. /package/dist/{esm/utils → utils}/half.js +0 -0
  134. /package/dist/{esm/utils → utils}/jsdocs.js +0 -0
  135. /package/dist/{esm/utils → utils}/omit.js +0 -0
  136. /package/dist/{esm/utils → utils}/resolveUri.js +0 -0
  137. /package/dist/{esm/utils → utils}/withMessage.js +0 -0
  138. /package/dist/{esm/zodToJsonSchema.js → zodToJsonSchema.js} +0 -0
@@ -1,157 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.emitZod = void 0;
4
- const parseSchema_js_1 = require("../parsers/parseSchema.js");
5
- const jsdocs_js_1 = require("../utils/jsdocs.js");
6
- const orderDeclarations = (entries, dependencies) => {
7
- const valueByName = new Map(entries);
8
- const depGraph = new Map();
9
- for (const [from, set] of dependencies.entries()) {
10
- const onlyKnown = new Set();
11
- for (const dep of set) {
12
- if (valueByName.has(dep) && dep !== from) {
13
- onlyKnown.add(dep);
14
- }
15
- }
16
- if (onlyKnown.size)
17
- depGraph.set(from, onlyKnown);
18
- }
19
- const names = Array.from(valueByName.keys());
20
- for (const [name, value] of entries) {
21
- const deps = depGraph.get(name) ?? new Set();
22
- for (const candidate of names) {
23
- if (candidate === name)
24
- continue;
25
- const matcher = new RegExp(`\\b${candidate}\\b`);
26
- if (matcher.test(value)) {
27
- deps.add(candidate);
28
- }
29
- }
30
- if (deps.size)
31
- depGraph.set(name, deps);
32
- }
33
- const ordered = [];
34
- const perm = new Set();
35
- const temp = new Set();
36
- const visit = (name) => {
37
- if (perm.has(name))
38
- return;
39
- if (temp.has(name)) {
40
- temp.delete(name);
41
- perm.add(name);
42
- ordered.push(name);
43
- return;
44
- }
45
- temp.add(name);
46
- const deps = depGraph.get(name);
47
- if (deps) {
48
- for (const dep of deps) {
49
- if (valueByName.has(dep)) {
50
- visit(dep);
51
- }
52
- }
53
- }
54
- temp.delete(name);
55
- perm.add(name);
56
- ordered.push(name);
57
- };
58
- for (const name of valueByName.keys()) {
59
- visit(name);
60
- }
61
- const unique = [];
62
- const seen = new Set();
63
- for (const name of ordered) {
64
- if (!seen.has(name)) {
65
- seen.add(name);
66
- unique.push(name);
67
- }
68
- }
69
- return unique.map((name) => [name, valueByName.get(name)]);
70
- };
71
- const emitZod = (analysis) => {
72
- const { schema, options, refNameByPointer, usedNames, cycleRefNames, cycleComponentByName, } = analysis;
73
- const { module, name, type, noImport, exportRefs, withMeta, ...rest } = options;
74
- const declarations = new Map();
75
- const dependencies = new Map();
76
- const reserveName = (base) => {
77
- let candidate = base;
78
- let i = 1;
79
- while (usedNames.has(candidate) || declarations.has(candidate)) {
80
- candidate = `${base}${i}`;
81
- i += 1;
82
- }
83
- usedNames.add(candidate);
84
- return candidate;
85
- };
86
- const parsedSchema = (0, parseSchema_js_1.parseSchema)(schema, {
87
- module,
88
- name,
89
- path: [],
90
- seen: new Map(),
91
- declarations,
92
- dependencies,
93
- inProgress: new Set(),
94
- refNameByPointer,
95
- usedNames,
96
- root: schema,
97
- currentSchemaName: name,
98
- cycleRefNames,
99
- cycleComponentByName,
100
- refRegistry: analysis.refRegistry,
101
- rootBaseUri: analysis.rootBaseUri,
102
- ...rest,
103
- withMeta,
104
- });
105
- const declarationBlock = declarations.size
106
- ? orderDeclarations(Array.from(declarations.entries()), dependencies)
107
- .flatMap(([refName, value]) => {
108
- const shouldExport = exportRefs && module === "esm";
109
- const isCycle = cycleRefNames.has(refName);
110
- if (!isCycle) {
111
- return [`${shouldExport ? "export " : ""}const ${refName} = ${value}`];
112
- }
113
- const baseName = `${refName}Def`;
114
- const lines = [`const ${baseName} = ${value}`];
115
- lines.push(`${shouldExport ? "export " : ""}const ${refName} = ${baseName}`);
116
- return lines;
117
- })
118
- .join("\n")
119
- : "";
120
- const jsdocs = rest.withJsdocs && typeof schema === "object" && schema !== null && "description" in schema
121
- ? (0, jsdocs_js_1.expandJsdocs)(String(schema.description ?? ""))
122
- : "";
123
- const lines = [];
124
- if (module === "cjs" && !noImport) {
125
- lines.push(`const { z } = require("zod")`);
126
- }
127
- if (module === "esm" && !noImport) {
128
- lines.push(`import { z } from "zod"`);
129
- }
130
- if (declarationBlock) {
131
- lines.push(declarationBlock);
132
- }
133
- if (module === "cjs") {
134
- const payload = name ? `{ ${JSON.stringify(name)}: ${parsedSchema} }` : parsedSchema;
135
- lines.push(`${jsdocs}module.exports = ${payload}`);
136
- }
137
- else if (module === "esm") {
138
- const exportLine = `${jsdocs}export ${name ? `const ${name} =` : `default`} ${parsedSchema}`;
139
- lines.push(exportLine);
140
- }
141
- else if (name) {
142
- lines.push(`${jsdocs}const ${name} = ${parsedSchema}`);
143
- }
144
- else {
145
- lines.push(`${jsdocs}${parsedSchema}`);
146
- }
147
- let typeLine;
148
- if (type && name) {
149
- const typeName = typeof type === "string" ? type : `${name[0].toUpperCase()}${name.substring(1)}`;
150
- typeLine = `export type ${typeName} = z.infer<typeof ${name}>`;
151
- }
152
- const joined = lines.filter(Boolean).join("\n\n");
153
- const combined = typeLine ? `${joined}\n${typeLine}` : joined;
154
- const shouldEndWithNewline = module === "esm" || module === "cjs";
155
- return `${combined}${shouldEndWithNewline ? "\n" : ""}`;
156
- };
157
- exports.emitZod = emitZod;
@@ -1,510 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.generateSchemaBundle = void 0;
4
- const analyzeSchema_js_1 = require("../core/analyzeSchema.js");
5
- const emitZod_js_1 = require("../core/emitZod.js");
6
- const generateSchemaBundle = (schema, options = {}) => {
7
- const module = options.module ?? "esm";
8
- if (!schema || typeof schema !== "object") {
9
- throw new Error("generateSchemaBundle requires an object schema");
10
- }
11
- const defs = schema.$defs || {};
12
- const definitions = schema.definitions || {};
13
- const defNames = Object.keys(defs);
14
- const { rootName, rootTypeName, defInfoMap, groups } = buildBundleContext(defNames, defs, options);
15
- const files = [];
16
- const targets = planBundleTargets(schema, defs, definitions, defNames, options, rootName, defInfoMap, rootTypeName);
17
- for (const target of targets) {
18
- const usedRefs = target.usedRefs;
19
- const zodParts = [];
20
- for (const member of target.members) {
21
- const analysis = (0, analyzeSchema_js_1.analyzeSchema)(member.schemaWithDefs, {
22
- ...options,
23
- module,
24
- name: member.schemaName,
25
- type: member.typeName,
26
- parserOverride: createRefHandler(member.defName, defInfoMap, usedRefs, {
27
- ...(member.schemaWithDefs.$defs || {}),
28
- ...(member.schemaWithDefs.definitions || {}),
29
- }, options, target.groupId),
30
- });
31
- const zodSchema = (0, emitZod_js_1.emitZod)(analysis);
32
- zodParts.push(zodSchema);
33
- }
34
- const finalSchema = buildSchemaFile(zodParts, usedRefs, defInfoMap, module, target);
35
- files.push({ fileName: target.fileName, contents: finalSchema });
36
- }
37
- // Nested types extraction (optional)
38
- const nestedTypesEnabled = options.nestedTypes?.enable;
39
- if (nestedTypesEnabled) {
40
- const nestedTypes = collectNestedTypes(schema, defs, defNames, rootTypeName ?? rootName);
41
- if (nestedTypes.length > 0) {
42
- const nestedFileName = options.nestedTypes?.fileName ?? "nested-types.ts";
43
- const nestedContent = generateNestedTypesFile(nestedTypes);
44
- files.push({ fileName: nestedFileName, contents: nestedContent });
45
- }
46
- }
47
- return { files, defNames };
48
- };
49
- exports.generateSchemaBundle = generateSchemaBundle;
50
- // ---------------------------------------------------------------------------
51
- // Internals
52
- // ---------------------------------------------------------------------------
53
- const toPascalCase = (str) => str
54
- .split(/[-_]/)
55
- .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
56
- .join("");
57
- const buildDefInfoMap = (defNames, defs, options) => {
58
- const map = new Map();
59
- for (const defName of defNames) {
60
- const dependencies = findRefDependencies(defs[defName], defNames);
61
- const pascalName = toPascalCase(defName);
62
- const schemaName = options.splitDefs?.schemaName?.(defName, { isRoot: false }) ?? `${pascalName}Schema`;
63
- const typeName = options.splitDefs?.typeName?.(defName, { isRoot: false }) ?? pascalName;
64
- const fileName = options.splitDefs?.fileName?.(defName, { isRoot: false }) ?? `${defName}.schema.ts`;
65
- map.set(defName, {
66
- name: defName,
67
- pascalName,
68
- schemaName,
69
- typeName,
70
- fileName,
71
- dependencies,
72
- hasCycle: false,
73
- groupId: "",
74
- });
75
- }
76
- return map;
77
- };
78
- const buildBundleContext = (defNames, defs, options) => {
79
- const defInfoMap = buildDefInfoMap(defNames, defs, options);
80
- const cycles = detectCycles(defInfoMap);
81
- for (const defName of cycles) {
82
- const info = defInfoMap.get(defName);
83
- if (info)
84
- info.hasCycle = true;
85
- }
86
- const groups = buildSccGroups(defInfoMap);
87
- for (const [groupId, members] of groups) {
88
- for (const defName of members) {
89
- const info = defInfoMap.get(defName);
90
- if (info)
91
- info.groupId = groupId;
92
- }
93
- }
94
- const rootName = options.splitDefs?.rootName ?? options.name ?? "RootSchema";
95
- const rootTypeName = typeof options.type === "string"
96
- ? options.type
97
- : options.splitDefs?.rootTypeName ?? (typeof options.type === "boolean" && options.type ? rootName : undefined);
98
- return { defInfoMap, rootName, rootTypeName, groups };
99
- };
100
- const createRefHandler = (currentDefName, defInfoMap, usedRefs, allDefs, options, currentGroupId) => {
101
- const useLazyCrossRefs = options.refResolution?.lazyCrossRefs ?? true;
102
- return (schema, refs) => {
103
- if (typeof schema["$ref"] === "string") {
104
- const refPath = schema["$ref"];
105
- const match = refPath.match(/^#\/(?:\$defs|definitions)\/(.+)$/);
106
- if (match) {
107
- const refName = match[1];
108
- // Only intercept top-level def refs (no nested path like a/$defs/x)
109
- if (refName.includes("/")) {
110
- return undefined;
111
- }
112
- const refInfo = defInfoMap.get(refName);
113
- if (refInfo) {
114
- // Track imports when referencing other defs
115
- if (refName !== currentDefName && refInfo.groupId !== currentGroupId) {
116
- usedRefs.add(refName);
117
- }
118
- const isCycle = refName === currentDefName || (refInfo.hasCycle && !!currentDefName);
119
- const resolved = options.refResolution?.onRef?.({
120
- ref: refPath,
121
- refName,
122
- currentDef: currentDefName,
123
- path: refs.path,
124
- isCycle,
125
- });
126
- if (resolved)
127
- return resolved;
128
- if (isCycle && useLazyCrossRefs) {
129
- const inObjectProperty = refs.path.includes("properties") ||
130
- refs.path.includes("patternProperties") ||
131
- refs.path.includes("additionalProperties");
132
- if (inObjectProperty && refName === currentDefName) {
133
- // Self-recursion inside object getters can safely reference the schema name
134
- return refInfo.schemaName;
135
- }
136
- return `z.lazy(() => ${refInfo.schemaName})`;
137
- }
138
- return refInfo.schemaName;
139
- }
140
- // If the ref points to a local/inline $def (not part of top-level defs),
141
- // let the default parser resolve it normally.
142
- if (allDefs && Object.prototype.hasOwnProperty.call(allDefs, refName)) {
143
- return undefined;
144
- }
145
- }
146
- const unknown = options.refResolution?.onUnknownRef?.({ ref: refPath, currentDef: currentDefName });
147
- if (unknown)
148
- return unknown;
149
- return options.useUnknown ? "z.unknown()" : "z.any()";
150
- }
151
- return undefined;
152
- };
153
- };
154
- const buildSchemaFile = (zodCodeParts, usedRefs, defInfoMap, module, target) => {
155
- if (module !== "esm")
156
- return zodCodeParts.join("\n");
157
- const groupFileById = new Map();
158
- for (const info of defInfoMap.values()) {
159
- if (!groupFileById.has(info.groupId)) {
160
- groupFileById.set(info.groupId, info.fileName.replace(/\.ts$/, ".js"));
161
- }
162
- }
163
- const importsByFile = new Map();
164
- for (const refName of [...usedRefs].sort()) {
165
- const refInfo = defInfoMap.get(refName);
166
- if (refInfo) {
167
- const groupFile = groupFileById.get(refInfo.groupId) ?? refInfo.fileName.replace(/\.ts$/, ".js");
168
- const path = `./${groupFile}`;
169
- const set = importsByFile.get(path) ?? new Set();
170
- set.add(refInfo.schemaName);
171
- importsByFile.set(path, set);
172
- }
173
- }
174
- const imports = [];
175
- for (const [path, names] of [...importsByFile.entries()].sort(([a], [b]) => a.localeCompare(b))) {
176
- imports.push(`import { ${[...names].sort().join(", ")} } from '${path}';`);
177
- }
178
- const body = zodCodeParts
179
- .map((code, idx) => {
180
- if (idx === 0)
181
- return code;
182
- return code.replace(/^import \{ z \} from "zod"\n?/, "");
183
- })
184
- .join("\n");
185
- return imports.length
186
- ? body.replace('import { z } from "zod"', `import { z } from "zod"\n${imports.join("\n")}`)
187
- : body;
188
- };
189
- const planBundleTargets = (rootSchema, defs, definitions, defNames, options, rootName, defInfoMap, rootTypeName) => {
190
- const targets = [];
191
- const groupById = new Map();
192
- for (const defName of defNames) {
193
- const info = defInfoMap.get(defName);
194
- const gid = info?.groupId || defName;
195
- if (!groupById.has(gid))
196
- groupById.set(gid, []);
197
- groupById.get(gid).push(defName);
198
- }
199
- for (const [groupId, memberDefs] of groupById.entries()) {
200
- const orderedDefs = orderGroupMembers(memberDefs, defInfoMap);
201
- const members = orderedDefs.map((defName) => {
202
- const defSchema = defs[defName];
203
- const defSchemaWithDefs = {
204
- ...defSchema,
205
- $defs: { ...defs, ...defSchema?.$defs },
206
- definitions: {
207
- ...defSchema.definitions,
208
- ...definitions,
209
- },
210
- };
211
- const pascalName = toPascalCase(defName);
212
- const schemaName = options.splitDefs?.schemaName?.(defName, { isRoot: false }) ?? `${pascalName}Schema`;
213
- const typeName = options.splitDefs?.typeName?.(defName, { isRoot: false }) ?? pascalName;
214
- return { defName, schemaWithDefs: defSchemaWithDefs, schemaName, typeName };
215
- });
216
- const fileName = defInfoMap.get(memberDefs[0])?.fileName ?? `${memberDefs[0]}.schema.ts`;
217
- targets.push({
218
- groupId,
219
- fileName,
220
- members,
221
- usedRefs: new Set(),
222
- isRoot: false,
223
- });
224
- }
225
- if (options.splitDefs?.includeRoot ?? true) {
226
- const rootFile = options.splitDefs?.fileName?.("root", { isRoot: true }) ?? "workflow.schema.ts";
227
- targets.push({
228
- groupId: "root",
229
- fileName: rootFile,
230
- members: [
231
- {
232
- defName: null,
233
- schemaWithDefs: {
234
- ...rootSchema,
235
- definitions: {
236
- ...rootSchema.definitions,
237
- ...definitions,
238
- },
239
- },
240
- schemaName: rootName,
241
- typeName: rootTypeName,
242
- },
243
- ],
244
- usedRefs: new Set(),
245
- isRoot: true,
246
- });
247
- }
248
- return targets;
249
- };
250
- const findRefDependencies = (schema, validDefNames) => {
251
- const deps = new Set();
252
- function traverse(obj) {
253
- if (obj === null || typeof obj !== "object")
254
- return;
255
- if (Array.isArray(obj)) {
256
- obj.forEach(traverse);
257
- return;
258
- }
259
- const record = obj;
260
- if (typeof record["$ref"] === "string") {
261
- const ref = record["$ref"];
262
- const match = ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/);
263
- if (match && validDefNames.includes(match[1])) {
264
- deps.add(match[1]);
265
- }
266
- }
267
- for (const value of Object.values(record)) {
268
- traverse(value);
269
- }
270
- }
271
- traverse(schema);
272
- return deps;
273
- };
274
- const orderGroupMembers = (defs, defInfoMap) => {
275
- const inGroup = new Set(defs);
276
- const visited = new Set();
277
- const temp = new Set();
278
- const result = [];
279
- const visit = (name) => {
280
- if (visited.has(name))
281
- return;
282
- if (temp.has(name)) {
283
- return;
284
- }
285
- temp.add(name);
286
- const info = defInfoMap.get(name);
287
- if (info) {
288
- for (const dep of info.dependencies) {
289
- if (inGroup.has(dep)) {
290
- visit(dep);
291
- }
292
- }
293
- }
294
- temp.delete(name);
295
- visited.add(name);
296
- result.push(name);
297
- };
298
- for (const name of defs) {
299
- visit(name);
300
- }
301
- return result;
302
- };
303
- const detectCycles = (defInfoMap) => {
304
- const cycleNodes = new Set();
305
- const visited = new Set();
306
- const recursionStack = new Set();
307
- function dfs(node, path) {
308
- if (recursionStack.has(node)) {
309
- const cycleStart = path.indexOf(node);
310
- for (let i = cycleStart; i < path.length; i++) {
311
- cycleNodes.add(path[i]);
312
- }
313
- cycleNodes.add(node);
314
- return true;
315
- }
316
- if (visited.has(node))
317
- return false;
318
- visited.add(node);
319
- recursionStack.add(node);
320
- const info = defInfoMap.get(node);
321
- if (info) {
322
- for (const dep of info.dependencies) {
323
- dfs(dep, [...path, node]);
324
- }
325
- }
326
- recursionStack.delete(node);
327
- return false;
328
- }
329
- for (const defName of defInfoMap.keys()) {
330
- if (!visited.has(defName)) {
331
- dfs(defName, []);
332
- }
333
- }
334
- return cycleNodes;
335
- };
336
- const buildSccGroups = (defInfoMap) => {
337
- const indexMap = new Map();
338
- const lowLink = new Map();
339
- const onStack = new Set();
340
- const stack = [];
341
- let index = 0;
342
- const groups = new Map();
343
- const strongConnect = (node) => {
344
- indexMap.set(node, index);
345
- lowLink.set(node, index);
346
- index += 1;
347
- stack.push(node);
348
- onStack.add(node);
349
- const info = defInfoMap.get(node);
350
- if (info) {
351
- for (const dep of info.dependencies) {
352
- if (!indexMap.has(dep)) {
353
- strongConnect(dep);
354
- lowLink.set(node, Math.min(lowLink.get(node), lowLink.get(dep)));
355
- }
356
- else if (onStack.has(dep)) {
357
- lowLink.set(node, Math.min(lowLink.get(node), indexMap.get(dep)));
358
- }
359
- }
360
- }
361
- if (lowLink.get(node) === indexMap.get(node)) {
362
- const members = [];
363
- let w;
364
- do {
365
- w = stack.pop();
366
- if (w) {
367
- onStack.delete(w);
368
- members.push(w);
369
- }
370
- } while (w && w !== node);
371
- const groupId = members.sort().join("__");
372
- groups.set(groupId, members);
373
- }
374
- };
375
- for (const name of defInfoMap.keys()) {
376
- if (!indexMap.has(name)) {
377
- strongConnect(name);
378
- }
379
- }
380
- return groups;
381
- };
382
- const collectNestedTypes = (rootSchema, defs, defNames, rootTypeName) => {
383
- const allNestedTypes = [];
384
- for (const defName of defNames) {
385
- const defSchema = defs[defName];
386
- const parentTypeName = toPascalCase(defName);
387
- const nestedTypes = findNestedTypesInSchema(defSchema, parentTypeName, defNames);
388
- for (const nested of nestedTypes) {
389
- nested.file = defName;
390
- nested.parentType = parentTypeName;
391
- allNestedTypes.push(nested);
392
- }
393
- }
394
- const workflowNestedTypes = findNestedTypesInSchema({ properties: rootSchema.properties, required: rootSchema.required }, rootTypeName, defNames);
395
- for (const nested of workflowNestedTypes) {
396
- nested.file = "workflow";
397
- nested.parentType = rootTypeName;
398
- allNestedTypes.push(nested);
399
- }
400
- const uniqueNestedTypes = new Map();
401
- for (const nested of allNestedTypes) {
402
- if (!uniqueNestedTypes.has(nested.typeName) && nested.propertyPath.length > 0) {
403
- uniqueNestedTypes.set(nested.typeName, nested);
404
- }
405
- }
406
- return [...uniqueNestedTypes.values()];
407
- };
408
- const findNestedTypesInSchema = (schema, parentTypeName, defNames, currentPath = []) => {
409
- const nestedTypes = [];
410
- if (schema === null || typeof schema !== "object")
411
- return nestedTypes;
412
- const record = schema;
413
- if (record.title && typeof record.title === "string") {
414
- const title = record.title;
415
- if (title !== parentTypeName && !defNames.map((d) => toPascalCase(d)).includes(title)) {
416
- nestedTypes.push({
417
- typeName: title,
418
- parentType: parentTypeName,
419
- propertyPath: [...currentPath],
420
- file: "",
421
- });
422
- }
423
- }
424
- // inline $defs
425
- if (record.$defs && typeof record.$defs === "object") {
426
- for (const [, defSchema] of Object.entries(record.$defs)) {
427
- nestedTypes.push(...findNestedTypesInSchema(defSchema, parentTypeName, defNames, currentPath));
428
- }
429
- }
430
- if (record.properties && typeof record.properties === "object") {
431
- for (const [propName, propSchema] of Object.entries(record.properties)) {
432
- nestedTypes.push(...findNestedTypesInSchema(propSchema, parentTypeName, defNames, [...currentPath, propName]));
433
- }
434
- }
435
- if (Array.isArray(record.allOf)) {
436
- for (const item of record.allOf) {
437
- nestedTypes.push(...findNestedTypesInSchema(item, parentTypeName, defNames, currentPath));
438
- }
439
- }
440
- if (record.items) {
441
- nestedTypes.push(...findNestedTypesInSchema(record.items, parentTypeName, defNames, [...currentPath, "items"]));
442
- }
443
- if (record.additionalProperties && typeof record.additionalProperties === "object") {
444
- nestedTypes.push(...findNestedTypesInSchema(record.additionalProperties, parentTypeName, defNames, [...currentPath, "additionalProperties"]));
445
- }
446
- return nestedTypes;
447
- };
448
- const generateNestedTypesFile = (nestedTypes) => {
449
- const lines = [
450
- "/**",
451
- " * Auto-generated nested type exports",
452
- " * ",
453
- " * These types are inline within parent schemas but commonly needed separately.",
454
- " * They are extracted using TypeScript indexed access types.",
455
- " */",
456
- "",
457
- "type Access<T, P extends readonly (string | number)[]> =",
458
- " P extends []",
459
- " ? NonNullable<T>",
460
- " : P extends readonly [infer H, ...infer R]",
461
- " ? H extends \"items\"",
462
- " ? Access<NonNullable<T> extends Array<infer U> ? U : unknown, Extract<R, (string | number)[]>>",
463
- " : H extends \"additionalProperties\"",
464
- " ? Access<NonNullable<T> extends Record<string, infer V> ? V : unknown, Extract<R, (string | number)[]>>",
465
- " : H extends number",
466
- " ? Access<NonNullable<T> extends Array<infer U> ? U : unknown, Extract<R, (string | number)[]>>",
467
- " : H extends string",
468
- " ? Access<",
469
- " H extends keyof NonNullable<T>",
470
- " ? NonNullable<NonNullable<T>[H]>",
471
- " : unknown,",
472
- " Extract<R, (string | number)[]>",
473
- " >",
474
- " : unknown",
475
- " : unknown;",
476
- "",
477
- ];
478
- const byParent = new Map();
479
- for (const info of nestedTypes) {
480
- if (!byParent.has(info.parentType)) {
481
- byParent.set(info.parentType, []);
482
- }
483
- byParent.get(info.parentType).push(info);
484
- }
485
- const imports = new Map(); // file -> type name
486
- for (const info of nestedTypes) {
487
- if (!imports.has(info.file)) {
488
- imports.set(info.file, info.parentType);
489
- }
490
- }
491
- for (const [file, typeName] of [...imports.entries()].sort()) {
492
- lines.push(`import type { ${typeName} } from './${file}.schema.js';`);
493
- }
494
- lines.push("");
495
- const buildAccessExpr = (parentType, propertyPath) => {
496
- const path = propertyPath.map((prop) => (typeof prop === "number" ? prop : JSON.stringify(prop))).join(", ");
497
- return `Access<${parentType}, [${path}]>`;
498
- };
499
- for (const [parentType, types] of [...byParent.entries()].sort()) {
500
- lines.push(`// From ${parentType}`);
501
- for (const info of types.sort((a, b) => a.typeName.localeCompare(b.typeName))) {
502
- if (info.propertyPath.length > 0) {
503
- const accessExpr = buildAccessExpr(parentType, info.propertyPath);
504
- lines.push(`export type ${info.typeName} = ${accessExpr};`);
505
- }
506
- }
507
- lines.push("");
508
- }
509
- return lines.join("\n");
510
- };