@manifesto-ai/codegen 0.1.0 → 0.1.2
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/README.md +2 -2
- package/dist/index.d.ts +114 -10
- package/dist/index.js +543 -13
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/dist/header.d.ts +0 -13
- package/dist/header.d.ts.map +0 -1
- package/dist/header.js +0 -18
- package/dist/header.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/path-safety.d.ts +0 -16
- package/dist/path-safety.d.ts.map +0 -1
- package/dist/path-safety.js +0 -47
- package/dist/path-safety.js.map +0 -1
- package/dist/plugins/index.d.ts +0 -5
- package/dist/plugins/index.d.ts.map +0 -1
- package/dist/plugins/index.js +0 -3
- package/dist/plugins/index.js.map +0 -1
- package/dist/plugins/ts-plugin.d.ts +0 -11
- package/dist/plugins/ts-plugin.d.ts.map +0 -1
- package/dist/plugins/ts-plugin.js +0 -202
- package/dist/plugins/ts-plugin.js.map +0 -1
- package/dist/plugins/zod-plugin.d.ts +0 -6
- package/dist/plugins/zod-plugin.d.ts.map +0 -1
- package/dist/plugins/zod-plugin.js +0 -138
- package/dist/plugins/zod-plugin.js.map +0 -1
- package/dist/runner.d.ts +0 -13
- package/dist/runner.d.ts.map +0 -1
- package/dist/runner.js +0 -126
- package/dist/runner.js.map +0 -1
- package/dist/stable-hash.d.ts +0 -7
- package/dist/stable-hash.d.ts.map +0 -1
- package/dist/stable-hash.js +0 -11
- package/dist/stable-hash.js.map +0 -1
- package/dist/types.d.ts +0 -50
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -2
- package/dist/types.js.map +0 -1
- package/dist/virtual-fs.d.ts +0 -27
- package/dist/virtual-fs.d.ts.map +0 -1
- package/dist/virtual-fs.js +0 -74
- package/dist/virtual-fs.js.map +0 -1
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ DomainSchema -> CODEGEN -> Generated Files
|
|
|
36
36
|
|
|
37
37
|
| NOT Responsible For | Who Is |
|
|
38
38
|
|--------------------|--------|
|
|
39
|
-
| Define schemas |
|
|
39
|
+
| Define schemas | SDK / Compiler (DomainSchema authoring) |
|
|
40
40
|
| Runtime validation | Application code using generated Zod schemas |
|
|
41
41
|
| Bundling or compilation | Build tools (tsc, esbuild, etc.) |
|
|
42
42
|
| Schema versioning | `@manifesto-ai/core` |
|
|
@@ -162,7 +162,7 @@ Same DomainSchema always produces byte-identical output files. Fields and types
|
|
|
162
162
|
| Relationship | Package | How |
|
|
163
163
|
|--------------|---------|-----|
|
|
164
164
|
| Depends on | `@manifesto-ai/core` | Reads DomainSchema, TypeDefinition, TypeSpec |
|
|
165
|
-
| Used by |
|
|
165
|
+
| Used by | Build scripts | Called during build to generate type-safe code |
|
|
166
166
|
|
|
167
167
|
---
|
|
168
168
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,114 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
import { DomainSchema } from '@manifesto-ai/core';
|
|
2
|
+
|
|
3
|
+
type Diagnostic = {
|
|
4
|
+
readonly level: "warn" | "error";
|
|
5
|
+
readonly plugin: string;
|
|
6
|
+
readonly message: string;
|
|
7
|
+
};
|
|
8
|
+
type FilePatch = {
|
|
9
|
+
readonly op: "set";
|
|
10
|
+
readonly path: string;
|
|
11
|
+
readonly content: string;
|
|
12
|
+
} | {
|
|
13
|
+
readonly op: "delete";
|
|
14
|
+
readonly path: string;
|
|
15
|
+
};
|
|
16
|
+
interface CodegenHelpers {
|
|
17
|
+
stableHash(input: unknown): string;
|
|
18
|
+
}
|
|
19
|
+
interface CodegenContext {
|
|
20
|
+
readonly schema: DomainSchema;
|
|
21
|
+
readonly sourceId?: string;
|
|
22
|
+
readonly outDir: string;
|
|
23
|
+
readonly artifacts: Readonly<Record<string, unknown>>;
|
|
24
|
+
readonly helpers: CodegenHelpers;
|
|
25
|
+
}
|
|
26
|
+
interface CodegenOutput {
|
|
27
|
+
readonly patches: readonly FilePatch[];
|
|
28
|
+
readonly artifacts?: Readonly<Record<string, unknown>>;
|
|
29
|
+
readonly diagnostics?: readonly Diagnostic[];
|
|
30
|
+
}
|
|
31
|
+
interface CodegenPlugin {
|
|
32
|
+
readonly name: string;
|
|
33
|
+
generate(ctx: CodegenContext): CodegenOutput | Promise<CodegenOutput>;
|
|
34
|
+
}
|
|
35
|
+
interface GenerateOptions {
|
|
36
|
+
readonly schema: DomainSchema;
|
|
37
|
+
readonly outDir: string;
|
|
38
|
+
readonly plugins: readonly CodegenPlugin[];
|
|
39
|
+
readonly sourceId?: string;
|
|
40
|
+
readonly stamp?: boolean;
|
|
41
|
+
}
|
|
42
|
+
interface GenerateResult {
|
|
43
|
+
readonly files: ReadonlyArray<{
|
|
44
|
+
readonly path: string;
|
|
45
|
+
readonly content: string;
|
|
46
|
+
}>;
|
|
47
|
+
readonly artifacts: Readonly<Record<string, unknown>>;
|
|
48
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Generate typed artifacts from a DomainSchema using plugins.
|
|
53
|
+
*
|
|
54
|
+
* This is the sole entry point for codegen. It orchestrates:
|
|
55
|
+
* - Plugin name uniqueness validation (GEN-2)
|
|
56
|
+
* - Sequential plugin execution (GEN-3, GEN-7)
|
|
57
|
+
* - FilePatch composition with collision detection
|
|
58
|
+
* - Error gating (GEN-5, GEN-8)
|
|
59
|
+
* - outDir clean + file flush (GEN-1)
|
|
60
|
+
*/
|
|
61
|
+
declare function generate(opts: GenerateOptions): Promise<GenerateResult>;
|
|
62
|
+
|
|
63
|
+
interface TsPluginOptions {
|
|
64
|
+
readonly typesFile?: string;
|
|
65
|
+
readonly actionsFile?: string;
|
|
66
|
+
}
|
|
67
|
+
interface TsPluginArtifacts {
|
|
68
|
+
readonly typeNames: string[];
|
|
69
|
+
readonly typeImportPath: string;
|
|
70
|
+
}
|
|
71
|
+
declare function createTsPlugin(options?: TsPluginOptions): CodegenPlugin;
|
|
72
|
+
|
|
73
|
+
interface ZodPluginOptions {
|
|
74
|
+
readonly schemasFile?: string;
|
|
75
|
+
}
|
|
76
|
+
declare function createZodPlugin(options?: ZodPluginOptions): CodegenPlugin;
|
|
77
|
+
|
|
78
|
+
type PathValidationResult = {
|
|
79
|
+
valid: true;
|
|
80
|
+
normalized: string;
|
|
81
|
+
} | {
|
|
82
|
+
valid: false;
|
|
83
|
+
reason: string;
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Validate and normalize a file path per FP-1, FP-2, GEN-6.
|
|
87
|
+
*
|
|
88
|
+
* - MUST be a POSIX relative path
|
|
89
|
+
* - MUST NOT contain `..`, absolute prefixes, drive letters, or null bytes
|
|
90
|
+
* - Normalizes backslashes, multiple slashes, and leading `./`
|
|
91
|
+
*/
|
|
92
|
+
declare function validatePath(path: string): PathValidationResult;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Deterministic hash function (DET-1).
|
|
96
|
+
* Same input always produces the same output.
|
|
97
|
+
* Uses Core's canonical form (sorted keys, no undefined) + SHA-256.
|
|
98
|
+
*/
|
|
99
|
+
declare function stableHash(input: unknown): string;
|
|
100
|
+
|
|
101
|
+
interface HeaderOptions {
|
|
102
|
+
readonly sourceId?: string;
|
|
103
|
+
readonly schemaHash: string;
|
|
104
|
+
readonly stamp?: boolean;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Generate the @generated file header (DET-2, DET-3, DET-4).
|
|
108
|
+
*
|
|
109
|
+
* Default mode: no timestamp (deterministic).
|
|
110
|
+
* With stamp=true: appends ISO 8601 timestamp line.
|
|
111
|
+
*/
|
|
112
|
+
declare function generateHeader(options: HeaderOptions): string;
|
|
113
|
+
|
|
114
|
+
export { type CodegenContext, type CodegenHelpers, type CodegenOutput, type CodegenPlugin, type Diagnostic, type FilePatch, type GenerateOptions, type GenerateResult, type HeaderOptions, type PathValidationResult, type TsPluginArtifacts, type TsPluginOptions, type ZodPluginOptions, createTsPlugin, createZodPlugin, generate, generateHeader, stableHash, validatePath };
|
package/dist/index.js
CHANGED
|
@@ -1,14 +1,544 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
1
|
+
// src/runner.ts
|
|
2
|
+
import * as fs from "fs/promises";
|
|
3
|
+
import * as nodePath from "path";
|
|
4
|
+
|
|
5
|
+
// src/virtual-fs.ts
|
|
6
|
+
var VirtualFS = class {
|
|
7
|
+
files = /* @__PURE__ */ new Map();
|
|
8
|
+
deleted = /* @__PURE__ */ new Set();
|
|
9
|
+
/**
|
|
10
|
+
* Apply a FilePatch to the virtual FS.
|
|
11
|
+
* Returns a Diagnostic if a collision/warning condition is detected.
|
|
12
|
+
*/
|
|
13
|
+
applyPatch(patch, pluginName) {
|
|
14
|
+
if (patch.op === "set") {
|
|
15
|
+
return this.applySet(patch.path, patch.content, pluginName);
|
|
16
|
+
}
|
|
17
|
+
return this.applyDelete(patch.path, pluginName);
|
|
18
|
+
}
|
|
19
|
+
applySet(path, content, pluginName) {
|
|
20
|
+
const existing = this.files.get(path);
|
|
21
|
+
if (existing) {
|
|
22
|
+
const samePlugin = existing.source === pluginName;
|
|
23
|
+
return {
|
|
24
|
+
level: "error",
|
|
25
|
+
plugin: pluginName,
|
|
26
|
+
message: samePlugin ? `Duplicate set on "${path}" within plugin "${pluginName}"` : `File "${path}" already set by plugin "${existing.source}", cannot be set again by "${pluginName}"`
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
this.deleted.delete(path);
|
|
30
|
+
this.files.set(path, { content, source: pluginName });
|
|
31
|
+
return void 0;
|
|
32
|
+
}
|
|
33
|
+
applyDelete(path, pluginName) {
|
|
34
|
+
const existing = this.files.get(path);
|
|
35
|
+
if (!existing && !this.deleted.has(path)) {
|
|
36
|
+
return {
|
|
37
|
+
level: "warn",
|
|
38
|
+
plugin: pluginName,
|
|
39
|
+
message: `Delete on nonexistent path "${path}"`
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
if (existing) {
|
|
43
|
+
this.files.delete(path);
|
|
44
|
+
this.deleted.add(path);
|
|
45
|
+
return {
|
|
46
|
+
level: "warn",
|
|
47
|
+
plugin: pluginName,
|
|
48
|
+
message: `File "${path}" set by plugin "${existing.source}" is being deleted by "${pluginName}". Prior work is voided.`
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
this.deleted.add(path);
|
|
52
|
+
return void 0;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Get all files in deterministic (lexicographic) order (DET-5).
|
|
56
|
+
*/
|
|
57
|
+
getFiles() {
|
|
58
|
+
const entries = Array.from(this.files.entries());
|
|
59
|
+
entries.sort(([a], [b]) => a.localeCompare(b));
|
|
60
|
+
return entries.map(([path, { content }]) => ({ path, content }));
|
|
61
|
+
}
|
|
62
|
+
has(path) {
|
|
63
|
+
return this.files.has(path);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// src/path-safety.ts
|
|
68
|
+
function validatePath(path) {
|
|
69
|
+
if (!path) {
|
|
70
|
+
return { valid: false, reason: "Path must not be empty" };
|
|
71
|
+
}
|
|
72
|
+
if (path.includes("\0")) {
|
|
73
|
+
return { valid: false, reason: "Path must not contain null bytes" };
|
|
74
|
+
}
|
|
75
|
+
let normalized = path.replace(/\\/g, "/");
|
|
76
|
+
if (/^[a-zA-Z]:/.test(normalized)) {
|
|
77
|
+
return { valid: false, reason: "Path must not contain drive letters" };
|
|
78
|
+
}
|
|
79
|
+
if (normalized.startsWith("/")) {
|
|
80
|
+
return { valid: false, reason: "Path must be relative, not absolute" };
|
|
81
|
+
}
|
|
82
|
+
normalized = normalized.replace(/\/+/g, "/");
|
|
83
|
+
if (normalized.startsWith("./")) {
|
|
84
|
+
normalized = normalized.slice(2);
|
|
85
|
+
}
|
|
86
|
+
if (normalized.endsWith("/") && normalized.length > 1) {
|
|
87
|
+
normalized = normalized.slice(0, -1);
|
|
88
|
+
}
|
|
89
|
+
const segments = normalized.split("/");
|
|
90
|
+
for (const segment of segments) {
|
|
91
|
+
if (segment === "..") {
|
|
92
|
+
return { valid: false, reason: "Path must not contain '..' traversal" };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (!normalized) {
|
|
96
|
+
return { valid: false, reason: "Path resolves to empty after normalization" };
|
|
97
|
+
}
|
|
98
|
+
return { valid: true, normalized };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// src/stable-hash.ts
|
|
102
|
+
import { toCanonical, sha256Sync } from "@manifesto-ai/core";
|
|
103
|
+
function stableHash(input) {
|
|
104
|
+
const canonical = toCanonical(input);
|
|
105
|
+
return sha256Sync(canonical);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// src/header.ts
|
|
109
|
+
function generateHeader(options) {
|
|
110
|
+
const source = options.sourceId ?? "unknown";
|
|
111
|
+
const lines = [
|
|
112
|
+
"// @generated by @manifesto-ai/codegen \u2014 DO NOT EDIT",
|
|
113
|
+
`// Source: ${source} | Schema hash: ${options.schemaHash}`
|
|
114
|
+
];
|
|
115
|
+
if (options.stamp) {
|
|
116
|
+
lines.push(`// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`);
|
|
117
|
+
}
|
|
118
|
+
return lines.join("\n") + "\n";
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// src/runner.ts
|
|
122
|
+
async function generate(opts) {
|
|
123
|
+
const diagnostics = [];
|
|
124
|
+
const allArtifacts = {};
|
|
125
|
+
const vfs = new VirtualFS();
|
|
126
|
+
const nameError = validatePluginNames(opts.plugins);
|
|
127
|
+
if (nameError) {
|
|
128
|
+
return {
|
|
129
|
+
files: [],
|
|
130
|
+
artifacts: {},
|
|
131
|
+
diagnostics: [nameError]
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
for (const plugin of opts.plugins) {
|
|
135
|
+
const ctx = {
|
|
136
|
+
schema: opts.schema,
|
|
137
|
+
sourceId: opts.sourceId,
|
|
138
|
+
outDir: opts.outDir,
|
|
139
|
+
artifacts: Object.freeze({ ...allArtifacts }),
|
|
140
|
+
// PLG-9: frozen snapshot
|
|
141
|
+
helpers: { stableHash }
|
|
142
|
+
};
|
|
143
|
+
let output;
|
|
144
|
+
try {
|
|
145
|
+
output = await plugin.generate(ctx);
|
|
146
|
+
} catch (err) {
|
|
147
|
+
diagnostics.push({
|
|
148
|
+
level: "error",
|
|
149
|
+
plugin: plugin.name,
|
|
150
|
+
message: `Plugin threw: ${err instanceof Error ? err.message : String(err)}`
|
|
151
|
+
});
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (output.diagnostics) {
|
|
155
|
+
diagnostics.push(...output.diagnostics);
|
|
156
|
+
}
|
|
157
|
+
for (const patch of output.patches) {
|
|
158
|
+
const validation = validatePath(patch.path);
|
|
159
|
+
if (!validation.valid) {
|
|
160
|
+
diagnostics.push({
|
|
161
|
+
level: "error",
|
|
162
|
+
plugin: plugin.name,
|
|
163
|
+
message: `Invalid path "${patch.path}": ${validation.reason}`
|
|
164
|
+
});
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const normalizedPatch = patch.op === "set" ? { op: "set", path: validation.normalized, content: patch.content } : { op: "delete", path: validation.normalized };
|
|
168
|
+
const collision = vfs.applyPatch(normalizedPatch, plugin.name);
|
|
169
|
+
if (collision) {
|
|
170
|
+
diagnostics.push(collision);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (output.artifacts) {
|
|
174
|
+
allArtifacts[plugin.name] = output.artifacts;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const files = vfs.getFiles();
|
|
178
|
+
const hasErrors = diagnostics.some((d) => d.level === "error");
|
|
179
|
+
if (hasErrors) {
|
|
180
|
+
return { files, artifacts: allArtifacts, diagnostics };
|
|
181
|
+
}
|
|
182
|
+
await fs.rm(opts.outDir, { recursive: true, force: true });
|
|
183
|
+
const header = generateHeader({
|
|
184
|
+
sourceId: opts.sourceId,
|
|
185
|
+
schemaHash: opts.schema.hash,
|
|
186
|
+
stamp: opts.stamp
|
|
187
|
+
});
|
|
188
|
+
for (const file of files) {
|
|
189
|
+
const absPath = nodePath.join(opts.outDir, ...file.path.split("/"));
|
|
190
|
+
const dir = nodePath.dirname(absPath);
|
|
191
|
+
await fs.mkdir(dir, { recursive: true });
|
|
192
|
+
await fs.writeFile(absPath, header + file.content, "utf-8");
|
|
193
|
+
}
|
|
194
|
+
return { files, artifacts: allArtifacts, diagnostics };
|
|
195
|
+
}
|
|
196
|
+
function validatePluginNames(plugins) {
|
|
197
|
+
const seen = /* @__PURE__ */ new Set();
|
|
198
|
+
for (const plugin of plugins) {
|
|
199
|
+
if (!plugin.name) {
|
|
200
|
+
return {
|
|
201
|
+
level: "error",
|
|
202
|
+
plugin: "",
|
|
203
|
+
message: "Plugin name must not be empty (PLG-1)"
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
if (seen.has(plugin.name)) {
|
|
207
|
+
return {
|
|
208
|
+
level: "error",
|
|
209
|
+
plugin: plugin.name,
|
|
210
|
+
message: `Duplicate plugin name "${plugin.name}" (GEN-2)`
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
seen.add(plugin.name);
|
|
214
|
+
}
|
|
215
|
+
return void 0;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// src/plugins/ts-plugin.ts
|
|
219
|
+
var PLUGIN_NAME = "codegen-plugin-ts";
|
|
220
|
+
function createTsPlugin(options) {
|
|
221
|
+
const typesFile = options?.typesFile ?? "types.ts";
|
|
222
|
+
const actionsFile = options?.actionsFile ?? "actions.ts";
|
|
223
|
+
return {
|
|
224
|
+
name: PLUGIN_NAME,
|
|
225
|
+
generate(ctx) {
|
|
226
|
+
const diagnostics = [];
|
|
227
|
+
const typeNames = [];
|
|
228
|
+
const sortedTypeNames = Object.keys(ctx.schema.types).sort();
|
|
229
|
+
const typeDecls = [];
|
|
230
|
+
for (const name of sortedTypeNames) {
|
|
231
|
+
const spec = ctx.schema.types[name];
|
|
232
|
+
typeNames.push(name);
|
|
233
|
+
typeDecls.push(renderNamedType(name, spec, diagnostics));
|
|
234
|
+
}
|
|
235
|
+
const typesContent = typeDecls.join("\n\n") + "\n";
|
|
236
|
+
const sortedActionNames = Object.keys(ctx.schema.actions).sort();
|
|
237
|
+
const actionDecls = [];
|
|
238
|
+
for (const actionName of sortedActionNames) {
|
|
239
|
+
const spec = ctx.schema.actions[actionName];
|
|
240
|
+
if (spec.input) {
|
|
241
|
+
const typeName = pascalCase(actionName) + "Input";
|
|
242
|
+
actionDecls.push(renderActionInputType(typeName, spec, diagnostics));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const patches = [
|
|
246
|
+
{ op: "set", path: typesFile, content: typesContent }
|
|
247
|
+
];
|
|
248
|
+
if (actionDecls.length > 0) {
|
|
249
|
+
const actionsContent = actionDecls.join("\n\n") + "\n";
|
|
250
|
+
patches.push({ op: "set", path: actionsFile, content: actionsContent });
|
|
251
|
+
}
|
|
252
|
+
const artifacts = {
|
|
253
|
+
typeNames,
|
|
254
|
+
typeImportPath: `./${typesFile.replace(/\.ts$/, "")}`
|
|
255
|
+
};
|
|
256
|
+
return { patches, artifacts, diagnostics };
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
function renderNamedType(name, spec, diagnostics) {
|
|
261
|
+
const def = spec.definition;
|
|
262
|
+
if (def.kind === "object") {
|
|
263
|
+
return renderInterface(name, def.fields, diagnostics);
|
|
264
|
+
}
|
|
265
|
+
const tsType = mapTypeDefinition(def, diagnostics);
|
|
266
|
+
return `export type ${name} = ${tsType};`;
|
|
267
|
+
}
|
|
268
|
+
function renderInterface(name, fields, diagnostics) {
|
|
269
|
+
const sortedFields = Object.keys(fields).sort();
|
|
270
|
+
const lines = [];
|
|
271
|
+
for (const fieldName of sortedFields) {
|
|
272
|
+
const field = fields[fieldName];
|
|
273
|
+
const tsType = mapTypeDefinition(field.type, diagnostics);
|
|
274
|
+
const opt = field.optional ? "?" : "";
|
|
275
|
+
lines.push(` ${fieldName}${opt}: ${tsType};`);
|
|
276
|
+
}
|
|
277
|
+
return `export interface ${name} {
|
|
278
|
+
${lines.join("\n")}
|
|
279
|
+
}`;
|
|
280
|
+
}
|
|
281
|
+
function mapTypeDefinition(def, diagnostics) {
|
|
282
|
+
switch (def.kind) {
|
|
283
|
+
case "primitive":
|
|
284
|
+
return mapPrimitive(def.type);
|
|
285
|
+
case "literal":
|
|
286
|
+
return renderLiteral(def.value);
|
|
287
|
+
case "array":
|
|
288
|
+
return `${wrapComplex(mapTypeDefinition(def.element, diagnostics), def.element)}[]`;
|
|
289
|
+
case "record":
|
|
290
|
+
return `Record<${mapTypeDefinition(def.key, diagnostics)}, ${mapTypeDefinition(def.value, diagnostics)}>`;
|
|
291
|
+
case "object": {
|
|
292
|
+
const sortedFields = Object.keys(def.fields).sort();
|
|
293
|
+
const parts = [];
|
|
294
|
+
for (const fieldName of sortedFields) {
|
|
295
|
+
const field = def.fields[fieldName];
|
|
296
|
+
const tsType = mapTypeDefinition(field.type, diagnostics);
|
|
297
|
+
const opt = field.optional ? "?" : "";
|
|
298
|
+
parts.push(`${fieldName}${opt}: ${tsType}`);
|
|
299
|
+
}
|
|
300
|
+
return `{ ${parts.join("; ")} }`;
|
|
301
|
+
}
|
|
302
|
+
case "union":
|
|
303
|
+
return def.types.map((t) => mapTypeDefinition(t, diagnostics)).join(" | ");
|
|
304
|
+
case "ref":
|
|
305
|
+
return def.name;
|
|
306
|
+
default: {
|
|
307
|
+
diagnostics.push({
|
|
308
|
+
level: "warn",
|
|
309
|
+
plugin: PLUGIN_NAME,
|
|
310
|
+
message: `Unknown TypeDefinition kind: "${def.kind}". Emitting "unknown".`
|
|
311
|
+
});
|
|
312
|
+
return "unknown";
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
function mapPrimitive(type) {
|
|
317
|
+
switch (type) {
|
|
318
|
+
case "string":
|
|
319
|
+
return "string";
|
|
320
|
+
case "number":
|
|
321
|
+
return "number";
|
|
322
|
+
case "boolean":
|
|
323
|
+
return "boolean";
|
|
324
|
+
case "null":
|
|
325
|
+
return "null";
|
|
326
|
+
default:
|
|
327
|
+
return "unknown";
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
function renderLiteral(value) {
|
|
331
|
+
if (typeof value === "string") {
|
|
332
|
+
return JSON.stringify(value);
|
|
333
|
+
}
|
|
334
|
+
return String(value);
|
|
335
|
+
}
|
|
336
|
+
function wrapComplex(tsType, def) {
|
|
337
|
+
if (def.kind === "union") {
|
|
338
|
+
return `(${tsType})`;
|
|
339
|
+
}
|
|
340
|
+
return tsType;
|
|
341
|
+
}
|
|
342
|
+
function renderActionInputType(typeName, spec, diagnostics) {
|
|
343
|
+
if (!spec.input) {
|
|
344
|
+
return "";
|
|
345
|
+
}
|
|
346
|
+
const tsType = mapFieldSpec(spec.input, diagnostics);
|
|
347
|
+
return `export type ${typeName} = ${tsType};`;
|
|
348
|
+
}
|
|
349
|
+
function mapFieldSpec(spec, diagnostics) {
|
|
350
|
+
const baseType = mapFieldType(spec.type, spec, diagnostics);
|
|
351
|
+
if (!spec.required && baseType !== "unknown") {
|
|
352
|
+
return `${baseType} | null`;
|
|
353
|
+
}
|
|
354
|
+
return baseType;
|
|
355
|
+
}
|
|
356
|
+
function mapFieldType(type, spec, diagnostics) {
|
|
357
|
+
if (typeof type === "object" && "enum" in type) {
|
|
358
|
+
return type.enum.map((v) => renderLiteral(v)).join(" | ");
|
|
359
|
+
}
|
|
360
|
+
switch (type) {
|
|
361
|
+
case "string":
|
|
362
|
+
return "string";
|
|
363
|
+
case "number":
|
|
364
|
+
return "number";
|
|
365
|
+
case "boolean":
|
|
366
|
+
return "boolean";
|
|
367
|
+
case "null":
|
|
368
|
+
return "null";
|
|
369
|
+
case "object": {
|
|
370
|
+
if (spec.fields) {
|
|
371
|
+
const sortedFields = Object.keys(spec.fields).sort();
|
|
372
|
+
const parts = [];
|
|
373
|
+
for (const name of sortedFields) {
|
|
374
|
+
const field = spec.fields[name];
|
|
375
|
+
const fieldType = mapFieldSpec(field, diagnostics);
|
|
376
|
+
const opt = field.required ? "" : "?";
|
|
377
|
+
parts.push(`${name}${opt}: ${fieldType}`);
|
|
378
|
+
}
|
|
379
|
+
return `{ ${parts.join("; ")} }`;
|
|
380
|
+
}
|
|
381
|
+
diagnostics.push({
|
|
382
|
+
level: "warn",
|
|
383
|
+
plugin: PLUGIN_NAME,
|
|
384
|
+
message: "Object field without fields spec, degrading to Record<string, unknown>"
|
|
385
|
+
});
|
|
386
|
+
return "Record<string, unknown>";
|
|
387
|
+
}
|
|
388
|
+
case "array": {
|
|
389
|
+
if (spec.items) {
|
|
390
|
+
return `${mapFieldSpec(spec.items, diagnostics)}[]`;
|
|
391
|
+
}
|
|
392
|
+
diagnostics.push({
|
|
393
|
+
level: "warn",
|
|
394
|
+
plugin: PLUGIN_NAME,
|
|
395
|
+
message: "Array field without items spec, degrading to unknown[]"
|
|
396
|
+
});
|
|
397
|
+
return "unknown[]";
|
|
398
|
+
}
|
|
399
|
+
default:
|
|
400
|
+
return "unknown";
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
function pascalCase(str) {
|
|
404
|
+
return str.split(/[-_\s]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// src/plugins/zod-plugin.ts
|
|
408
|
+
var PLUGIN_NAME2 = "codegen-plugin-zod";
|
|
409
|
+
var TS_PLUGIN_NAME = "codegen-plugin-ts";
|
|
410
|
+
function createZodPlugin(options) {
|
|
411
|
+
const schemasFile = options?.schemasFile ?? "base.ts";
|
|
412
|
+
return {
|
|
413
|
+
name: PLUGIN_NAME2,
|
|
414
|
+
generate(ctx) {
|
|
415
|
+
const diagnostics = [];
|
|
416
|
+
const tsArtifacts = ctx.artifacts[TS_PLUGIN_NAME];
|
|
417
|
+
const sortedTypeNames = Object.keys(ctx.schema.types).sort();
|
|
418
|
+
const schemaDecls = [];
|
|
419
|
+
const allTypeNames = new Set(sortedTypeNames);
|
|
420
|
+
for (const name of sortedTypeNames) {
|
|
421
|
+
const spec = ctx.schema.types[name];
|
|
422
|
+
schemaDecls.push(renderNamedSchema(name, spec, allTypeNames, tsArtifacts, diagnostics));
|
|
423
|
+
}
|
|
424
|
+
const imports = ['import { z } from "zod";'];
|
|
425
|
+
if (tsArtifacts && tsArtifacts.typeNames.length > 0) {
|
|
426
|
+
const typeImports = sortedTypeNames.filter((n) => tsArtifacts.typeNames.includes(n)).join(", ");
|
|
427
|
+
if (typeImports) {
|
|
428
|
+
imports.push(`import type { ${typeImports} } from "${tsArtifacts.typeImportPath}";`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
const content = imports.join("\n") + "\n\n" + schemaDecls.join("\n\n") + "\n";
|
|
432
|
+
return {
|
|
433
|
+
patches: [{ op: "set", path: schemasFile, content }],
|
|
434
|
+
diagnostics
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
function renderNamedSchema(name, spec, allTypeNames, tsArtifacts, diagnostics) {
|
|
440
|
+
const schemaName = `${name}Schema`;
|
|
441
|
+
const zodExpr = mapTypeDefinition2(spec.definition, allTypeNames, diagnostics);
|
|
442
|
+
const hasTypeAnnotation = tsArtifacts && tsArtifacts.typeNames.includes(name);
|
|
443
|
+
const annotation = hasTypeAnnotation ? `: z.ZodType<${name}>` : "";
|
|
444
|
+
return `export const ${schemaName}${annotation} = ${zodExpr};`;
|
|
445
|
+
}
|
|
446
|
+
function mapTypeDefinition2(def, allTypeNames, diagnostics) {
|
|
447
|
+
switch (def.kind) {
|
|
448
|
+
case "primitive":
|
|
449
|
+
return mapPrimitiveZod(def.type);
|
|
450
|
+
case "literal":
|
|
451
|
+
return `z.literal(${renderLiteralValue(def.value)})`;
|
|
452
|
+
case "array":
|
|
453
|
+
return `z.array(${mapTypeDefinition2(def.element, allTypeNames, diagnostics)})`;
|
|
454
|
+
case "record":
|
|
455
|
+
return handleRecord(def, allTypeNames, diagnostics);
|
|
456
|
+
case "object":
|
|
457
|
+
return renderZodObject(def.fields, allTypeNames, diagnostics);
|
|
458
|
+
case "union":
|
|
459
|
+
return handleUnion(def.types, allTypeNames, diagnostics);
|
|
460
|
+
case "ref":
|
|
461
|
+
return `z.lazy(() => ${def.name}Schema)`;
|
|
462
|
+
default: {
|
|
463
|
+
diagnostics.push({
|
|
464
|
+
level: "warn",
|
|
465
|
+
plugin: PLUGIN_NAME2,
|
|
466
|
+
message: `Unknown TypeDefinition kind: "${def.kind}". Emitting "z.unknown()".`
|
|
467
|
+
});
|
|
468
|
+
return "z.unknown()";
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
function mapPrimitiveZod(type) {
|
|
473
|
+
switch (type) {
|
|
474
|
+
case "string":
|
|
475
|
+
return "z.string()";
|
|
476
|
+
case "number":
|
|
477
|
+
return "z.number()";
|
|
478
|
+
case "boolean":
|
|
479
|
+
return "z.boolean()";
|
|
480
|
+
case "null":
|
|
481
|
+
return "z.null()";
|
|
482
|
+
default:
|
|
483
|
+
return "z.unknown()";
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
function renderLiteralValue(value) {
|
|
487
|
+
if (typeof value === "string") {
|
|
488
|
+
return JSON.stringify(value);
|
|
489
|
+
}
|
|
490
|
+
if (value === null) {
|
|
491
|
+
return "null";
|
|
492
|
+
}
|
|
493
|
+
return String(value);
|
|
494
|
+
}
|
|
495
|
+
function handleRecord(def, allTypeNames, diagnostics) {
|
|
496
|
+
const valueSchema = mapTypeDefinition2(def.value, allTypeNames, diagnostics);
|
|
497
|
+
if (def.key.kind !== "primitive" || def.key.type !== "string") {
|
|
498
|
+
diagnostics.push({
|
|
499
|
+
level: "warn",
|
|
500
|
+
plugin: PLUGIN_NAME2,
|
|
501
|
+
message: `Record key type is not string (got ${JSON.stringify(def.key)}). Degrading to z.record(z.string(), ...).`
|
|
502
|
+
});
|
|
503
|
+
return `z.record(z.string(), ${valueSchema})`;
|
|
504
|
+
}
|
|
505
|
+
return `z.record(z.string(), ${valueSchema})`;
|
|
506
|
+
}
|
|
507
|
+
function renderZodObject(fields, allTypeNames, diagnostics) {
|
|
508
|
+
const sortedFields = Object.keys(fields).sort();
|
|
509
|
+
const parts = [];
|
|
510
|
+
for (const fieldName of sortedFields) {
|
|
511
|
+
const field = fields[fieldName];
|
|
512
|
+
let zodType = mapTypeDefinition2(field.type, allTypeNames, diagnostics);
|
|
513
|
+
if (field.optional) {
|
|
514
|
+
zodType += ".optional()";
|
|
515
|
+
}
|
|
516
|
+
parts.push(` ${fieldName}: ${zodType},`);
|
|
517
|
+
}
|
|
518
|
+
return `z.object({
|
|
519
|
+
${parts.join("\n")}
|
|
520
|
+
})`;
|
|
521
|
+
}
|
|
522
|
+
function handleUnion(types, allTypeNames, diagnostics) {
|
|
523
|
+
if (types.length === 2) {
|
|
524
|
+
const nullIdx = types.findIndex(
|
|
525
|
+
(t) => t.kind === "primitive" && t.type === "null"
|
|
526
|
+
);
|
|
527
|
+
if (nullIdx !== -1) {
|
|
528
|
+
const otherIdx = nullIdx === 0 ? 1 : 0;
|
|
529
|
+
const otherSchema = mapTypeDefinition2(types[otherIdx], allTypeNames, diagnostics);
|
|
530
|
+
return `z.nullable(${otherSchema})`;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
const schemas = types.map((t) => mapTypeDefinition2(t, allTypeNames, diagnostics));
|
|
534
|
+
return `z.union([${schemas.join(", ")}])`;
|
|
535
|
+
}
|
|
536
|
+
export {
|
|
537
|
+
createTsPlugin,
|
|
538
|
+
createZodPlugin,
|
|
539
|
+
generate,
|
|
540
|
+
generateHeader,
|
|
541
|
+
stableHash,
|
|
542
|
+
validatePath
|
|
543
|
+
};
|
|
14
544
|
//# sourceMappingURL=index.js.map
|