@specverse/engines 4.2.2 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/dist/inference/core/specly-converter.d.ts.map +1 -1
  2. package/dist/inference/core/specly-converter.js +43 -22
  3. package/dist/inference/core/specly-converter.js.map +1 -1
  4. package/dist/inference/index.d.ts.map +1 -1
  5. package/dist/inference/index.js +29 -0
  6. package/dist/inference/index.js.map +1 -1
  7. package/dist/libs/instance-factories/applications/templates/react/index-html-generator.js +2 -2
  8. package/dist/libs/instance-factories/applications/templates/react-starter/app-tsx-generator.js +291 -59
  9. package/dist/libs/instance-factories/applications/templates/react-starter/belongs-to.js +37 -0
  10. package/dist/libs/instance-factories/applications/templates/react-starter/dashboard-body-composer.js +9 -124
  11. package/dist/libs/instance-factories/applications/templates/react-starter/detail-body-composer.js +9 -75
  12. package/dist/libs/instance-factories/applications/templates/react-starter/emit-jsx-source.js +129 -0
  13. package/dist/libs/instance-factories/applications/templates/react-starter/form-body-composer.js +9 -212
  14. package/dist/libs/instance-factories/applications/templates/react-starter/helpers-emitter.js +260 -5
  15. package/dist/libs/instance-factories/applications/templates/react-starter/list-body-composer.js +8 -50
  16. package/dist/libs/instance-factories/applications/templates/react-starter/operation-emitters.js +470 -0
  17. package/dist/libs/instance-factories/applications/templates/react-starter/operation-view-generator.js +136 -0
  18. package/dist/libs/instance-factories/applications/templates/react-starter/package-json-generator.js +2 -1
  19. package/dist/libs/instance-factories/applications/templates/react-starter/skeletons/detail.tsx.template +54 -61
  20. package/dist/libs/instance-factories/applications/templates/react-starter/skeletons/form.tsx.template +115 -27
  21. package/dist/libs/instance-factories/applications/templates/react-starter/skeletons/list.tsx.template +17 -9
  22. package/dist/libs/instance-factories/applications/templates/react-starter/use-api-hooks-starter-generator.js +49 -10
  23. package/dist/libs/instance-factories/applications/templates/react-starter/view-emitter.js +223 -10
  24. package/dist/libs/instance-factories/applications/templates/react-starter/views-generator.js +14 -1
  25. package/dist/libs/instance-factories/controllers/templates/fastify/routes-generator.js +13 -1
  26. package/dist/libs/instance-factories/controllers/templates/fastify/server-generator.js +18 -0
  27. package/libs/instance-factories/applications/templates/react/index-html-generator.ts +2 -2
  28. package/libs/instance-factories/applications/templates/react-starter/__tests__/dashboard-body-composer.test.ts +3 -1
  29. package/libs/instance-factories/applications/templates/react-starter/__tests__/detail-body-composer.test.ts +24 -25
  30. package/libs/instance-factories/applications/templates/react-starter/__tests__/list-body-composer.test.ts +3 -3
  31. package/libs/instance-factories/applications/templates/react-starter/__tests__/orchestrator.test.ts +2 -0
  32. package/libs/instance-factories/applications/templates/react-starter/__tests__/parity-p3-rendered-output.test.ts +1 -1
  33. package/libs/instance-factories/applications/templates/react-starter/__tests__/starter-generators.test.ts +5 -4
  34. package/libs/instance-factories/applications/templates/react-starter/__tests__/views-generator.test.ts +11 -4
  35. package/libs/instance-factories/applications/templates/react-starter/app-tsx-generator.ts +377 -71
  36. package/libs/instance-factories/applications/templates/react-starter/belongs-to.ts +66 -0
  37. package/libs/instance-factories/applications/templates/react-starter/dashboard-body-composer.ts +15 -182
  38. package/libs/instance-factories/applications/templates/react-starter/detail-body-composer.ts +16 -128
  39. package/libs/instance-factories/applications/templates/react-starter/emit-jsx-source.ts +233 -0
  40. package/libs/instance-factories/applications/templates/react-starter/form-body-composer.ts +16 -376
  41. package/libs/instance-factories/applications/templates/react-starter/helpers-emitter.ts +282 -4
  42. package/libs/instance-factories/applications/templates/react-starter/list-body-composer.ts +26 -135
  43. package/libs/instance-factories/applications/templates/react-starter/operation-emitters.ts +497 -0
  44. package/libs/instance-factories/applications/templates/react-starter/operation-view-generator.ts +209 -0
  45. package/libs/instance-factories/applications/templates/react-starter/package-json-generator.ts +1 -0
  46. package/libs/instance-factories/applications/templates/react-starter/skeletons/detail.tsx.template +54 -61
  47. package/libs/instance-factories/applications/templates/react-starter/skeletons/form.tsx.template +115 -27
  48. package/libs/instance-factories/applications/templates/react-starter/skeletons/list.tsx.template +17 -9
  49. package/libs/instance-factories/applications/templates/react-starter/use-api-hooks-starter-generator.ts +71 -10
  50. package/libs/instance-factories/applications/templates/react-starter/view-emitter.ts +359 -18
  51. package/libs/instance-factories/applications/templates/react-starter/views-generator.ts +28 -1
  52. package/libs/instance-factories/controllers/templates/fastify/routes-generator.ts +13 -1
  53. package/libs/instance-factories/controllers/templates/fastify/server-generator.ts +18 -0
  54. package/package.json +2 -2
@@ -1,79 +1,13 @@
1
- import { METADATA_FIELDS } from "@specverse/runtime/views/core";
2
- import { extractBelongsToTargets } from "./belongs-to.js";
3
- const METADATA_FIELD_NAMES = new Set(METADATA_FIELDS);
1
+ import { walkPattern } from "@specverse/runtime/views/core";
2
+ import { emitJsxSource } from "./emit-jsx-source.js";
4
3
  function composeDetailBody(context) {
5
- const belongsTo = extractBelongsToTargets(context.model);
6
- const fkExcludes = new Set(belongsTo.map((r) => `${r.name}Id`));
7
- const { business, metadata } = partitionFields(context.model, fkExcludes);
8
- const lines = [];
9
- lines.push('<div className="rounded-lg border border-gray-200 bg-white p-6 dark:border-gray-700 dark:bg-gray-900">');
10
- if (business.length > 0) {
11
- lines.push(' <dl className="grid grid-cols-1 gap-4 sm:grid-cols-2">');
12
- for (const field of business) {
13
- lines.push(...renderField(field, { muted: false }));
14
- }
15
- lines.push(" </dl>");
16
- } else if (belongsTo.length === 0) {
17
- lines.push(' <p className="text-sm text-gray-400">No business fields defined for this model.</p>');
18
- }
19
- if (belongsTo.length > 0) {
20
- lines.push("");
21
- lines.push(' <dl className="mt-6 grid grid-cols-1 gap-4 sm:grid-cols-2 border-t border-gray-200 dark:border-gray-700 pt-4">');
22
- for (const rel of belongsTo) {
23
- lines.push(...renderRelationshipField(rel));
24
- }
25
- lines.push(" </dl>");
26
- }
27
- if (metadata.length > 0) {
28
- lines.push("");
29
- lines.push(' <dl className="mt-6 grid grid-cols-1 gap-2 sm:grid-cols-2 border-t border-gray-200 dark:border-gray-700 pt-4 text-xs text-gray-500 dark:text-gray-400">');
30
- for (const field of metadata) {
31
- lines.push(...renderField(field, { muted: true }));
32
- }
33
- lines.push(" </dl>");
34
- }
35
- lines.push("</div>");
36
- return lines.join("\n");
37
- }
38
- function partitionFields(model, exclude = /* @__PURE__ */ new Set()) {
39
- const attrs = Object.keys(model.attributes ?? {});
40
- const business = [];
41
- const metadata = [];
42
- for (const name of attrs) {
43
- if (exclude.has(name)) continue;
44
- const field = { name, label: humanize(name) };
45
- if (METADATA_FIELD_NAMES.has(name)) {
46
- metadata.push(field);
47
- } else {
48
- business.push(field);
49
- }
50
- }
51
- return { business, metadata };
52
- }
53
- function renderRelationshipField(rel) {
54
- const label = humanize(rel.name);
55
- const fkName = `${rel.name}Id`;
56
- const optionsVar = `${rel.name}Options`;
57
- return [
58
- " <div>",
59
- ` <dt className="text-sm font-medium text-gray-500 dark:text-gray-400">${label}</dt>`,
60
- ` <dd className="mt-1 text-sm text-gray-900 dark:text-gray-100 break-words">{resolveEntityDisplayName((item as any).${fkName}, ${optionsVar})}</dd>`,
61
- " </div>"
62
- ];
63
- }
64
- function renderField(field, opts) {
65
- const label = field.label ?? humanize(field.name);
66
- const labelCls = opts.muted ? "font-medium uppercase tracking-wide text-gray-400 dark:text-gray-500" : "text-sm font-medium text-gray-500 dark:text-gray-400";
67
- const valueCls = opts.muted ? "text-gray-500 dark:text-gray-400" : "mt-1 text-sm text-gray-900 dark:text-gray-100 break-words";
68
- return [
69
- " <div>",
70
- ` <dt className="${labelCls}">${label}</dt>`,
71
- ` <dd className="${valueCls}">{String((item as any).${field.name} ?? '')}</dd>`,
72
- " </div>"
73
- ];
74
- }
75
- function humanize(name) {
76
- return name.replace(/([A-Z])/g, " $1").replace(/^./, (c) => c.toUpperCase()).trim();
4
+ const imports = /* @__PURE__ */ new Set();
5
+ const nodes = walkPattern("detail-view", {
6
+ model: context.model,
7
+ modelSchemas: context.modelSchemas,
8
+ view: context.view
9
+ });
10
+ return emitJsxSource(nodes, { imports, indent: 6 });
77
11
  }
78
12
  export {
79
13
  composeDetailBody
@@ -0,0 +1,129 @@
1
+ function emitJsxSource(nodes, options) {
2
+ const indent = options.indent ?? 0;
3
+ return nodes.map((n) => emitNode(n, indent, options.imports)).join("\n");
4
+ }
5
+ const ATTR_BREAK_WIDTH = 80;
6
+ function emitNode(node, indent, imports) {
7
+ const pad = " ".repeat(indent);
8
+ if (node.component === "__text__") {
9
+ return `${pad}${node.props?.text ?? ""}`;
10
+ }
11
+ if (node.component === "__todo__") {
12
+ const msg = node.props?.message ?? "TODO";
13
+ return `${pad}{/* TODO: ${msg} */}`;
14
+ }
15
+ if (node.component === "__when_empty__") {
16
+ const source = node.props?.source ?? "items";
17
+ const inner = (node.children ?? []).map((c) => emitNode(c, indent + 2, imports)).join("\n");
18
+ return [
19
+ `${pad}{${source}.length === 0 && (`,
20
+ inner,
21
+ `${pad})}`
22
+ ].join("\n");
23
+ }
24
+ if (node.component === "__when__") {
25
+ const condition = node.props?.condition ?? "true";
26
+ const inner = (node.children ?? []).map((c) => emitNode(c, indent + 2, imports)).join("\n");
27
+ return [
28
+ `${pad}{${condition} && (`,
29
+ inner,
30
+ `${pad})}`
31
+ ].join("\n");
32
+ }
33
+ const isComposite = /^[A-Z]/.test(node.component);
34
+ if (isComposite) imports.add(node.component);
35
+ const attrParts = formatAttrParts(node.props, node.expressions);
36
+ const inlineAttrs = attrParts.length > 0 ? " " + attrParts.join(" ") : "";
37
+ const hasInlineExpression = Boolean(
38
+ node.expressions?.children && (!node.children || node.children.length === 0)
39
+ );
40
+ const hasSingleTextChild = Boolean(
41
+ node.children?.length === 1 && node.children[0].component === "__text__" && !node.children[0].iterate
42
+ );
43
+ const isCompact = hasInlineExpression || hasSingleTextChild;
44
+ const shouldBreakAttrs = !isCompact && inlineAttrs.length > ATTR_BREAK_WIDTH;
45
+ const multiLineAttrs = attrParts.length > 0 ? "\n" + attrParts.map((a) => " ".repeat(indent + 2) + a).join("\n") + "\n" + pad : "";
46
+ const attrs = shouldBreakAttrs ? multiLineAttrs : inlineAttrs;
47
+ if (hasInlineExpression) {
48
+ return `${pad}<${node.component}${attrs}>{${node.expressions.children}}</${node.component}>`;
49
+ }
50
+ if (hasSingleTextChild) {
51
+ const text = node.children[0].props?.text ?? "";
52
+ return `${pad}<${node.component}${attrs}>${text}</${node.component}>`;
53
+ }
54
+ const bodyLines = renderChildren(node, indent + 2, imports);
55
+ if (VOID_ELEMENTS.has(node.component) && bodyLines === null) {
56
+ return `${pad}<${node.component}${attrs} />`;
57
+ }
58
+ if (bodyLines === null || bodyLines === "") {
59
+ return `${pad}<${node.component}${attrs} />`;
60
+ }
61
+ return [
62
+ `${pad}<${node.component}${attrs}>`,
63
+ bodyLines,
64
+ `${pad}</${node.component}>`
65
+ ].join("\n");
66
+ }
67
+ function renderChildren(node, indent, imports) {
68
+ const pad = " ".repeat(indent);
69
+ const children = node.children ?? [];
70
+ if (node.iterate) {
71
+ const { source, itemName, keyExpression } = node.iterate;
72
+ return children.map((c) => emitNode(c, indent, imports)).join("\n");
73
+ }
74
+ if (children.length === 0 && !node.expressions?.children) return null;
75
+ if (children.length === 0 && node.expressions?.children) return "";
76
+ return children.map((c) => {
77
+ if (c.iterate) return emitIterated(c, indent, imports);
78
+ return emitNode(c, indent, imports);
79
+ }).join("\n");
80
+ }
81
+ function emitIterated(node, indent, imports) {
82
+ const pad = " ".repeat(indent);
83
+ const { source, itemName, keyExpression } = node.iterate;
84
+ const inner = emitNode({ ...node, iterate: void 0 }, indent + 2, imports);
85
+ const args = keyExpression ? `(${itemName}, ${keyExpression})` : `(${itemName})`;
86
+ return [
87
+ `${pad}{${source}.map(${args} => (`,
88
+ inner,
89
+ `${pad}))}`
90
+ ].join("\n");
91
+ }
92
+ function formatAttrParts(props, expressions) {
93
+ const parts = [];
94
+ for (const [k, v] of Object.entries(props ?? {})) {
95
+ if (v === void 0 || v === null) continue;
96
+ parts.push(formatStaticProp(k, v));
97
+ }
98
+ for (const [k, expr] of Object.entries(expressions ?? {})) {
99
+ if (k === "children") continue;
100
+ parts.push(`${k}={${expr}}`);
101
+ }
102
+ return parts;
103
+ }
104
+ function formatStaticProp(name, value) {
105
+ if (value === true) return name;
106
+ if (typeof value === "string") return `${name}=${JSON.stringify(value)}`;
107
+ if (typeof value === "number" || typeof value === "boolean") {
108
+ return `${name}={${value}}`;
109
+ }
110
+ return `${name}={${JSON.stringify(value)}}`;
111
+ }
112
+ const VOID_ELEMENTS = /* @__PURE__ */ new Set([
113
+ "area",
114
+ "base",
115
+ "br",
116
+ "col",
117
+ "embed",
118
+ "hr",
119
+ "img",
120
+ "input",
121
+ "link",
122
+ "meta",
123
+ "source",
124
+ "track",
125
+ "wbr"
126
+ ]);
127
+ export {
128
+ emitJsxSource
129
+ };
@@ -1,216 +1,13 @@
1
- import { METADATA_FIELDS } from "@specverse/runtime/views/core";
2
- import { extractBelongsToTargets } from "./belongs-to.js";
3
- const AUTO_GENERATED_FIELD_NAMES = new Set(METADATA_FIELDS);
1
+ import { walkPattern } from "@specverse/runtime/views/core";
2
+ import { emitJsxSource } from "./emit-jsx-source.js";
4
3
  function composeFormBody(context) {
5
- const belongsTo = extractBelongsToTargets(context.model);
6
- const shadowedFKs = new Set(belongsTo.map((rel) => `${rel.name}Id`));
7
- const fields = selectFormFields(context.model, shadowedFKs);
8
- const lines = [];
9
- lines.push('<div className="space-y-4">');
10
- if (fields.length === 0 && belongsTo.length === 0) {
11
- lines.push(' <p className="text-sm text-gray-400">No editable fields for this model.</p>');
12
- lines.push("</div>");
13
- return lines.join("\n");
14
- }
15
- for (const field of fields) {
16
- lines.push(...renderInput(field));
17
- }
18
- for (const rel of belongsTo) {
19
- lines.push(...renderRelationshipSelect(rel));
20
- }
21
- lines.push("</div>");
22
- return lines.join("\n");
23
- }
24
- function renderRelationshipSelect(rel) {
25
- const fkName = `${rel.name}Id`;
26
- const varName = `${rel.name}Options`;
27
- const label = humanize(rel.name);
28
- return [
29
- " <div>",
30
- ` <label className="${LABEL_CLS}" htmlFor="${fkName}">${label} *</label>`,
31
- ` <select`,
32
- ` id="${fkName}"`,
33
- ` className="${INPUT_CLS}"`,
34
- ` value={String((formData as any).${fkName} ?? '')}`,
35
- ` onChange={e => handleChange('${fkName}', e.target.value)} required`,
36
- ` >`,
37
- ` <option value="">\u2014 choose \u2014</option>`,
38
- ` {${varName}.map((opt: any) => (`,
39
- ` <option key={opt.id} value={opt.id}>{getEntityDisplayName(opt)}</option>`,
40
- ` ))}`,
41
- ` </select>`,
42
- " </div>"
43
- ];
44
- }
45
- function selectFormFields(model, skip = /* @__PURE__ */ new Set()) {
46
- const out = [];
47
- const attrs = model.attributes ?? {};
48
- const lifecycleStates = extractLifecycleStates(model);
49
- for (const [name, rawDef] of Object.entries(attrs)) {
50
- if (AUTO_GENERATED_FIELD_NAMES.has(name)) continue;
51
- if (skip.has(name)) continue;
52
- const def = rawDef;
53
- if (def?.auto) continue;
54
- const type = def?.type ?? parseTypeFromConvention(def) ?? "String";
55
- const required = def?.required === true || hasConventionFlag(def, "required");
56
- const declaredValues = def?.values;
57
- const lifecycleValues = lifecycleStates.get(name);
58
- const values = declaredValues ?? lifecycleValues;
59
- const maxLength = def?.maxLength ?? def?.max;
60
- const isLongString = typeof maxLength === "number" && maxLength > 100;
61
- out.push({
62
- name,
63
- label: humanize(name),
64
- type: normalizeType(type),
65
- required,
66
- values,
67
- isLongString
68
- });
69
- }
70
- return out;
71
- }
72
- function extractLifecycleStates(model) {
73
- const out = /* @__PURE__ */ new Map();
74
- const lifecycles = model.lifecycles ?? {};
75
- for (const [name, rawDef] of Object.entries(lifecycles)) {
76
- if (!rawDef || typeof rawDef !== "object") continue;
77
- const def = rawDef;
78
- if (Array.isArray(def.states) && def.states.length > 0) {
79
- const names = def.states.map((s) => typeof s === "string" ? s : s?.name ?? s?.id ?? "").filter(Boolean);
80
- if (names.length > 0) {
81
- out.set(name, names);
82
- continue;
83
- }
84
- }
85
- if (typeof def.flow === "string") {
86
- const states = def.flow.split(/\s*(?:->|,)\s*/).map((s) => s.trim()).filter(Boolean);
87
- if (states.length > 0) out.set(name, states);
88
- }
89
- }
90
- return out;
91
- }
92
- function parseTypeFromConvention(def) {
93
- if (typeof def === "string") {
94
- const first = def.trim().split(/\s+/)[0];
95
- return first;
96
- }
97
- return void 0;
98
- }
99
- function hasConventionFlag(def, flag) {
100
- if (typeof def === "string") {
101
- return def.split(/\s+/).includes(flag);
102
- }
103
- return false;
104
- }
105
- function normalizeType(type) {
106
- return type.replace(/[^A-Za-z].*$/, "");
107
- }
108
- const INPUT_CLS = "w-full rounded border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100";
109
- const LABEL_CLS = "block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1";
110
- function renderInput(field) {
111
- const requiredMark = field.required ? " *" : "";
112
- const reqAttr = field.required ? " required" : "";
113
- if (field.values && field.values.length > 0) {
114
- const options = field.values.map((v) => ` <option value="${escapeAttr(v)}">${escapeText(v)}</option>`).join("\n");
115
- return [
116
- " <div>",
117
- ` <label className="${LABEL_CLS}" htmlFor="${field.name}">${field.label}${requiredMark}</label>`,
118
- ` <select`,
119
- ` id="${field.name}"`,
120
- ` className="${INPUT_CLS}"`,
121
- ` value={String((formData as any).${field.name} ?? '')}`,
122
- ` onChange={e => handleChange('${field.name}', e.target.value)}${reqAttr}`,
123
- ` >`,
124
- ` <option value="">\u2014 choose \u2014</option>`,
125
- options,
126
- ` </select>`,
127
- " </div>"
128
- ];
129
- }
130
- if (field.type === "Text" || field.isLongString) {
131
- return [
132
- " <div>",
133
- ` <label className="${LABEL_CLS}" htmlFor="${field.name}">${field.label}${requiredMark}</label>`,
134
- ` <textarea`,
135
- ` id="${field.name}"`,
136
- ` className="${INPUT_CLS}"`,
137
- ` rows={4}`,
138
- ` value={String((formData as any).${field.name} ?? '')}`,
139
- ` onChange={e => handleChange('${field.name}', e.target.value)}${reqAttr}`,
140
- ` />`,
141
- " </div>"
142
- ];
143
- }
144
- if (field.type === "Boolean") {
145
- return [
146
- " <div>",
147
- ` <label className="inline-flex items-center gap-2">`,
148
- ` <input`,
149
- ` id="${field.name}"`,
150
- ` type="checkbox"`,
151
- ` checked={Boolean((formData as any).${field.name})}`,
152
- ` onChange={e => handleChange('${field.name}', e.target.checked)}`,
153
- ` />`,
154
- ` <span className="text-sm text-gray-700 dark:text-gray-300">${field.label}${requiredMark}</span>`,
155
- ` </label>`,
156
- " </div>"
157
- ];
158
- }
159
- const inputType = mapInputType(field.type);
160
- return [
161
- " <div>",
162
- ` <label className="${LABEL_CLS}" htmlFor="${field.name}">${field.label}${requiredMark}</label>`,
163
- ` <input`,
164
- ` id="${field.name}"`,
165
- ` type="${inputType}"`,
166
- ` className="${INPUT_CLS}"`,
167
- ` value={String((formData as any).${field.name} ?? '')}`,
168
- ` onChange={e => handleChange('${field.name}', ${coerceOnChange(field.type)})}${reqAttr}`,
169
- ` />`,
170
- " </div>"
171
- ];
172
- }
173
- function mapInputType(type) {
174
- switch (type) {
175
- case "Integer":
176
- case "Float":
177
- case "Number":
178
- case "Money":
179
- return "number";
180
- case "DateTime":
181
- return "datetime-local";
182
- case "Date":
183
- return "date";
184
- case "Email":
185
- return "email";
186
- case "URL":
187
- return "url";
188
- case "String":
189
- case "UUID":
190
- default:
191
- return "text";
192
- }
193
- }
194
- function coerceOnChange(type) {
195
- switch (type) {
196
- case "Integer":
197
- case "Number":
198
- return "e.target.value === '' ? undefined : Number(e.target.value)";
199
- case "Float":
200
- case "Money":
201
- return "e.target.value === '' ? undefined : parseFloat(e.target.value)";
202
- default:
203
- return "e.target.value";
204
- }
205
- }
206
- function humanize(name) {
207
- return name.replace(/([A-Z])/g, " $1").replace(/^./, (c) => c.toUpperCase()).trim();
208
- }
209
- function escapeAttr(s) {
210
- return s.replace(/"/g, "&quot;");
211
- }
212
- function escapeText(s) {
213
- return s.replace(/</g, "&lt;").replace(/>/g, "&gt;");
4
+ const imports = /* @__PURE__ */ new Set();
5
+ const nodes = walkPattern("form-view", {
6
+ model: context.model,
7
+ modelSchemas: context.modelSchemas,
8
+ view: context.view
9
+ });
10
+ return emitJsxSource(nodes, { imports, indent: 6 });
214
11
  }
215
12
  export {
216
13
  composeFormBody