@gabrielbryk/json-schema-to-zod 2.10.0 → 2.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/dist/cjs/core/emitZod.js +19 -3
- package/dist/cjs/generators/generateBundle.js +207 -62
- package/dist/cjs/parsers/parseIfThenElse.js +1 -1
- package/dist/cjs/parsers/parseObject.js +3 -0
- package/dist/cjs/parsers/parseSchema.js +8 -0
- package/dist/esm/core/emitZod.js +19 -3
- package/dist/esm/generators/generateBundle.js +207 -62
- package/dist/esm/parsers/parseIfThenElse.js +1 -1
- package/dist/esm/parsers/parseObject.js +3 -0
- package/dist/esm/parsers/parseSchema.js +8 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# @gabrielbryk/json-schema-to-zod
|
|
2
2
|
|
|
3
|
+
## 2.10.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 1c318a0: Ensure bundled outputs remain type-safe by grouping strongly connected defs, emitting lazy getters for recursive object refs, and reordering bundle members to avoid duplicate exports.
|
|
8
|
+
- 87526f0: Preserve precise types for recursive schemas by emitting typed lazy wrappers instead of erasing types, and add a workflow regression type-check to guard against inference blowups.
|
|
9
|
+
|
|
3
10
|
## 2.9.0
|
|
4
11
|
|
|
5
12
|
### Minor Changes
|
package/dist/cjs/core/emitZod.js
CHANGED
|
@@ -73,6 +73,16 @@ const emitZod = (analysis) => {
|
|
|
73
73
|
const { module, name, type, noImport, exportRefs, withMeta, ...rest } = options;
|
|
74
74
|
const declarations = new Map();
|
|
75
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
|
+
};
|
|
76
86
|
const parsedSchema = (0, parseSchema_js_1.parseSchema)(schema, {
|
|
77
87
|
module,
|
|
78
88
|
name,
|
|
@@ -94,10 +104,16 @@ const emitZod = (analysis) => {
|
|
|
94
104
|
});
|
|
95
105
|
const declarationBlock = declarations.size
|
|
96
106
|
? orderDeclarations(Array.from(declarations.entries()), dependencies)
|
|
97
|
-
.
|
|
107
|
+
.flatMap(([refName, value]) => {
|
|
98
108
|
const shouldExport = exportRefs && module === "esm";
|
|
99
|
-
const
|
|
100
|
-
|
|
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;
|
|
101
117
|
})
|
|
102
118
|
.join("\n")
|
|
103
119
|
: "";
|
|
@@ -11,23 +11,27 @@ const generateSchemaBundle = (schema, options = {}) => {
|
|
|
11
11
|
const defs = schema.$defs || {};
|
|
12
12
|
const definitions = schema.definitions || {};
|
|
13
13
|
const defNames = Object.keys(defs);
|
|
14
|
-
const { rootName, rootTypeName, defInfoMap } = buildBundleContext(defNames, defs, options);
|
|
14
|
+
const { rootName, rootTypeName, defInfoMap, groups } = buildBundleContext(defNames, defs, options);
|
|
15
15
|
const files = [];
|
|
16
|
-
const targets = planBundleTargets(schema, defs, definitions, defNames, options, rootName, rootTypeName);
|
|
16
|
+
const targets = planBundleTargets(schema, defs, definitions, defNames, options, rootName, defInfoMap, rootTypeName);
|
|
17
17
|
for (const target of targets) {
|
|
18
18
|
const usedRefs = target.usedRefs;
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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);
|
|
31
35
|
files.push({ fileName: target.fileName, contents: finalSchema });
|
|
32
36
|
}
|
|
33
37
|
// Nested types extraction (optional)
|
|
@@ -57,13 +61,16 @@ const buildDefInfoMap = (defNames, defs, options) => {
|
|
|
57
61
|
const pascalName = toPascalCase(defName);
|
|
58
62
|
const schemaName = options.splitDefs?.schemaName?.(defName, { isRoot: false }) ?? `${pascalName}Schema`;
|
|
59
63
|
const typeName = options.splitDefs?.typeName?.(defName, { isRoot: false }) ?? pascalName;
|
|
64
|
+
const fileName = options.splitDefs?.fileName?.(defName, { isRoot: false }) ?? `${defName}.schema.ts`;
|
|
60
65
|
map.set(defName, {
|
|
61
66
|
name: defName,
|
|
62
67
|
pascalName,
|
|
63
68
|
schemaName,
|
|
64
69
|
typeName,
|
|
70
|
+
fileName,
|
|
65
71
|
dependencies,
|
|
66
72
|
hasCycle: false,
|
|
73
|
+
groupId: "",
|
|
67
74
|
});
|
|
68
75
|
}
|
|
69
76
|
return map;
|
|
@@ -76,13 +83,21 @@ const buildBundleContext = (defNames, defs, options) => {
|
|
|
76
83
|
if (info)
|
|
77
84
|
info.hasCycle = true;
|
|
78
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
|
+
}
|
|
79
94
|
const rootName = options.splitDefs?.rootName ?? options.name ?? "RootSchema";
|
|
80
95
|
const rootTypeName = typeof options.type === "string"
|
|
81
96
|
? options.type
|
|
82
97
|
: options.splitDefs?.rootTypeName ?? (typeof options.type === "boolean" && options.type ? rootName : undefined);
|
|
83
|
-
return { defInfoMap, rootName, rootTypeName };
|
|
98
|
+
return { defInfoMap, rootName, rootTypeName, groups };
|
|
84
99
|
};
|
|
85
|
-
const createRefHandler = (currentDefName, defInfoMap, usedRefs, allDefs, options) => {
|
|
100
|
+
const createRefHandler = (currentDefName, defInfoMap, usedRefs, allDefs, options, currentGroupId) => {
|
|
86
101
|
const useLazyCrossRefs = options.refResolution?.lazyCrossRefs ?? true;
|
|
87
102
|
return (schema, refs) => {
|
|
88
103
|
if (typeof schema["$ref"] === "string") {
|
|
@@ -97,7 +112,7 @@ const createRefHandler = (currentDefName, defInfoMap, usedRefs, allDefs, options
|
|
|
97
112
|
const refInfo = defInfoMap.get(refName);
|
|
98
113
|
if (refInfo) {
|
|
99
114
|
// Track imports when referencing other defs
|
|
100
|
-
if (refName !== currentDefName) {
|
|
115
|
+
if (refName !== currentDefName && refInfo.groupId !== currentGroupId) {
|
|
101
116
|
usedRefs.add(refName);
|
|
102
117
|
}
|
|
103
118
|
const isCycle = refName === currentDefName || (refInfo.hasCycle && !!currentDefName);
|
|
@@ -111,6 +126,13 @@ const createRefHandler = (currentDefName, defInfoMap, usedRefs, allDefs, options
|
|
|
111
126
|
if (resolved)
|
|
112
127
|
return resolved;
|
|
113
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
|
+
}
|
|
114
136
|
return `z.lazy(() => ${refInfo.schemaName})`;
|
|
115
137
|
}
|
|
116
138
|
return refInfo.schemaName;
|
|
@@ -129,42 +151,73 @@ const createRefHandler = (currentDefName, defInfoMap, usedRefs, allDefs, options
|
|
|
129
151
|
return undefined;
|
|
130
152
|
};
|
|
131
153
|
};
|
|
132
|
-
const buildSchemaFile = (
|
|
154
|
+
const buildSchemaFile = (zodCodeParts, usedRefs, defInfoMap, module, target) => {
|
|
133
155
|
if (module !== "esm")
|
|
134
|
-
return
|
|
135
|
-
const
|
|
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();
|
|
136
164
|
for (const refName of [...usedRefs].sort()) {
|
|
137
165
|
const refInfo = defInfoMap.get(refName);
|
|
138
166
|
if (refInfo) {
|
|
139
|
-
|
|
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);
|
|
140
172
|
}
|
|
141
173
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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;
|
|
145
188
|
};
|
|
146
|
-
const planBundleTargets = (rootSchema, defs, definitions, defNames, options, rootName, rootTypeName) => {
|
|
189
|
+
const planBundleTargets = (rootSchema, defs, definitions, defNames, options, rootName, defInfoMap, rootTypeName) => {
|
|
147
190
|
const targets = [];
|
|
191
|
+
const groupById = new Map();
|
|
148
192
|
for (const defName of defNames) {
|
|
149
|
-
const
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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`;
|
|
162
217
|
targets.push({
|
|
163
|
-
|
|
164
|
-
schemaWithDefs: defSchemaWithDefs,
|
|
165
|
-
schemaName,
|
|
166
|
-
typeName,
|
|
218
|
+
groupId,
|
|
167
219
|
fileName,
|
|
220
|
+
members,
|
|
168
221
|
usedRefs: new Set(),
|
|
169
222
|
isRoot: false,
|
|
170
223
|
});
|
|
@@ -172,17 +225,22 @@ const planBundleTargets = (rootSchema, defs, definitions, defNames, options, roo
|
|
|
172
225
|
if (options.splitDefs?.includeRoot ?? true) {
|
|
173
226
|
const rootFile = options.splitDefs?.fileName?.("root", { isRoot: true }) ?? "workflow.schema.ts";
|
|
174
227
|
targets.push({
|
|
175
|
-
|
|
176
|
-
schemaWithDefs: {
|
|
177
|
-
...rootSchema,
|
|
178
|
-
definitions: {
|
|
179
|
-
...rootSchema.definitions,
|
|
180
|
-
...definitions,
|
|
181
|
-
},
|
|
182
|
-
},
|
|
183
|
-
schemaName: rootName,
|
|
184
|
-
typeName: rootTypeName,
|
|
228
|
+
groupId: "root",
|
|
185
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
|
+
],
|
|
186
244
|
usedRefs: new Set(),
|
|
187
245
|
isRoot: true,
|
|
188
246
|
});
|
|
@@ -213,6 +271,35 @@ const findRefDependencies = (schema, validDefNames) => {
|
|
|
213
271
|
traverse(schema);
|
|
214
272
|
return deps;
|
|
215
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
|
+
};
|
|
216
303
|
const detectCycles = (defInfoMap) => {
|
|
217
304
|
const cycleNodes = new Set();
|
|
218
305
|
const visited = new Set();
|
|
@@ -246,6 +333,52 @@ const detectCycles = (defInfoMap) => {
|
|
|
246
333
|
}
|
|
247
334
|
return cycleNodes;
|
|
248
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
|
+
};
|
|
249
382
|
const collectNestedTypes = (rootSchema, defs, defNames, rootTypeName) => {
|
|
250
383
|
const allNestedTypes = [];
|
|
251
384
|
for (const defName of defNames) {
|
|
@@ -321,6 +454,26 @@ const generateNestedTypesFile = (nestedTypes) => {
|
|
|
321
454
|
" * They are extracted using TypeScript indexed access types.",
|
|
322
455
|
" */",
|
|
323
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
|
+
"",
|
|
324
477
|
];
|
|
325
478
|
const byParent = new Map();
|
|
326
479
|
for (const info of nestedTypes) {
|
|
@@ -340,16 +493,8 @@ const generateNestedTypesFile = (nestedTypes) => {
|
|
|
340
493
|
}
|
|
341
494
|
lines.push("");
|
|
342
495
|
const buildAccessExpr = (parentType, propertyPath) => {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
const accessor = prop === "items"
|
|
346
|
-
? "[number]"
|
|
347
|
-
: typeof prop === "number"
|
|
348
|
-
? `[${prop}]`
|
|
349
|
-
: `[${JSON.stringify(prop)}]`;
|
|
350
|
-
accessExpr = `NonNullable<${accessExpr}${accessor}>`;
|
|
351
|
-
}
|
|
352
|
-
return accessExpr;
|
|
496
|
+
const path = propertyPath.map((prop) => (typeof prop === "number" ? prop : JSON.stringify(prop))).join(", ");
|
|
497
|
+
return `Access<${parentType}, [${path}]>`;
|
|
353
498
|
};
|
|
354
499
|
for (const [parentType, types] of [...byParent.entries()].sort()) {
|
|
355
500
|
lines.push(`// From ${parentType}`);
|
|
@@ -18,7 +18,7 @@ const parseIfThenElse = (schema, refs) => {
|
|
|
18
18
|
: ${$else}.safeParse(value);
|
|
19
19
|
if (!result.success) {
|
|
20
20
|
const issues = result.error.issues;
|
|
21
|
-
issues.forEach((issue) => ctx.addIssue(issue))
|
|
21
|
+
issues.forEach((issue) => ctx.addIssue({ ...issue }))
|
|
22
22
|
}
|
|
23
23
|
})`;
|
|
24
24
|
// Store original if/then/else for JSON Schema round-trip
|
|
@@ -311,5 +311,8 @@ const shouldUseGetter = (parsed, refs) => {
|
|
|
311
311
|
if (refs.currentSchemaName && parsed.includes(refs.currentSchemaName)) {
|
|
312
312
|
return true;
|
|
313
313
|
}
|
|
314
|
+
if (refs.cycleRefNames?.has(parsed)) {
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
314
317
|
return Boolean(refs.inProgress && refs.inProgress.has(parsed));
|
|
315
318
|
};
|
|
@@ -124,6 +124,14 @@ const parseRef = (schema, refs) => {
|
|
|
124
124
|
// (or is currently being resolved). This avoids TDZ on true cycles while
|
|
125
125
|
// letting ordered, acyclic refs stay direct.
|
|
126
126
|
if (isSameCycle || refs.inProgress.has(refName)) {
|
|
127
|
+
const inObjectProperty = refs.path.includes("properties") ||
|
|
128
|
+
refs.path.includes("patternProperties") ||
|
|
129
|
+
refs.path.includes("additionalProperties");
|
|
130
|
+
if (inObjectProperty && refName === refs.currentSchemaName) {
|
|
131
|
+
// Getter properties defer evaluation, so a direct reference avoids extra lazies
|
|
132
|
+
// for self-recursion.
|
|
133
|
+
return refName;
|
|
134
|
+
}
|
|
127
135
|
return `z.lazy(() => ${refName})`;
|
|
128
136
|
}
|
|
129
137
|
return refName;
|
package/dist/esm/core/emitZod.js
CHANGED
|
@@ -70,6 +70,16 @@ export const emitZod = (analysis) => {
|
|
|
70
70
|
const { module, name, type, noImport, exportRefs, withMeta, ...rest } = options;
|
|
71
71
|
const declarations = new Map();
|
|
72
72
|
const dependencies = new Map();
|
|
73
|
+
const reserveName = (base) => {
|
|
74
|
+
let candidate = base;
|
|
75
|
+
let i = 1;
|
|
76
|
+
while (usedNames.has(candidate) || declarations.has(candidate)) {
|
|
77
|
+
candidate = `${base}${i}`;
|
|
78
|
+
i += 1;
|
|
79
|
+
}
|
|
80
|
+
usedNames.add(candidate);
|
|
81
|
+
return candidate;
|
|
82
|
+
};
|
|
73
83
|
const parsedSchema = parseSchema(schema, {
|
|
74
84
|
module,
|
|
75
85
|
name,
|
|
@@ -91,10 +101,16 @@ export const emitZod = (analysis) => {
|
|
|
91
101
|
});
|
|
92
102
|
const declarationBlock = declarations.size
|
|
93
103
|
? orderDeclarations(Array.from(declarations.entries()), dependencies)
|
|
94
|
-
.
|
|
104
|
+
.flatMap(([refName, value]) => {
|
|
95
105
|
const shouldExport = exportRefs && module === "esm";
|
|
96
|
-
const
|
|
97
|
-
|
|
106
|
+
const isCycle = cycleRefNames.has(refName);
|
|
107
|
+
if (!isCycle) {
|
|
108
|
+
return [`${shouldExport ? "export " : ""}const ${refName} = ${value}`];
|
|
109
|
+
}
|
|
110
|
+
const baseName = `${refName}Def`;
|
|
111
|
+
const lines = [`const ${baseName} = ${value}`];
|
|
112
|
+
lines.push(`${shouldExport ? "export " : ""}const ${refName} = ${baseName}`);
|
|
113
|
+
return lines;
|
|
98
114
|
})
|
|
99
115
|
.join("\n")
|
|
100
116
|
: "";
|
|
@@ -8,23 +8,27 @@ export const generateSchemaBundle = (schema, options = {}) => {
|
|
|
8
8
|
const defs = schema.$defs || {};
|
|
9
9
|
const definitions = schema.definitions || {};
|
|
10
10
|
const defNames = Object.keys(defs);
|
|
11
|
-
const { rootName, rootTypeName, defInfoMap } = buildBundleContext(defNames, defs, options);
|
|
11
|
+
const { rootName, rootTypeName, defInfoMap, groups } = buildBundleContext(defNames, defs, options);
|
|
12
12
|
const files = [];
|
|
13
|
-
const targets = planBundleTargets(schema, defs, definitions, defNames, options, rootName, rootTypeName);
|
|
13
|
+
const targets = planBundleTargets(schema, defs, definitions, defNames, options, rootName, defInfoMap, rootTypeName);
|
|
14
14
|
for (const target of targets) {
|
|
15
15
|
const usedRefs = target.usedRefs;
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
16
|
+
const zodParts = [];
|
|
17
|
+
for (const member of target.members) {
|
|
18
|
+
const analysis = analyzeSchema(member.schemaWithDefs, {
|
|
19
|
+
...options,
|
|
20
|
+
module,
|
|
21
|
+
name: member.schemaName,
|
|
22
|
+
type: member.typeName,
|
|
23
|
+
parserOverride: createRefHandler(member.defName, defInfoMap, usedRefs, {
|
|
24
|
+
...(member.schemaWithDefs.$defs || {}),
|
|
25
|
+
...(member.schemaWithDefs.definitions || {}),
|
|
26
|
+
}, options, target.groupId),
|
|
27
|
+
});
|
|
28
|
+
const zodSchema = emitZod(analysis);
|
|
29
|
+
zodParts.push(zodSchema);
|
|
30
|
+
}
|
|
31
|
+
const finalSchema = buildSchemaFile(zodParts, usedRefs, defInfoMap, module, target);
|
|
28
32
|
files.push({ fileName: target.fileName, contents: finalSchema });
|
|
29
33
|
}
|
|
30
34
|
// Nested types extraction (optional)
|
|
@@ -53,13 +57,16 @@ const buildDefInfoMap = (defNames, defs, options) => {
|
|
|
53
57
|
const pascalName = toPascalCase(defName);
|
|
54
58
|
const schemaName = options.splitDefs?.schemaName?.(defName, { isRoot: false }) ?? `${pascalName}Schema`;
|
|
55
59
|
const typeName = options.splitDefs?.typeName?.(defName, { isRoot: false }) ?? pascalName;
|
|
60
|
+
const fileName = options.splitDefs?.fileName?.(defName, { isRoot: false }) ?? `${defName}.schema.ts`;
|
|
56
61
|
map.set(defName, {
|
|
57
62
|
name: defName,
|
|
58
63
|
pascalName,
|
|
59
64
|
schemaName,
|
|
60
65
|
typeName,
|
|
66
|
+
fileName,
|
|
61
67
|
dependencies,
|
|
62
68
|
hasCycle: false,
|
|
69
|
+
groupId: "",
|
|
63
70
|
});
|
|
64
71
|
}
|
|
65
72
|
return map;
|
|
@@ -72,13 +79,21 @@ const buildBundleContext = (defNames, defs, options) => {
|
|
|
72
79
|
if (info)
|
|
73
80
|
info.hasCycle = true;
|
|
74
81
|
}
|
|
82
|
+
const groups = buildSccGroups(defInfoMap);
|
|
83
|
+
for (const [groupId, members] of groups) {
|
|
84
|
+
for (const defName of members) {
|
|
85
|
+
const info = defInfoMap.get(defName);
|
|
86
|
+
if (info)
|
|
87
|
+
info.groupId = groupId;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
75
90
|
const rootName = options.splitDefs?.rootName ?? options.name ?? "RootSchema";
|
|
76
91
|
const rootTypeName = typeof options.type === "string"
|
|
77
92
|
? options.type
|
|
78
93
|
: options.splitDefs?.rootTypeName ?? (typeof options.type === "boolean" && options.type ? rootName : undefined);
|
|
79
|
-
return { defInfoMap, rootName, rootTypeName };
|
|
94
|
+
return { defInfoMap, rootName, rootTypeName, groups };
|
|
80
95
|
};
|
|
81
|
-
const createRefHandler = (currentDefName, defInfoMap, usedRefs, allDefs, options) => {
|
|
96
|
+
const createRefHandler = (currentDefName, defInfoMap, usedRefs, allDefs, options, currentGroupId) => {
|
|
82
97
|
const useLazyCrossRefs = options.refResolution?.lazyCrossRefs ?? true;
|
|
83
98
|
return (schema, refs) => {
|
|
84
99
|
if (typeof schema["$ref"] === "string") {
|
|
@@ -93,7 +108,7 @@ const createRefHandler = (currentDefName, defInfoMap, usedRefs, allDefs, options
|
|
|
93
108
|
const refInfo = defInfoMap.get(refName);
|
|
94
109
|
if (refInfo) {
|
|
95
110
|
// Track imports when referencing other defs
|
|
96
|
-
if (refName !== currentDefName) {
|
|
111
|
+
if (refName !== currentDefName && refInfo.groupId !== currentGroupId) {
|
|
97
112
|
usedRefs.add(refName);
|
|
98
113
|
}
|
|
99
114
|
const isCycle = refName === currentDefName || (refInfo.hasCycle && !!currentDefName);
|
|
@@ -107,6 +122,13 @@ const createRefHandler = (currentDefName, defInfoMap, usedRefs, allDefs, options
|
|
|
107
122
|
if (resolved)
|
|
108
123
|
return resolved;
|
|
109
124
|
if (isCycle && useLazyCrossRefs) {
|
|
125
|
+
const inObjectProperty = refs.path.includes("properties") ||
|
|
126
|
+
refs.path.includes("patternProperties") ||
|
|
127
|
+
refs.path.includes("additionalProperties");
|
|
128
|
+
if (inObjectProperty && refName === currentDefName) {
|
|
129
|
+
// Self-recursion inside object getters can safely reference the schema name
|
|
130
|
+
return refInfo.schemaName;
|
|
131
|
+
}
|
|
110
132
|
return `z.lazy(() => ${refInfo.schemaName})`;
|
|
111
133
|
}
|
|
112
134
|
return refInfo.schemaName;
|
|
@@ -125,42 +147,73 @@ const createRefHandler = (currentDefName, defInfoMap, usedRefs, allDefs, options
|
|
|
125
147
|
return undefined;
|
|
126
148
|
};
|
|
127
149
|
};
|
|
128
|
-
const buildSchemaFile = (
|
|
150
|
+
const buildSchemaFile = (zodCodeParts, usedRefs, defInfoMap, module, target) => {
|
|
129
151
|
if (module !== "esm")
|
|
130
|
-
return
|
|
131
|
-
const
|
|
152
|
+
return zodCodeParts.join("\n");
|
|
153
|
+
const groupFileById = new Map();
|
|
154
|
+
for (const info of defInfoMap.values()) {
|
|
155
|
+
if (!groupFileById.has(info.groupId)) {
|
|
156
|
+
groupFileById.set(info.groupId, info.fileName.replace(/\.ts$/, ".js"));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const importsByFile = new Map();
|
|
132
160
|
for (const refName of [...usedRefs].sort()) {
|
|
133
161
|
const refInfo = defInfoMap.get(refName);
|
|
134
162
|
if (refInfo) {
|
|
135
|
-
|
|
163
|
+
const groupFile = groupFileById.get(refInfo.groupId) ?? refInfo.fileName.replace(/\.ts$/, ".js");
|
|
164
|
+
const path = `./${groupFile}`;
|
|
165
|
+
const set = importsByFile.get(path) ?? new Set();
|
|
166
|
+
set.add(refInfo.schemaName);
|
|
167
|
+
importsByFile.set(path, set);
|
|
136
168
|
}
|
|
137
169
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
170
|
+
const imports = [];
|
|
171
|
+
for (const [path, names] of [...importsByFile.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
172
|
+
imports.push(`import { ${[...names].sort().join(", ")} } from '${path}';`);
|
|
173
|
+
}
|
|
174
|
+
const body = zodCodeParts
|
|
175
|
+
.map((code, idx) => {
|
|
176
|
+
if (idx === 0)
|
|
177
|
+
return code;
|
|
178
|
+
return code.replace(/^import \{ z \} from "zod"\n?/, "");
|
|
179
|
+
})
|
|
180
|
+
.join("\n");
|
|
181
|
+
return imports.length
|
|
182
|
+
? body.replace('import { z } from "zod"', `import { z } from "zod"\n${imports.join("\n")}`)
|
|
183
|
+
: body;
|
|
141
184
|
};
|
|
142
|
-
const planBundleTargets = (rootSchema, defs, definitions, defNames, options, rootName, rootTypeName) => {
|
|
185
|
+
const planBundleTargets = (rootSchema, defs, definitions, defNames, options, rootName, defInfoMap, rootTypeName) => {
|
|
143
186
|
const targets = [];
|
|
187
|
+
const groupById = new Map();
|
|
144
188
|
for (const defName of defNames) {
|
|
145
|
-
const
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
189
|
+
const info = defInfoMap.get(defName);
|
|
190
|
+
const gid = info?.groupId || defName;
|
|
191
|
+
if (!groupById.has(gid))
|
|
192
|
+
groupById.set(gid, []);
|
|
193
|
+
groupById.get(gid).push(defName);
|
|
194
|
+
}
|
|
195
|
+
for (const [groupId, memberDefs] of groupById.entries()) {
|
|
196
|
+
const orderedDefs = orderGroupMembers(memberDefs, defInfoMap);
|
|
197
|
+
const members = orderedDefs.map((defName) => {
|
|
198
|
+
const defSchema = defs[defName];
|
|
199
|
+
const defSchemaWithDefs = {
|
|
200
|
+
...defSchema,
|
|
201
|
+
$defs: { ...defs, ...defSchema?.$defs },
|
|
202
|
+
definitions: {
|
|
203
|
+
...defSchema.definitions,
|
|
204
|
+
...definitions,
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
const pascalName = toPascalCase(defName);
|
|
208
|
+
const schemaName = options.splitDefs?.schemaName?.(defName, { isRoot: false }) ?? `${pascalName}Schema`;
|
|
209
|
+
const typeName = options.splitDefs?.typeName?.(defName, { isRoot: false }) ?? pascalName;
|
|
210
|
+
return { defName, schemaWithDefs: defSchemaWithDefs, schemaName, typeName };
|
|
211
|
+
});
|
|
212
|
+
const fileName = defInfoMap.get(memberDefs[0])?.fileName ?? `${memberDefs[0]}.schema.ts`;
|
|
158
213
|
targets.push({
|
|
159
|
-
|
|
160
|
-
schemaWithDefs: defSchemaWithDefs,
|
|
161
|
-
schemaName,
|
|
162
|
-
typeName,
|
|
214
|
+
groupId,
|
|
163
215
|
fileName,
|
|
216
|
+
members,
|
|
164
217
|
usedRefs: new Set(),
|
|
165
218
|
isRoot: false,
|
|
166
219
|
});
|
|
@@ -168,17 +221,22 @@ const planBundleTargets = (rootSchema, defs, definitions, defNames, options, roo
|
|
|
168
221
|
if (options.splitDefs?.includeRoot ?? true) {
|
|
169
222
|
const rootFile = options.splitDefs?.fileName?.("root", { isRoot: true }) ?? "workflow.schema.ts";
|
|
170
223
|
targets.push({
|
|
171
|
-
|
|
172
|
-
schemaWithDefs: {
|
|
173
|
-
...rootSchema,
|
|
174
|
-
definitions: {
|
|
175
|
-
...rootSchema.definitions,
|
|
176
|
-
...definitions,
|
|
177
|
-
},
|
|
178
|
-
},
|
|
179
|
-
schemaName: rootName,
|
|
180
|
-
typeName: rootTypeName,
|
|
224
|
+
groupId: "root",
|
|
181
225
|
fileName: rootFile,
|
|
226
|
+
members: [
|
|
227
|
+
{
|
|
228
|
+
defName: null,
|
|
229
|
+
schemaWithDefs: {
|
|
230
|
+
...rootSchema,
|
|
231
|
+
definitions: {
|
|
232
|
+
...rootSchema.definitions,
|
|
233
|
+
...definitions,
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
schemaName: rootName,
|
|
237
|
+
typeName: rootTypeName,
|
|
238
|
+
},
|
|
239
|
+
],
|
|
182
240
|
usedRefs: new Set(),
|
|
183
241
|
isRoot: true,
|
|
184
242
|
});
|
|
@@ -209,6 +267,35 @@ const findRefDependencies = (schema, validDefNames) => {
|
|
|
209
267
|
traverse(schema);
|
|
210
268
|
return deps;
|
|
211
269
|
};
|
|
270
|
+
const orderGroupMembers = (defs, defInfoMap) => {
|
|
271
|
+
const inGroup = new Set(defs);
|
|
272
|
+
const visited = new Set();
|
|
273
|
+
const temp = new Set();
|
|
274
|
+
const result = [];
|
|
275
|
+
const visit = (name) => {
|
|
276
|
+
if (visited.has(name))
|
|
277
|
+
return;
|
|
278
|
+
if (temp.has(name)) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
temp.add(name);
|
|
282
|
+
const info = defInfoMap.get(name);
|
|
283
|
+
if (info) {
|
|
284
|
+
for (const dep of info.dependencies) {
|
|
285
|
+
if (inGroup.has(dep)) {
|
|
286
|
+
visit(dep);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
temp.delete(name);
|
|
291
|
+
visited.add(name);
|
|
292
|
+
result.push(name);
|
|
293
|
+
};
|
|
294
|
+
for (const name of defs) {
|
|
295
|
+
visit(name);
|
|
296
|
+
}
|
|
297
|
+
return result;
|
|
298
|
+
};
|
|
212
299
|
const detectCycles = (defInfoMap) => {
|
|
213
300
|
const cycleNodes = new Set();
|
|
214
301
|
const visited = new Set();
|
|
@@ -242,6 +329,52 @@ const detectCycles = (defInfoMap) => {
|
|
|
242
329
|
}
|
|
243
330
|
return cycleNodes;
|
|
244
331
|
};
|
|
332
|
+
const buildSccGroups = (defInfoMap) => {
|
|
333
|
+
const indexMap = new Map();
|
|
334
|
+
const lowLink = new Map();
|
|
335
|
+
const onStack = new Set();
|
|
336
|
+
const stack = [];
|
|
337
|
+
let index = 0;
|
|
338
|
+
const groups = new Map();
|
|
339
|
+
const strongConnect = (node) => {
|
|
340
|
+
indexMap.set(node, index);
|
|
341
|
+
lowLink.set(node, index);
|
|
342
|
+
index += 1;
|
|
343
|
+
stack.push(node);
|
|
344
|
+
onStack.add(node);
|
|
345
|
+
const info = defInfoMap.get(node);
|
|
346
|
+
if (info) {
|
|
347
|
+
for (const dep of info.dependencies) {
|
|
348
|
+
if (!indexMap.has(dep)) {
|
|
349
|
+
strongConnect(dep);
|
|
350
|
+
lowLink.set(node, Math.min(lowLink.get(node), lowLink.get(dep)));
|
|
351
|
+
}
|
|
352
|
+
else if (onStack.has(dep)) {
|
|
353
|
+
lowLink.set(node, Math.min(lowLink.get(node), indexMap.get(dep)));
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (lowLink.get(node) === indexMap.get(node)) {
|
|
358
|
+
const members = [];
|
|
359
|
+
let w;
|
|
360
|
+
do {
|
|
361
|
+
w = stack.pop();
|
|
362
|
+
if (w) {
|
|
363
|
+
onStack.delete(w);
|
|
364
|
+
members.push(w);
|
|
365
|
+
}
|
|
366
|
+
} while (w && w !== node);
|
|
367
|
+
const groupId = members.sort().join("__");
|
|
368
|
+
groups.set(groupId, members);
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
for (const name of defInfoMap.keys()) {
|
|
372
|
+
if (!indexMap.has(name)) {
|
|
373
|
+
strongConnect(name);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return groups;
|
|
377
|
+
};
|
|
245
378
|
const collectNestedTypes = (rootSchema, defs, defNames, rootTypeName) => {
|
|
246
379
|
const allNestedTypes = [];
|
|
247
380
|
for (const defName of defNames) {
|
|
@@ -317,6 +450,26 @@ const generateNestedTypesFile = (nestedTypes) => {
|
|
|
317
450
|
" * They are extracted using TypeScript indexed access types.",
|
|
318
451
|
" */",
|
|
319
452
|
"",
|
|
453
|
+
"type Access<T, P extends readonly (string | number)[]> =",
|
|
454
|
+
" P extends []",
|
|
455
|
+
" ? NonNullable<T>",
|
|
456
|
+
" : P extends readonly [infer H, ...infer R]",
|
|
457
|
+
" ? H extends \"items\"",
|
|
458
|
+
" ? Access<NonNullable<T> extends Array<infer U> ? U : unknown, Extract<R, (string | number)[]>>",
|
|
459
|
+
" : H extends \"additionalProperties\"",
|
|
460
|
+
" ? Access<NonNullable<T> extends Record<string, infer V> ? V : unknown, Extract<R, (string | number)[]>>",
|
|
461
|
+
" : H extends number",
|
|
462
|
+
" ? Access<NonNullable<T> extends Array<infer U> ? U : unknown, Extract<R, (string | number)[]>>",
|
|
463
|
+
" : H extends string",
|
|
464
|
+
" ? Access<",
|
|
465
|
+
" H extends keyof NonNullable<T>",
|
|
466
|
+
" ? NonNullable<NonNullable<T>[H]>",
|
|
467
|
+
" : unknown,",
|
|
468
|
+
" Extract<R, (string | number)[]>",
|
|
469
|
+
" >",
|
|
470
|
+
" : unknown",
|
|
471
|
+
" : unknown;",
|
|
472
|
+
"",
|
|
320
473
|
];
|
|
321
474
|
const byParent = new Map();
|
|
322
475
|
for (const info of nestedTypes) {
|
|
@@ -336,16 +489,8 @@ const generateNestedTypesFile = (nestedTypes) => {
|
|
|
336
489
|
}
|
|
337
490
|
lines.push("");
|
|
338
491
|
const buildAccessExpr = (parentType, propertyPath) => {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
const accessor = prop === "items"
|
|
342
|
-
? "[number]"
|
|
343
|
-
: typeof prop === "number"
|
|
344
|
-
? `[${prop}]`
|
|
345
|
-
: `[${JSON.stringify(prop)}]`;
|
|
346
|
-
accessExpr = `NonNullable<${accessExpr}${accessor}>`;
|
|
347
|
-
}
|
|
348
|
-
return accessExpr;
|
|
492
|
+
const path = propertyPath.map((prop) => (typeof prop === "number" ? prop : JSON.stringify(prop))).join(", ");
|
|
493
|
+
return `Access<${parentType}, [${path}]>`;
|
|
349
494
|
};
|
|
350
495
|
for (const [parentType, types] of [...byParent.entries()].sort()) {
|
|
351
496
|
lines.push(`// From ${parentType}`);
|
|
@@ -15,7 +15,7 @@ export const parseIfThenElse = (schema, refs) => {
|
|
|
15
15
|
: ${$else}.safeParse(value);
|
|
16
16
|
if (!result.success) {
|
|
17
17
|
const issues = result.error.issues;
|
|
18
|
-
issues.forEach((issue) => ctx.addIssue(issue))
|
|
18
|
+
issues.forEach((issue) => ctx.addIssue({ ...issue }))
|
|
19
19
|
}
|
|
20
20
|
})`;
|
|
21
21
|
// Store original if/then/else for JSON Schema round-trip
|
|
@@ -308,5 +308,8 @@ const shouldUseGetter = (parsed, refs) => {
|
|
|
308
308
|
if (refs.currentSchemaName && parsed.includes(refs.currentSchemaName)) {
|
|
309
309
|
return true;
|
|
310
310
|
}
|
|
311
|
+
if (refs.cycleRefNames?.has(parsed)) {
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
311
314
|
return Boolean(refs.inProgress && refs.inProgress.has(parsed));
|
|
312
315
|
};
|
|
@@ -120,6 +120,14 @@ const parseRef = (schema, refs) => {
|
|
|
120
120
|
// (or is currently being resolved). This avoids TDZ on true cycles while
|
|
121
121
|
// letting ordered, acyclic refs stay direct.
|
|
122
122
|
if (isSameCycle || refs.inProgress.has(refName)) {
|
|
123
|
+
const inObjectProperty = refs.path.includes("properties") ||
|
|
124
|
+
refs.path.includes("patternProperties") ||
|
|
125
|
+
refs.path.includes("additionalProperties");
|
|
126
|
+
if (inObjectProperty && refName === refs.currentSchemaName) {
|
|
127
|
+
// Getter properties defer evaluation, so a direct reference avoids extra lazies
|
|
128
|
+
// for self-recursion.
|
|
129
|
+
return refName;
|
|
130
|
+
}
|
|
123
131
|
return `z.lazy(() => ${refName})`;
|
|
124
132
|
}
|
|
125
133
|
return refName;
|