@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.
- package/CHANGELOG.md +23 -1
- package/dist/cjs/core/analyzeSchema.js +62 -0
- package/dist/cjs/core/emitZod.js +141 -0
- package/dist/cjs/generators/generateBundle.js +355 -0
- package/dist/cjs/index.js +6 -0
- package/dist/cjs/jsonSchemaToZod.js +5 -73
- package/dist/cjs/parsers/parseArray.js +34 -15
- package/dist/cjs/parsers/parseIfThenElse.js +2 -1
- package/dist/cjs/parsers/parseNumber.js +81 -39
- package/dist/cjs/parsers/parseObject.js +24 -0
- package/dist/cjs/parsers/parseSchema.js +147 -25
- package/dist/cjs/parsers/parseString.js +294 -54
- package/dist/cjs/utils/buildRefRegistry.js +56 -0
- package/dist/cjs/utils/cycles.js +113 -0
- package/dist/cjs/utils/resolveUri.js +16 -0
- package/dist/cjs/utils/withMessage.js +4 -5
- package/dist/esm/core/analyzeSchema.js +58 -0
- package/dist/esm/core/emitZod.js +137 -0
- package/dist/esm/generators/generateBundle.js +351 -0
- package/dist/esm/index.js +6 -0
- package/dist/esm/jsonSchemaToZod.js +5 -73
- package/dist/esm/parsers/parseArray.js +34 -15
- package/dist/esm/parsers/parseIfThenElse.js +2 -1
- package/dist/esm/parsers/parseNumber.js +81 -39
- package/dist/esm/parsers/parseObject.js +24 -0
- package/dist/esm/parsers/parseSchema.js +147 -25
- package/dist/esm/parsers/parseString.js +294 -54
- package/dist/esm/utils/buildRefRegistry.js +52 -0
- package/dist/esm/utils/cycles.js +107 -0
- package/dist/esm/utils/resolveUri.js +12 -0
- package/dist/esm/utils/withMessage.js +4 -5
- package/dist/types/Types.d.ts +36 -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 +62 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/jsonSchemaToZod.d.ts +1 -1
- package/dist/types/parsers/parseSchema.d.ts +2 -1
- package/dist/types/parsers/parseString.d.ts +2 -2
- package/dist/types/utils/buildRefRegistry.d.ts +12 -0
- package/dist/types/utils/cycles.d.ts +7 -0
- package/dist/types/utils/jsdocs.d.ts +1 -1
- package/dist/types/utils/resolveUri.d.ts +1 -0
- package/dist/types/utils/withMessage.d.ts +6 -1
- 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
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,31 @@
|
|
|
1
1
|
# @gabrielbryk/json-schema-to-zod
|
|
2
2
|
|
|
3
|
-
## 2.
|
|
3
|
+
## 2.9.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 0ef12db: - Extract analyzer/emitter core, refactor bundle generation to avoid recursion and preserve inline defs/definitions.
|
|
8
|
+
- Add scoped naming for inline $defs, lazy cross-ref handling, and nested types emission.
|
|
9
|
+
- Expand bundle test coverage with snapshots, inline-def fixtures, and nested-type extraction.
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- b8f6248: - Commit remaining utility, ref resolution test, and config updates.
|
|
14
|
+
- 7d257dd: - Ensure dist/esm emits real ESM with NodeNext settings and type:module, and update tests to run under ESM by providing createRequire shims.
|
|
15
|
+
|
|
16
|
+
## 2.8.0
|
|
17
|
+
|
|
18
|
+
### Minor Changes
|
|
19
|
+
|
|
20
|
+
- 0065ee8: - Add support for JSON Schema `dependentRequired` on objects with optional custom error message.
|
|
21
|
+
- Extend format handling and add bigint format helpers while warning on unknown string formats.
|
|
4
22
|
|
|
5
23
|
### Patch Changes
|
|
6
24
|
|
|
25
|
+
- 3d57690: - Make $ref handling cycle-aware with SCC-based ordering and minimal z.lazy usage.
|
|
26
|
+
- Add workflow spec fixture to compiled-output tests to guard against TDZ issues.
|
|
27
|
+
- Fix parseString to build a full Refs context when missing, keeping type checks happy.
|
|
28
|
+
- 3d57690: - Switch ESM/typings builds to NodeNext resolution and ensure relative imports include .js extensions for Node ESM compatibility.
|
|
7
29
|
- 82aa953: Fix patternProperties validation under Zod v4 by preserving regex patterns and handling missing `ctx.path`.
|
|
8
30
|
- a501e7d: Adjust release workflow to rely on the default npm from setup-node and drop unused tokens.
|
|
9
31
|
- 43f2abc: Update object record generation to use `z.record(z.string(), …)` for Zod v4 compatibility.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.analyzeSchema = void 0;
|
|
4
|
+
const parseSchema_js_1 = require("../parsers/parseSchema.js");
|
|
5
|
+
const cycles_js_1 = require("../utils/cycles.js");
|
|
6
|
+
const buildRefRegistry_js_1 = require("../utils/buildRefRegistry.js");
|
|
7
|
+
const analyzeSchema = (schema, options = {}) => {
|
|
8
|
+
const { module, name, type, ...rest } = options;
|
|
9
|
+
if (type && (!name || module !== "esm")) {
|
|
10
|
+
throw new Error("Option `type` requires `name` to be set and `module` to be `esm`");
|
|
11
|
+
}
|
|
12
|
+
const normalized = {
|
|
13
|
+
module,
|
|
14
|
+
name,
|
|
15
|
+
type,
|
|
16
|
+
...rest,
|
|
17
|
+
exportRefs: rest.exportRefs ?? true,
|
|
18
|
+
withMeta: rest.withMeta ?? true,
|
|
19
|
+
};
|
|
20
|
+
const refNameByPointer = new Map();
|
|
21
|
+
const usedNames = new Set();
|
|
22
|
+
if (name) {
|
|
23
|
+
usedNames.add(name);
|
|
24
|
+
}
|
|
25
|
+
const declarations = new Map();
|
|
26
|
+
const dependencies = new Map();
|
|
27
|
+
const { registry: refRegistry, rootBaseUri } = (0, buildRefRegistry_js_1.buildRefRegistry)(schema);
|
|
28
|
+
const pass1 = {
|
|
29
|
+
module,
|
|
30
|
+
name,
|
|
31
|
+
path: [],
|
|
32
|
+
seen: new Map(),
|
|
33
|
+
declarations,
|
|
34
|
+
dependencies,
|
|
35
|
+
inProgress: new Set(),
|
|
36
|
+
refNameByPointer,
|
|
37
|
+
usedNames,
|
|
38
|
+
root: schema,
|
|
39
|
+
currentSchemaName: name,
|
|
40
|
+
refRegistry,
|
|
41
|
+
rootBaseUri,
|
|
42
|
+
...rest,
|
|
43
|
+
withMeta: normalized.withMeta,
|
|
44
|
+
};
|
|
45
|
+
(0, parseSchema_js_1.parseSchema)(schema, pass1);
|
|
46
|
+
const names = Array.from(declarations.keys());
|
|
47
|
+
const cycleRefNames = (0, cycles_js_1.detectCycles)(names, dependencies);
|
|
48
|
+
const { componentByName } = (0, cycles_js_1.computeScc)(names, dependencies);
|
|
49
|
+
return {
|
|
50
|
+
schema,
|
|
51
|
+
options: normalized,
|
|
52
|
+
refNameByPointer,
|
|
53
|
+
usedNames,
|
|
54
|
+
declarations,
|
|
55
|
+
dependencies,
|
|
56
|
+
cycleRefNames,
|
|
57
|
+
cycleComponentByName: componentByName,
|
|
58
|
+
refRegistry,
|
|
59
|
+
rootBaseUri,
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
exports.analyzeSchema = analyzeSchema;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.emitZod = void 0;
|
|
4
|
+
const parseSchema_js_1 = require("../parsers/parseSchema.js");
|
|
5
|
+
const jsdocs_js_1 = require("../utils/jsdocs.js");
|
|
6
|
+
const orderDeclarations = (entries, dependencies) => {
|
|
7
|
+
const valueByName = new Map(entries);
|
|
8
|
+
const depGraph = new Map();
|
|
9
|
+
for (const [from, set] of dependencies.entries()) {
|
|
10
|
+
const onlyKnown = new Set();
|
|
11
|
+
for (const dep of set) {
|
|
12
|
+
if (valueByName.has(dep) && dep !== from) {
|
|
13
|
+
onlyKnown.add(dep);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
if (onlyKnown.size)
|
|
17
|
+
depGraph.set(from, onlyKnown);
|
|
18
|
+
}
|
|
19
|
+
const names = Array.from(valueByName.keys());
|
|
20
|
+
for (const [name, value] of entries) {
|
|
21
|
+
const deps = depGraph.get(name) ?? new Set();
|
|
22
|
+
for (const candidate of names) {
|
|
23
|
+
if (candidate === name)
|
|
24
|
+
continue;
|
|
25
|
+
const matcher = new RegExp(`\\b${candidate}\\b`);
|
|
26
|
+
if (matcher.test(value)) {
|
|
27
|
+
deps.add(candidate);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (deps.size)
|
|
31
|
+
depGraph.set(name, deps);
|
|
32
|
+
}
|
|
33
|
+
const ordered = [];
|
|
34
|
+
const perm = new Set();
|
|
35
|
+
const temp = new Set();
|
|
36
|
+
const visit = (name) => {
|
|
37
|
+
if (perm.has(name))
|
|
38
|
+
return;
|
|
39
|
+
if (temp.has(name)) {
|
|
40
|
+
temp.delete(name);
|
|
41
|
+
perm.add(name);
|
|
42
|
+
ordered.push(name);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
temp.add(name);
|
|
46
|
+
const deps = depGraph.get(name);
|
|
47
|
+
if (deps) {
|
|
48
|
+
for (const dep of deps) {
|
|
49
|
+
if (valueByName.has(dep)) {
|
|
50
|
+
visit(dep);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
temp.delete(name);
|
|
55
|
+
perm.add(name);
|
|
56
|
+
ordered.push(name);
|
|
57
|
+
};
|
|
58
|
+
for (const name of valueByName.keys()) {
|
|
59
|
+
visit(name);
|
|
60
|
+
}
|
|
61
|
+
const unique = [];
|
|
62
|
+
const seen = new Set();
|
|
63
|
+
for (const name of ordered) {
|
|
64
|
+
if (!seen.has(name)) {
|
|
65
|
+
seen.add(name);
|
|
66
|
+
unique.push(name);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return unique.map((name) => [name, valueByName.get(name)]);
|
|
70
|
+
};
|
|
71
|
+
const emitZod = (analysis) => {
|
|
72
|
+
const { schema, options, refNameByPointer, usedNames, cycleRefNames, cycleComponentByName, } = analysis;
|
|
73
|
+
const { module, name, type, noImport, exportRefs, withMeta, ...rest } = options;
|
|
74
|
+
const declarations = new Map();
|
|
75
|
+
const dependencies = new Map();
|
|
76
|
+
const parsedSchema = (0, parseSchema_js_1.parseSchema)(schema, {
|
|
77
|
+
module,
|
|
78
|
+
name,
|
|
79
|
+
path: [],
|
|
80
|
+
seen: new Map(),
|
|
81
|
+
declarations,
|
|
82
|
+
dependencies,
|
|
83
|
+
inProgress: new Set(),
|
|
84
|
+
refNameByPointer,
|
|
85
|
+
usedNames,
|
|
86
|
+
root: schema,
|
|
87
|
+
currentSchemaName: name,
|
|
88
|
+
cycleRefNames,
|
|
89
|
+
cycleComponentByName,
|
|
90
|
+
refRegistry: analysis.refRegistry,
|
|
91
|
+
rootBaseUri: analysis.rootBaseUri,
|
|
92
|
+
...rest,
|
|
93
|
+
withMeta,
|
|
94
|
+
});
|
|
95
|
+
const declarationBlock = declarations.size
|
|
96
|
+
? orderDeclarations(Array.from(declarations.entries()), dependencies)
|
|
97
|
+
.map(([refName, value]) => {
|
|
98
|
+
const shouldExport = exportRefs && module === "esm";
|
|
99
|
+
const decl = `${shouldExport ? "export " : ""}const ${refName} = ${value}`;
|
|
100
|
+
return decl;
|
|
101
|
+
})
|
|
102
|
+
.join("\n")
|
|
103
|
+
: "";
|
|
104
|
+
const jsdocs = rest.withJsdocs && typeof schema !== "boolean" && schema.description
|
|
105
|
+
? (0, jsdocs_js_1.expandJsdocs)(schema.description)
|
|
106
|
+
: "";
|
|
107
|
+
const lines = [];
|
|
108
|
+
if (module === "cjs" && !noImport) {
|
|
109
|
+
lines.push(`const { z } = require("zod")`);
|
|
110
|
+
}
|
|
111
|
+
if (module === "esm" && !noImport) {
|
|
112
|
+
lines.push(`import { z } from "zod"`);
|
|
113
|
+
}
|
|
114
|
+
if (declarationBlock) {
|
|
115
|
+
lines.push(declarationBlock);
|
|
116
|
+
}
|
|
117
|
+
if (module === "cjs") {
|
|
118
|
+
const payload = name ? `{ ${JSON.stringify(name)}: ${parsedSchema} }` : parsedSchema;
|
|
119
|
+
lines.push(`${jsdocs}module.exports = ${payload}`);
|
|
120
|
+
}
|
|
121
|
+
else if (module === "esm") {
|
|
122
|
+
const exportLine = `${jsdocs}export ${name ? `const ${name} =` : `default`} ${parsedSchema}`;
|
|
123
|
+
lines.push(exportLine);
|
|
124
|
+
}
|
|
125
|
+
else if (name) {
|
|
126
|
+
lines.push(`${jsdocs}const ${name} = ${parsedSchema}`);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
lines.push(`${jsdocs}${parsedSchema}`);
|
|
130
|
+
}
|
|
131
|
+
let typeLine;
|
|
132
|
+
if (type && name) {
|
|
133
|
+
const typeName = typeof type === "string" ? type : `${name[0].toUpperCase()}${name.substring(1)}`;
|
|
134
|
+
typeLine = `export type ${typeName} = z.infer<typeof ${name}>`;
|
|
135
|
+
}
|
|
136
|
+
const joined = lines.filter(Boolean).join("\n\n");
|
|
137
|
+
const combined = typeLine ? `${joined}\n${typeLine}` : joined;
|
|
138
|
+
const shouldEndWithNewline = module === "esm" || module === "cjs";
|
|
139
|
+
return `${combined}${shouldEndWithNewline ? "\n" : ""}`;
|
|
140
|
+
};
|
|
141
|
+
exports.emitZod = emitZod;
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.generateSchemaBundle = void 0;
|
|
4
|
+
const analyzeSchema_js_1 = require("../core/analyzeSchema.js");
|
|
5
|
+
const emitZod_js_1 = require("../core/emitZod.js");
|
|
6
|
+
const generateSchemaBundle = (schema, options = {}) => {
|
|
7
|
+
const module = options.module ?? "esm";
|
|
8
|
+
if (!schema || typeof schema !== "object") {
|
|
9
|
+
throw new Error("generateSchemaBundle requires an object schema");
|
|
10
|
+
}
|
|
11
|
+
const defs = schema.$defs || {};
|
|
12
|
+
const definitions = schema.definitions || {};
|
|
13
|
+
const defNames = Object.keys(defs);
|
|
14
|
+
const { rootName, rootTypeName, defInfoMap } = buildBundleContext(defNames, defs, options);
|
|
15
|
+
const files = [];
|
|
16
|
+
const targets = planBundleTargets(schema, defs, definitions, defNames, options, rootName, rootTypeName);
|
|
17
|
+
for (const target of targets) {
|
|
18
|
+
const usedRefs = target.usedRefs;
|
|
19
|
+
const analysis = (0, analyzeSchema_js_1.analyzeSchema)(target.schemaWithDefs, {
|
|
20
|
+
...options,
|
|
21
|
+
module,
|
|
22
|
+
name: target.schemaName,
|
|
23
|
+
type: target.typeName,
|
|
24
|
+
parserOverride: createRefHandler(target.defName, defInfoMap, usedRefs, {
|
|
25
|
+
...(target.schemaWithDefs.$defs || {}),
|
|
26
|
+
...(target.schemaWithDefs.definitions || {}),
|
|
27
|
+
}, options),
|
|
28
|
+
});
|
|
29
|
+
const zodSchema = (0, emitZod_js_1.emitZod)(analysis);
|
|
30
|
+
const finalSchema = buildSchemaFile(zodSchema, usedRefs, defInfoMap, module);
|
|
31
|
+
files.push({ fileName: target.fileName, contents: finalSchema });
|
|
32
|
+
}
|
|
33
|
+
// Nested types extraction (optional)
|
|
34
|
+
const nestedTypesEnabled = options.nestedTypes?.enable;
|
|
35
|
+
if (nestedTypesEnabled) {
|
|
36
|
+
const nestedTypes = collectNestedTypes(schema, defs, defNames, rootTypeName ?? rootName);
|
|
37
|
+
if (nestedTypes.length > 0) {
|
|
38
|
+
const nestedFileName = options.nestedTypes?.fileName ?? "nested-types.ts";
|
|
39
|
+
const nestedContent = generateNestedTypesFile(nestedTypes);
|
|
40
|
+
files.push({ fileName: nestedFileName, contents: nestedContent });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return { files, defNames };
|
|
44
|
+
};
|
|
45
|
+
exports.generateSchemaBundle = generateSchemaBundle;
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Internals
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
const toPascalCase = (str) => str
|
|
50
|
+
.split(/[-_]/)
|
|
51
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
52
|
+
.join("");
|
|
53
|
+
const buildDefInfoMap = (defNames, defs, options) => {
|
|
54
|
+
const map = new Map();
|
|
55
|
+
for (const defName of defNames) {
|
|
56
|
+
const dependencies = findRefDependencies(defs[defName], defNames);
|
|
57
|
+
const pascalName = toPascalCase(defName);
|
|
58
|
+
const schemaName = options.splitDefs?.schemaName?.(defName, { isRoot: false }) ?? `${pascalName}Schema`;
|
|
59
|
+
const typeName = options.splitDefs?.typeName?.(defName, { isRoot: false }) ?? pascalName;
|
|
60
|
+
map.set(defName, {
|
|
61
|
+
name: defName,
|
|
62
|
+
pascalName,
|
|
63
|
+
schemaName,
|
|
64
|
+
typeName,
|
|
65
|
+
dependencies,
|
|
66
|
+
hasCycle: false,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return map;
|
|
70
|
+
};
|
|
71
|
+
const buildBundleContext = (defNames, defs, options) => {
|
|
72
|
+
const defInfoMap = buildDefInfoMap(defNames, defs, options);
|
|
73
|
+
const cycles = detectCycles(defInfoMap);
|
|
74
|
+
for (const defName of cycles) {
|
|
75
|
+
const info = defInfoMap.get(defName);
|
|
76
|
+
if (info)
|
|
77
|
+
info.hasCycle = true;
|
|
78
|
+
}
|
|
79
|
+
const rootName = options.splitDefs?.rootName ?? options.name ?? "RootSchema";
|
|
80
|
+
const rootTypeName = typeof options.type === "string"
|
|
81
|
+
? options.type
|
|
82
|
+
: options.splitDefs?.rootTypeName ?? (typeof options.type === "boolean" && options.type ? rootName : undefined);
|
|
83
|
+
return { defInfoMap, rootName, rootTypeName };
|
|
84
|
+
};
|
|
85
|
+
const createRefHandler = (currentDefName, defInfoMap, usedRefs, allDefs, options) => {
|
|
86
|
+
return (schema, refs) => {
|
|
87
|
+
if (typeof schema["$ref"] === "string") {
|
|
88
|
+
const refPath = schema["$ref"];
|
|
89
|
+
const match = refPath.match(/^#\/(?:\$defs|definitions)\/(.+)$/);
|
|
90
|
+
if (match) {
|
|
91
|
+
const refName = match[1];
|
|
92
|
+
// Only intercept top-level def refs (no nested path like a/$defs/x)
|
|
93
|
+
if (refName.includes("/")) {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
const refInfo = defInfoMap.get(refName);
|
|
97
|
+
if (refInfo) {
|
|
98
|
+
// Track imports when referencing other defs
|
|
99
|
+
if (refName !== currentDefName) {
|
|
100
|
+
usedRefs.add(refName);
|
|
101
|
+
}
|
|
102
|
+
const isCycle = refName === currentDefName || (refInfo.hasCycle && !!currentDefName);
|
|
103
|
+
const resolved = options.refResolution?.onRef?.({
|
|
104
|
+
ref: refPath,
|
|
105
|
+
refName,
|
|
106
|
+
currentDef: currentDefName,
|
|
107
|
+
path: refs.path,
|
|
108
|
+
isCycle,
|
|
109
|
+
});
|
|
110
|
+
if (resolved)
|
|
111
|
+
return resolved;
|
|
112
|
+
if (isCycle && options.refResolution?.lazyCrossRefs) {
|
|
113
|
+
return `z.lazy(() => ${refInfo.schemaName})`;
|
|
114
|
+
}
|
|
115
|
+
return refInfo.schemaName;
|
|
116
|
+
}
|
|
117
|
+
// If the ref points to a local/inline $def (not part of top-level defs),
|
|
118
|
+
// let the default parser resolve it normally.
|
|
119
|
+
if (allDefs && Object.prototype.hasOwnProperty.call(allDefs, refName)) {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const unknown = options.refResolution?.onUnknownRef?.({ ref: refPath, currentDef: currentDefName });
|
|
124
|
+
if (unknown)
|
|
125
|
+
return unknown;
|
|
126
|
+
return options.useUnknown ? "z.unknown()" : "z.any()";
|
|
127
|
+
}
|
|
128
|
+
return undefined;
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
const buildSchemaFile = (zodCode, usedRefs, defInfoMap, module) => {
|
|
132
|
+
if (module !== "esm")
|
|
133
|
+
return zodCode;
|
|
134
|
+
const imports = [];
|
|
135
|
+
for (const refName of [...usedRefs].sort()) {
|
|
136
|
+
const refInfo = defInfoMap.get(refName);
|
|
137
|
+
if (refInfo) {
|
|
138
|
+
imports.push(`import { ${refInfo.schemaName} } from './${refName}.schema.js';`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (!imports.length)
|
|
142
|
+
return zodCode;
|
|
143
|
+
return zodCode.replace('import { z } from "zod"', `import { z } from "zod"\n${imports.join("\n")}`);
|
|
144
|
+
};
|
|
145
|
+
const planBundleTargets = (rootSchema, defs, definitions, defNames, options, rootName, rootTypeName) => {
|
|
146
|
+
const targets = [];
|
|
147
|
+
for (const defName of defNames) {
|
|
148
|
+
const defSchema = defs[defName];
|
|
149
|
+
const defSchemaWithDefs = {
|
|
150
|
+
...defSchema,
|
|
151
|
+
$defs: { ...defs, ...defSchema?.$defs },
|
|
152
|
+
definitions: {
|
|
153
|
+
...defSchema.definitions,
|
|
154
|
+
...definitions,
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
const pascalName = toPascalCase(defName);
|
|
158
|
+
const schemaName = options.splitDefs?.schemaName?.(defName, { isRoot: false }) ?? `${pascalName}Schema`;
|
|
159
|
+
const typeName = options.splitDefs?.typeName?.(defName, { isRoot: false }) ?? pascalName;
|
|
160
|
+
const fileName = options.splitDefs?.fileName?.(defName, { isRoot: false }) ?? `${defName}.schema.ts`;
|
|
161
|
+
targets.push({
|
|
162
|
+
defName,
|
|
163
|
+
schemaWithDefs: defSchemaWithDefs,
|
|
164
|
+
schemaName,
|
|
165
|
+
typeName,
|
|
166
|
+
fileName,
|
|
167
|
+
usedRefs: new Set(),
|
|
168
|
+
isRoot: false,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
if (options.splitDefs?.includeRoot ?? true) {
|
|
172
|
+
const rootFile = options.splitDefs?.fileName?.("root", { isRoot: true }) ?? "workflow.schema.ts";
|
|
173
|
+
targets.push({
|
|
174
|
+
defName: null,
|
|
175
|
+
schemaWithDefs: {
|
|
176
|
+
...rootSchema,
|
|
177
|
+
definitions: {
|
|
178
|
+
...rootSchema.definitions,
|
|
179
|
+
...definitions,
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
schemaName: rootName,
|
|
183
|
+
typeName: rootTypeName,
|
|
184
|
+
fileName: rootFile,
|
|
185
|
+
usedRefs: new Set(),
|
|
186
|
+
isRoot: true,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
return targets;
|
|
190
|
+
};
|
|
191
|
+
const findRefDependencies = (schema, validDefNames) => {
|
|
192
|
+
const deps = new Set();
|
|
193
|
+
function traverse(obj) {
|
|
194
|
+
if (obj === null || typeof obj !== "object")
|
|
195
|
+
return;
|
|
196
|
+
if (Array.isArray(obj)) {
|
|
197
|
+
obj.forEach(traverse);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const record = obj;
|
|
201
|
+
if (typeof record["$ref"] === "string") {
|
|
202
|
+
const ref = record["$ref"];
|
|
203
|
+
const match = ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/);
|
|
204
|
+
if (match && validDefNames.includes(match[1])) {
|
|
205
|
+
deps.add(match[1]);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
for (const value of Object.values(record)) {
|
|
209
|
+
traverse(value);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
traverse(schema);
|
|
213
|
+
return deps;
|
|
214
|
+
};
|
|
215
|
+
const detectCycles = (defInfoMap) => {
|
|
216
|
+
const cycleNodes = new Set();
|
|
217
|
+
const visited = new Set();
|
|
218
|
+
const recursionStack = new Set();
|
|
219
|
+
function dfs(node, path) {
|
|
220
|
+
if (recursionStack.has(node)) {
|
|
221
|
+
const cycleStart = path.indexOf(node);
|
|
222
|
+
for (let i = cycleStart; i < path.length; i++) {
|
|
223
|
+
cycleNodes.add(path[i]);
|
|
224
|
+
}
|
|
225
|
+
cycleNodes.add(node);
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
if (visited.has(node))
|
|
229
|
+
return false;
|
|
230
|
+
visited.add(node);
|
|
231
|
+
recursionStack.add(node);
|
|
232
|
+
const info = defInfoMap.get(node);
|
|
233
|
+
if (info) {
|
|
234
|
+
for (const dep of info.dependencies) {
|
|
235
|
+
dfs(dep, [...path, node]);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
recursionStack.delete(node);
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
for (const defName of defInfoMap.keys()) {
|
|
242
|
+
if (!visited.has(defName)) {
|
|
243
|
+
dfs(defName, []);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return cycleNodes;
|
|
247
|
+
};
|
|
248
|
+
const collectNestedTypes = (rootSchema, defs, defNames, rootTypeName) => {
|
|
249
|
+
const allNestedTypes = [];
|
|
250
|
+
for (const defName of defNames) {
|
|
251
|
+
const defSchema = defs[defName];
|
|
252
|
+
const parentTypeName = toPascalCase(defName);
|
|
253
|
+
const nestedTypes = findNestedTypesInSchema(defSchema, parentTypeName, defNames);
|
|
254
|
+
for (const nested of nestedTypes) {
|
|
255
|
+
nested.file = defName;
|
|
256
|
+
nested.parentType = parentTypeName;
|
|
257
|
+
allNestedTypes.push(nested);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const workflowNestedTypes = findNestedTypesInSchema({ properties: rootSchema.properties, required: rootSchema.required }, rootTypeName, defNames);
|
|
261
|
+
for (const nested of workflowNestedTypes) {
|
|
262
|
+
nested.file = "workflow";
|
|
263
|
+
nested.parentType = rootTypeName;
|
|
264
|
+
allNestedTypes.push(nested);
|
|
265
|
+
}
|
|
266
|
+
const uniqueNestedTypes = new Map();
|
|
267
|
+
for (const nested of allNestedTypes) {
|
|
268
|
+
if (!uniqueNestedTypes.has(nested.typeName) && nested.propertyPath.length > 0) {
|
|
269
|
+
uniqueNestedTypes.set(nested.typeName, nested);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return [...uniqueNestedTypes.values()];
|
|
273
|
+
};
|
|
274
|
+
const findNestedTypesInSchema = (schema, parentTypeName, defNames, currentPath = []) => {
|
|
275
|
+
const nestedTypes = [];
|
|
276
|
+
if (schema === null || typeof schema !== "object")
|
|
277
|
+
return nestedTypes;
|
|
278
|
+
const record = schema;
|
|
279
|
+
if (record.title && typeof record.title === "string") {
|
|
280
|
+
const title = record.title;
|
|
281
|
+
if (title !== parentTypeName && !defNames.map((d) => toPascalCase(d)).includes(title)) {
|
|
282
|
+
nestedTypes.push({
|
|
283
|
+
typeName: title,
|
|
284
|
+
parentType: parentTypeName,
|
|
285
|
+
propertyPath: [...currentPath],
|
|
286
|
+
file: "",
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// inline $defs
|
|
291
|
+
if (record.$defs && typeof record.$defs === "object") {
|
|
292
|
+
for (const [_defName, defSchema] of Object.entries(record.$defs)) {
|
|
293
|
+
nestedTypes.push(...findNestedTypesInSchema(defSchema, parentTypeName, defNames, currentPath));
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (record.properties && typeof record.properties === "object") {
|
|
297
|
+
for (const [propName, propSchema] of Object.entries(record.properties)) {
|
|
298
|
+
nestedTypes.push(...findNestedTypesInSchema(propSchema, parentTypeName, defNames, [...currentPath, propName]));
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (Array.isArray(record.allOf)) {
|
|
302
|
+
for (const item of record.allOf) {
|
|
303
|
+
nestedTypes.push(...findNestedTypesInSchema(item, parentTypeName, defNames, currentPath));
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (record.items) {
|
|
307
|
+
nestedTypes.push(...findNestedTypesInSchema(record.items, parentTypeName, defNames, [...currentPath, "items"]));
|
|
308
|
+
}
|
|
309
|
+
if (record.additionalProperties && typeof record.additionalProperties === "object") {
|
|
310
|
+
nestedTypes.push(...findNestedTypesInSchema(record.additionalProperties, parentTypeName, defNames, [...currentPath, "additionalProperties"]));
|
|
311
|
+
}
|
|
312
|
+
return nestedTypes;
|
|
313
|
+
};
|
|
314
|
+
const generateNestedTypesFile = (nestedTypes) => {
|
|
315
|
+
const lines = [
|
|
316
|
+
"/**",
|
|
317
|
+
" * Auto-generated nested type exports",
|
|
318
|
+
" * ",
|
|
319
|
+
" * These types are inline within parent schemas but commonly needed separately.",
|
|
320
|
+
" * They are extracted using TypeScript indexed access types.",
|
|
321
|
+
" */",
|
|
322
|
+
"",
|
|
323
|
+
];
|
|
324
|
+
const byParent = new Map();
|
|
325
|
+
for (const info of nestedTypes) {
|
|
326
|
+
if (!byParent.has(info.parentType)) {
|
|
327
|
+
byParent.set(info.parentType, []);
|
|
328
|
+
}
|
|
329
|
+
byParent.get(info.parentType).push(info);
|
|
330
|
+
}
|
|
331
|
+
const imports = new Map(); // file -> type name
|
|
332
|
+
for (const info of nestedTypes) {
|
|
333
|
+
if (!imports.has(info.file)) {
|
|
334
|
+
imports.set(info.file, info.parentType);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
for (const [file, typeName] of [...imports.entries()].sort()) {
|
|
338
|
+
lines.push(`import type { ${typeName} } from './${file}.schema.js';`);
|
|
339
|
+
}
|
|
340
|
+
lines.push("");
|
|
341
|
+
for (const [parentType, types] of [...byParent.entries()].sort()) {
|
|
342
|
+
lines.push(`// From ${parentType}`);
|
|
343
|
+
for (const info of types.sort((a, b) => a.typeName.localeCompare(b.typeName))) {
|
|
344
|
+
if (info.propertyPath.length > 0) {
|
|
345
|
+
let accessExpr = parentType;
|
|
346
|
+
for (const prop of info.propertyPath) {
|
|
347
|
+
accessExpr = `NonNullable<${accessExpr}['${prop}']>`;
|
|
348
|
+
}
|
|
349
|
+
lines.push(`export type ${info.typeName} = ${accessExpr};`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
lines.push("");
|
|
353
|
+
}
|
|
354
|
+
return lines.join("\n");
|
|
355
|
+
};
|
package/dist/cjs/index.js
CHANGED
|
@@ -15,6 +15,9 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
__exportStar(require("./Types.js"), exports);
|
|
18
|
+
__exportStar(require("./core/analyzeSchema.js"), exports);
|
|
19
|
+
__exportStar(require("./core/emitZod.js"), exports);
|
|
20
|
+
__exportStar(require("./generators/generateBundle.js"), exports);
|
|
18
21
|
__exportStar(require("./jsonSchemaToZod.js"), exports);
|
|
19
22
|
__exportStar(require("./parsers/parseAllOf.js"), exports);
|
|
20
23
|
__exportStar(require("./parsers/parseAnyOf.js"), exports);
|
|
@@ -35,9 +38,12 @@ __exportStar(require("./parsers/parseSchema.js"), exports);
|
|
|
35
38
|
__exportStar(require("./parsers/parseSimpleDiscriminatedOneOf.js"), exports);
|
|
36
39
|
__exportStar(require("./parsers/parseString.js"), exports);
|
|
37
40
|
__exportStar(require("./utils/anyOrUnknown.js"), exports);
|
|
41
|
+
__exportStar(require("./utils/buildRefRegistry.js"), exports);
|
|
42
|
+
__exportStar(require("./utils/cycles.js"), exports);
|
|
38
43
|
__exportStar(require("./utils/half.js"), exports);
|
|
39
44
|
__exportStar(require("./utils/jsdocs.js"), exports);
|
|
40
45
|
__exportStar(require("./utils/omit.js"), exports);
|
|
46
|
+
__exportStar(require("./utils/resolveUri.js"), exports);
|
|
41
47
|
__exportStar(require("./utils/withMessage.js"), exports);
|
|
42
48
|
__exportStar(require("./zodToJsonSchema.js"), exports);
|
|
43
49
|
const jsonSchemaToZod_js_1 = require("./jsonSchemaToZod.js");
|