@gabrielbryk/json-schema-to-zod 2.7.4 → 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.
Files changed (51) hide show
  1. package/CHANGELOG.md +23 -1
  2. package/dist/cjs/core/analyzeSchema.js +62 -0
  3. package/dist/cjs/core/emitZod.js +141 -0
  4. package/dist/cjs/generators/generateBundle.js +355 -0
  5. package/dist/cjs/index.js +6 -0
  6. package/dist/cjs/jsonSchemaToZod.js +5 -73
  7. package/dist/cjs/parsers/parseArray.js +34 -15
  8. package/dist/cjs/parsers/parseIfThenElse.js +2 -1
  9. package/dist/cjs/parsers/parseNumber.js +81 -39
  10. package/dist/cjs/parsers/parseObject.js +24 -0
  11. package/dist/cjs/parsers/parseSchema.js +147 -25
  12. package/dist/cjs/parsers/parseString.js +294 -54
  13. package/dist/cjs/utils/buildRefRegistry.js +56 -0
  14. package/dist/cjs/utils/cycles.js +113 -0
  15. package/dist/cjs/utils/resolveUri.js +16 -0
  16. package/dist/cjs/utils/withMessage.js +4 -5
  17. package/dist/esm/core/analyzeSchema.js +58 -0
  18. package/dist/esm/core/emitZod.js +137 -0
  19. package/dist/esm/generators/generateBundle.js +351 -0
  20. package/dist/esm/index.js +6 -0
  21. package/dist/esm/jsonSchemaToZod.js +5 -73
  22. package/dist/esm/parsers/parseArray.js +34 -15
  23. package/dist/esm/parsers/parseIfThenElse.js +2 -1
  24. package/dist/esm/parsers/parseNumber.js +81 -39
  25. package/dist/esm/parsers/parseObject.js +24 -0
  26. package/dist/esm/parsers/parseSchema.js +147 -25
  27. package/dist/esm/parsers/parseString.js +294 -54
  28. package/dist/esm/utils/buildRefRegistry.js +52 -0
  29. package/dist/esm/utils/cycles.js +107 -0
  30. package/dist/esm/utils/resolveUri.js +12 -0
  31. package/dist/esm/utils/withMessage.js +4 -5
  32. package/dist/types/Types.d.ts +36 -0
  33. package/dist/types/core/analyzeSchema.d.ts +24 -0
  34. package/dist/types/core/emitZod.d.ts +2 -0
  35. package/dist/types/generators/generateBundle.d.ts +62 -0
  36. package/dist/types/index.d.ts +6 -0
  37. package/dist/types/jsonSchemaToZod.d.ts +1 -1
  38. package/dist/types/parsers/parseSchema.d.ts +2 -1
  39. package/dist/types/parsers/parseString.d.ts +2 -2
  40. package/dist/types/utils/buildRefRegistry.d.ts +12 -0
  41. package/dist/types/utils/cycles.d.ts +7 -0
  42. package/dist/types/utils/jsdocs.d.ts +1 -1
  43. package/dist/types/utils/resolveUri.d.ts +1 -0
  44. package/dist/types/utils/withMessage.d.ts +6 -1
  45. package/docs/proposals/bundle-refactor.md +43 -0
  46. package/docs/proposals/ref-anchor-support.md +65 -0
  47. package/eslint.config.js +26 -0
  48. package/package.json +10 -4
  49. /package/{jest.config.js → jest.config.cjs} +0 -0
  50. /package/{postcjs.js → postcjs.cjs} +0 -0
  51. /package/{postesm.js → postesm.cjs} +0 -0
@@ -1,73 +1,313 @@
1
1
  import { withMessage } from "../utils/withMessage.js";
2
2
  import { parseSchema } from "./parseSchema.js";
3
- export const parseString = (schema) => {
4
- let r = "z.string()";
5
- r += withMessage(schema, "format", ({ value }) => {
6
- switch (value) {
7
- case "email":
8
- return [".email(", ")"];
9
- case "ip":
10
- return [".ip(", ")"];
11
- case "ipv4":
12
- return ['.ip({ version: "v4"', ", message: ", " })"];
13
- case "ipv6":
14
- return ['.ip({ version: "v6"', ", message: ", " })"];
15
- case "uri":
16
- return [".url(", ")"];
17
- case "uuid":
18
- return [".uuid(", ")"];
19
- case "date-time":
20
- return [".datetime({ offset: true", ", message: ", " })"];
21
- case "time":
22
- return [".time(", ")"];
23
- case "date":
24
- return [".date(", ")"];
25
- case "binary":
26
- return [".base64(", ")"];
27
- case "duration":
28
- return [".duration(", ")"];
29
- }
30
- });
31
- r += withMessage(schema, "pattern", ({ json }) => [
32
- `.regex(new RegExp(${json})`,
33
- ", ",
34
- ")",
35
- ]);
36
- r += withMessage(schema, "minLength", ({ json }) => [
37
- `.min(${json}`,
38
- ", ",
39
- ")",
40
- ]);
41
- r += withMessage(schema, "maxLength", ({ json }) => [
42
- `.max(${json}`,
43
- ", ",
44
- ")",
45
- ]);
3
+ export const parseString = (schema, refs) => {
4
+ const formatError = schema.errorMessage?.format;
5
+ const refContext = ensureRefs(refs);
6
+ const topLevelFormatMap = {
7
+ email: "z.email",
8
+ ipv4: "z.ipv4",
9
+ ipv6: "z.ipv6",
10
+ uri: "z.url",
11
+ uuid: "z.uuid",
12
+ cuid: "z.cuid",
13
+ cuid2: "z.cuid2",
14
+ nanoid: "z.nanoid",
15
+ ulid: "z.ulid",
16
+ jwt: "z.jwt",
17
+ e164: "z.e164",
18
+ base64url: "z.base64url",
19
+ base64: "z.base64",
20
+ emoji: "z.emoji",
21
+ "idn-email": "z.email",
22
+ };
23
+ const formatFn = schema.format && topLevelFormatMap[schema.format];
24
+ const formatParam = formatError !== undefined ? `{ error: ${JSON.stringify(formatError)} }` : "";
25
+ let r = formatFn ? `${formatFn}(${formatParam})` : "z.string()";
26
+ const formatHandled = Boolean(formatFn);
27
+ let formatWasHandled = formatHandled;
28
+ if (!formatHandled) {
29
+ r += withMessage(schema, "format", ({ value }) => {
30
+ switch (value) {
31
+ case "email":
32
+ formatWasHandled = true;
33
+ return {
34
+ opener: ".email(",
35
+ closer: ")",
36
+ messagePrefix: "{ error: ",
37
+ messageCloser: " })",
38
+ };
39
+ case "ip":
40
+ formatWasHandled = true;
41
+ return {
42
+ opener: ".ip(",
43
+ closer: ")",
44
+ messagePrefix: "{ error: ",
45
+ messageCloser: " })",
46
+ };
47
+ case "ipv4":
48
+ formatWasHandled = true;
49
+ return {
50
+ opener: '.ip({ version: "v4"',
51
+ closer: " })",
52
+ messagePrefix: ", error: ",
53
+ messageCloser: " })",
54
+ };
55
+ case "ipv6":
56
+ formatWasHandled = true;
57
+ return {
58
+ opener: '.ip({ version: "v6"',
59
+ closer: " })",
60
+ messagePrefix: ", error: ",
61
+ messageCloser: " })",
62
+ };
63
+ case "uri":
64
+ formatWasHandled = true;
65
+ return {
66
+ opener: ".url(",
67
+ closer: ")",
68
+ messagePrefix: "{ error: ",
69
+ messageCloser: " })",
70
+ };
71
+ case "uuid":
72
+ formatWasHandled = true;
73
+ return {
74
+ opener: ".uuid(",
75
+ closer: ")",
76
+ messagePrefix: "{ error: ",
77
+ messageCloser: " })",
78
+ };
79
+ case "cuid":
80
+ formatWasHandled = true;
81
+ return {
82
+ opener: ".cuid(",
83
+ closer: ")",
84
+ messagePrefix: "{ error: ",
85
+ messageCloser: " })",
86
+ };
87
+ case "cuid2":
88
+ formatWasHandled = true;
89
+ return {
90
+ opener: ".cuid2(",
91
+ closer: ")",
92
+ messagePrefix: "{ error: ",
93
+ messageCloser: " })",
94
+ };
95
+ case "nanoid":
96
+ formatWasHandled = true;
97
+ return {
98
+ opener: ".nanoid(",
99
+ closer: ")",
100
+ messagePrefix: "{ error: ",
101
+ messageCloser: " })",
102
+ };
103
+ case "ulid":
104
+ formatWasHandled = true;
105
+ return {
106
+ opener: ".ulid(",
107
+ closer: ")",
108
+ messagePrefix: "{ error: ",
109
+ messageCloser: " })",
110
+ };
111
+ case "jwt":
112
+ formatWasHandled = true;
113
+ return {
114
+ opener: ".jwt(",
115
+ closer: ")",
116
+ messagePrefix: "{ error: ",
117
+ messageCloser: " })",
118
+ };
119
+ case "e164":
120
+ formatWasHandled = true;
121
+ return {
122
+ opener: ".e164(",
123
+ closer: ")",
124
+ messagePrefix: "{ error: ",
125
+ messageCloser: " })",
126
+ };
127
+ case "base64url":
128
+ formatWasHandled = true;
129
+ return {
130
+ opener: ".base64url(",
131
+ closer: ")",
132
+ messagePrefix: "{ error: ",
133
+ messageCloser: " })",
134
+ };
135
+ case "emoji":
136
+ formatWasHandled = true;
137
+ return {
138
+ opener: ".emoji(",
139
+ closer: ")",
140
+ messagePrefix: "{ error: ",
141
+ messageCloser: " })",
142
+ };
143
+ case "date-time":
144
+ formatWasHandled = true;
145
+ return {
146
+ opener: ".datetime({ offset: true",
147
+ closer: " })",
148
+ messagePrefix: ", error: ",
149
+ messageCloser: " })",
150
+ };
151
+ case "time":
152
+ formatWasHandled = true;
153
+ return {
154
+ opener: ".time(",
155
+ closer: ")",
156
+ messagePrefix: "{ error: ",
157
+ messageCloser: " })",
158
+ };
159
+ case "date":
160
+ formatWasHandled = true;
161
+ return {
162
+ opener: ".date(",
163
+ closer: ")",
164
+ messagePrefix: "{ error: ",
165
+ messageCloser: " })",
166
+ };
167
+ case "binary":
168
+ formatWasHandled = true;
169
+ return {
170
+ opener: ".base64(",
171
+ closer: ")",
172
+ messagePrefix: "{ error: ",
173
+ messageCloser: " })",
174
+ };
175
+ case "duration":
176
+ formatWasHandled = true;
177
+ return {
178
+ opener: ".duration(",
179
+ closer: ")",
180
+ messagePrefix: "{ error: ",
181
+ messageCloser: " })",
182
+ };
183
+ case "hostname":
184
+ case "idn-hostname":
185
+ formatWasHandled = true;
186
+ return {
187
+ opener: ".refine((val) => { if (typeof val !== \"string\" || val.length === 0 || val.length > 253) return false; return val.split(\".\").every((label) => label.length > 0 && label.length <= 63 && /^[A-Za-z0-9-]+$/.test(label) && label[0] !== \"-\" && label[label.length - 1] !== \"-\"); }",
188
+ closer: ")",
189
+ messagePrefix: ", { error: ",
190
+ messageCloser: " })",
191
+ };
192
+ case "idn-email":
193
+ formatWasHandled = true;
194
+ return {
195
+ opener: ".email(",
196
+ closer: ")",
197
+ messagePrefix: "{ error: ",
198
+ messageCloser: " })",
199
+ };
200
+ case "uri-reference":
201
+ case "iri":
202
+ case "iri-reference":
203
+ formatWasHandled = true;
204
+ return {
205
+ opener: '.refine((val) => { try { new URL(val, "http://example.com"); return true; } catch { return false; } }',
206
+ closer: ")",
207
+ messagePrefix: ", { error: ",
208
+ messageCloser: " })",
209
+ };
210
+ case "json-pointer":
211
+ formatWasHandled = true;
212
+ return {
213
+ opener: ".refine((val) => typeof val === \"string\" && /^(?:\\/(?:[^/~]|~[01])*)*$/.test(val)",
214
+ closer: ")",
215
+ messagePrefix: ", { error: ",
216
+ messageCloser: " })",
217
+ };
218
+ case "relative-json-pointer":
219
+ formatWasHandled = true;
220
+ return {
221
+ opener: ".refine((val) => typeof val === \"string\" && /^(?:0|[1-9][0-9]*)(?:#|(?:\\/(?:[^/~]|~[01])*))*$/.test(val)",
222
+ closer: ")",
223
+ messagePrefix: ", { error: ",
224
+ messageCloser: " })",
225
+ };
226
+ case "uri-template":
227
+ formatWasHandled = true;
228
+ return {
229
+ opener: ".refine((val) => { if (typeof val !== \"string\") return false; const opens = (val.match(/\\{/g) || []).length; const closes = (val.match(/\\}/g) || []).length; return opens === closes; }",
230
+ closer: ")",
231
+ messagePrefix: ", { error: ",
232
+ messageCloser: " })",
233
+ };
234
+ case "regex":
235
+ formatWasHandled = true;
236
+ return {
237
+ opener: ".refine((val) => { try { new RegExp(val); return true; } catch { return false; } }",
238
+ closer: ")",
239
+ messagePrefix: ", { error: ",
240
+ messageCloser: " })",
241
+ };
242
+ }
243
+ });
244
+ }
245
+ if (schema.format && !formatWasHandled) {
246
+ refContext.onUnknownFormat?.(schema.format, refContext.path);
247
+ }
248
+ r += withMessage(schema, "pattern", ({ json }) => ({
249
+ opener: `.regex(new RegExp(${json})`,
250
+ closer: ")",
251
+ messagePrefix: ", { error: ",
252
+ messageCloser: " })",
253
+ }));
254
+ r += withMessage(schema, "minLength", ({ json }) => ({
255
+ opener: `.min(${json}`,
256
+ closer: ")",
257
+ messagePrefix: ", { error: ",
258
+ messageCloser: " })",
259
+ }));
260
+ r += withMessage(schema, "maxLength", ({ json }) => ({
261
+ opener: `.max(${json}`,
262
+ closer: ")",
263
+ messagePrefix: ", { error: ",
264
+ messageCloser: " })",
265
+ }));
46
266
  r += withMessage(schema, "contentEncoding", ({ value }) => {
47
267
  if (value === "base64") {
48
- return [".base64(", ")"];
268
+ return {
269
+ opener: ".base64(",
270
+ closer: ")",
271
+ messagePrefix: "{ error: ",
272
+ messageCloser: " })",
273
+ };
49
274
  }
50
275
  });
51
276
  const contentMediaType = withMessage(schema, "contentMediaType", ({ value }) => {
52
277
  if (value === "application/json") {
53
- return [
54
- ".transform((str, ctx) => { try { return JSON.parse(str); } catch (err) { ctx.addIssue({ code: \"custom\", message: \"Invalid JSON\" }); }}",
55
- ", ",
56
- ")"
57
- ];
278
+ return {
279
+ opener: '.transform((str, ctx) => { try { return JSON.parse(str); } catch (err) { ctx.addIssue({ code: "custom", message: "Invalid JSON" }); }}',
280
+ closer: ")",
281
+ messagePrefix: ", { error: ",
282
+ messageCloser: " })",
283
+ };
58
284
  }
59
285
  });
60
286
  if (contentMediaType != "") {
61
287
  r += contentMediaType;
62
288
  r += withMessage(schema, "contentSchema", ({ value }) => {
63
289
  if (value && value instanceof Object) {
64
- return [
65
- `.pipe(${parseSchema(value)}`,
66
- ", ",
67
- ")"
68
- ];
290
+ return {
291
+ opener: `.pipe(${parseSchema(value, refContext)}`,
292
+ closer: ")",
293
+ messagePrefix: ", { error: ",
294
+ messageCloser: " })",
295
+ };
69
296
  }
70
297
  });
71
298
  }
72
299
  return r;
73
300
  };
301
+ function ensureRefs(refs) {
302
+ if (refs)
303
+ return refs;
304
+ return {
305
+ path: [],
306
+ seen: new Map(),
307
+ declarations: new Map(),
308
+ dependencies: new Map(),
309
+ inProgress: new Set(),
310
+ refNameByPointer: new Map(),
311
+ usedNames: new Set(),
312
+ };
313
+ }
@@ -0,0 +1,52 @@
1
+ import { resolveUri } from "./resolveUri.js";
2
+ export const buildRefRegistry = (schema, rootBaseUri = "root:///", opts = {}) => {
3
+ const registry = new Map();
4
+ const walk = (node, baseUri, path) => {
5
+ if (typeof node !== "object" || node === null)
6
+ return;
7
+ const obj = node;
8
+ const nextBase = obj.$id ? resolveUri(baseUri, obj.$id) : baseUri;
9
+ // Legacy recursive anchor
10
+ if (obj.$recursiveAnchor === true) {
11
+ const name = "__recursive__";
12
+ registry.set(`${nextBase}#${name}`, {
13
+ schema: node,
14
+ path,
15
+ baseUri: nextBase,
16
+ dynamic: true,
17
+ anchor: name,
18
+ });
19
+ }
20
+ // Register base entry
21
+ registry.set(nextBase, { schema: node, path, baseUri: nextBase });
22
+ if (typeof obj.$anchor === "string") {
23
+ registry.set(`${nextBase}#${obj.$anchor}`, {
24
+ schema: node,
25
+ path,
26
+ baseUri: nextBase,
27
+ anchor: obj.$anchor,
28
+ });
29
+ }
30
+ if (typeof obj.$dynamicAnchor === "string") {
31
+ const name = obj.$dynamicAnchor;
32
+ registry.set(`${nextBase}#${name}`, {
33
+ schema: node,
34
+ path,
35
+ baseUri: nextBase,
36
+ dynamic: true,
37
+ anchor: name,
38
+ });
39
+ }
40
+ for (const key in obj) {
41
+ const value = obj[key];
42
+ if (Array.isArray(value)) {
43
+ value.forEach((v, i) => walk(v, nextBase, [...path, key, i]));
44
+ }
45
+ else if (typeof value === "object" && value !== null) {
46
+ walk(value, nextBase, [...path, key]);
47
+ }
48
+ }
49
+ };
50
+ walk(schema, rootBaseUri, []);
51
+ return { registry, rootBaseUri };
52
+ };
@@ -0,0 +1,107 @@
1
+ export const findRefDependencies = (schema, validDefNames) => {
2
+ const deps = new Set();
3
+ function traverse(obj) {
4
+ if (obj === null || typeof obj !== "object")
5
+ return;
6
+ if (Array.isArray(obj)) {
7
+ obj.forEach(traverse);
8
+ return;
9
+ }
10
+ const record = obj;
11
+ if (typeof record["$ref"] === "string") {
12
+ const ref = record["$ref"];
13
+ const match = ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/);
14
+ if (match && validDefNames.includes(match[1])) {
15
+ deps.add(match[1]);
16
+ }
17
+ }
18
+ for (const value of Object.values(record)) {
19
+ traverse(value);
20
+ }
21
+ }
22
+ traverse(schema);
23
+ return deps;
24
+ };
25
+ export const detectCycles = (defNames, deps) => {
26
+ const cycleNodes = new Set();
27
+ const visited = new Set();
28
+ const recursionStack = new Set();
29
+ function dfs(node, path) {
30
+ if (recursionStack.has(node)) {
31
+ const cycleStart = path.indexOf(node);
32
+ for (let i = cycleStart; i < path.length; i++) {
33
+ cycleNodes.add(path[i]);
34
+ }
35
+ cycleNodes.add(node);
36
+ return;
37
+ }
38
+ if (visited.has(node))
39
+ return;
40
+ visited.add(node);
41
+ recursionStack.add(node);
42
+ const targets = deps.get(node);
43
+ if (targets) {
44
+ for (const dep of targets) {
45
+ dfs(dep, [...path, node]);
46
+ }
47
+ }
48
+ recursionStack.delete(node);
49
+ }
50
+ for (const defName of defNames) {
51
+ if (!visited.has(defName))
52
+ dfs(defName, []);
53
+ }
54
+ return cycleNodes;
55
+ };
56
+ export const computeScc = (defNames, deps) => {
57
+ // Tarjan's algorithm, keeps mapping for quick "is this ref in my cycle?"
58
+ const index = new Map();
59
+ const lowlink = new Map();
60
+ const stack = [];
61
+ const onStack = new Set();
62
+ const componentByName = new Map();
63
+ const cycleMembers = new Set();
64
+ let currentIndex = 0;
65
+ let componentId = 0;
66
+ const strongConnect = (v) => {
67
+ index.set(v, currentIndex);
68
+ lowlink.set(v, currentIndex);
69
+ currentIndex += 1;
70
+ stack.push(v);
71
+ onStack.add(v);
72
+ const neighbors = deps.get(v);
73
+ if (neighbors) {
74
+ for (const w of neighbors) {
75
+ if (!index.has(w)) {
76
+ strongConnect(w);
77
+ lowlink.set(v, Math.min(lowlink.get(v), lowlink.get(w)));
78
+ }
79
+ else if (onStack.has(w)) {
80
+ lowlink.set(v, Math.min(lowlink.get(v), index.get(w)));
81
+ }
82
+ }
83
+ }
84
+ if (lowlink.get(v) === index.get(v)) {
85
+ const component = [];
86
+ let w;
87
+ do {
88
+ w = stack.pop();
89
+ if (w === undefined)
90
+ break;
91
+ onStack.delete(w);
92
+ component.push(w);
93
+ componentByName.set(w, componentId);
94
+ } while (w !== v);
95
+ if (component.length > 1) {
96
+ component.forEach((name) => cycleMembers.add(name));
97
+ }
98
+ componentId += 1;
99
+ }
100
+ };
101
+ for (const name of defNames) {
102
+ if (!index.has(name)) {
103
+ strongConnect(name);
104
+ }
105
+ }
106
+ return { cycleMembers, componentByName };
107
+ };
@@ -0,0 +1,12 @@
1
+ export const resolveUri = (base, ref) => {
2
+ try {
3
+ // If ref is absolute, new URL will accept it; otherwise resolves against base
4
+ return new URL(ref, base).toString();
5
+ }
6
+ catch {
7
+ // Fallback: simple concatenation to avoid throwing; keep ref as-is
8
+ if (ref.startsWith("#"))
9
+ return `${base}${ref}`;
10
+ return ref;
11
+ }
12
+ };
@@ -4,14 +4,13 @@ export function withMessage(schema, key, get) {
4
4
  if (value !== undefined) {
5
5
  const got = get({ value, json: JSON.stringify(value) });
6
6
  if (got) {
7
- const opener = got[0];
8
- const prefix = got.length === 3 ? got[1] : "";
9
- const closer = got.length === 3 ? got[2] : got[1];
7
+ const { opener, closer, messagePrefix = "", messageCloser } = got;
10
8
  r += opener;
11
9
  if (schema.errorMessage?.[key] !== undefined) {
12
- r += prefix + JSON.stringify(schema.errorMessage[key]);
10
+ r += messagePrefix + JSON.stringify(schema.errorMessage[key]);
11
+ r += messageCloser ?? closer;
12
+ return r;
13
13
  }
14
- r;
15
14
  r += closer;
16
15
  }
17
16
  }
@@ -94,6 +94,21 @@ export type Options = {
94
94
  * @default false
95
95
  */
96
96
  strictOneOf?: boolean;
97
+ /**
98
+ * Called when a string format is encountered that has no built-in mapping.
99
+ * Can be used to log or throw on unknown formats.
100
+ */
101
+ onUnknownFormat?: (format: string, path: (string | number)[]) => void;
102
+ /**
103
+ * Called when a $ref/$dynamicRef cannot be resolved.
104
+ * Can be used to log or throw on unknown references.
105
+ */
106
+ onUnresolvedRef?: (ref: string, path: (string | number)[]) => void;
107
+ /**
108
+ * Optional resolver for external $ref URIs.
109
+ * Return a JsonSchema to register, or undefined if not found.
110
+ */
111
+ resolveExternalRef?: (uri: string) => JsonSchema | Promise<JsonSchema> | undefined;
97
112
  };
98
113
  export type Refs = Options & {
99
114
  path: (string | number)[];
@@ -103,10 +118,31 @@ export type Refs = Options & {
103
118
  }>;
104
119
  root?: JsonSchema;
105
120
  declarations?: Map<string, string>;
121
+ dependencies?: Map<string, Set<string>>;
106
122
  inProgress?: Set<string>;
107
123
  refNameByPointer?: Map<string, string>;
108
124
  usedNames?: Set<string>;
109
125
  currentSchemaName?: string;
126
+ cycleRefNames?: Set<string>;
127
+ cycleComponentByName?: Map<string, number>;
128
+ /** Base URI in scope while traversing */
129
+ currentBaseUri?: string;
130
+ /** Root/base URI for the document */
131
+ rootBaseUri?: string;
132
+ /** Prebuilt registry of resolved URIs/anchors */
133
+ refRegistry?: Map<string, {
134
+ schema: JsonSchema;
135
+ path: (string | number)[];
136
+ baseUri: string;
137
+ dynamic?: boolean;
138
+ anchor?: string;
139
+ }>;
140
+ /** Stack of active dynamic anchors (nearest last) */
141
+ dynamicAnchors?: {
142
+ name: string;
143
+ uri: string;
144
+ path: (string | number)[];
145
+ }[];
110
146
  };
111
147
  export type SimpleDiscriminatedOneOfSchema<D extends string = string> = JsonSchemaObject & {
112
148
  oneOf: (JsonSchemaObject & {
@@ -0,0 +1,24 @@
1
+ import { Options, JsonSchema } from "../Types.js";
2
+ export type NormalizedOptions = Options & {
3
+ exportRefs: boolean;
4
+ withMeta: boolean;
5
+ };
6
+ export type AnalysisResult = {
7
+ schema: JsonSchema;
8
+ options: NormalizedOptions;
9
+ refNameByPointer: Map<string, string>;
10
+ usedNames: Set<string>;
11
+ declarations: Map<string, string>;
12
+ dependencies: Map<string, Set<string>>;
13
+ cycleRefNames: Set<string>;
14
+ cycleComponentByName: Map<string, number>;
15
+ refRegistry: Map<string, {
16
+ schema: JsonSchema;
17
+ path: (string | number)[];
18
+ baseUri: string;
19
+ dynamic?: boolean;
20
+ anchor?: string;
21
+ }>;
22
+ rootBaseUri: string;
23
+ };
24
+ export declare const analyzeSchema: (schema: JsonSchema, options?: Options) => AnalysisResult;
@@ -0,0 +1,2 @@
1
+ import { AnalysisResult } from "./analyzeSchema.js";
2
+ export declare const emitZod: (analysis: AnalysisResult) => string;