@kurotako/gen-angular 0.1.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 +15 -0
- package/LICENSE +21 -0
- package/README.md +40 -0
- package/dist/index.cjs +888 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +82 -0
- package/dist/index.d.ts +82 -0
- package/dist/index.js +847 -0
- package/dist/index.js.map +1 -0
- package/package.json +65 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,847 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var AngularGenError = class extends Error {
|
|
3
|
+
code;
|
|
4
|
+
constructor(code, message, options) {
|
|
5
|
+
super(message, options);
|
|
6
|
+
this.name = new.target.name;
|
|
7
|
+
this.code = code;
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
var MissingZodSymbolError = class extends AngularGenError {
|
|
11
|
+
entityKey;
|
|
12
|
+
role;
|
|
13
|
+
constructor(entityKey2, role) {
|
|
14
|
+
super(
|
|
15
|
+
"angular_missing_zod_symbol",
|
|
16
|
+
`Zod artifact for '${entityKey2}' has no '${role}' symbol; regenerate with a gen-zod version that exposes it`
|
|
17
|
+
);
|
|
18
|
+
this.entityKey = entityKey2;
|
|
19
|
+
this.role = role;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
var MissingZodNamespaceError = class extends AngularGenError {
|
|
23
|
+
namespace;
|
|
24
|
+
constructor(namespace) {
|
|
25
|
+
super(
|
|
26
|
+
"angular_missing_zod_namespace",
|
|
27
|
+
`Zod artifact has no 'extra.perNamespace[${JSON.stringify(namespace)}]' entry`
|
|
28
|
+
);
|
|
29
|
+
this.namespace = namespace;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// src/generator.ts
|
|
34
|
+
import { defineGenerator } from "@kurotako/config";
|
|
35
|
+
|
|
36
|
+
// src/artifact.ts
|
|
37
|
+
import { iterEntities } from "@kurotako/ir";
|
|
38
|
+
|
|
39
|
+
// src/names.ts
|
|
40
|
+
function lowerFirst(s) {
|
|
41
|
+
return s.length === 0 ? s : s.charAt(0).toLowerCase() + s.slice(1);
|
|
42
|
+
}
|
|
43
|
+
function controlsTypeName(entity, variant, family = "") {
|
|
44
|
+
return `${entity}${variant}${family}FormControls`;
|
|
45
|
+
}
|
|
46
|
+
function formTypeName(entity, variant, family = "") {
|
|
47
|
+
return `${entity}${variant}${family}Form`;
|
|
48
|
+
}
|
|
49
|
+
function factoryName(entity) {
|
|
50
|
+
return `${entity}FormFactory`;
|
|
51
|
+
}
|
|
52
|
+
function factoryMethod(variant) {
|
|
53
|
+
return `create${variant}Form`;
|
|
54
|
+
}
|
|
55
|
+
function relationBuilderMethod(relationName, variant) {
|
|
56
|
+
const cap = `${relationName.charAt(0).toUpperCase()}${relationName.slice(1)}`;
|
|
57
|
+
return `add${cap}${variant}`;
|
|
58
|
+
}
|
|
59
|
+
function signalSchemaName(entity, variant) {
|
|
60
|
+
return `${lowerFirst(entity)}${variant}FormSchema`;
|
|
61
|
+
}
|
|
62
|
+
function modelFactoryName(entity, variant) {
|
|
63
|
+
return `create${entity}${variant}Model`;
|
|
64
|
+
}
|
|
65
|
+
function signalFormFactoryName(entity, variant) {
|
|
66
|
+
return `create${entity}${variant}Form`;
|
|
67
|
+
}
|
|
68
|
+
function entityModule(namespace, entity) {
|
|
69
|
+
return `${namespace}/angular/${entity}.form`;
|
|
70
|
+
}
|
|
71
|
+
function runtimeModule(namespace) {
|
|
72
|
+
return `${namespace}/angular/zod-forms.runtime`;
|
|
73
|
+
}
|
|
74
|
+
function barrelModule(namespace) {
|
|
75
|
+
return `${namespace}/angular`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/zod-artifact.ts
|
|
79
|
+
function entityKey(namespace, entity) {
|
|
80
|
+
return `${namespace}.${entity}`;
|
|
81
|
+
}
|
|
82
|
+
function zodEntity(zod, namespace, entity) {
|
|
83
|
+
const key = entityKey(namespace, entity);
|
|
84
|
+
const entry = zod.entities[key];
|
|
85
|
+
if (entry === void 0) {
|
|
86
|
+
throw new MissingZodSymbolError(key, "<entity>");
|
|
87
|
+
}
|
|
88
|
+
return entry;
|
|
89
|
+
}
|
|
90
|
+
function zodSymbol(zod, namespace, entity, role) {
|
|
91
|
+
const key = entityKey(namespace, entity);
|
|
92
|
+
const entry = zodEntity(zod, namespace, entity);
|
|
93
|
+
const id = entry.symbols[role];
|
|
94
|
+
if (id === void 0) {
|
|
95
|
+
throw new MissingZodSymbolError(key, role);
|
|
96
|
+
}
|
|
97
|
+
return id;
|
|
98
|
+
}
|
|
99
|
+
function zodModule(zod, namespace, entity) {
|
|
100
|
+
return zodEntity(zod, namespace, entity).module;
|
|
101
|
+
}
|
|
102
|
+
function zodExtra(zod) {
|
|
103
|
+
return zod.extra;
|
|
104
|
+
}
|
|
105
|
+
function zodNamespaceExtra(zod, namespace) {
|
|
106
|
+
const per = zodExtra(zod).perNamespace[namespace];
|
|
107
|
+
if (per === void 0) {
|
|
108
|
+
throw new MissingZodNamespaceError(namespace);
|
|
109
|
+
}
|
|
110
|
+
return per;
|
|
111
|
+
}
|
|
112
|
+
function zodEnum(zod, namespace, ref) {
|
|
113
|
+
const per = zodNamespaceExtra(zod, namespace);
|
|
114
|
+
const def = per.enums[ref];
|
|
115
|
+
if (def === void 0) {
|
|
116
|
+
throw new MissingZodSymbolError(`${namespace}.<enum>`, ref);
|
|
117
|
+
}
|
|
118
|
+
return { typeName: def.typeName, module: def.module };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// src/artifact.ts
|
|
122
|
+
function entitySymbols(entityName, options) {
|
|
123
|
+
const symbols = {};
|
|
124
|
+
const deep = options.relations === "deep";
|
|
125
|
+
const family = deep ? "Deep" : "";
|
|
126
|
+
if (options.forms.includes("reactive")) {
|
|
127
|
+
const createControls = controlsTypeName(entityName, "Create", family);
|
|
128
|
+
const createForm = formTypeName(entityName, "Create", family);
|
|
129
|
+
const updateControls = controlsTypeName(entityName, "Update", family);
|
|
130
|
+
const updateForm = formTypeName(entityName, "Update", family);
|
|
131
|
+
symbols.createControls = createControls;
|
|
132
|
+
symbols.createForm = createForm;
|
|
133
|
+
symbols.updateControls = updateControls;
|
|
134
|
+
symbols.updateForm = updateForm;
|
|
135
|
+
symbols.factory = factoryName(entityName);
|
|
136
|
+
if (deep) {
|
|
137
|
+
symbols.createDeepControls = createControls;
|
|
138
|
+
symbols.createDeepForm = createForm;
|
|
139
|
+
symbols.updateDeepControls = updateControls;
|
|
140
|
+
symbols.updateDeepForm = updateForm;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (options.forms.includes("signal")) {
|
|
144
|
+
symbols.createSchema = signalSchemaName(entityName, "Create");
|
|
145
|
+
symbols.updateSchema = signalSchemaName(entityName, "Update");
|
|
146
|
+
symbols.createModel = modelFactoryName(entityName, "Create");
|
|
147
|
+
symbols.updateModel = modelFactoryName(entityName, "Update");
|
|
148
|
+
symbols.createSignalForm = signalFormFactoryName(entityName, "Create");
|
|
149
|
+
symbols.updateSignalForm = signalFormFactoryName(entityName, "Update");
|
|
150
|
+
}
|
|
151
|
+
return symbols;
|
|
152
|
+
}
|
|
153
|
+
function buildArtifact(ir, zod, options) {
|
|
154
|
+
const entities = {};
|
|
155
|
+
for (const { namespace, entity } of iterEntities(ir)) {
|
|
156
|
+
entities[`${namespace}.${entity.name}`] = {
|
|
157
|
+
module: entityModule(namespace, entity.name),
|
|
158
|
+
symbols: entitySymbols(entity.name, options)
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
const perNamespace = {};
|
|
162
|
+
for (const namespace of Object.keys(ir.sources)) {
|
|
163
|
+
perNamespace[namespace] = {
|
|
164
|
+
runtimeModule: runtimeModule(namespace),
|
|
165
|
+
barrelModule: barrelModule(namespace)
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
const extra = {
|
|
169
|
+
forms: options.forms,
|
|
170
|
+
relations: options.relations,
|
|
171
|
+
zodVersion: zodExtra(zod).zodVersion,
|
|
172
|
+
perNamespace
|
|
173
|
+
};
|
|
174
|
+
const peerDependencies = options.forms.includes("signal") ? { "@angular/core": ">=22", "@angular/forms": ">=22" } : { "@angular/core": ">=17", "@angular/forms": ">=17" };
|
|
175
|
+
return { entities, peerDependencies, extra };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// src/emit/barrel.ts
|
|
179
|
+
function emitBarrel(source, options) {
|
|
180
|
+
const lines = [];
|
|
181
|
+
const entities = Object.values(source.entities);
|
|
182
|
+
if (entities.length > 0 && options.forms.length > 0) {
|
|
183
|
+
lines.push("export * from './zod-forms.runtime';");
|
|
184
|
+
}
|
|
185
|
+
for (const entity of entities) {
|
|
186
|
+
lines.push(`export * from './${entity.name}.form';`);
|
|
187
|
+
}
|
|
188
|
+
return `${lines.join("\n")}
|
|
189
|
+
`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// src/render/imports.ts
|
|
193
|
+
var ImportsRecorder = class {
|
|
194
|
+
values = /* @__PURE__ */ new Map();
|
|
195
|
+
types = /* @__PURE__ */ new Map();
|
|
196
|
+
value(module, name) {
|
|
197
|
+
add(this.values, module, name);
|
|
198
|
+
}
|
|
199
|
+
type(module, name) {
|
|
200
|
+
add(this.types, module, name);
|
|
201
|
+
}
|
|
202
|
+
/** The full `import ...` block text, one statement per line, no trailing blank line. */
|
|
203
|
+
render() {
|
|
204
|
+
const lines = [];
|
|
205
|
+
for (const [spec, names] of this.values) {
|
|
206
|
+
lines.push({ spec, rank: 0, stmt: importStmt(spec, [...names], false) });
|
|
207
|
+
}
|
|
208
|
+
for (const [spec, names] of this.types) {
|
|
209
|
+
lines.push({ spec, rank: 1, stmt: importStmt(spec, [...names], true) });
|
|
210
|
+
}
|
|
211
|
+
lines.sort((a, b) => a.spec.localeCompare(b.spec) || a.rank - b.rank);
|
|
212
|
+
return lines.map((l) => l.stmt).join("\n");
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
function add(map, module, name) {
|
|
216
|
+
const set = map.get(module) ?? /* @__PURE__ */ new Set();
|
|
217
|
+
set.add(name);
|
|
218
|
+
map.set(module, set);
|
|
219
|
+
}
|
|
220
|
+
function importStmt(spec, names, typeOnly) {
|
|
221
|
+
const sorted = [...names].sort((a, b) => a.localeCompare(b)).join(", ");
|
|
222
|
+
const kw = typeOnly ? "import type" : "import";
|
|
223
|
+
return `${kw} { ${sorted} } from '${spec}';`;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// src/render/controls.ts
|
|
227
|
+
import { resolveEnum } from "@kurotako/ir";
|
|
228
|
+
var SCALAR_BASE = {
|
|
229
|
+
string: "string",
|
|
230
|
+
uuid: "string",
|
|
231
|
+
decimal: "string",
|
|
232
|
+
bytes: "string",
|
|
233
|
+
int: "number",
|
|
234
|
+
float: "number",
|
|
235
|
+
bigint: "bigint",
|
|
236
|
+
boolean: "boolean",
|
|
237
|
+
date: "Date",
|
|
238
|
+
datetime: "Date",
|
|
239
|
+
json: "unknown"
|
|
240
|
+
};
|
|
241
|
+
function baseType(field, zodEnumTypeName) {
|
|
242
|
+
switch (field.type.kind) {
|
|
243
|
+
case "scalar":
|
|
244
|
+
return SCALAR_BASE[field.type.scalar];
|
|
245
|
+
case "enum":
|
|
246
|
+
return zodEnumTypeName(field.type.ref);
|
|
247
|
+
case "unknown":
|
|
248
|
+
return "unknown";
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
function controlType(field, zodEnumTypeName) {
|
|
252
|
+
let t = baseType(field, zodEnumTypeName);
|
|
253
|
+
if (field.list) {
|
|
254
|
+
t = `${t}[]`;
|
|
255
|
+
}
|
|
256
|
+
if (field.nullable) {
|
|
257
|
+
t = `${t} | null`;
|
|
258
|
+
}
|
|
259
|
+
return t;
|
|
260
|
+
}
|
|
261
|
+
function enumZeroFromSource(source, entity) {
|
|
262
|
+
return (ref) => resolveEnum(source, entity, ref)?.values[0]?.name;
|
|
263
|
+
}
|
|
264
|
+
function zeroValue(field, enumZero) {
|
|
265
|
+
if (field.type.kind === "scalar") {
|
|
266
|
+
switch (field.type.scalar) {
|
|
267
|
+
case "string":
|
|
268
|
+
case "uuid":
|
|
269
|
+
case "decimal":
|
|
270
|
+
case "bytes":
|
|
271
|
+
return "''";
|
|
272
|
+
case "int":
|
|
273
|
+
case "float":
|
|
274
|
+
return "0";
|
|
275
|
+
case "bigint":
|
|
276
|
+
return "0n";
|
|
277
|
+
case "boolean":
|
|
278
|
+
return "false";
|
|
279
|
+
case "date":
|
|
280
|
+
case "datetime":
|
|
281
|
+
return "new Date(0)";
|
|
282
|
+
case "json":
|
|
283
|
+
return "undefined";
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (field.type.kind === "enum") {
|
|
287
|
+
const value = enumZero?.(field.type.ref);
|
|
288
|
+
return value === void 0 ? "undefined" : JSON.stringify(value);
|
|
289
|
+
}
|
|
290
|
+
return "undefined";
|
|
291
|
+
}
|
|
292
|
+
function initExpr(field, enumZero) {
|
|
293
|
+
if (field.list) {
|
|
294
|
+
return field.default?.kind === "value" ? JSON.stringify(field.default.value) : "[]";
|
|
295
|
+
}
|
|
296
|
+
if (field.default?.kind === "value") {
|
|
297
|
+
return JSON.stringify(field.default.value);
|
|
298
|
+
}
|
|
299
|
+
if (field.nullable) {
|
|
300
|
+
return "null";
|
|
301
|
+
}
|
|
302
|
+
return zeroValue(field, enumZero);
|
|
303
|
+
}
|
|
304
|
+
function controlExpr(field, typeArg, sourceExpr) {
|
|
305
|
+
if (field.nullable) {
|
|
306
|
+
return `new FormControl<${typeArg}>(${sourceExpr})`;
|
|
307
|
+
}
|
|
308
|
+
return `new FormControl(${sourceExpr}, { nonNullable: true })`;
|
|
309
|
+
}
|
|
310
|
+
function fieldControlEntry(field, zodEnumTypeName) {
|
|
311
|
+
return {
|
|
312
|
+
name: field.name,
|
|
313
|
+
fullType: `FormControl<${controlType(field, zodEnumTypeName)}>`
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
function controlsInterface(interfaceName, entries) {
|
|
317
|
+
if (entries.length === 0) {
|
|
318
|
+
return `export interface ${interfaceName} {}`;
|
|
319
|
+
}
|
|
320
|
+
const body = entries.map((e) => ` ${e.name}: ${e.fullType};`).join("\n");
|
|
321
|
+
return `export interface ${interfaceName} {
|
|
322
|
+
${body}
|
|
323
|
+
}`;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// src/render/relations.ts
|
|
327
|
+
import { isCrossSource } from "@kurotako/ir";
|
|
328
|
+
function deepRelations(entity, variant, namespace, logger) {
|
|
329
|
+
const out = [];
|
|
330
|
+
for (const relation of entity.relations) {
|
|
331
|
+
if (isCrossSource(namespace, relation)) {
|
|
332
|
+
logger?.debug(
|
|
333
|
+
`gen-angular: relation '${relation.name}' targets another source ('${relation.target.namespace}.${relation.target.entity}'); degrading to the flat FK scalar in deep mode`
|
|
334
|
+
);
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
const many = relation.cardinality === "many";
|
|
338
|
+
const targetControls = controlsTypeName(
|
|
339
|
+
relation.target.entity,
|
|
340
|
+
variant,
|
|
341
|
+
"Deep"
|
|
342
|
+
);
|
|
343
|
+
const targetFormType = formTypeName(
|
|
344
|
+
relation.target.entity,
|
|
345
|
+
variant,
|
|
346
|
+
"Deep"
|
|
347
|
+
);
|
|
348
|
+
const groupType = `FormGroup<${targetControls}>`;
|
|
349
|
+
const fullType = many ? `FormArray<${groupType}>` : groupType;
|
|
350
|
+
out.push({
|
|
351
|
+
relation,
|
|
352
|
+
many,
|
|
353
|
+
entry: { name: relation.name, fullType },
|
|
354
|
+
targetFormType,
|
|
355
|
+
builderMethod: relationBuilderMethod(relation.name, variant)
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
return out;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// src/render/variants.ts
|
|
362
|
+
import { createFields, updateFields } from "@kurotako/ir";
|
|
363
|
+
function variantFields(entity, variant) {
|
|
364
|
+
return variant === "Create" ? createFields(entity) : updateFields(entity);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// src/render/reactive.ts
|
|
368
|
+
function lowerFirst2(s) {
|
|
369
|
+
return s.length === 0 ? s : s.charAt(0).toLowerCase() + s.slice(1);
|
|
370
|
+
}
|
|
371
|
+
function reactiveEntity(entity, namespace, source, options, zod, imports, logger) {
|
|
372
|
+
const deep = options.relations === "deep";
|
|
373
|
+
const enumZero = enumZeroFromSource(source, entity);
|
|
374
|
+
imports.value("@angular/core", "Injectable");
|
|
375
|
+
imports.value("@angular/forms", "FormControl");
|
|
376
|
+
imports.value("@angular/forms", "FormGroup");
|
|
377
|
+
const zodEnumTypeName = (ref) => {
|
|
378
|
+
const e = zodEnum(zod, namespace, ref);
|
|
379
|
+
imports.type(e.module, e.typeName);
|
|
380
|
+
return e.typeName;
|
|
381
|
+
};
|
|
382
|
+
const blocks = [];
|
|
383
|
+
const injectedFactories = /* @__PURE__ */ new Map();
|
|
384
|
+
for (const variant of ["Create", "Update"]) {
|
|
385
|
+
const family = deep ? "Deep" : "";
|
|
386
|
+
const schemaRole = deep ? variant === "Create" ? "createDeepSchema" : "updateDeepSchema" : variant === "Create" ? "createSchema" : "updateSchema";
|
|
387
|
+
const typeRole = deep ? variant === "Create" ? "createDeepType" : "updateDeepType" : variant === "Create" ? "createType" : "updateType";
|
|
388
|
+
const module = zodModule(zod, namespace, entity.name);
|
|
389
|
+
const schemaId = zodSymbol(zod, namespace, entity.name, schemaRole);
|
|
390
|
+
const typeId = zodSymbol(zod, namespace, entity.name, typeRole);
|
|
391
|
+
imports.value(module, schemaId);
|
|
392
|
+
imports.type(module, typeId);
|
|
393
|
+
imports.value(`${namespace}/angular/zod-forms.runtime`, "zodValidator");
|
|
394
|
+
const interfaceName = controlsTypeName(entity.name, variant, family);
|
|
395
|
+
const formType = formTypeName(entity.name, variant, family);
|
|
396
|
+
const fieldEntries = variantFields(entity, variant).map(
|
|
397
|
+
(field) => fieldControlEntry(field, zodEnumTypeName)
|
|
398
|
+
);
|
|
399
|
+
const relations = deep ? deepRelations(entity, variant, namespace, logger) : [];
|
|
400
|
+
const manyRelations = relations.filter((r) => r.many);
|
|
401
|
+
if (manyRelations.length > 0) {
|
|
402
|
+
imports.value("@angular/forms", "FormArray");
|
|
403
|
+
}
|
|
404
|
+
for (const rel of relations) {
|
|
405
|
+
const targetModule = `${namespace}/angular/${rel.relation.target.entity}.form`;
|
|
406
|
+
imports.type(
|
|
407
|
+
targetModule,
|
|
408
|
+
controlsTypeName(rel.relation.target.entity, variant, "Deep")
|
|
409
|
+
);
|
|
410
|
+
imports.type(targetModule, rel.targetFormType);
|
|
411
|
+
if (!injectedFactories.has(rel.relation.target.entity)) {
|
|
412
|
+
const paramName = `${lowerFirst2(rel.relation.target.entity)}FormFactory`;
|
|
413
|
+
injectedFactories.set(rel.relation.target.entity, paramName);
|
|
414
|
+
imports.value(targetModule, factoryName(rel.relation.target.entity));
|
|
415
|
+
}
|
|
416
|
+
if (rel.many) {
|
|
417
|
+
const targetTypeRole = deep && variant === "Create" ? "createDeepType" : deep && variant === "Update" ? "updateDeepType" : variant === "Create" ? "createType" : "updateType";
|
|
418
|
+
const targetZodModule = zodModule(
|
|
419
|
+
zod,
|
|
420
|
+
namespace,
|
|
421
|
+
rel.relation.target.entity
|
|
422
|
+
);
|
|
423
|
+
const targetTypeId = zodSymbol(
|
|
424
|
+
zod,
|
|
425
|
+
namespace,
|
|
426
|
+
rel.relation.target.entity,
|
|
427
|
+
targetTypeRole
|
|
428
|
+
);
|
|
429
|
+
imports.type(targetZodModule, targetTypeId);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
blocks.push(
|
|
433
|
+
controlsInterface(interfaceName, [
|
|
434
|
+
...fieldEntries,
|
|
435
|
+
...relations.map((r) => r.entry)
|
|
436
|
+
])
|
|
437
|
+
);
|
|
438
|
+
blocks.push(`export type ${formType} = FormGroup<${interfaceName}>;`);
|
|
439
|
+
}
|
|
440
|
+
blocks.push(
|
|
441
|
+
renderFactoryClass(
|
|
442
|
+
entity,
|
|
443
|
+
namespace,
|
|
444
|
+
options,
|
|
445
|
+
zod,
|
|
446
|
+
injectedFactories,
|
|
447
|
+
enumZero
|
|
448
|
+
)
|
|
449
|
+
);
|
|
450
|
+
return blocks.join("\n\n");
|
|
451
|
+
}
|
|
452
|
+
function renderFactoryClass(entity, namespace, options, zod, injectedFactories, enumZero) {
|
|
453
|
+
const deep = options.relations === "deep";
|
|
454
|
+
const className = factoryName(entity.name);
|
|
455
|
+
const ctorParams = [...injectedFactories.entries()].map(
|
|
456
|
+
([target, param]) => `private readonly ${param}: ${factoryName(target)}`
|
|
457
|
+
).join(", ");
|
|
458
|
+
const ctor = ctorParams.length > 0 ? `
|
|
459
|
+
constructor(${ctorParams}) {}
|
|
460
|
+
` : "";
|
|
461
|
+
const methods = [];
|
|
462
|
+
for (const variant of ["Create", "Update"]) {
|
|
463
|
+
methods.push(
|
|
464
|
+
renderFactoryMethod(
|
|
465
|
+
entity,
|
|
466
|
+
namespace,
|
|
467
|
+
variant,
|
|
468
|
+
options,
|
|
469
|
+
zod,
|
|
470
|
+
injectedFactories,
|
|
471
|
+
enumZero
|
|
472
|
+
)
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
if (deep) {
|
|
476
|
+
for (const variant of ["Create", "Update"]) {
|
|
477
|
+
const relations = deepRelations(
|
|
478
|
+
entity,
|
|
479
|
+
variant,
|
|
480
|
+
namespace,
|
|
481
|
+
void 0
|
|
482
|
+
).filter((r) => r.many);
|
|
483
|
+
for (const rel of relations) {
|
|
484
|
+
methods.push(
|
|
485
|
+
renderBuilderMethod(
|
|
486
|
+
entity,
|
|
487
|
+
namespace,
|
|
488
|
+
variant,
|
|
489
|
+
options,
|
|
490
|
+
zod,
|
|
491
|
+
rel,
|
|
492
|
+
injectedFactories
|
|
493
|
+
)
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
const body = [
|
|
499
|
+
ctor,
|
|
500
|
+
...methods.map((m) => `
|
|
501
|
+
${m.split("\n").join("\n ")}
|
|
502
|
+
`)
|
|
503
|
+
].join("").trimEnd();
|
|
504
|
+
return `@Injectable({ providedIn: 'root' })
|
|
505
|
+
export class ${className} {
|
|
506
|
+
${body}
|
|
507
|
+
}`;
|
|
508
|
+
}
|
|
509
|
+
function renderFactoryMethod(entity, namespace, variant, options, zod, injectedFactories, enumZero) {
|
|
510
|
+
const deep = options.relations === "deep";
|
|
511
|
+
const family = deep ? "Deep" : "";
|
|
512
|
+
const typeRole = deep && variant === "Create" ? "createDeepType" : deep && variant === "Update" ? "updateDeepType" : variant === "Create" ? "createType" : "updateType";
|
|
513
|
+
const schemaRole = deep && variant === "Create" ? "createDeepSchema" : deep && variant === "Update" ? "updateDeepSchema" : variant === "Create" ? "createSchema" : "updateSchema";
|
|
514
|
+
const typeId = zodSymbol(zod, namespace, entity.name, typeRole);
|
|
515
|
+
const schemaId = zodSymbol(zod, namespace, entity.name, schemaRole);
|
|
516
|
+
const interfaceName = controlsTypeName(entity.name, variant, family);
|
|
517
|
+
const formType = formTypeName(entity.name, variant, family);
|
|
518
|
+
const methodName = factoryMethod(variant);
|
|
519
|
+
const zodEnumTypeName = (ref) => ref;
|
|
520
|
+
const fields = variantFields(entity, variant);
|
|
521
|
+
const lines = fields.map((field) => {
|
|
522
|
+
const typeArg = controlType(field, zodEnumTypeName);
|
|
523
|
+
const accessor = variant === "Create" ? `init?.${field.name}` : `value.${field.name}`;
|
|
524
|
+
const source = `${accessor} ?? ${initExpr(field, enumZero)}`;
|
|
525
|
+
return ` ${field.name}: ${controlExpr(field, typeArg, source)},`;
|
|
526
|
+
});
|
|
527
|
+
const relations = deep ? deepRelations(entity, variant, namespace, void 0) : [];
|
|
528
|
+
const relationLines = relations.map((r) => {
|
|
529
|
+
if (r.many) {
|
|
530
|
+
const targetControls = controlsTypeName(
|
|
531
|
+
r.relation.target.entity,
|
|
532
|
+
variant,
|
|
533
|
+
"Deep"
|
|
534
|
+
);
|
|
535
|
+
return ` ${r.entry.name}: new FormArray<FormGroup<${targetControls}>>([]),`;
|
|
536
|
+
}
|
|
537
|
+
const factoryParam = injectedFactories.get(r.relation.target.entity) ?? `${lowerFirst2(r.relation.target.entity)}FormFactory`;
|
|
538
|
+
const nestedArg = variant === "Create" ? `init?.${r.relation.name}` : `value.${r.relation.name}!`;
|
|
539
|
+
return ` ${r.entry.name}: this.${factoryParam}.${factoryMethod(variant)}(${nestedArg}),`;
|
|
540
|
+
});
|
|
541
|
+
const groupBody = [...lines, ...relationLines].join("\n");
|
|
542
|
+
const paramList = variant === "Create" ? `init?: Partial<${typeId}>` : `value: ${typeId}`;
|
|
543
|
+
return `${methodName}(${paramList}): ${formType} {
|
|
544
|
+
return new FormGroup<${interfaceName}>({
|
|
545
|
+
${groupBody}
|
|
546
|
+
}, { validators: [zodValidator(${schemaId})] });
|
|
547
|
+
}`;
|
|
548
|
+
}
|
|
549
|
+
function renderBuilderMethod(entity, namespace, variant, options, zod, rel, injectedFactories) {
|
|
550
|
+
const deep = options.relations === "deep";
|
|
551
|
+
const target = rel.relation.target.entity;
|
|
552
|
+
const factoryParam = injectedFactories.get(target) ?? `${lowerFirst2(target)}FormFactory`;
|
|
553
|
+
const formType = formTypeName(entity.name, variant, "Deep");
|
|
554
|
+
const targetFormType = rel.targetFormType;
|
|
555
|
+
const method = rel.builderMethod;
|
|
556
|
+
const targetTypeRole = deep && variant === "Create" ? "createDeepType" : deep && variant === "Update" ? "updateDeepType" : variant === "Create" ? "createType" : "updateType";
|
|
557
|
+
const targetTypeId = zodSymbol(zod, namespace, target, targetTypeRole);
|
|
558
|
+
const param = variant === "Create" ? `init?: Partial<${targetTypeId}>` : `value: ${targetTypeId}`;
|
|
559
|
+
const createCall = `this.${factoryParam}.${factoryMethod(variant)}(${variant === "Create" ? "init" : "value"})`;
|
|
560
|
+
return `${method}(form: ${formType}, ${param}): ${targetFormType} {
|
|
561
|
+
const group = ${createCall};
|
|
562
|
+
form.controls.${rel.relation.name}.push(group);
|
|
563
|
+
return group;
|
|
564
|
+
}`;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// src/render/signal.ts
|
|
568
|
+
function signalEntity(entity, namespace, source, options, zod, imports, logger) {
|
|
569
|
+
const deep = options.relations === "deep";
|
|
570
|
+
const enumZero = enumZeroFromSource(source, entity);
|
|
571
|
+
imports.value("@angular/forms/signals", "schema");
|
|
572
|
+
imports.value("@angular/forms/signals", "form");
|
|
573
|
+
imports.type("@angular/forms/signals", "FieldTree");
|
|
574
|
+
imports.value("@angular/core", "signal");
|
|
575
|
+
const blocks = [];
|
|
576
|
+
for (const variant of ["Create", "Update"]) {
|
|
577
|
+
const typeRole = deep && variant === "Create" ? "createDeepType" : deep && variant === "Update" ? "updateDeepType" : variant === "Create" ? "createType" : "updateType";
|
|
578
|
+
const schemaRole = deep && variant === "Create" ? "createDeepSchema" : deep && variant === "Update" ? "updateDeepSchema" : variant === "Create" ? "createSchema" : "updateSchema";
|
|
579
|
+
const module = zodModule(zod, namespace, entity.name);
|
|
580
|
+
const typeId = zodSymbol(zod, namespace, entity.name, typeRole);
|
|
581
|
+
const schemaId = zodSymbol(zod, namespace, entity.name, schemaRole);
|
|
582
|
+
imports.type(module, typeId);
|
|
583
|
+
imports.value(module, schemaId);
|
|
584
|
+
imports.value(`${namespace}/angular/zod-forms.runtime`, "zodTreeValidate");
|
|
585
|
+
const fields = variantFields(entity, variant);
|
|
586
|
+
const fieldLines = fields.map(
|
|
587
|
+
(field) => ` ${field.name}: init?.${field.name} ?? ${initExpr(field, enumZero)},`
|
|
588
|
+
);
|
|
589
|
+
const relations = deep ? deepRelations(entity, variant, namespace, logger) : [];
|
|
590
|
+
const relationLines = relations.map((rel) => {
|
|
591
|
+
if (rel.many) {
|
|
592
|
+
return ` ${rel.relation.name}: [],`;
|
|
593
|
+
}
|
|
594
|
+
const targetModule = `${namespace}/angular/${rel.relation.target.entity}.form`;
|
|
595
|
+
const targetModelFactory = modelFactoryName(
|
|
596
|
+
rel.relation.target.entity,
|
|
597
|
+
variant
|
|
598
|
+
);
|
|
599
|
+
imports.value(targetModule, targetModelFactory);
|
|
600
|
+
return ` ${rel.relation.name}: ${targetModelFactory}(init?.${rel.relation.name}),`;
|
|
601
|
+
});
|
|
602
|
+
const modelBody = [...fieldLines, ...relationLines].join("\n");
|
|
603
|
+
const modelName = modelFactoryName(entity.name, variant);
|
|
604
|
+
blocks.push(
|
|
605
|
+
`export function ${modelName}(init?: Partial<${typeId}>): ${typeId} {
|
|
606
|
+
return {
|
|
607
|
+
${modelBody}
|
|
608
|
+
};
|
|
609
|
+
}`
|
|
610
|
+
);
|
|
611
|
+
const schemaConst = signalSchemaName(entity.name, variant);
|
|
612
|
+
blocks.push(
|
|
613
|
+
`export const ${schemaConst} = schema<${typeId}>((path) => {
|
|
614
|
+
zodTreeValidate(path, ${schemaId});
|
|
615
|
+
});`
|
|
616
|
+
);
|
|
617
|
+
const formFactoryName = signalFormFactoryName(entity.name, variant);
|
|
618
|
+
blocks.push(
|
|
619
|
+
`export function ${formFactoryName}(init?: Partial<${typeId}>): FieldTree<${typeId}> {
|
|
620
|
+
return form(signal(${modelName}(init)), ${schemaConst});
|
|
621
|
+
}`
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
return blocks.join("\n\n");
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// src/emit/entity.ts
|
|
628
|
+
function emitEntity(entity, namespace, source, options, zod, logger) {
|
|
629
|
+
const imports = new ImportsRecorder();
|
|
630
|
+
const blocks = [];
|
|
631
|
+
if (options.forms.includes("reactive")) {
|
|
632
|
+
blocks.push(
|
|
633
|
+
reactiveEntity(entity, namespace, source, options, zod, imports, logger)
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
if (options.forms.includes("signal")) {
|
|
637
|
+
blocks.push(
|
|
638
|
+
signalEntity(entity, namespace, source, options, zod, imports, logger)
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
const importBlock = imports.render();
|
|
642
|
+
return `${[importBlock, "", ...blocks].join("\n").trimEnd()}
|
|
643
|
+
`;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// src/emit/runtime.ts
|
|
647
|
+
var ZOD_VALIDATOR = `export function zodValidator(schema: ZodType): ValidatorFn {
|
|
648
|
+
return (group: AbstractControl) => {
|
|
649
|
+
const result = schema.safeParse(group.getRawValue());
|
|
650
|
+
const touched = new Set<AbstractControl>();
|
|
651
|
+
const rootIssues: { path: (string | number)[]; message: string }[] = [];
|
|
652
|
+
|
|
653
|
+
if (!result.success) {
|
|
654
|
+
for (const issue of result.error.issues) {
|
|
655
|
+
const path = issue.path.map(String).join('.');
|
|
656
|
+
const control = path === '' ? null : group.get(path);
|
|
657
|
+
if (control !== null && control !== undefined) {
|
|
658
|
+
setZodError(control, issue.message);
|
|
659
|
+
touched.add(control);
|
|
660
|
+
} else {
|
|
661
|
+
rootIssues.push({ path: issue.path as (string | number)[], message: issue.message });
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
for (const control of collectControls(group)) {
|
|
667
|
+
if (control !== group && !touched.has(control)) {
|
|
668
|
+
clearZodError(control);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if (rootIssues.length === 0) {
|
|
673
|
+
clearZodError(group);
|
|
674
|
+
return null;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const formErrors: string[] = [];
|
|
678
|
+
const fieldErrors: Record<string, string[]> = {};
|
|
679
|
+
for (const issue of rootIssues) {
|
|
680
|
+
if (issue.path.length === 0) {
|
|
681
|
+
formErrors.push(issue.message);
|
|
682
|
+
} else {
|
|
683
|
+
const key = String(issue.path[0]);
|
|
684
|
+
(fieldErrors[key] ??= []).push(issue.message);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const zodError = { formErrors, fieldErrors };
|
|
689
|
+
setZodError(group, zodError);
|
|
690
|
+
return { zod: zodError };
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function setZodError(control: AbstractControl, message: unknown): void {
|
|
695
|
+
const current = control.errors;
|
|
696
|
+
if (current !== null && sameZodError(current.zod, message)) {
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
control.setErrors({ ...current, zod: message }, { emitEvent: false });
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function clearZodError(control: AbstractControl): void {
|
|
703
|
+
const current = control.errors;
|
|
704
|
+
if (current === null || current === undefined || !('zod' in current)) {
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
const { zod: _discard, ...rest } = current;
|
|
708
|
+
control.setErrors(Object.keys(rest).length > 0 ? rest : null, {
|
|
709
|
+
emitEvent: false,
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function sameZodError(a: unknown, b: unknown): boolean {
|
|
714
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function collectControls(control: AbstractControl): AbstractControl[] {
|
|
718
|
+
const out: AbstractControl[] = [control];
|
|
719
|
+
const children = (control as { controls?: unknown }).controls;
|
|
720
|
+
if (children !== null && typeof children === 'object') {
|
|
721
|
+
for (const child of Object.values(children as Record<string, AbstractControl>)) {
|
|
722
|
+
out.push(...collectControls(child));
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
return out;
|
|
726
|
+
}`;
|
|
727
|
+
var ZOD_TREE_VALIDATE = `export function zodTreeValidate<T>(
|
|
728
|
+
path: SchemaPath<T>,
|
|
729
|
+
schema: ZodType<T>,
|
|
730
|
+
): void {
|
|
731
|
+
validateTree(path, (ctx) => {
|
|
732
|
+
const result = schema.safeParse(ctx.value());
|
|
733
|
+
if (result.success) {
|
|
734
|
+
return undefined;
|
|
735
|
+
}
|
|
736
|
+
return result.error.issues.map((issue) => ({
|
|
737
|
+
kind: 'custom' as const,
|
|
738
|
+
message: issue.message,
|
|
739
|
+
// Dynamically walked from the Zod issue path against the field tree's
|
|
740
|
+
// runtime shape; ValidationError.fieldTree accepts undefined for a
|
|
741
|
+
// pathless issue, so a same-shaped object (rather than branching on
|
|
742
|
+
// whether one was found) keeps this a single, uniform return type.
|
|
743
|
+
fieldTree: resolveFieldTree(ctx.fieldTree, issue.path) as
|
|
744
|
+
| ReadonlyFieldTree<unknown>
|
|
745
|
+
| undefined,
|
|
746
|
+
}));
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function resolveFieldTree(root: unknown, path: readonly PropertyKey[]): unknown {
|
|
751
|
+
return path.reduce<unknown>((node, key) => {
|
|
752
|
+
if (node === null || typeof node !== 'object') {
|
|
753
|
+
return undefined;
|
|
754
|
+
}
|
|
755
|
+
return (node as Record<PropertyKey, unknown>)[key];
|
|
756
|
+
}, root);
|
|
757
|
+
}`;
|
|
758
|
+
function emitRuntime(_source, options) {
|
|
759
|
+
const reactive = options.forms.includes("reactive");
|
|
760
|
+
const signal = options.forms.includes("signal");
|
|
761
|
+
const imports = [];
|
|
762
|
+
if (reactive) {
|
|
763
|
+
imports.push(
|
|
764
|
+
"import type { AbstractControl, ValidatorFn } from '@angular/forms';"
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
if (signal) {
|
|
768
|
+
imports.push(
|
|
769
|
+
"import type { ReadonlyFieldTree, SchemaPath } from '@angular/forms/signals';",
|
|
770
|
+
"import { validateTree } from '@angular/forms/signals';"
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
imports.push("import type { ZodType } from 'zod';");
|
|
774
|
+
const blocks = [];
|
|
775
|
+
if (reactive) {
|
|
776
|
+
blocks.push(ZOD_VALIDATOR);
|
|
777
|
+
}
|
|
778
|
+
if (signal) {
|
|
779
|
+
blocks.push(ZOD_TREE_VALIDATE);
|
|
780
|
+
}
|
|
781
|
+
return `${[imports.join("\n"), "", ...blocks].join("\n").trimEnd()}
|
|
782
|
+
`;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// src/options.ts
|
|
786
|
+
import * as v from "valibot";
|
|
787
|
+
var AngularGeneratorOptions = v.object({
|
|
788
|
+
/** Which form surfaces to emit. Default: both. */
|
|
789
|
+
forms: v.optional(v.array(v.picklist(["reactive", "signal"])), [
|
|
790
|
+
"reactive",
|
|
791
|
+
"signal"
|
|
792
|
+
]),
|
|
793
|
+
/** Relation handling: flat (FK scalars only) or deep (nested FormGroup / FormArray). */
|
|
794
|
+
relations: v.optional(v.picklist(["flat", "deep"]), "flat")
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
// src/generator.ts
|
|
798
|
+
var angularGenerator = defineGenerator({
|
|
799
|
+
name: "angular",
|
|
800
|
+
dependsOn: ["zod"],
|
|
801
|
+
optionsSchema: AngularGeneratorOptions,
|
|
802
|
+
generate(ctx, options) {
|
|
803
|
+
const zod = ctx.dependencies.zod;
|
|
804
|
+
if (zod === void 0) {
|
|
805
|
+
throw new Error(
|
|
806
|
+
"gen-angular: 'zod' dependency artifact is missing at runtime despite dependsOn: ['zod']"
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
const files = [];
|
|
810
|
+
for (const [namespace, source] of Object.entries(ctx.ir.sources)) {
|
|
811
|
+
const prefix = `${namespace}/angular`;
|
|
812
|
+
const entities = Object.values(source.entities);
|
|
813
|
+
if (entities.length > 0 && options.forms.length > 0) {
|
|
814
|
+
files.push({
|
|
815
|
+
path: `${prefix}/zod-forms.runtime.ts`,
|
|
816
|
+
content: emitRuntime(source, options)
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
for (const entity of entities) {
|
|
820
|
+
files.push({
|
|
821
|
+
path: `${prefix}/${entity.name}.form.ts`,
|
|
822
|
+
content: emitEntity(
|
|
823
|
+
entity,
|
|
824
|
+
namespace,
|
|
825
|
+
source,
|
|
826
|
+
options,
|
|
827
|
+
zod,
|
|
828
|
+
ctx.logger
|
|
829
|
+
)
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
files.push({
|
|
833
|
+
path: `${prefix}/index.ts`,
|
|
834
|
+
content: emitBarrel(source, options)
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
return { files, artifact: buildArtifact(ctx.ir, zod, options) };
|
|
838
|
+
}
|
|
839
|
+
});
|
|
840
|
+
export {
|
|
841
|
+
AngularGenError,
|
|
842
|
+
AngularGeneratorOptions,
|
|
843
|
+
MissingZodNamespaceError,
|
|
844
|
+
MissingZodSymbolError,
|
|
845
|
+
angularGenerator
|
|
846
|
+
};
|
|
847
|
+
//# sourceMappingURL=index.js.map
|