@gabrielbryk/json-schema-to-zod 2.12.1 → 2.14.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 (57) hide show
  1. package/.github/RELEASE_SETUP.md +120 -0
  2. package/.github/TOOLING_GUIDE.md +169 -0
  3. package/.github/dependabot.yml +52 -0
  4. package/.github/workflows/ci.yml +33 -0
  5. package/.github/workflows/release.yml +12 -4
  6. package/.github/workflows/security.yml +40 -0
  7. package/.husky/commit-msg +1 -0
  8. package/.husky/pre-commit +1 -0
  9. package/.lintstagedrc.json +3 -0
  10. package/.prettierrc +20 -0
  11. package/AGENTS.md +7 -0
  12. package/CHANGELOG.md +13 -4
  13. package/README.md +24 -9
  14. package/commitlint.config.js +24 -0
  15. package/createIndex.ts +4 -4
  16. package/dist/cli.js +3 -4
  17. package/dist/core/analyzeSchema.js +56 -11
  18. package/dist/core/emitZod.js +43 -12
  19. package/dist/generators/generateBundle.js +67 -92
  20. package/dist/index.js +1 -0
  21. package/dist/parsers/parseAllOf.js +11 -12
  22. package/dist/parsers/parseAnyOf.js +2 -2
  23. package/dist/parsers/parseArray.js +38 -12
  24. package/dist/parsers/parseMultipleType.js +2 -2
  25. package/dist/parsers/parseNumber.js +44 -102
  26. package/dist/parsers/parseObject.js +136 -443
  27. package/dist/parsers/parseOneOf.js +57 -110
  28. package/dist/parsers/parseSchema.js +176 -71
  29. package/dist/parsers/parseSimpleDiscriminatedOneOf.js +2 -2
  30. package/dist/parsers/parseString.js +113 -253
  31. package/dist/types/Types.d.ts +37 -1
  32. package/dist/types/core/analyzeSchema.d.ts +4 -0
  33. package/dist/types/generators/generateBundle.d.ts +1 -1
  34. package/dist/types/index.d.ts +1 -0
  35. package/dist/types/utils/schemaNaming.d.ts +6 -0
  36. package/dist/utils/cliTools.js +1 -2
  37. package/dist/utils/esmEmitter.js +6 -2
  38. package/dist/utils/extractInlineObject.js +1 -3
  39. package/dist/utils/jsdocs.js +1 -4
  40. package/dist/utils/liftInlineObjects.js +76 -15
  41. package/dist/utils/resolveRef.js +35 -10
  42. package/dist/utils/schemaNaming.js +31 -0
  43. package/dist/utils/schemaRepresentation.js +35 -66
  44. package/dist/zodToJsonSchema.js +1 -2
  45. package/docs/IMPROVEMENT-PLAN.md +30 -12
  46. package/docs/ZOD-V4-RECURSIVE-TYPE-LIMITATIONS.md +70 -25
  47. package/docs/proposals/allof-required-merging.md +10 -4
  48. package/docs/proposals/bundle-refactor.md +10 -4
  49. package/docs/proposals/discriminated-union-with-default.md +18 -14
  50. package/docs/proposals/inline-object-lifting.md +15 -5
  51. package/docs/proposals/ref-anchor-support.md +11 -0
  52. package/output.txt +67 -0
  53. package/package.json +18 -5
  54. package/scripts/generateWorkflowSchema.ts +5 -14
  55. package/scripts/regenerate_bundle.ts +25 -0
  56. package/tsc_output.txt +542 -0
  57. package/tsc_output_2.txt +489 -0
@@ -3,246 +3,114 @@ import { parseSchema } from "./parseSchema.js";
3
3
  export const parseString = (schema, refs) => {
4
4
  const formatError = schema.errorMessage?.format;
5
5
  const refContext = ensureRefs(refs);
6
- // Map formats to top-level Zod functions and their return types
6
+ // Map formats to Zod string methods
7
7
  const topLevelFormatMap = {
8
- email: { fn: "z.email", zodType: "z.ZodEmail" },
9
- ipv4: { fn: "z.ipv4", zodType: "z.ZodIPv4" },
10
- ipv6: { fn: "z.ipv6", zodType: "z.ZodIPv6" },
11
- uri: { fn: "z.url", zodType: "z.ZodURL" },
12
- uuid: { fn: "z.uuid", zodType: "z.ZodUUID" },
13
- cuid: { fn: "z.cuid", zodType: "z.ZodCUID" },
14
- cuid2: { fn: "z.cuid2", zodType: "z.ZodCUID2" },
15
- nanoid: { fn: "z.nanoid", zodType: "z.ZodNanoID" },
16
- ulid: { fn: "z.ulid", zodType: "z.ZodULID" },
17
- jwt: { fn: "z.jwt", zodType: "z.ZodJWT" },
18
- e164: { fn: "z.e164", zodType: "z.ZodE164" },
19
- base64url: { fn: "z.base64url", zodType: "z.ZodBase64URL" },
20
- base64: { fn: "z.base64", zodType: "z.ZodBase64" },
21
- emoji: { fn: "z.emoji", zodType: "z.ZodEmoji" },
22
- "idn-email": { fn: "z.email", zodType: "z.ZodEmail" },
8
+ email: { fn: "z.email", zodType: "z.ZodString" },
9
+ ipv4: { fn: "z.ipv4", zodType: "z.ZodString" },
10
+ ipv6: { fn: "z.ipv6", zodType: "z.ZodString" },
11
+ uri: { fn: "z.url", zodType: "z.ZodString" },
12
+ uuid: { fn: "z.uuid", zodType: "z.ZodString" },
13
+ cuid: { fn: "z.cuid", zodType: "z.ZodString" },
14
+ cuid2: { fn: "z.cuid2", zodType: "z.ZodString" },
15
+ nanoid: { fn: "z.nanoid", zodType: "z.ZodString" },
16
+ ulid: { fn: "z.ulid", zodType: "z.ZodString" },
17
+ jwt: { fn: "z.jwt", zodType: "z.ZodString" },
18
+ e164: { fn: "z.e164", zodType: "z.ZodString" },
19
+ base64url: { fn: "z.base64url", zodType: "z.ZodString" },
20
+ base64: { fn: "z.base64", zodType: "z.ZodString" },
21
+ emoji: { fn: "z.emoji", zodType: "z.ZodString" },
22
+ "idn-email": { fn: "z.email", zodType: "z.ZodString" },
23
+ "date-time": { fn: "z.iso.datetime", zodType: "z.ZodString" },
24
+ date: { fn: "z.iso.date", zodType: "z.ZodString" },
25
+ time: { fn: "z.iso.time", zodType: "z.ZodString" },
26
+ duration: { fn: "z.iso.duration", zodType: "z.ZodString" },
23
27
  };
24
28
  const formatInfo = schema.format ? topLevelFormatMap[schema.format] : undefined;
25
29
  const formatFn = formatInfo?.fn;
26
- const formatParam = formatError !== undefined ? `{ error: ${JSON.stringify(formatError)} }` : "";
27
- let r = formatFn ? `${formatFn}(${formatParam})` : "z.string()";
28
- const formatHandled = Boolean(formatFn);
29
- let formatWasHandled = formatHandled;
30
- if (!formatHandled) {
31
- r += withMessage(schema, "format", ({ value }) => {
32
- switch (value) {
33
- case "email":
34
- formatWasHandled = true;
35
- return {
36
- opener: ".email(",
37
- closer: ")",
38
- messagePrefix: "{ error: ",
39
- messageCloser: " })",
40
- };
41
- case "ip":
42
- formatWasHandled = true;
43
- return {
44
- opener: ".ip(",
45
- closer: ")",
46
- messagePrefix: "{ error: ",
47
- messageCloser: " })",
48
- };
49
- case "ipv4":
50
- formatWasHandled = true;
51
- return {
52
- opener: '.ip({ version: "v4"',
53
- closer: " })",
54
- messagePrefix: ", error: ",
55
- messageCloser: " })",
56
- };
57
- case "ipv6":
58
- formatWasHandled = true;
59
- return {
60
- opener: '.ip({ version: "v6"',
61
- closer: " })",
62
- messagePrefix: ", error: ",
63
- messageCloser: " })",
64
- };
65
- case "uri":
66
- formatWasHandled = true;
67
- return {
68
- opener: ".url(",
69
- closer: ")",
70
- messagePrefix: "{ error: ",
71
- messageCloser: " })",
72
- };
73
- case "uuid":
74
- formatWasHandled = true;
75
- return {
76
- opener: ".uuid(",
77
- closer: ")",
78
- messagePrefix: "{ error: ",
79
- messageCloser: " })",
80
- };
81
- case "cuid":
82
- formatWasHandled = true;
83
- return {
84
- opener: ".cuid(",
85
- closer: ")",
86
- messagePrefix: "{ error: ",
87
- messageCloser: " })",
88
- };
89
- case "cuid2":
90
- formatWasHandled = true;
91
- return {
92
- opener: ".cuid2(",
93
- closer: ")",
94
- messagePrefix: "{ error: ",
95
- messageCloser: " })",
96
- };
97
- case "nanoid":
98
- formatWasHandled = true;
99
- return {
100
- opener: ".nanoid(",
101
- closer: ")",
102
- messagePrefix: "{ error: ",
103
- messageCloser: " })",
104
- };
105
- case "ulid":
106
- formatWasHandled = true;
107
- return {
108
- opener: ".ulid(",
109
- closer: ")",
110
- messagePrefix: "{ error: ",
111
- messageCloser: " })",
112
- };
113
- case "jwt":
114
- formatWasHandled = true;
115
- return {
116
- opener: ".jwt(",
117
- closer: ")",
118
- messagePrefix: "{ error: ",
119
- messageCloser: " })",
120
- };
121
- case "e164":
122
- formatWasHandled = true;
123
- return {
124
- opener: ".e164(",
125
- closer: ")",
126
- messagePrefix: "{ error: ",
127
- messageCloser: " })",
128
- };
129
- case "base64url":
130
- formatWasHandled = true;
131
- return {
132
- opener: ".base64url(",
133
- closer: ")",
134
- messagePrefix: "{ error: ",
135
- messageCloser: " })",
136
- };
137
- case "emoji":
138
- formatWasHandled = true;
139
- return {
140
- opener: ".emoji(",
141
- closer: ")",
142
- messagePrefix: "{ error: ",
143
- messageCloser: " })",
144
- };
145
- case "date-time":
146
- formatWasHandled = true;
147
- return {
148
- opener: ".datetime({ offset: true",
149
- closer: " })",
150
- messagePrefix: ", error: ",
151
- messageCloser: " })",
152
- };
153
- case "time":
154
- formatWasHandled = true;
155
- return {
156
- opener: ".time(",
157
- closer: ")",
158
- messagePrefix: "{ error: ",
159
- messageCloser: " })",
160
- };
161
- case "date":
162
- formatWasHandled = true;
163
- return {
164
- opener: ".date(",
165
- closer: ")",
166
- messagePrefix: "{ error: ",
167
- messageCloser: " })",
168
- };
169
- case "binary":
170
- formatWasHandled = true;
171
- return {
172
- opener: ".base64(",
173
- closer: ")",
174
- messagePrefix: "{ error: ",
175
- messageCloser: " })",
176
- };
177
- case "duration":
178
- formatWasHandled = true;
179
- return {
180
- opener: ".duration(",
181
- closer: ")",
182
- messagePrefix: "{ error: ",
183
- messageCloser: " })",
184
- };
185
- case "hostname":
186
- case "idn-hostname":
187
- formatWasHandled = true;
188
- return {
189
- 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] !== \"-\"); }",
190
- closer: ")",
191
- messagePrefix: ", { error: ",
192
- messageCloser: " })",
193
- };
194
- case "idn-email":
195
- formatWasHandled = true;
196
- return {
197
- opener: ".email(",
198
- closer: ")",
199
- messagePrefix: "{ error: ",
200
- messageCloser: " })",
201
- };
202
- case "uri-reference":
203
- case "iri":
204
- case "iri-reference":
205
- formatWasHandled = true;
206
- return {
207
- opener: '.refine((val) => { try { new URL(val, "http://example.com"); return true; } catch { return false; } }',
208
- closer: ")",
209
- messagePrefix: ", { error: ",
210
- messageCloser: " })",
211
- };
212
- case "json-pointer":
213
- formatWasHandled = true;
214
- return {
215
- opener: ".refine((val) => typeof val === \"string\" && /^(?:\\/(?:[^/~]|~[01])*)*$/.test(val)",
216
- closer: ")",
217
- messagePrefix: ", { error: ",
218
- messageCloser: " })",
219
- };
220
- case "relative-json-pointer":
221
- formatWasHandled = true;
222
- return {
223
- opener: ".refine((val) => typeof val === \"string\" && /^(?:0|[1-9][0-9]*)(?:#|(?:\\/(?:[^/~]|~[01])*))*$/.test(val)",
224
- closer: ")",
225
- messagePrefix: ", { error: ",
226
- messageCloser: " })",
227
- };
228
- case "uri-template":
229
- formatWasHandled = true;
230
- return {
231
- opener: ".refine((val) => { if (typeof val !== \"string\") return false; const opens = (val.match(/\\{/g) || []).length; const closes = (val.match(/\\}/g) || []).length; return opens === closes; }",
232
- closer: ")",
233
- messagePrefix: ", { error: ",
234
- messageCloser: " })",
235
- };
236
- case "regex":
237
- formatWasHandled = true;
238
- return {
239
- opener: ".refine((val) => { try { new RegExp(val); return true; } catch { return false; } }",
240
- closer: ")",
241
- messagePrefix: ", { error: ",
242
- messageCloser: " })",
243
- };
244
- }
245
- });
30
+ let r = "z.string()";
31
+ let zodType = "z.ZodString";
32
+ if (formatFn) {
33
+ const params = formatError !== undefined ? `{ message: ${JSON.stringify(formatError)} }` : "";
34
+ if (schema.format === "date-time") {
35
+ r = `z.iso.datetime({ offset: true${formatError ? `, message: ${JSON.stringify(formatError)}` : ""} })`;
36
+ }
37
+ else if (schema.format === "ipv4") {
38
+ r = `z.ipv4(${formatError ? `{ message: ${JSON.stringify(formatError)} }` : ""})`;
39
+ }
40
+ else if (schema.format === "ipv6") {
41
+ r = `z.ipv6(${formatError ? `{ message: ${JSON.stringify(formatError)} }` : ""})`;
42
+ }
43
+ else {
44
+ r = `${formatFn}(${params})`;
45
+ }
46
+ }
47
+ let formatWasHandled = Boolean(formatFn);
48
+ if (!formatWasHandled && schema.format) {
49
+ switch (schema.format) {
50
+ case "ip":
51
+ r += `.refine((val) => {
52
+ const v4 = z.ipv4().safeParse(val).success;
53
+ const v6 = z.ipv6().safeParse(val).success;
54
+ return v4 || v6;
55
+ }${formatError ? `, { message: ${JSON.stringify(formatError)} }` : ""})`;
56
+ formatWasHandled = true;
57
+ break;
58
+ case "binary":
59
+ r = `z.base64(${formatError ? `{ message: ${JSON.stringify(formatError)} }` : ""})`;
60
+ formatWasHandled = true;
61
+ break;
62
+ case "hostname":
63
+ case "idn-hostname":
64
+ r += `.refine((val) => {
65
+ if (typeof val !== "string" || val.length === 0 || val.length > 253) return false;
66
+ return val.split(".").every((label) => {
67
+ return label.length > 0 && label.length <= 63 && /^[A-Za-z0-9-]+$/.test(label) && label[0] !== "-" && label[label.length - 1] !== "-";
68
+ });
69
+ }${formatError ? `, { message: ${JSON.stringify(formatError)} }` : ""})`;
70
+ formatWasHandled = true;
71
+ break;
72
+ case "uri-reference":
73
+ case "iri":
74
+ case "iri-reference":
75
+ r += `.refine((val) => {
76
+ try {
77
+ new URL(val, "http://example.com");
78
+ return true;
79
+ } catch {
80
+ return false;
81
+ }
82
+ }${formatError ? `, { message: ${JSON.stringify(formatError)} }` : ""})`;
83
+ formatWasHandled = true;
84
+ break;
85
+ case "json-pointer":
86
+ r += `.refine((val) => typeof val === "string" && /^(?:\\/(?:[^/~]|~[01])*)*$/.test(val)${formatError ? `, { message: ${JSON.stringify(formatError)} }` : ""})`;
87
+ formatWasHandled = true;
88
+ break;
89
+ case "relative-json-pointer":
90
+ r += `.refine((val) => typeof val === "string" && /^(?:0|[1-9][0-9]*)(?:#|(?:\\/(?:[^/~]|~[01])*))*$/.test(val)${formatError ? `, { message: ${JSON.stringify(formatError)} }` : ""})`;
91
+ formatWasHandled = true;
92
+ break;
93
+ case "uri-template":
94
+ r += `.refine((val) => {
95
+ if (typeof val !== "string") return false;
96
+ const opens = (val.match(/\\{/g) || []).length;
97
+ const closes = (val.match(/\\}/g) || []).length;
98
+ return opens === closes;
99
+ }${formatError ? `, { message: ${JSON.stringify(formatError)} }` : ""})`;
100
+ formatWasHandled = true;
101
+ break;
102
+ case "regex":
103
+ r += `.refine((val) => {
104
+ try {
105
+ new RegExp(val);
106
+ return true;
107
+ } catch {
108
+ return false;
109
+ }
110
+ }${formatError ? `, { message: ${JSON.stringify(formatError)} }` : ""})`;
111
+ formatWasHandled = true;
112
+ break;
113
+ }
246
114
  }
247
115
  if (schema.format && !formatWasHandled) {
248
116
  refContext.onUnknownFormat?.(schema.format, refContext.path);
@@ -250,37 +118,31 @@ export const parseString = (schema, refs) => {
250
118
  r += withMessage(schema, "pattern", ({ json }) => ({
251
119
  opener: `.regex(new RegExp(${json})`,
252
120
  closer: ")",
253
- messagePrefix: ", { error: ",
121
+ messagePrefix: ", { message: ",
254
122
  messageCloser: " })",
255
123
  }));
256
124
  r += withMessage(schema, "minLength", ({ json }) => ({
257
125
  opener: `.min(${json}`,
258
126
  closer: ")",
259
- messagePrefix: ", { error: ",
127
+ messagePrefix: ", { message: ",
260
128
  messageCloser: " })",
261
129
  }));
262
130
  r += withMessage(schema, "maxLength", ({ json }) => ({
263
131
  opener: `.max(${json}`,
264
132
  closer: ")",
265
- messagePrefix: ", { error: ",
133
+ messagePrefix: ", { message: ",
266
134
  messageCloser: " })",
267
135
  }));
268
- r += withMessage(schema, "contentEncoding", ({ value }) => {
269
- if (value === "base64") {
270
- return {
271
- opener: ".base64(",
272
- closer: ")",
273
- messagePrefix: "{ error: ",
274
- messageCloser: " })",
275
- };
276
- }
277
- });
136
+ if (schema.contentEncoding === "base64" && schema.format !== "base64") {
137
+ const encodingError = schema.errorMessage?.contentEncoding;
138
+ r = `z.base64(${encodingError ? `{ message: ${JSON.stringify(encodingError)} }` : ""})`;
139
+ }
278
140
  const contentMediaType = withMessage(schema, "contentMediaType", ({ value }) => {
279
141
  if (value === "application/json") {
280
142
  return {
281
143
  opener: '.transform((str, ctx) => { try { return JSON.parse(str); } catch (err) { ctx.addIssue({ code: "custom", message: "Invalid JSON" }); }}',
282
144
  closer: ")",
283
- messagePrefix: ", { error: ",
145
+ messagePrefix: ", { message: ",
284
146
  messageCloser: " })",
285
147
  };
286
148
  }
@@ -296,14 +158,12 @@ export const parseString = (schema, refs) => {
296
158
  return {
297
159
  opener: `.pipe(${contentExpr}`,
298
160
  closer: ")",
299
- messagePrefix: ", { error: ",
161
+ messagePrefix: ", { message: ",
300
162
  messageCloser: " })",
301
163
  };
302
164
  }
303
165
  });
304
166
  }
305
- // Use the correct Zod type based on whether a format function was used
306
- const zodType = formatInfo?.zodType ?? "z.ZodString";
307
167
  return {
308
168
  expression: r,
309
169
  type: zodType,
@@ -25,9 +25,10 @@ export type JsonSchemaObject = {
25
25
  definitions?: Record<string, JsonSchema>;
26
26
  title?: string;
27
27
  description?: string;
28
- examples?: Serializable[];
28
+ examples?: Serializable | Serializable[];
29
29
  deprecated?: boolean;
30
30
  dependentSchemas?: Record<string, JsonSchema>;
31
+ dependentRequired?: Record<string, string[]>;
31
32
  contains?: JsonSchema;
32
33
  minContains?: number;
33
34
  maxContains?: number;
@@ -44,6 +45,7 @@ export type JsonSchemaObject = {
44
45
  required?: string[] | boolean;
45
46
  propertyNames?: JsonSchema;
46
47
  items?: JsonSchema | JsonSchema[];
48
+ prefixItems?: JsonSchema[];
47
49
  additionalItems?: JsonSchema;
48
50
  minItems?: number;
49
51
  maxItems?: number;
@@ -71,6 +73,16 @@ export type JsonSchemaObject = {
71
73
  } & Record<string, unknown>;
72
74
  export type ParserSelector = (schema: JsonSchemaObject, refs: Refs) => SchemaRepresentation;
73
75
  export type ParserOverride = (schema: JsonSchemaObject, refs: Refs) => string | void;
76
+ export type NamingContext = {
77
+ isRoot: boolean;
78
+ isLifted: boolean;
79
+ };
80
+ export type NamingOptions = {
81
+ /** Customize the const name for schemas. Defaults to appending "Schema". */
82
+ schemaName?: (baseName: string, ctx: NamingContext) => string;
83
+ /** Customize the type name for schemas. Defaults to baseName when naming is enabled. */
84
+ typeName?: (baseName: string, ctx: NamingContext) => string | undefined;
85
+ };
74
86
  export type Options = {
75
87
  name?: string;
76
88
  withoutDefaults?: boolean;
@@ -78,6 +90,8 @@ export type Options = {
78
90
  withJsdocs?: boolean;
79
91
  /** Use .meta() instead of .describe() - includes id, title, description */
80
92
  withMeta?: boolean;
93
+ /** Customize schema and type naming for root and lifted schemas. */
94
+ naming?: NamingOptions;
81
95
  parserOverride?: ParserOverride;
82
96
  depth?: number;
83
97
  type?: boolean | string;
@@ -118,6 +132,14 @@ export type Options = {
118
132
  * @default false
119
133
  */
120
134
  strictOneOf?: boolean;
135
+ /**
136
+ * Root schema instance for JSON Pointer resolution (#/...).
137
+ */
138
+ root?: JsonSchema;
139
+ /**
140
+ * Full document root schema instance for cross-reference resolution.
141
+ */
142
+ documentRoot?: JsonSchema;
121
143
  /**
122
144
  * Called when a string format is encountered that has no built-in mapping.
123
145
  * Can be used to log or throw on unknown formats.
@@ -133,6 +155,16 @@ export type Options = {
133
155
  * Return a JsonSchema to register, or undefined if not found.
134
156
  */
135
157
  resolveExternalRef?: (uri: string) => JsonSchema | Promise<JsonSchema> | undefined;
158
+ /** Root/base URI for the document */
159
+ rootBaseUri?: string;
160
+ /** Prebuilt registry of resolved URIs/anchors */
161
+ refRegistry?: Map<string, {
162
+ schema: JsonSchema;
163
+ path: (string | number)[];
164
+ baseUri: string;
165
+ dynamic?: boolean;
166
+ anchor?: string;
167
+ }>;
136
168
  /**
137
169
  * Lift inline object schemas into top-level defs to improve reusability.
138
170
  * Default is ON; set enable: false to opt out.
@@ -164,7 +196,10 @@ export type Refs = Options & {
164
196
  dependencies?: Map<string, Set<string>>;
165
197
  inProgress?: Set<string>;
166
198
  refNameByPointer?: Map<string, string>;
199
+ refBaseNameByPointer?: Map<string, string>;
200
+ baseNameBySchema?: Map<string, string>;
167
201
  usedNames?: Set<string>;
202
+ usedBaseNames?: Set<string>;
168
203
  currentSchemaName?: string;
169
204
  cycleRefNames?: Set<string>;
170
205
  cycleComponentByName?: Map<string, number>;
@@ -180,6 +215,7 @@ export type Refs = Options & {
180
215
  dynamic?: boolean;
181
216
  anchor?: string;
182
217
  }>;
218
+ definitions?: Record<string, JsonSchema>;
183
219
  /** Stack of active dynamic anchors (nearest last) */
184
220
  dynamicAnchors?: {
185
221
  name: string;
@@ -8,6 +8,9 @@ export type AnalysisResult = {
8
8
  schema: JsonSchema;
9
9
  options: NormalizedOptions;
10
10
  refNameByPointer: Map<string, string>;
11
+ refBaseNameByPointer: Map<string, string>;
12
+ baseNameBySchema: Map<string, string>;
13
+ rootBaseName?: string;
11
14
  usedNames: Set<string>;
12
15
  declarations: Map<string, SchemaRepresentation>;
13
16
  dependencies: Map<string, Set<string>>;
@@ -21,5 +24,6 @@ export type AnalysisResult = {
21
24
  anchor?: string;
22
25
  }>;
23
26
  rootBaseUri: string;
27
+ definitions?: Record<string, JsonSchema>;
24
28
  };
25
29
  export declare const analyzeSchema: (schema: JsonSchema, options?: Options) => AnalysisResult;
@@ -2,7 +2,7 @@ import { JsonSchema, Options } from "../Types.js";
2
2
  export type SplitDefsOptions = {
3
3
  /** Include a root schema file in addition to $defs */
4
4
  includeRoot?: boolean;
5
- /** Override file name for each schema (default: `${def}.schema.ts`) */
5
+ /** Override file name for each schema (default: `${ def }.schema.ts`) */
6
6
  fileName?: (defName: string, ctx: {
7
7
  isRoot: boolean;
8
8
  }) => string;
@@ -33,6 +33,7 @@ export * from "./utils/namingService.js";
33
33
  export * from "./utils/omit.js";
34
34
  export * from "./utils/resolveRef.js";
35
35
  export * from "./utils/resolveUri.js";
36
+ export * from "./utils/schemaNaming.js";
36
37
  export * from "./utils/schemaRepresentation.js";
37
38
  export * from "./utils/withMessage.js";
38
39
  export * from "./zodToJsonSchema.js";
@@ -0,0 +1,6 @@
1
+ import { NamingContext, NamingOptions } from "../Types.js";
2
+ export declare const defaultSchemaName: (baseName: string) => string;
3
+ export declare const sanitizeIdentifier: (value: string) => string;
4
+ export declare const ensureUnique: (candidate: string, used?: Set<string>) => string;
5
+ export declare const resolveSchemaName: (baseName: string, naming: NamingOptions, ctx: NamingContext, used?: Set<string>) => string;
6
+ export declare const resolveTypeName: (baseName: string, naming: NamingOptions, ctx: NamingContext, used?: Set<string>) => string | undefined;
@@ -58,8 +58,7 @@ export function parseArgs(params, args, help) {
58
58
  }
59
59
  export function parseOrReadJSON(jsonOrPath) {
60
60
  jsonOrPath = jsonOrPath.trim();
61
- if (jsonOrPath.length < 255 &&
62
- statSync(jsonOrPath, { throwIfNoEntry: false })?.isFile()) {
61
+ if (jsonOrPath.length < 255 && statSync(jsonOrPath, { throwIfNoEntry: false })?.isFile()) {
63
62
  jsonOrPath = readFileSync(jsonOrPath, "utf-8");
64
63
  }
65
64
  return JSON.parse(jsonOrPath);
@@ -52,7 +52,9 @@ export class EsmEmitter {
52
52
  const initializer = parseExpression(statement.expression);
53
53
  const typeNode = statement.typeAnnotation ? parseType(statement.typeAnnotation) : undefined;
54
54
  const decl = ts.factory.createVariableDeclaration(statement.name, undefined, typeNode, initializer);
55
- const modifiers = statement.exported ? [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)] : undefined;
55
+ const modifiers = statement.exported
56
+ ? [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)]
57
+ : undefined;
56
58
  const varStmt = ts.factory.createVariableStatement(modifiers, ts.factory.createVariableDeclarationList([decl], ts.NodeFlags.Const));
57
59
  __classPrivateFieldGet(this, _EsmEmitter_statements, "f").push({ node: attachJsdoc(varStmt, statement.jsdoc) });
58
60
  }
@@ -72,7 +74,9 @@ export class EsmEmitter {
72
74
  const sf = ts.createSourceFile("out.ts", "", ts.ScriptTarget.ES2020, false, ts.ScriptKind.TS);
73
75
  const importStmts = [...__classPrivateFieldGet(this, _EsmEmitter_imports, "f").entries()]
74
76
  .sort(([a], [b]) => a.localeCompare(b))
75
- .map(([source, names]) => ts.factory.createImportDeclaration(undefined, ts.factory.createImportClause(false, undefined, ts.factory.createNamedImports([...names].sort().map((name) => ts.factory.createImportSpecifier(false, undefined, ts.factory.createIdentifier(name))))), ts.factory.createStringLiteral(source)));
77
+ .map(([source, names]) => ts.factory.createImportDeclaration(undefined, ts.factory.createImportClause(false, undefined, ts.factory.createNamedImports([...names]
78
+ .sort()
79
+ .map((name) => ts.factory.createImportSpecifier(false, undefined, ts.factory.createIdentifier(name))))), ts.factory.createStringLiteral(source)));
76
80
  const allStatements = [
77
81
  ...importStmts.map((node) => ({ node })),
78
82
  ...__classPrivateFieldGet(this, _EsmEmitter_statements, "f"),
@@ -99,9 +99,7 @@ const sanitizeIdentifier = (value) => {
99
99
  .replace(/[^a-zA-Z0-9_$\s]/g, " ")
100
100
  .split(/\s+/)
101
101
  .filter(Boolean);
102
- const pascalCase = words
103
- .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
104
- .join("");
102
+ const pascalCase = words.map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
105
103
  const cleaned = pascalCase.replace(/^[^a-zA-Z_$]+/, "").replace(/[^a-zA-Z0-9_$]/g, "");
106
104
  return cleaned || "InlineSchema";
107
105
  };
@@ -1,9 +1,6 @@
1
1
  export const expandJsdocs = (jsdocs) => {
2
2
  const lines = jsdocs.split("\n");
3
- const result = lines.length === 1
4
- ? lines[0]
5
- : `\n${lines.map(x => `* ${x}`)
6
- .join("\n")}\n`;
3
+ const result = lines.length === 1 ? lines[0] : `\n${lines.map((x) => `* ${x}`).join("\n")}\n`;
7
4
  return `/**${result}*/\n`;
8
5
  };
9
6
  export const addJsdocs = (schema, parsed) => {