@gabrielbryk/json-schema-to-zod 2.8.0 → 2.9.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.
- package/CHANGELOG.md +13 -0
- package/dist/cjs/core/analyzeSchema.js +62 -0
- package/dist/cjs/core/emitZod.js +141 -0
- package/dist/cjs/generators/generateBundle.js +103 -59
- package/dist/cjs/index.js +4 -0
- package/dist/cjs/jsonSchemaToZod.js +5 -167
- package/dist/cjs/parsers/parseSchema.js +124 -24
- package/dist/cjs/utils/buildRefRegistry.js +56 -0
- package/dist/cjs/utils/resolveUri.js +16 -0
- package/dist/esm/Types.js +1 -2
- package/dist/esm/cli.js +10 -12
- package/dist/esm/core/analyzeSchema.js +58 -0
- package/dist/esm/core/emitZod.js +137 -0
- package/dist/esm/generators/generateBundle.js +104 -64
- package/dist/esm/index.js +34 -46
- package/dist/esm/jsonSchemaToZod.js +5 -171
- package/dist/esm/parsers/parseAllOf.js +5 -8
- package/dist/esm/parsers/parseAnyOf.js +6 -10
- package/dist/esm/parsers/parseArray.js +11 -15
- package/dist/esm/parsers/parseBoolean.js +1 -5
- package/dist/esm/parsers/parseConst.js +1 -5
- package/dist/esm/parsers/parseDefault.js +3 -7
- package/dist/esm/parsers/parseEnum.js +1 -5
- package/dist/esm/parsers/parseIfThenElse.js +5 -9
- package/dist/esm/parsers/parseMultipleType.js +3 -7
- package/dist/esm/parsers/parseNot.js +4 -8
- package/dist/esm/parsers/parseNull.js +1 -5
- package/dist/esm/parsers/parseNullable.js +4 -8
- package/dist/esm/parsers/parseNumber.js +11 -15
- package/dist/esm/parsers/parseObject.js +25 -28
- package/dist/esm/parsers/parseOneOf.js +6 -10
- package/dist/esm/parsers/parseSchema.js +183 -87
- package/dist/esm/parsers/parseSimpleDiscriminatedOneOf.js +6 -10
- package/dist/esm/parsers/parseString.js +11 -15
- package/dist/esm/utils/anyOrUnknown.js +1 -5
- package/dist/esm/utils/buildRefRegistry.js +52 -0
- package/dist/esm/utils/cliTools.js +7 -13
- package/dist/esm/utils/cycles.js +3 -9
- package/dist/esm/utils/half.js +1 -5
- package/dist/esm/utils/jsdocs.js +3 -8
- package/dist/esm/utils/omit.js +1 -5
- package/dist/esm/utils/resolveUri.js +12 -0
- package/dist/esm/utils/withMessage.js +1 -4
- package/dist/esm/zodToJsonSchema.js +1 -4
- package/dist/types/Types.d.ts +28 -0
- package/dist/types/core/analyzeSchema.d.ts +24 -0
- package/dist/types/core/emitZod.d.ts +2 -0
- package/dist/types/generators/generateBundle.d.ts +5 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/jsonSchemaToZod.d.ts +1 -1
- package/dist/types/parsers/parseSchema.d.ts +2 -1
- package/dist/types/utils/buildRefRegistry.d.ts +12 -0
- package/dist/types/utils/resolveUri.d.ts +1 -0
- package/docs/proposals/bundle-refactor.md +43 -0
- package/docs/proposals/ref-anchor-support.md +65 -0
- package/eslint.config.js +26 -0
- package/package.json +10 -4
- /package/{jest.config.js → jest.config.cjs} +0 -0
- /package/{postcjs.js → postcjs.cjs} +0 -0
- /package/{postesm.js → postesm.cjs} +0 -0
|
@@ -19,9 +19,12 @@ const parseOneOf_js_1 = require("./parseOneOf.js");
|
|
|
19
19
|
const parseSimpleDiscriminatedOneOf_js_1 = require("./parseSimpleDiscriminatedOneOf.js");
|
|
20
20
|
const parseNullable_js_1 = require("./parseNullable.js");
|
|
21
21
|
const anyOrUnknown_js_1 = require("../utils/anyOrUnknown.js");
|
|
22
|
+
const resolveUri_js_1 = require("../utils/resolveUri.js");
|
|
23
|
+
const buildRefRegistry_js_1 = require("../utils/buildRefRegistry.js");
|
|
22
24
|
const parseSchema = (schema, refs = { seen: new Map(), path: [] }, blockMeta) => {
|
|
23
25
|
// Ensure ref bookkeeping exists so $ref declarations and getter-based recursion work
|
|
24
26
|
refs.root = refs.root ?? schema;
|
|
27
|
+
refs.rootBaseUri = refs.rootBaseUri ?? "root:///";
|
|
25
28
|
refs.declarations = refs.declarations ?? new Map();
|
|
26
29
|
refs.dependencies = refs.dependencies ?? new Map();
|
|
27
30
|
refs.inProgress = refs.inProgress ?? new Set();
|
|
@@ -29,8 +32,20 @@ const parseSchema = (schema, refs = { seen: new Map(), path: [] }, blockMeta) =>
|
|
|
29
32
|
refs.usedNames = refs.usedNames ?? new Set();
|
|
30
33
|
if (typeof schema !== "object")
|
|
31
34
|
return schema ? (0, anyOrUnknown_js_1.anyOrUnknown)(refs) : "z.never()";
|
|
35
|
+
const parentBase = refs.currentBaseUri ?? refs.rootBaseUri ?? "root:///";
|
|
36
|
+
const baseUri = typeof schema.$id === "string"
|
|
37
|
+
? (0, resolveUri_js_1.resolveUri)(parentBase, schema.$id)
|
|
38
|
+
: parentBase;
|
|
39
|
+
const dynamicAnchors = Array.isArray(refs.dynamicAnchors) ? [...refs.dynamicAnchors] : [];
|
|
40
|
+
if (typeof schema.$dynamicAnchor === "string") {
|
|
41
|
+
dynamicAnchors.push({
|
|
42
|
+
name: schema.$dynamicAnchor,
|
|
43
|
+
uri: baseUri,
|
|
44
|
+
path: refs.path,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
32
47
|
if (refs.parserOverride) {
|
|
33
|
-
const custom = refs.parserOverride(schema, refs);
|
|
48
|
+
const custom = refs.parserOverride(schema, { ...refs, currentBaseUri: baseUri, dynamicAnchors });
|
|
34
49
|
if (typeof custom === "string") {
|
|
35
50
|
return custom;
|
|
36
51
|
}
|
|
@@ -50,14 +65,14 @@ const parseSchema = (schema, refs = { seen: new Map(), path: [] }, blockMeta) =>
|
|
|
50
65
|
refs.seen.set(schema, seen);
|
|
51
66
|
}
|
|
52
67
|
if (exports.its.a.ref(schema)) {
|
|
53
|
-
const parsedRef = parseRef(schema, refs);
|
|
68
|
+
const parsedRef = parseRef(schema, { ...refs, currentBaseUri: baseUri, dynamicAnchors });
|
|
54
69
|
seen.r = parsedRef;
|
|
55
70
|
return parsedRef;
|
|
56
71
|
}
|
|
57
|
-
let parsed = selectParser(schema, refs);
|
|
72
|
+
let parsed = selectParser(schema, { ...refs, currentBaseUri: baseUri, dynamicAnchors });
|
|
58
73
|
if (!blockMeta) {
|
|
59
74
|
if (!refs.withoutDescribes) {
|
|
60
|
-
parsed = addDescribes(schema, parsed, refs);
|
|
75
|
+
parsed = addDescribes(schema, parsed, { ...refs, currentBaseUri: baseUri, dynamicAnchors });
|
|
61
76
|
}
|
|
62
77
|
if (!refs.withoutDefaults) {
|
|
63
78
|
parsed = addDefaults(schema, parsed);
|
|
@@ -69,17 +84,20 @@ const parseSchema = (schema, refs = { seen: new Map(), path: [] }, blockMeta) =>
|
|
|
69
84
|
};
|
|
70
85
|
exports.parseSchema = parseSchema;
|
|
71
86
|
const parseRef = (schema, refs) => {
|
|
72
|
-
const
|
|
87
|
+
const refValue = schema.$dynamicRef ?? schema.$ref;
|
|
88
|
+
const resolved = resolveRef(schema, refValue, refs);
|
|
73
89
|
if (!resolved) {
|
|
90
|
+
refs.onUnresolvedRef?.(refValue, refs.path);
|
|
74
91
|
return (0, anyOrUnknown_js_1.anyOrUnknown)(refs);
|
|
75
92
|
}
|
|
76
|
-
const { schema: target, path } = resolved;
|
|
77
|
-
const refName = getOrCreateRefName(
|
|
93
|
+
const { schema: target, path, pointerKey } = resolved;
|
|
94
|
+
const refName = getOrCreateRefName(pointerKey, path, refs);
|
|
78
95
|
if (!refs.declarations.has(refName) && !refs.inProgress.has(refName)) {
|
|
79
96
|
refs.inProgress.add(refName);
|
|
80
97
|
const declaration = (0, exports.parseSchema)(target, {
|
|
81
98
|
...refs,
|
|
82
99
|
path,
|
|
100
|
+
currentBaseUri: resolved.baseUri,
|
|
83
101
|
currentSchemaName: refName,
|
|
84
102
|
root: refs.root,
|
|
85
103
|
});
|
|
@@ -132,23 +150,93 @@ const addDescribes = (schema, parsed, refs) => {
|
|
|
132
150
|
}
|
|
133
151
|
return parsed;
|
|
134
152
|
};
|
|
135
|
-
const resolveRef = (
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
.
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
153
|
+
const resolveRef = (schemaNode, ref, refs) => {
|
|
154
|
+
const base = refs.currentBaseUri ?? refs.rootBaseUri ?? "root:///";
|
|
155
|
+
// Handle dynamicRef lookup via dynamicAnchors stack
|
|
156
|
+
const isDynamic = typeof schemaNode.$dynamicRef === "string";
|
|
157
|
+
if (isDynamic && refs.dynamicAnchors && ref.startsWith("#")) {
|
|
158
|
+
const name = ref.slice(1);
|
|
159
|
+
for (let i = refs.dynamicAnchors.length - 1; i >= 0; i -= 1) {
|
|
160
|
+
const entry = refs.dynamicAnchors[i];
|
|
161
|
+
if (entry.name === name) {
|
|
162
|
+
const key = `${entry.uri}#${name}`;
|
|
163
|
+
const target = refs.refRegistry?.get(key);
|
|
164
|
+
if (target) {
|
|
165
|
+
return { schema: target.schema, path: target.path, baseUri: target.baseUri, pointerKey: key };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// Resolve URI against base
|
|
171
|
+
const resolvedUri = (0, resolveUri_js_1.resolveUri)(base, ref);
|
|
172
|
+
const [uriBase, fragment] = resolvedUri.split("#");
|
|
173
|
+
const key = fragment ? `${uriBase}#${fragment}` : uriBase;
|
|
174
|
+
let regEntry = refs.refRegistry?.get(key);
|
|
175
|
+
if (regEntry) {
|
|
176
|
+
return { schema: regEntry.schema, path: regEntry.path, baseUri: regEntry.baseUri, pointerKey: key };
|
|
177
|
+
}
|
|
178
|
+
// Legacy recursive ref: treat as dynamic to __recursive__
|
|
179
|
+
if (schemaNode.$recursiveRef) {
|
|
180
|
+
const recursiveKey = `${base}#__recursive__`;
|
|
181
|
+
regEntry = refs.refRegistry?.get(recursiveKey);
|
|
182
|
+
if (regEntry) {
|
|
183
|
+
return {
|
|
184
|
+
schema: regEntry.schema,
|
|
185
|
+
path: regEntry.path,
|
|
186
|
+
baseUri: regEntry.baseUri,
|
|
187
|
+
pointerKey: recursiveKey,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// External resolver hook
|
|
192
|
+
const extBase = uriBaseFromRef(resolvedUri);
|
|
193
|
+
if (refs.resolveExternalRef && extBase && !isLocalBase(extBase, refs.rootBaseUri ?? "")) {
|
|
194
|
+
const loaded = refs.resolveExternalRef(extBase);
|
|
195
|
+
if (loaded) {
|
|
196
|
+
// If async resolver is used synchronously here, it will be ignored; keep simple sync for now
|
|
197
|
+
const schema = loaded.then ? undefined : loaded;
|
|
198
|
+
if (schema) {
|
|
199
|
+
const { registry } = (0, buildRefRegistry_js_1.buildRefRegistry)(schema, extBase);
|
|
200
|
+
registry.forEach((entry, k) => refs.refRegistry?.set(k, entry));
|
|
201
|
+
regEntry = refs.refRegistry?.get(key);
|
|
202
|
+
if (regEntry) {
|
|
203
|
+
return {
|
|
204
|
+
schema: regEntry.schema,
|
|
205
|
+
path: regEntry.path,
|
|
206
|
+
baseUri: regEntry.baseUri,
|
|
207
|
+
pointerKey: key,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// Backward compatibility: JSON Pointer into root
|
|
214
|
+
if (refs.root && ref.startsWith("#/")) {
|
|
215
|
+
const rawSegments = ref
|
|
216
|
+
.slice(2)
|
|
217
|
+
.split("/")
|
|
218
|
+
.filter((segment) => segment.length > 0)
|
|
219
|
+
.map(decodePointerSegment);
|
|
220
|
+
let current = refs.root;
|
|
221
|
+
for (const segment of rawSegments) {
|
|
222
|
+
if (typeof current !== "object" || current === null)
|
|
223
|
+
return undefined;
|
|
224
|
+
current = current[segment];
|
|
225
|
+
}
|
|
226
|
+
return { schema: current, path: rawSegments, baseUri: base, pointerKey: ref };
|
|
227
|
+
}
|
|
228
|
+
return undefined;
|
|
150
229
|
};
|
|
151
230
|
const decodePointerSegment = (segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
231
|
+
const uriBaseFromRef = (resolvedUri) => {
|
|
232
|
+
const hashIdx = resolvedUri.indexOf("#");
|
|
233
|
+
return hashIdx === -1 ? resolvedUri : resolvedUri.slice(0, hashIdx);
|
|
234
|
+
};
|
|
235
|
+
const isLocalBase = (base, rootBase) => {
|
|
236
|
+
if (!rootBase)
|
|
237
|
+
return false;
|
|
238
|
+
return base === rootBase;
|
|
239
|
+
};
|
|
152
240
|
const getOrCreateRefName = (pointer, path, refs) => {
|
|
153
241
|
if (refs.refNameByPointer?.has(pointer)) {
|
|
154
242
|
return refs.refNameByPointer.get(pointer);
|
|
@@ -159,12 +247,24 @@ const getOrCreateRefName = (pointer, path, refs) => {
|
|
|
159
247
|
return preferred;
|
|
160
248
|
};
|
|
161
249
|
const buildNameFromPath = (path, used) => {
|
|
162
|
-
const filtered = path
|
|
250
|
+
const filtered = path
|
|
251
|
+
.map((segment, idx) => {
|
|
252
|
+
if (idx === 0 && (segment === "$defs" || segment === "definitions")) {
|
|
253
|
+
return undefined; // root-level defs prefix is redundant for naming
|
|
254
|
+
}
|
|
255
|
+
if (segment === "properties")
|
|
256
|
+
return undefined; // skip noisy properties segment
|
|
257
|
+
if (segment === "$defs" || segment === "definitions")
|
|
258
|
+
return "Defs";
|
|
259
|
+
return segment;
|
|
260
|
+
})
|
|
261
|
+
.filter((segment) => segment !== undefined);
|
|
163
262
|
const base = filtered.length
|
|
164
263
|
? filtered
|
|
165
264
|
.map((segment) => typeof segment === "number"
|
|
166
265
|
? `Ref${segment}`
|
|
167
266
|
: segment
|
|
267
|
+
.toString()
|
|
168
268
|
.replace(/[^a-zA-Z0-9_$]/g, " ")
|
|
169
269
|
.split(" ")
|
|
170
270
|
.filter(Boolean)
|
|
@@ -266,7 +366,7 @@ exports.its = {
|
|
|
266
366
|
nullable: (x) => x.nullable === true,
|
|
267
367
|
multipleType: (x) => Array.isArray(x.type),
|
|
268
368
|
not: (x) => x.not !== undefined,
|
|
269
|
-
ref: (x) => typeof x.$ref === "string",
|
|
369
|
+
ref: (x) => typeof x.$ref === "string" || typeof x.$dynamicRef === "string",
|
|
270
370
|
const: (x) => x.const !== undefined,
|
|
271
371
|
primitive: (x, p) => x.type === p,
|
|
272
372
|
conditional: (x) => Boolean("if" in x && x.if && "then" in x && "else" in x && x.then && x.else),
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildRefRegistry = void 0;
|
|
4
|
+
const resolveUri_js_1 = require("./resolveUri.js");
|
|
5
|
+
const buildRefRegistry = (schema, rootBaseUri = "root:///", opts = {}) => {
|
|
6
|
+
const registry = new Map();
|
|
7
|
+
const walk = (node, baseUri, path) => {
|
|
8
|
+
if (typeof node !== "object" || node === null)
|
|
9
|
+
return;
|
|
10
|
+
const obj = node;
|
|
11
|
+
const nextBase = obj.$id ? (0, resolveUri_js_1.resolveUri)(baseUri, obj.$id) : baseUri;
|
|
12
|
+
// Legacy recursive anchor
|
|
13
|
+
if (obj.$recursiveAnchor === true) {
|
|
14
|
+
const name = "__recursive__";
|
|
15
|
+
registry.set(`${nextBase}#${name}`, {
|
|
16
|
+
schema: node,
|
|
17
|
+
path,
|
|
18
|
+
baseUri: nextBase,
|
|
19
|
+
dynamic: true,
|
|
20
|
+
anchor: name,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
// Register base entry
|
|
24
|
+
registry.set(nextBase, { schema: node, path, baseUri: nextBase });
|
|
25
|
+
if (typeof obj.$anchor === "string") {
|
|
26
|
+
registry.set(`${nextBase}#${obj.$anchor}`, {
|
|
27
|
+
schema: node,
|
|
28
|
+
path,
|
|
29
|
+
baseUri: nextBase,
|
|
30
|
+
anchor: obj.$anchor,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
if (typeof obj.$dynamicAnchor === "string") {
|
|
34
|
+
const name = obj.$dynamicAnchor;
|
|
35
|
+
registry.set(`${nextBase}#${name}`, {
|
|
36
|
+
schema: node,
|
|
37
|
+
path,
|
|
38
|
+
baseUri: nextBase,
|
|
39
|
+
dynamic: true,
|
|
40
|
+
anchor: name,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
for (const key in obj) {
|
|
44
|
+
const value = obj[key];
|
|
45
|
+
if (Array.isArray(value)) {
|
|
46
|
+
value.forEach((v, i) => walk(v, nextBase, [...path, key, i]));
|
|
47
|
+
}
|
|
48
|
+
else if (typeof value === "object" && value !== null) {
|
|
49
|
+
walk(value, nextBase, [...path, key]);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
walk(schema, rootBaseUri, []);
|
|
54
|
+
return { registry, rootBaseUri };
|
|
55
|
+
};
|
|
56
|
+
exports.buildRefRegistry = buildRefRegistry;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveUri = void 0;
|
|
4
|
+
const resolveUri = (base, ref) => {
|
|
5
|
+
try {
|
|
6
|
+
// If ref is absolute, new URL will accept it; otherwise resolves against base
|
|
7
|
+
return new URL(ref, base).toString();
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
// Fallback: simple concatenation to avoid throwing; keep ref as-is
|
|
11
|
+
if (ref.startsWith("#"))
|
|
12
|
+
return `${base}${ref}`;
|
|
13
|
+
return ref;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
exports.resolveUri = resolveUri;
|
package/dist/esm/Types.js
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
1
|
+
export {};
|
package/dist/esm/cli.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const path_1 = require("path");
|
|
7
|
-
const cliTools_js_1 = require("./utils/cliTools.js");
|
|
2
|
+
import { jsonSchemaToZod } from "./jsonSchemaToZod.js";
|
|
3
|
+
import { writeFileSync, mkdirSync } from "fs";
|
|
4
|
+
import { dirname } from "path";
|
|
5
|
+
import { parseArgs, parseOrReadJSON, readPipe } from "./utils/cliTools.js";
|
|
8
6
|
const params = {
|
|
9
7
|
input: {
|
|
10
8
|
shorthand: "i",
|
|
@@ -48,10 +46,10 @@ const params = {
|
|
|
48
46
|
},
|
|
49
47
|
};
|
|
50
48
|
async function main() {
|
|
51
|
-
const args =
|
|
52
|
-
const input = args.input || (await
|
|
53
|
-
const jsonSchema =
|
|
54
|
-
const zodSchema =
|
|
49
|
+
const args = parseArgs(params, process.argv, true);
|
|
50
|
+
const input = args.input || (await readPipe());
|
|
51
|
+
const jsonSchema = parseOrReadJSON(input);
|
|
52
|
+
const zodSchema = jsonSchemaToZod(jsonSchema, {
|
|
55
53
|
name: args.name,
|
|
56
54
|
depth: args.depth,
|
|
57
55
|
module: args.module || "esm",
|
|
@@ -60,8 +58,8 @@ async function main() {
|
|
|
60
58
|
withJsdocs: args.withJsdocs,
|
|
61
59
|
});
|
|
62
60
|
if (args.output) {
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
mkdirSync(dirname(args.output), { recursive: true });
|
|
62
|
+
writeFileSync(args.output, zodSchema);
|
|
65
63
|
}
|
|
66
64
|
else {
|
|
67
65
|
console.log(zodSchema);
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { parseSchema } from "../parsers/parseSchema.js";
|
|
2
|
+
import { detectCycles, computeScc } from "../utils/cycles.js";
|
|
3
|
+
import { buildRefRegistry } from "../utils/buildRefRegistry.js";
|
|
4
|
+
export const analyzeSchema = (schema, options = {}) => {
|
|
5
|
+
const { module, name, type, ...rest } = options;
|
|
6
|
+
if (type && (!name || module !== "esm")) {
|
|
7
|
+
throw new Error("Option `type` requires `name` to be set and `module` to be `esm`");
|
|
8
|
+
}
|
|
9
|
+
const normalized = {
|
|
10
|
+
module,
|
|
11
|
+
name,
|
|
12
|
+
type,
|
|
13
|
+
...rest,
|
|
14
|
+
exportRefs: rest.exportRefs ?? true,
|
|
15
|
+
withMeta: rest.withMeta ?? true,
|
|
16
|
+
};
|
|
17
|
+
const refNameByPointer = new Map();
|
|
18
|
+
const usedNames = new Set();
|
|
19
|
+
if (name) {
|
|
20
|
+
usedNames.add(name);
|
|
21
|
+
}
|
|
22
|
+
const declarations = new Map();
|
|
23
|
+
const dependencies = new Map();
|
|
24
|
+
const { registry: refRegistry, rootBaseUri } = buildRefRegistry(schema);
|
|
25
|
+
const pass1 = {
|
|
26
|
+
module,
|
|
27
|
+
name,
|
|
28
|
+
path: [],
|
|
29
|
+
seen: new Map(),
|
|
30
|
+
declarations,
|
|
31
|
+
dependencies,
|
|
32
|
+
inProgress: new Set(),
|
|
33
|
+
refNameByPointer,
|
|
34
|
+
usedNames,
|
|
35
|
+
root: schema,
|
|
36
|
+
currentSchemaName: name,
|
|
37
|
+
refRegistry,
|
|
38
|
+
rootBaseUri,
|
|
39
|
+
...rest,
|
|
40
|
+
withMeta: normalized.withMeta,
|
|
41
|
+
};
|
|
42
|
+
parseSchema(schema, pass1);
|
|
43
|
+
const names = Array.from(declarations.keys());
|
|
44
|
+
const cycleRefNames = detectCycles(names, dependencies);
|
|
45
|
+
const { componentByName } = computeScc(names, dependencies);
|
|
46
|
+
return {
|
|
47
|
+
schema,
|
|
48
|
+
options: normalized,
|
|
49
|
+
refNameByPointer,
|
|
50
|
+
usedNames,
|
|
51
|
+
declarations,
|
|
52
|
+
dependencies,
|
|
53
|
+
cycleRefNames,
|
|
54
|
+
cycleComponentByName: componentByName,
|
|
55
|
+
refRegistry,
|
|
56
|
+
rootBaseUri,
|
|
57
|
+
};
|
|
58
|
+
};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { parseSchema } from "../parsers/parseSchema.js";
|
|
2
|
+
import { expandJsdocs } from "../utils/jsdocs.js";
|
|
3
|
+
const orderDeclarations = (entries, dependencies) => {
|
|
4
|
+
const valueByName = new Map(entries);
|
|
5
|
+
const depGraph = new Map();
|
|
6
|
+
for (const [from, set] of dependencies.entries()) {
|
|
7
|
+
const onlyKnown = new Set();
|
|
8
|
+
for (const dep of set) {
|
|
9
|
+
if (valueByName.has(dep) && dep !== from) {
|
|
10
|
+
onlyKnown.add(dep);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
if (onlyKnown.size)
|
|
14
|
+
depGraph.set(from, onlyKnown);
|
|
15
|
+
}
|
|
16
|
+
const names = Array.from(valueByName.keys());
|
|
17
|
+
for (const [name, value] of entries) {
|
|
18
|
+
const deps = depGraph.get(name) ?? new Set();
|
|
19
|
+
for (const candidate of names) {
|
|
20
|
+
if (candidate === name)
|
|
21
|
+
continue;
|
|
22
|
+
const matcher = new RegExp(`\\b${candidate}\\b`);
|
|
23
|
+
if (matcher.test(value)) {
|
|
24
|
+
deps.add(candidate);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (deps.size)
|
|
28
|
+
depGraph.set(name, deps);
|
|
29
|
+
}
|
|
30
|
+
const ordered = [];
|
|
31
|
+
const perm = new Set();
|
|
32
|
+
const temp = new Set();
|
|
33
|
+
const visit = (name) => {
|
|
34
|
+
if (perm.has(name))
|
|
35
|
+
return;
|
|
36
|
+
if (temp.has(name)) {
|
|
37
|
+
temp.delete(name);
|
|
38
|
+
perm.add(name);
|
|
39
|
+
ordered.push(name);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
temp.add(name);
|
|
43
|
+
const deps = depGraph.get(name);
|
|
44
|
+
if (deps) {
|
|
45
|
+
for (const dep of deps) {
|
|
46
|
+
if (valueByName.has(dep)) {
|
|
47
|
+
visit(dep);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
temp.delete(name);
|
|
52
|
+
perm.add(name);
|
|
53
|
+
ordered.push(name);
|
|
54
|
+
};
|
|
55
|
+
for (const name of valueByName.keys()) {
|
|
56
|
+
visit(name);
|
|
57
|
+
}
|
|
58
|
+
const unique = [];
|
|
59
|
+
const seen = new Set();
|
|
60
|
+
for (const name of ordered) {
|
|
61
|
+
if (!seen.has(name)) {
|
|
62
|
+
seen.add(name);
|
|
63
|
+
unique.push(name);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return unique.map((name) => [name, valueByName.get(name)]);
|
|
67
|
+
};
|
|
68
|
+
export const emitZod = (analysis) => {
|
|
69
|
+
const { schema, options, refNameByPointer, usedNames, cycleRefNames, cycleComponentByName, } = analysis;
|
|
70
|
+
const { module, name, type, noImport, exportRefs, withMeta, ...rest } = options;
|
|
71
|
+
const declarations = new Map();
|
|
72
|
+
const dependencies = new Map();
|
|
73
|
+
const parsedSchema = parseSchema(schema, {
|
|
74
|
+
module,
|
|
75
|
+
name,
|
|
76
|
+
path: [],
|
|
77
|
+
seen: new Map(),
|
|
78
|
+
declarations,
|
|
79
|
+
dependencies,
|
|
80
|
+
inProgress: new Set(),
|
|
81
|
+
refNameByPointer,
|
|
82
|
+
usedNames,
|
|
83
|
+
root: schema,
|
|
84
|
+
currentSchemaName: name,
|
|
85
|
+
cycleRefNames,
|
|
86
|
+
cycleComponentByName,
|
|
87
|
+
refRegistry: analysis.refRegistry,
|
|
88
|
+
rootBaseUri: analysis.rootBaseUri,
|
|
89
|
+
...rest,
|
|
90
|
+
withMeta,
|
|
91
|
+
});
|
|
92
|
+
const declarationBlock = declarations.size
|
|
93
|
+
? orderDeclarations(Array.from(declarations.entries()), dependencies)
|
|
94
|
+
.map(([refName, value]) => {
|
|
95
|
+
const shouldExport = exportRefs && module === "esm";
|
|
96
|
+
const decl = `${shouldExport ? "export " : ""}const ${refName} = ${value}`;
|
|
97
|
+
return decl;
|
|
98
|
+
})
|
|
99
|
+
.join("\n")
|
|
100
|
+
: "";
|
|
101
|
+
const jsdocs = rest.withJsdocs && typeof schema !== "boolean" && schema.description
|
|
102
|
+
? expandJsdocs(schema.description)
|
|
103
|
+
: "";
|
|
104
|
+
const lines = [];
|
|
105
|
+
if (module === "cjs" && !noImport) {
|
|
106
|
+
lines.push(`const { z } = require("zod")`);
|
|
107
|
+
}
|
|
108
|
+
if (module === "esm" && !noImport) {
|
|
109
|
+
lines.push(`import { z } from "zod"`);
|
|
110
|
+
}
|
|
111
|
+
if (declarationBlock) {
|
|
112
|
+
lines.push(declarationBlock);
|
|
113
|
+
}
|
|
114
|
+
if (module === "cjs") {
|
|
115
|
+
const payload = name ? `{ ${JSON.stringify(name)}: ${parsedSchema} }` : parsedSchema;
|
|
116
|
+
lines.push(`${jsdocs}module.exports = ${payload}`);
|
|
117
|
+
}
|
|
118
|
+
else if (module === "esm") {
|
|
119
|
+
const exportLine = `${jsdocs}export ${name ? `const ${name} =` : `default`} ${parsedSchema}`;
|
|
120
|
+
lines.push(exportLine);
|
|
121
|
+
}
|
|
122
|
+
else if (name) {
|
|
123
|
+
lines.push(`${jsdocs}const ${name} = ${parsedSchema}`);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
lines.push(`${jsdocs}${parsedSchema}`);
|
|
127
|
+
}
|
|
128
|
+
let typeLine;
|
|
129
|
+
if (type && name) {
|
|
130
|
+
const typeName = typeof type === "string" ? type : `${name[0].toUpperCase()}${name.substring(1)}`;
|
|
131
|
+
typeLine = `export type ${typeName} = z.infer<typeof ${name}>`;
|
|
132
|
+
}
|
|
133
|
+
const joined = lines.filter(Boolean).join("\n\n");
|
|
134
|
+
const combined = typeLine ? `${joined}\n${typeLine}` : joined;
|
|
135
|
+
const shouldEndWithNewline = module === "esm" || module === "cjs";
|
|
136
|
+
return `${combined}${shouldEndWithNewline ? "\n" : ""}`;
|
|
137
|
+
};
|