@zod-to-form/cli 0.2.1 → 0.2.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/dist/codegen.d.ts +11 -0
- package/dist/codegen.d.ts.map +1 -0
- package/dist/codegen.js +125 -0
- package/dist/codegen.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +115 -0
- package/dist/index.js.map +1 -0
- package/dist/loader.d.ts +2 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +32 -0
- package/dist/loader.js.map +1 -0
- package/dist/server-action.d.ts +13 -0
- package/dist/server-action.d.ts.map +1 -0
- package/dist/server-action.js +57 -0
- package/dist/server-action.js.map +1 -0
- package/dist/templates.d.ts +4 -0
- package/dist/templates.d.ts.map +1 -0
- package/dist/templates.js +50 -0
- package/dist/templates.js.map +1 -0
- package/dist/watcher.d.ts +11 -0
- package/dist/watcher.d.ts.map +1 -0
- package/dist/watcher.js +51 -0
- package/dist/watcher.js.map +1 -0
- package/package.json +3 -3
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { FormField } from '@zod-to-form/core';
|
|
2
|
+
export type CodegenConfig = {
|
|
3
|
+
schemaPath: string;
|
|
4
|
+
exportName: string;
|
|
5
|
+
outputPath: string;
|
|
6
|
+
componentName: string;
|
|
7
|
+
ui: 'shadcn' | 'unstyled';
|
|
8
|
+
serverAction: boolean;
|
|
9
|
+
};
|
|
10
|
+
export declare function generateFormComponent(fields: FormField[], config: CodegenConfig): Promise<string>;
|
|
11
|
+
//# sourceMappingURL=codegen.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codegen.d.ts","sourceRoot":"","sources":["../src/codegen.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAGnD,MAAM,MAAM,aAAa,GAAG;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,EAAE,EAAE,QAAQ,GAAG,UAAU,CAAC;IAC1B,YAAY,EAAE,OAAO,CAAC;CACvB,CAAC;AAuGF,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,SAAS,EAAE,EACnB,MAAM,EAAE,aAAa,GACpB,OAAO,CAAC,MAAM,CAAC,CAuCjB"}
|
package/dist/codegen.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { getFileHeader, renderField } from './templates.js';
|
|
3
|
+
function getSchemaImportPath(config) {
|
|
4
|
+
const relative = path
|
|
5
|
+
.relative(path.dirname(config.outputPath), config.schemaPath)
|
|
6
|
+
.replace(/\\/g, '/');
|
|
7
|
+
if (relative.startsWith('.')) {
|
|
8
|
+
return relative;
|
|
9
|
+
}
|
|
10
|
+
return `./${relative}`;
|
|
11
|
+
}
|
|
12
|
+
/** Convert a field key to a safe camelCase variable prefix (e.g. 'address.street' → 'addressStreet') */
|
|
13
|
+
function toVarName(key) {
|
|
14
|
+
return key.replace(/[^a-zA-Z0-9]+([a-zA-Z0-9])/g, (_, c) => c.toUpperCase());
|
|
15
|
+
}
|
|
16
|
+
/** Collect all array fields (recursively through nested objects) */
|
|
17
|
+
function collectArrayFields(fields) {
|
|
18
|
+
const result = [];
|
|
19
|
+
for (const field of fields) {
|
|
20
|
+
if (field.component === 'ArrayField') {
|
|
21
|
+
result.push(field);
|
|
22
|
+
}
|
|
23
|
+
if (field.component === 'Fieldset' && field.children) {
|
|
24
|
+
result.push(...collectArrayFields(field.children));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
function renderNestedBlock(field, indent) {
|
|
30
|
+
const children = (field.children ?? [])
|
|
31
|
+
.map((child) => renderFieldBlock(child, `${indent} `))
|
|
32
|
+
.join('\n');
|
|
33
|
+
return [
|
|
34
|
+
`${indent}<div>`,
|
|
35
|
+
`${indent} <label>${field.label}</label>`,
|
|
36
|
+
`${indent} <fieldset>`,
|
|
37
|
+
`${indent} <legend>${field.label}</legend>`,
|
|
38
|
+
children,
|
|
39
|
+
`${indent} </fieldset>`,
|
|
40
|
+
`${indent}</div>`
|
|
41
|
+
].join('\n');
|
|
42
|
+
}
|
|
43
|
+
function renderArrayBlock(field, indent) {
|
|
44
|
+
const varName = toVarName(field.key);
|
|
45
|
+
const itemField = field.arrayItem;
|
|
46
|
+
const itemJsx = itemField
|
|
47
|
+
? renderField({ ...itemField, key: `\${${varName}Fields[index].id}` })
|
|
48
|
+
: `<input {...register(\`${field.key}.\${index}\`)} />`;
|
|
49
|
+
return [
|
|
50
|
+
`${indent}<div>`,
|
|
51
|
+
`${indent} <label>${field.label}</label>`,
|
|
52
|
+
`${indent} {${varName}Fields.map((item, index) => (`,
|
|
53
|
+
`${indent} <div key={item.id}>`,
|
|
54
|
+
`${indent} ${itemJsx.replace(new RegExp(`register\\('${field.key}\\.0'\\)`), `register(\`${field.key}.\${index}\`)`)}`,
|
|
55
|
+
`${indent} <button type="button" onClick={() => remove${capitalize(varName)}(index)}>Remove</button>`,
|
|
56
|
+
`${indent} </div>`,
|
|
57
|
+
`${indent} ))}`,
|
|
58
|
+
`${indent} <button type="button" onClick={() => append${capitalize(varName)}('')}>Add</button>`,
|
|
59
|
+
`${indent}</div>`
|
|
60
|
+
].join('\n');
|
|
61
|
+
}
|
|
62
|
+
function capitalize(s) {
|
|
63
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
64
|
+
}
|
|
65
|
+
function renderFieldBlock(field, indent = ' ') {
|
|
66
|
+
if (field.hasCustomRender) {
|
|
67
|
+
const styleAttr = field.gridColumn ? ` style={{ gridColumn: '${field.gridColumn}' }}` : '';
|
|
68
|
+
return [
|
|
69
|
+
`${indent}<div${styleAttr}>`,
|
|
70
|
+
`${indent} <label htmlFor="${field.key}">${field.label}</label>`,
|
|
71
|
+
`${indent} {/* TODO: custom renderer for ${field.key} — replace with your component */}`,
|
|
72
|
+
`${indent}</div>`
|
|
73
|
+
].join('\n');
|
|
74
|
+
}
|
|
75
|
+
if (field.component === 'Fieldset') {
|
|
76
|
+
return renderNestedBlock(field, indent);
|
|
77
|
+
}
|
|
78
|
+
if (field.component === 'ArrayField') {
|
|
79
|
+
return renderArrayBlock(field, indent);
|
|
80
|
+
}
|
|
81
|
+
const styleAttr = field.gridColumn ? ` style={{ gridColumn: '${field.gridColumn}' }}` : '';
|
|
82
|
+
return [
|
|
83
|
+
`${indent}<div${styleAttr}>`,
|
|
84
|
+
`${indent} <label htmlFor="${field.key}">${field.label}</label>`,
|
|
85
|
+
`${indent} ${renderField(field)}`,
|
|
86
|
+
`${indent}</div>`
|
|
87
|
+
].join('\n');
|
|
88
|
+
}
|
|
89
|
+
export async function generateFormComponent(fields, config) {
|
|
90
|
+
const schemaImportPath = getSchemaImportPath(config);
|
|
91
|
+
const arrayFields = collectArrayFields(fields);
|
|
92
|
+
const hasArrays = arrayFields.length > 0;
|
|
93
|
+
const header = getFileHeader(schemaImportPath, config.exportName, hasArrays);
|
|
94
|
+
const body = fields.map((field) => renderFieldBlock(field)).join('\n');
|
|
95
|
+
// useFieldArray hook declarations
|
|
96
|
+
const arrayHooks = arrayFields
|
|
97
|
+
.map((f) => {
|
|
98
|
+
const varName = toVarName(f.key);
|
|
99
|
+
return ` const { fields: ${varName}Fields, append: append${capitalize(varName)}, remove: remove${capitalize(varName)} } = useFieldArray({ control, name: '${f.key}' });`;
|
|
100
|
+
})
|
|
101
|
+
.join('\n');
|
|
102
|
+
const useFormDestructure = hasArrays
|
|
103
|
+
? `{ register, handleSubmit, control }`
|
|
104
|
+
: `{ register, handleSubmit }`;
|
|
105
|
+
return [
|
|
106
|
+
header,
|
|
107
|
+
'',
|
|
108
|
+
`export function ${config.componentName}(props: {`,
|
|
109
|
+
` onSubmit: (data: FormData) => void;`,
|
|
110
|
+
`}) {`,
|
|
111
|
+
` const ${useFormDestructure} = useForm<FormData>({`,
|
|
112
|
+
` resolver: zodResolver(${config.exportName})`,
|
|
113
|
+
` });`,
|
|
114
|
+
...(hasArrays ? [arrayHooks] : []),
|
|
115
|
+
'',
|
|
116
|
+
` return (`,
|
|
117
|
+
` <form onSubmit={handleSubmit(props.onSubmit)}>`,
|
|
118
|
+
body,
|
|
119
|
+
` <button type="submit">Submit</button>`,
|
|
120
|
+
` </form>`,
|
|
121
|
+
` );`,
|
|
122
|
+
`}`
|
|
123
|
+
].join('\n');
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=codegen.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codegen.js","sourceRoot":"","sources":["../src/codegen.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @zod-to-form/cli — Build-time code generator for Zod v4 forms
|
|
4
|
+
*/
|
|
5
|
+
import { Command } from 'commander';
|
|
6
|
+
type GenerateOptions = {
|
|
7
|
+
schema: string;
|
|
8
|
+
export: string;
|
|
9
|
+
out?: string;
|
|
10
|
+
name?: string;
|
|
11
|
+
ui?: 'shadcn' | 'unstyled';
|
|
12
|
+
force?: boolean;
|
|
13
|
+
dryRun?: boolean;
|
|
14
|
+
serverAction?: boolean;
|
|
15
|
+
watch?: boolean;
|
|
16
|
+
};
|
|
17
|
+
export declare function runGenerate(options: GenerateOptions): Promise<{
|
|
18
|
+
outputPath: string;
|
|
19
|
+
code: string;
|
|
20
|
+
wroteFile: boolean;
|
|
21
|
+
actionPath?: string;
|
|
22
|
+
actionCode?: string;
|
|
23
|
+
}>;
|
|
24
|
+
export declare function createProgram(): Command;
|
|
25
|
+
export {};
|
|
26
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA;;GAEG;AAIH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAOpC,KAAK,eAAe,GAAG;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,EAAE,CAAC,EAAE,QAAQ,GAAG,UAAU,CAAC;IAC3B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AAwCF,wBAAsB,WAAW,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC;IACnE,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC,CA8CD;AAED,wBAAgB,aAAa,IAAI,OAAO,CAgCvC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @zod-to-form/cli — Build-time code generator for Zod v4 forms
|
|
4
|
+
*/
|
|
5
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { Command } from 'commander';
|
|
8
|
+
import { walkSchema } from '@zod-to-form/core';
|
|
9
|
+
import { generateFormComponent } from './codegen.js';
|
|
10
|
+
import { loadSchema } from './loader.js';
|
|
11
|
+
import { generateServerAction } from './server-action.js';
|
|
12
|
+
import { startWatch } from './watcher.js';
|
|
13
|
+
function toPascalCase(value) {
|
|
14
|
+
return value
|
|
15
|
+
.replace(/[^a-zA-Z0-9]+/g, ' ')
|
|
16
|
+
.trim()
|
|
17
|
+
.split(/\s+/)
|
|
18
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
19
|
+
.join('');
|
|
20
|
+
}
|
|
21
|
+
function toKebabCase(value) {
|
|
22
|
+
return value.replace(/([A-Z])/g, (m) => `-${m.toLowerCase()}`).replace(/^-/, '');
|
|
23
|
+
}
|
|
24
|
+
function resolveComponentName(exportName, explicitName) {
|
|
25
|
+
if (explicitName?.trim()) {
|
|
26
|
+
return toPascalCase(explicitName.trim());
|
|
27
|
+
}
|
|
28
|
+
const normalized = exportName.endsWith('Schema')
|
|
29
|
+
? exportName.slice(0, -'Schema'.length)
|
|
30
|
+
: exportName;
|
|
31
|
+
return `${toPascalCase(normalized)}Form`;
|
|
32
|
+
}
|
|
33
|
+
function resolveOutputPath(cwd, out, componentName) {
|
|
34
|
+
if (!out) {
|
|
35
|
+
return path.resolve(cwd, `${componentName}.tsx`);
|
|
36
|
+
}
|
|
37
|
+
const absoluteOut = path.resolve(cwd, out);
|
|
38
|
+
if (absoluteOut.endsWith('.tsx')) {
|
|
39
|
+
return absoluteOut;
|
|
40
|
+
}
|
|
41
|
+
return path.join(absoluteOut, `${componentName}.tsx`);
|
|
42
|
+
}
|
|
43
|
+
export async function runGenerate(options) {
|
|
44
|
+
const cwd = process.cwd();
|
|
45
|
+
const schemaPath = path.resolve(cwd, options.schema);
|
|
46
|
+
const exportName = options.export;
|
|
47
|
+
const componentName = resolveComponentName(exportName, options.name);
|
|
48
|
+
const outputPath = resolveOutputPath(cwd, options.out, componentName);
|
|
49
|
+
const schema = await loadSchema(schemaPath, exportName);
|
|
50
|
+
const fields = walkSchema(schema);
|
|
51
|
+
const config = {
|
|
52
|
+
schemaPath,
|
|
53
|
+
exportName,
|
|
54
|
+
outputPath,
|
|
55
|
+
componentName,
|
|
56
|
+
ui: options.ui ?? 'shadcn',
|
|
57
|
+
serverAction: options.serverAction ?? false
|
|
58
|
+
};
|
|
59
|
+
const generated = await generateFormComponent(fields, config);
|
|
60
|
+
const code = generated;
|
|
61
|
+
if (options.dryRun) {
|
|
62
|
+
process.stdout.write(code);
|
|
63
|
+
return { outputPath, code, wroteFile: false };
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
await readFile(outputPath, 'utf8');
|
|
67
|
+
if (!options.force) {
|
|
68
|
+
return { outputPath, code, wroteFile: false };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch { }
|
|
72
|
+
await mkdir(path.dirname(outputPath), { recursive: true });
|
|
73
|
+
await writeFile(outputPath, code, 'utf8');
|
|
74
|
+
// Generate server action alongside the form component when requested
|
|
75
|
+
if (options.serverAction) {
|
|
76
|
+
const actionFileName = `${toKebabCase(componentName)}-action.ts`;
|
|
77
|
+
const actionPath = path.join(path.dirname(outputPath), actionFileName);
|
|
78
|
+
const actionCode = await generateServerAction({ ...config, outputPath: actionPath });
|
|
79
|
+
await writeFile(actionPath, actionCode, 'utf8');
|
|
80
|
+
return { outputPath, code, wroteFile: true, actionPath, actionCode };
|
|
81
|
+
}
|
|
82
|
+
return { outputPath, code, wroteFile: true };
|
|
83
|
+
}
|
|
84
|
+
export function createProgram() {
|
|
85
|
+
const program = new Command();
|
|
86
|
+
program
|
|
87
|
+
.name('zodform')
|
|
88
|
+
.description('Generate form components from Zod v4 schemas')
|
|
89
|
+
.version('0.0.0');
|
|
90
|
+
program
|
|
91
|
+
.command('generate')
|
|
92
|
+
.requiredOption('--schema <path>', 'Path to schema file')
|
|
93
|
+
.requiredOption('--export <name>', 'Named export containing the schema')
|
|
94
|
+
.option('--out <path>', 'Output directory or file path')
|
|
95
|
+
.option('--name <componentName>', 'Generated component name')
|
|
96
|
+
.option('--ui <preset>', 'UI preset (shadcn|unstyled)', 'shadcn')
|
|
97
|
+
.option('--force', 'Overwrite existing output file', false)
|
|
98
|
+
.option('--dry-run', 'Print generated code without writing files', false)
|
|
99
|
+
.option('--server-action', 'Generate a Next.js server action alongside the form', false)
|
|
100
|
+
.option('--watch', 'Watch schema file for changes and regenerate on change', false)
|
|
101
|
+
.action(async (commandOptions) => {
|
|
102
|
+
await runGenerate(commandOptions);
|
|
103
|
+
if (commandOptions.watch) {
|
|
104
|
+
const schemaPath = path.resolve(process.cwd(), commandOptions.schema);
|
|
105
|
+
console.log('Watching for changes...');
|
|
106
|
+
await startWatch(schemaPath, () => runGenerate({ ...commandOptions, force: true }).then(() => { }));
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
return program;
|
|
110
|
+
}
|
|
111
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
112
|
+
const program = createProgram();
|
|
113
|
+
await program.parseAsync();
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/loader.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../src/loader.ts"],"names":[],"mappings":"AAWA,wBAAsB,UAAU,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAyBzF"}
|
package/dist/loader.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { createJiti } from 'jiti';
|
|
3
|
+
function isZodSchema(value) {
|
|
4
|
+
if (!value || typeof value !== 'object') {
|
|
5
|
+
return false;
|
|
6
|
+
}
|
|
7
|
+
return '_zod' in value;
|
|
8
|
+
}
|
|
9
|
+
export async function loadSchema(schemaPath, exportName) {
|
|
10
|
+
const absolutePath = path.resolve(schemaPath);
|
|
11
|
+
const jiti = createJiti(import.meta.url, {
|
|
12
|
+
moduleCache: false,
|
|
13
|
+
interopDefault: true
|
|
14
|
+
});
|
|
15
|
+
let moduleExports;
|
|
16
|
+
try {
|
|
17
|
+
moduleExports = await jiti.import(absolutePath);
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
21
|
+
throw new Error(`Unable to load schema file "${absolutePath}": ${message}`);
|
|
22
|
+
}
|
|
23
|
+
if (!(exportName in moduleExports)) {
|
|
24
|
+
throw new Error(`Export "${exportName}" was not found in schema file "${absolutePath}".`);
|
|
25
|
+
}
|
|
26
|
+
const candidate = moduleExports[exportName];
|
|
27
|
+
if (!isZodSchema(candidate)) {
|
|
28
|
+
throw new Error(`Export "${exportName}" from "${absolutePath}" is not a Zod schema.`);
|
|
29
|
+
}
|
|
30
|
+
return candidate;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=loader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loader.js","sourceRoot":"","sources":["../src/loader.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;AAElC,SAAS,WAAW,CAAC,KAAc,EAAW;IAC5C,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QACxC,OAAO,KAAK,CAAC;IACf,CAAC;IAED,OAAO,MAAM,IAAK,KAAiC,CAAC;AAAA,CACrD;AAED,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,UAAkB,EAAE,UAAkB,EAAoB;IACzF,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IAC9C,MAAM,IAAI,GAAG,UAAU,CAAC,OAAO,IAAI,CAAC,GAAG,EAAE;QACvC,WAAW,EAAE,KAAK;QAClB,cAAc,EAAE,IAAI;KACrB,CAAC,CAAC;IAEH,IAAI,aAAsC,CAAC;IAC3C,IAAI,CAAC;QACH,aAAa,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;IAClD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACvE,MAAM,IAAI,KAAK,CAAC,+BAA+B,YAAY,MAAM,OAAO,EAAE,CAAC,CAAC;IAC9E,CAAC;IAED,IAAI,CAAC,CAAC,UAAU,IAAI,aAAa,CAAC,EAAE,CAAC;QACnC,MAAM,IAAI,KAAK,CAAC,WAAW,UAAU,mCAAmC,YAAY,IAAI,CAAC,CAAC;IAC5F,CAAC;IAED,MAAM,SAAS,GAAG,aAAa,CAAC,UAAU,CAAC,CAAC;IAC5C,IAAI,CAAC,WAAW,CAAC,SAAS,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,KAAK,CAAC,WAAW,UAAU,WAAW,YAAY,wBAAwB,CAAC,CAAC;IACxF,CAAC;IAED,OAAO,SAAS,CAAC;AAAA,CAClB"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { CodegenConfig } from './codegen.js';
|
|
2
|
+
/**
|
|
3
|
+
* Generate a Next.js server action TypeScript file from the codegen config.
|
|
4
|
+
*
|
|
5
|
+
* The generated file:
|
|
6
|
+
* - Has a `"use server"` directive
|
|
7
|
+
* - Imports the Zod schema (no @zod-to-form/* imports)
|
|
8
|
+
* - Declares a typed FormState type
|
|
9
|
+
* - Exports an async action that validates formData via safeParse
|
|
10
|
+
* - Returns fieldErrors on validation failure or a success message on pass
|
|
11
|
+
*/
|
|
12
|
+
export declare function generateServerAction(config: CodegenConfig): Promise<string>;
|
|
13
|
+
//# sourceMappingURL=server-action.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server-action.d.ts","sourceRoot":"","sources":["../src/server-action.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAkBlD;;;;;;;;;GASG;AACH,wBAAsB,oBAAoB,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CA+BjF"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
/**
|
|
3
|
+
* Compute the relative import path from the action file to the schema file.
|
|
4
|
+
* The action file sits alongside the form component (same directory).
|
|
5
|
+
*/
|
|
6
|
+
function schemaImportPath(config) {
|
|
7
|
+
const rel = path.relative(path.dirname(config.outputPath), config.schemaPath).replace(/\\/g, '/');
|
|
8
|
+
const withSlash = rel.startsWith('.') ? rel : `./${rel}`;
|
|
9
|
+
// Replace .ts extension with .js for ESM compatibility
|
|
10
|
+
return withSlash.replace(/\.ts$/, '.js');
|
|
11
|
+
}
|
|
12
|
+
/** Derive camelCase action function name from PascalCase component name */
|
|
13
|
+
function toActionName(componentName) {
|
|
14
|
+
return componentName.charAt(0).toLowerCase() + componentName.slice(1) + 'Action';
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Generate a Next.js server action TypeScript file from the codegen config.
|
|
18
|
+
*
|
|
19
|
+
* The generated file:
|
|
20
|
+
* - Has a `"use server"` directive
|
|
21
|
+
* - Imports the Zod schema (no @zod-to-form/* imports)
|
|
22
|
+
* - Declares a typed FormState type
|
|
23
|
+
* - Exports an async action that validates formData via safeParse
|
|
24
|
+
* - Returns fieldErrors on validation failure or a success message on pass
|
|
25
|
+
*/
|
|
26
|
+
export async function generateServerAction(config) {
|
|
27
|
+
const { exportName, componentName } = config;
|
|
28
|
+
const importPath = schemaImportPath(config);
|
|
29
|
+
const stateName = `${componentName}State`;
|
|
30
|
+
const actionName = toActionName(componentName);
|
|
31
|
+
return [
|
|
32
|
+
'"use server";',
|
|
33
|
+
'',
|
|
34
|
+
`import { ${exportName} } from '${importPath}';`,
|
|
35
|
+
'',
|
|
36
|
+
`export type ${stateName} = {`,
|
|
37
|
+
` errors: Partial<Record<string, string[]>>;`,
|
|
38
|
+
` message: string | null;`,
|
|
39
|
+
`};`,
|
|
40
|
+
'',
|
|
41
|
+
`export async function ${actionName}(`,
|
|
42
|
+
` _prevState: ${stateName},`,
|
|
43
|
+
` formData: FormData`,
|
|
44
|
+
`): Promise<${stateName}> {`,
|
|
45
|
+
` const result = ${exportName}.safeParse(Object.fromEntries(formData));`,
|
|
46
|
+
'',
|
|
47
|
+
` if (!result.success) {`,
|
|
48
|
+
` const fieldErrors = result.error.flatten().fieldErrors;`,
|
|
49
|
+
` return { errors: fieldErrors, message: null };`,
|
|
50
|
+
` }`,
|
|
51
|
+
'',
|
|
52
|
+
` return { errors: {}, message: 'Form submitted successfully.' };`,
|
|
53
|
+
`}`,
|
|
54
|
+
''
|
|
55
|
+
].join('\n');
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=server-action.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server-action.js","sourceRoot":"","sources":["../src/server-action.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAG7B;;;GAGG;AACH,SAAS,gBAAgB,CAAC,MAAqB,EAAU;IACvD,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IAClG,MAAM,SAAS,GAAG,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;IACzD,uDAAuD;IACvD,OAAO,SAAS,CAAC,OAAO,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;AAAA,CAC1C;AAED,2EAA2E;AAC3E,SAAS,YAAY,CAAC,aAAqB,EAAU;IACnD,OAAO,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC;AAAA,CAClF;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,MAAqB,EAAmB;IACjF,MAAM,EAAE,UAAU,EAAE,aAAa,EAAE,GAAG,MAAM,CAAC;IAC7C,MAAM,UAAU,GAAG,gBAAgB,CAAC,MAAM,CAAC,CAAC;IAC5C,MAAM,SAAS,GAAG,GAAG,aAAa,OAAO,CAAC;IAC1C,MAAM,UAAU,GAAG,YAAY,CAAC,aAAa,CAAC,CAAC;IAE/C,OAAO;QACL,eAAe;QACf,EAAE;QACF,YAAY,UAAU,YAAY,UAAU,IAAI;QAChD,EAAE;QACF,eAAe,SAAS,MAAM;QAC9B,8CAA8C;QAC9C,2BAA2B;QAC3B,IAAI;QACJ,EAAE;QACF,yBAAyB,UAAU,GAAG;QACtC,iBAAiB,SAAS,GAAG;QAC7B,sBAAsB;QACtB,cAAc,SAAS,KAAK;QAC5B,oBAAoB,UAAU,2CAA2C;QACzE,EAAE;QACF,0BAA0B;QAC1B,6DAA6D;QAC7D,oDAAoD;QACpD,KAAK;QACL,EAAE;QACF,mEAAmE;QACnE,GAAG;QACH,EAAE;KACH,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAAA,CACd"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { FormField } from '@zod-to-form/core';
|
|
2
|
+
export declare function getFileHeader(schemaImportPath: string, exportName: string, hasArrays?: boolean): string;
|
|
3
|
+
export declare function renderField(field: FormField): string;
|
|
4
|
+
//# sourceMappingURL=templates.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../src/templates.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAEnD,wBAAgB,aAAa,CAC3B,gBAAgB,EAAE,MAAM,EACxB,UAAU,EAAE,MAAM,EAClB,SAAS,UAAQ,GAChB,MAAM,CAYR;AA2BD,wBAAgB,WAAW,CAAC,KAAK,EAAE,SAAS,GAAG,MAAM,CAiBpD"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export function getFileHeader(schemaImportPath, exportName, hasArrays = false) {
|
|
2
|
+
const rhfImports = hasArrays
|
|
3
|
+
? `import { useForm, useFieldArray } from 'react-hook-form';`
|
|
4
|
+
: `import { useForm } from 'react-hook-form';`;
|
|
5
|
+
return [
|
|
6
|
+
rhfImports,
|
|
7
|
+
`import { zodResolver } from '@hookform/resolvers/zod';`,
|
|
8
|
+
`import { ${exportName} } from '${schemaImportPath}';`,
|
|
9
|
+
``,
|
|
10
|
+
`type FormData = (typeof ${exportName})['_zod']['output'];`
|
|
11
|
+
].join('\n');
|
|
12
|
+
}
|
|
13
|
+
function renderInput(field) {
|
|
14
|
+
const inputType = typeof field.props['type'] === 'string' ? field.props['type'] : 'text';
|
|
15
|
+
return `<input id="${field.key}" type="${inputType}" {...register('${field.key}')} />`;
|
|
16
|
+
}
|
|
17
|
+
function renderCheckbox(field) {
|
|
18
|
+
return `<input id="${field.key}" type="checkbox" {...register('${field.key}')} />`;
|
|
19
|
+
}
|
|
20
|
+
function renderDatePicker(field) {
|
|
21
|
+
return `<input id="${field.key}" type="date" {...register('${field.key}', { valueAsDate: true })} />`;
|
|
22
|
+
}
|
|
23
|
+
function renderFileInput(field) {
|
|
24
|
+
return `<input id="${field.key}" type="file" {...register('${field.key}')} />`;
|
|
25
|
+
}
|
|
26
|
+
function renderSelect(field) {
|
|
27
|
+
const options = (field.options ?? [])
|
|
28
|
+
.map((option) => `<option value="${String(option.value)}">${option.label}</option>`)
|
|
29
|
+
.join('');
|
|
30
|
+
return `<select id="${field.key}" {...register('${field.key}')}>${options}</select>`;
|
|
31
|
+
}
|
|
32
|
+
export function renderField(field) {
|
|
33
|
+
switch (field.component) {
|
|
34
|
+
case 'Checkbox':
|
|
35
|
+
case 'Switch':
|
|
36
|
+
return renderCheckbox(field);
|
|
37
|
+
case 'DatePicker':
|
|
38
|
+
return renderDatePicker(field);
|
|
39
|
+
case 'FileInput':
|
|
40
|
+
return renderFileInput(field);
|
|
41
|
+
case 'Select':
|
|
42
|
+
case 'RadioGroup':
|
|
43
|
+
return renderSelect(field);
|
|
44
|
+
case 'Textarea':
|
|
45
|
+
return `<textarea id="${field.key}" {...register('${field.key}')} />`;
|
|
46
|
+
default:
|
|
47
|
+
return renderInput(field);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=templates.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"templates.js","sourceRoot":"","sources":["../src/templates.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,aAAa,CAC3B,gBAAwB,EACxB,UAAkB,EAClB,SAAS,GAAG,KAAK,EACT;IACR,MAAM,UAAU,GAAG,SAAS;QAC1B,CAAC,CAAC,2DAA2D;QAC7D,CAAC,CAAC,4CAA4C,CAAC;IAEjD,OAAO;QACL,UAAU;QACV,wDAAwD;QACxD,YAAY,UAAU,YAAY,gBAAgB,IAAI;QACtD,EAAE;QACF,2BAA2B,UAAU,sBAAsB;KAC5D,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAAA,CACd;AAED,SAAS,WAAW,CAAC,KAAgB,EAAU;IAC7C,MAAM,SAAS,GAAG,OAAO,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;IACzF,OAAO,cAAc,KAAK,CAAC,GAAG,WAAW,SAAS,mBAAmB,KAAK,CAAC,GAAG,QAAQ,CAAC;AAAA,CACxF;AAED,SAAS,cAAc,CAAC,KAAgB,EAAU;IAChD,OAAO,cAAc,KAAK,CAAC,GAAG,mCAAmC,KAAK,CAAC,GAAG,QAAQ,CAAC;AAAA,CACpF;AAED,SAAS,gBAAgB,CAAC,KAAgB,EAAU;IAClD,OAAO,cAAc,KAAK,CAAC,GAAG,+BAA+B,KAAK,CAAC,GAAG,+BAA+B,CAAC;AAAA,CACvG;AAED,SAAS,eAAe,CAAC,KAAgB,EAAU;IACjD,OAAO,cAAc,KAAK,CAAC,GAAG,+BAA+B,KAAK,CAAC,GAAG,QAAQ,CAAC;AAAA,CAChF;AAED,SAAS,YAAY,CAAC,KAAgB,EAAU;IAC9C,MAAM,OAAO,GAAG,CAAC,KAAK,CAAC,OAAO,IAAI,EAAE,CAAC;SAClC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,kBAAkB,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,MAAM,CAAC,KAAK,WAAW,CAAC;SACnF,IAAI,CAAC,EAAE,CAAC,CAAC;IAEZ,OAAO,eAAe,KAAK,CAAC,GAAG,mBAAmB,KAAK,CAAC,GAAG,OAAO,OAAO,WAAW,CAAC;AAAA,CACtF;AAED,MAAM,UAAU,WAAW,CAAC,KAAgB,EAAU;IACpD,QAAQ,KAAK,CAAC,SAAS,EAAE,CAAC;QACxB,KAAK,UAAU,CAAC;QAChB,KAAK,QAAQ;YACX,OAAO,cAAc,CAAC,KAAK,CAAC,CAAC;QAC/B,KAAK,YAAY;YACf,OAAO,gBAAgB,CAAC,KAAK,CAAC,CAAC;QACjC,KAAK,WAAW;YACd,OAAO,eAAe,CAAC,KAAK,CAAC,CAAC;QAChC,KAAK,QAAQ,CAAC;QACd,KAAK,YAAY;YACf,OAAO,YAAY,CAAC,KAAK,CAAC,CAAC;QAC7B,KAAK,UAAU;YACb,OAAO,iBAAiB,KAAK,CAAC,GAAG,mBAAmB,KAAK,CAAC,GAAG,QAAQ,CAAC;QACxE;YACE,OAAO,WAAW,CAAC,KAAK,CAAC,CAAC;IAC9B,CAAC;AAAA,CACF"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type FSWatcher } from 'chokidar';
|
|
2
|
+
/**
|
|
3
|
+
* Start watching a schema file for changes.
|
|
4
|
+
* Debounces rapid changes by 200ms before calling regenerate().
|
|
5
|
+
*
|
|
6
|
+
* @param schemaPath - Absolute path to the schema file to watch
|
|
7
|
+
* @param regenerate - Async callback to invoke after a debounced change event
|
|
8
|
+
* @returns The chokidar FSWatcher instance (with patched close() for state tracking)
|
|
9
|
+
*/
|
|
10
|
+
export declare function startWatch(schemaPath: string, regenerate: () => Promise<void>): Promise<FSWatcher>;
|
|
11
|
+
//# sourceMappingURL=watcher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"watcher.d.ts","sourceRoot":"","sources":["../src/watcher.ts"],"names":[],"mappings":"AAAA,OAAO,EAA0B,KAAK,SAAS,EAAE,MAAM,UAAU,CAAC;AAIlE;;;;;;;GAOG;AACH,wBAAsB,UAAU,CAC9B,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAC9B,OAAO,CAAC,SAAS,CAAC,CA6CpB"}
|
package/dist/watcher.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { watch as chokidarWatch } from 'chokidar';
|
|
2
|
+
const DEBOUNCE_MS = 200;
|
|
3
|
+
/**
|
|
4
|
+
* Start watching a schema file for changes.
|
|
5
|
+
* Debounces rapid changes by 200ms before calling regenerate().
|
|
6
|
+
*
|
|
7
|
+
* @param schemaPath - Absolute path to the schema file to watch
|
|
8
|
+
* @param regenerate - Async callback to invoke after a debounced change event
|
|
9
|
+
* @returns The chokidar FSWatcher instance (with patched close() for state tracking)
|
|
10
|
+
*/
|
|
11
|
+
export async function startWatch(schemaPath, regenerate) {
|
|
12
|
+
let debounceTimer = null;
|
|
13
|
+
let closed = false;
|
|
14
|
+
const watcher = chokidarWatch(schemaPath, {
|
|
15
|
+
persistent: true,
|
|
16
|
+
ignoreInitial: true
|
|
17
|
+
});
|
|
18
|
+
watcher.on('change', (file) => {
|
|
19
|
+
if (closed)
|
|
20
|
+
return;
|
|
21
|
+
console.log(`Change detected: ${file}`);
|
|
22
|
+
if (debounceTimer !== null) {
|
|
23
|
+
clearTimeout(debounceTimer);
|
|
24
|
+
}
|
|
25
|
+
debounceTimer = setTimeout(() => {
|
|
26
|
+
debounceTimer = null;
|
|
27
|
+
if (closed)
|
|
28
|
+
return;
|
|
29
|
+
regenerate()
|
|
30
|
+
.then(() => {
|
|
31
|
+
console.log('Regeneration complete.');
|
|
32
|
+
})
|
|
33
|
+
.catch((err) => {
|
|
34
|
+
console.error('Regeneration failed:', err);
|
|
35
|
+
});
|
|
36
|
+
}, DEBOUNCE_MS);
|
|
37
|
+
});
|
|
38
|
+
// Patch close() to set the closed flag and clear any pending debounce
|
|
39
|
+
const nativeClose = watcher.close.bind(watcher);
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/require-await -- wrapping native close for flag tracking
|
|
41
|
+
watcher.close = async () => {
|
|
42
|
+
closed = true;
|
|
43
|
+
if (debounceTimer !== null) {
|
|
44
|
+
clearTimeout(debounceTimer);
|
|
45
|
+
debounceTimer = null;
|
|
46
|
+
}
|
|
47
|
+
return nativeClose();
|
|
48
|
+
};
|
|
49
|
+
return watcher;
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=watcher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"watcher.js","sourceRoot":"","sources":["../src/watcher.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,IAAI,aAAa,EAAkB,MAAM,UAAU,CAAC;AAElE,MAAM,WAAW,GAAG,GAAG,CAAC;AAExB;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,UAAkB,EAClB,UAA+B,EACX;IACpB,IAAI,aAAa,GAAyC,IAAI,CAAC;IAC/D,IAAI,MAAM,GAAG,KAAK,CAAC;IAEnB,MAAM,OAAO,GAAG,aAAa,CAAC,UAAU,EAAE;QACxC,UAAU,EAAE,IAAI;QAChB,aAAa,EAAE,IAAI;KACpB,CAAC,CAAC;IAEH,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,IAAY,EAAE,EAAE,CAAC;QACrC,IAAI,MAAM;YAAE,OAAO;QAEnB,OAAO,CAAC,GAAG,CAAC,oBAAoB,IAAI,EAAE,CAAC,CAAC;QAExC,IAAI,aAAa,KAAK,IAAI,EAAE,CAAC;YAC3B,YAAY,CAAC,aAAa,CAAC,CAAC;QAC9B,CAAC;QAED,aAAa,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC;YAC/B,aAAa,GAAG,IAAI,CAAC;YACrB,IAAI,MAAM;gBAAE,OAAO;YAEnB,UAAU,EAAE;iBACT,IAAI,CAAC,GAAG,EAAE,CAAC;gBACV,OAAO,CAAC,GAAG,CAAC,wBAAwB,CAAC,CAAC;YAAA,CACvC,CAAC;iBACD,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE,CAAC;gBACvB,OAAO,CAAC,KAAK,CAAC,sBAAsB,EAAE,GAAG,CAAC,CAAC;YAAA,CAC5C,CAAC,CAAC;QAAA,CACN,EAAE,WAAW,CAAC,CAAC;IAAA,CACjB,CAAC,CAAC;IAEH,sEAAsE;IACtE,MAAM,WAAW,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAChD,uGAAuG;IACvG,OAAO,CAAC,KAAK,GAAG,KAAK,IAAmB,EAAE,CAAC;QACzC,MAAM,GAAG,IAAI,CAAC;QACd,IAAI,aAAa,KAAK,IAAI,EAAE,CAAC;YAC3B,YAAY,CAAC,aAAa,CAAC,CAAC;YAC5B,aAAa,GAAG,IAAI,CAAC;QACvB,CAAC;QACD,OAAO,WAAW,EAAE,CAAC;IAAA,CACtB,CAAC;IAEF,OAAO,OAAO,CAAC;AAAA,CAChB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zod-to-form/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Build-time code generator for Zod v4 form components",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/pradeepmouli/zodforms#readme",
|
|
@@ -36,14 +36,14 @@
|
|
|
36
36
|
"chokidar": "^5.0.0",
|
|
37
37
|
"commander": "^14.0.3",
|
|
38
38
|
"jiti": "^2.6.1",
|
|
39
|
-
"@zod-to-form/core": "0.2.
|
|
39
|
+
"@zod-to-form/core": "0.2.2"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"zod": "^4.3.6"
|
|
43
43
|
},
|
|
44
44
|
"scripts": {
|
|
45
45
|
"build": "tsgo -p tsconfig.build.json",
|
|
46
|
-
"clean": "rm -rf dist",
|
|
46
|
+
"clean": "rm -rf dist tsconfig.build.tsbuildinfo tsconfig.tsbuildinfo",
|
|
47
47
|
"dev": "tsgo -p tsconfig.build.json --watch",
|
|
48
48
|
"test": "vitest run",
|
|
49
49
|
"test:coverage": "vitest run --coverage",
|