@specverse/engines 4.1.28 → 4.2.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 (237) hide show
  1. package/assets/examples/manifests/frontend-only.yaml +3 -6
  2. package/assets/examples/manifests/fullstack-app.yaml +5 -7
  3. package/assets/examples/manifests/fullstack-monorepo.yaml +3 -6
  4. package/dist/inference/comprehensive-engine.d.ts.map +1 -1
  5. package/dist/inference/comprehensive-engine.js +3 -19
  6. package/dist/inference/comprehensive-engine.js.map +1 -1
  7. package/dist/inference/core/rule-engine.d.ts +31 -0
  8. package/dist/inference/core/rule-engine.d.ts.map +1 -1
  9. package/dist/inference/core/rule-engine.js +117 -33
  10. package/dist/inference/core/rule-engine.js.map +1 -1
  11. package/dist/inference/core/rule-file-types.d.ts +0 -2
  12. package/dist/inference/core/rule-file-types.d.ts.map +1 -1
  13. package/dist/inference/core/rule-file-types.js +3 -6
  14. package/dist/inference/core/rule-file-types.js.map +1 -1
  15. package/dist/inference/core/rule-loader.d.ts +5 -15
  16. package/dist/inference/core/rule-loader.d.ts.map +1 -1
  17. package/dist/inference/core/rule-loader.js +43 -132
  18. package/dist/inference/core/rule-loader.js.map +1 -1
  19. package/dist/inference/core/types.d.ts +0 -6
  20. package/dist/inference/core/types.d.ts.map +1 -1
  21. package/dist/inference/core/types.js +0 -4
  22. package/dist/inference/core/types.js.map +1 -1
  23. package/dist/inference/logical/generators/component-type-resolver.d.ts +0 -26
  24. package/dist/inference/logical/generators/component-type-resolver.d.ts.map +1 -1
  25. package/dist/inference/logical/generators/component-type-resolver.js +0 -19
  26. package/dist/inference/logical/generators/component-type-resolver.js.map +1 -1
  27. package/dist/inference/logical/generators/specialist-view-expander.d.ts +1 -17
  28. package/dist/inference/logical/generators/specialist-view-expander.d.ts.map +1 -1
  29. package/dist/inference/logical/generators/specialist-view-expander.js +0 -15
  30. package/dist/inference/logical/generators/specialist-view-expander.js.map +1 -1
  31. package/dist/inference/logical/generators/view-generator.d.ts +4 -14
  32. package/dist/inference/logical/generators/view-generator.d.ts.map +1 -1
  33. package/dist/inference/logical/generators/view-generator.js +6 -26
  34. package/dist/inference/logical/generators/view-generator.js.map +1 -1
  35. package/dist/inference/logical/index.d.ts +2 -2
  36. package/dist/inference/logical/index.d.ts.map +1 -1
  37. package/dist/inference/logical/logical-engine.d.ts.map +1 -1
  38. package/dist/inference/logical/logical-engine.js +17 -80
  39. package/dist/inference/logical/logical-engine.js.map +1 -1
  40. package/dist/inference/quint-transpiler.d.ts +5 -3
  41. package/dist/inference/quint-transpiler.d.ts.map +1 -1
  42. package/dist/inference/quint-transpiler.js +11 -6
  43. package/dist/inference/quint-transpiler.js.map +1 -1
  44. package/dist/libs/instance-factories/applications/templates/generic/main-generator.js +3 -3
  45. package/dist/libs/instance-factories/applications/templates/react/api-client-generator.js +16 -6
  46. package/dist/libs/instance-factories/applications/templates/react-starter/app-tsx-generator.js +110 -0
  47. package/dist/libs/instance-factories/applications/templates/react-starter/dashboard-body-composer.js +121 -0
  48. package/dist/libs/instance-factories/applications/templates/react-starter/detail-body-composer.js +78 -0
  49. package/dist/libs/instance-factories/applications/templates/react-starter/form-body-composer.js +190 -0
  50. package/dist/libs/instance-factories/applications/templates/react-starter/helpers-emitter.js +45 -0
  51. package/dist/libs/instance-factories/applications/templates/react-starter/html-to-jsx.js +192 -0
  52. package/dist/libs/instance-factories/applications/templates/react-starter/list-body-composer.js +46 -0
  53. package/dist/libs/instance-factories/applications/templates/react-starter/orchestrator.js +30 -0
  54. package/dist/libs/instance-factories/applications/templates/react-starter/package-json-generator.js +38 -0
  55. package/dist/libs/instance-factories/applications/templates/react-starter/regen-safety.js +89 -0
  56. package/dist/libs/instance-factories/applications/templates/react-starter/view-emitter.js +56 -0
  57. package/dist/libs/instance-factories/applications/templates/react-starter/views-generator.js +66 -0
  58. package/dist/libs/instance-factories/cli/templates/commander/command-generator.js +14 -11
  59. package/dist/libs/instance-factories/controllers/templates/fastify/server-generator.js +11 -3
  60. package/dist/libs/instance-factories/services/templates/prisma/controller-generator.js +27 -17
  61. package/dist/libs/instance-factories/shared/path-resolver.js +1 -1
  62. package/dist/realize/index.d.ts.map +1 -1
  63. package/dist/realize/index.js +15 -22
  64. package/dist/realize/index.js.map +1 -1
  65. package/dist/registry/utils/manifest-adapter.d.ts +8 -1
  66. package/dist/registry/utils/manifest-adapter.d.ts.map +1 -1
  67. package/dist/registry/utils/manifest-adapter.js +8 -1
  68. package/dist/registry/utils/manifest-adapter.js.map +1 -1
  69. package/libs/instance-factories/applications/react-app-starter.yaml +150 -0
  70. package/libs/instance-factories/applications/templates/generic/main-generator.ts +3 -3
  71. package/libs/instance-factories/applications/templates/react/api-client-generator.ts +16 -6
  72. package/libs/instance-factories/applications/templates/react-starter/README.md +211 -0
  73. package/libs/instance-factories/applications/templates/react-starter/__tests__/dashboard-body-composer.test.ts +153 -0
  74. package/libs/instance-factories/applications/templates/react-starter/__tests__/detail-body-composer.test.ts +145 -0
  75. package/libs/instance-factories/applications/templates/react-starter/__tests__/form-body-composer.test.ts +175 -0
  76. package/libs/instance-factories/applications/templates/react-starter/__tests__/helpers-emitter.test.ts +55 -0
  77. package/libs/instance-factories/applications/templates/react-starter/__tests__/html-to-jsx.test.ts +140 -0
  78. package/libs/instance-factories/applications/templates/react-starter/__tests__/list-body-composer.test.ts +146 -0
  79. package/libs/instance-factories/applications/templates/react-starter/__tests__/orchestrator.test.ts +163 -0
  80. package/libs/instance-factories/applications/templates/react-starter/__tests__/parity-p2-factory-imports.test.ts +116 -0
  81. package/libs/instance-factories/applications/templates/react-starter/__tests__/parity-p3-rendered-output.test.ts +183 -0
  82. package/libs/instance-factories/applications/templates/react-starter/__tests__/regen-safety.test.ts +144 -0
  83. package/libs/instance-factories/applications/templates/react-starter/__tests__/starter-generators.test.ts +114 -0
  84. package/libs/instance-factories/applications/templates/react-starter/__tests__/view-emitter.test.ts +107 -0
  85. package/libs/instance-factories/applications/templates/react-starter/__tests__/views-generator.test.ts +139 -0
  86. package/libs/instance-factories/applications/templates/react-starter/app-tsx-generator.ts +141 -0
  87. package/libs/instance-factories/applications/templates/react-starter/dashboard-body-composer.ts +174 -0
  88. package/libs/instance-factories/applications/templates/react-starter/detail-body-composer.ts +135 -0
  89. package/libs/instance-factories/applications/templates/react-starter/form-body-composer.ts +306 -0
  90. package/libs/instance-factories/applications/templates/react-starter/helpers-emitter.ts +60 -0
  91. package/libs/instance-factories/applications/templates/react-starter/html-to-jsx.ts +334 -0
  92. package/libs/instance-factories/applications/templates/react-starter/list-body-composer.ts +120 -0
  93. package/libs/instance-factories/applications/templates/react-starter/orchestrator.ts +80 -0
  94. package/libs/instance-factories/applications/templates/react-starter/package-json-generator.ts +57 -0
  95. package/libs/instance-factories/applications/templates/react-starter/regen-safety.ts +157 -0
  96. package/libs/instance-factories/applications/templates/react-starter/skeletons/dashboard.tsx.template +47 -0
  97. package/libs/instance-factories/applications/templates/react-starter/skeletons/detail.tsx.template +94 -0
  98. package/libs/instance-factories/applications/templates/react-starter/skeletons/form.tsx.template +114 -0
  99. package/libs/instance-factories/applications/templates/react-starter/skeletons/list.tsx.template +72 -0
  100. package/libs/instance-factories/applications/templates/react-starter/view-emitter.ts +151 -0
  101. package/libs/instance-factories/applications/templates/react-starter/views-generator.ts +137 -0
  102. package/libs/instance-factories/cli/templates/commander/command-generator.ts +14 -11
  103. package/libs/instance-factories/controllers/templates/fastify/server-generator.ts +11 -3
  104. package/libs/instance-factories/services/templates/prisma/controller-generator.ts +27 -17
  105. package/libs/instance-factories/shared/path-resolver.ts +8 -2
  106. package/package.json +3 -3
  107. package/dist/libs/instance-factories/applications/templates/react/_view-components-source.js +0 -530
  108. package/dist/libs/instance-factories/applications/templates/react/app-tsx-generator.js +0 -73
  109. package/dist/libs/instance-factories/applications/templates/react/field-helpers-generator.js +0 -99
  110. package/dist/libs/instance-factories/applications/templates/react/package-json-generator.js +0 -49
  111. package/dist/libs/instance-factories/applications/templates/react/pattern-adapter-generator.js +0 -156
  112. package/dist/libs/instance-factories/applications/templates/react/react-pattern-adapter.js +0 -935
  113. package/dist/libs/instance-factories/applications/templates/react/relationship-field-generator.js +0 -143
  114. package/dist/libs/instance-factories/applications/templates/react/tailwind-adapter-generator.js +0 -646
  115. package/dist/libs/instance-factories/applications/templates/react/tailwind-adapter-wrapper-generator.js +0 -65
  116. package/dist/libs/instance-factories/applications/templates/react/view-dashboard-generator.js +0 -143
  117. package/dist/libs/instance-factories/applications/templates/react/view-detail-generator.js +0 -143
  118. package/dist/libs/instance-factories/applications/templates/react/view-form-generator.js +0 -355
  119. package/dist/libs/instance-factories/applications/templates/react/view-list-generator.js +0 -91
  120. package/dist/libs/instance-factories/applications/templates/react/view-router-generator.js +0 -79
  121. package/dist/libs/instance-factories/tools/templates/vscode/vscode-extension-generator.js.bak +0 -244
  122. package/dist/libs/instance-factories/views/index.js +0 -48
  123. package/dist/libs/instance-factories/views/templates/react/adapters/antd-adapter.js +0 -742
  124. package/dist/libs/instance-factories/views/templates/react/adapters/mui-adapter.js +0 -824
  125. package/dist/libs/instance-factories/views/templates/react/adapters/shadcn-adapter.js +0 -719
  126. package/dist/libs/instance-factories/views/templates/react/app-generator.js +0 -45
  127. package/dist/libs/instance-factories/views/templates/react/components-generator.js +0 -820
  128. package/dist/libs/instance-factories/views/templates/react/forms-generator.js +0 -275
  129. package/dist/libs/instance-factories/views/templates/react/frontend-package-json-generator.js +0 -46
  130. package/dist/libs/instance-factories/views/templates/react/hooks-generator.js +0 -81
  131. package/dist/libs/instance-factories/views/templates/react/index-css-generator.js +0 -9
  132. package/dist/libs/instance-factories/views/templates/react/index-html-generator.js +0 -23
  133. package/dist/libs/instance-factories/views/templates/react/main-tsx-generator.js +0 -21
  134. package/dist/libs/instance-factories/views/templates/react/react-component-generator.js +0 -299
  135. package/dist/libs/instance-factories/views/templates/react/router-generator.js +0 -136
  136. package/dist/libs/instance-factories/views/templates/react/router-generic-generator.js +0 -107
  137. package/dist/libs/instance-factories/views/templates/react/shared-utils-generator.js +0 -187
  138. package/dist/libs/instance-factories/views/templates/react/spec-json-generator.js +0 -7
  139. package/dist/libs/instance-factories/views/templates/react/types-generator.js +0 -56
  140. package/dist/libs/instance-factories/views/templates/react/views-metadata-generator.js +0 -27
  141. package/dist/libs/instance-factories/views/templates/react/vite-config-generator.js +0 -29
  142. package/dist/libs/instance-factories/views/templates/runtime/runtime-view-renderer.js +0 -261
  143. package/dist/libs/instance-factories/views/templates/shared/adapter-types.js +0 -34
  144. package/dist/libs/instance-factories/views/templates/shared/atomic-components-registry.js +0 -800
  145. package/dist/libs/instance-factories/views/templates/shared/base-generator.js +0 -305
  146. package/dist/libs/instance-factories/views/templates/shared/component-metadata.js +0 -517
  147. package/dist/libs/instance-factories/views/templates/shared/composite-pattern-types.js +0 -0
  148. package/dist/libs/instance-factories/views/templates/shared/composite-patterns.js +0 -445
  149. package/dist/libs/instance-factories/views/templates/shared/index.js +0 -80
  150. package/dist/libs/instance-factories/views/templates/shared/pattern-validator.js +0 -210
  151. package/dist/libs/instance-factories/views/templates/shared/property-mapper.js +0 -492
  152. package/dist/libs/instance-factories/views/templates/shared/syntax-mapper.js +0 -321
  153. package/dist/realize/index.js.bak +0 -758
  154. package/libs/instance-factories/applications/react-app.yaml +0 -186
  155. package/libs/instance-factories/applications/templates/react/_view-components-source.ts +0 -555
  156. package/libs/instance-factories/applications/templates/react/app-tsx-generator.ts +0 -94
  157. package/libs/instance-factories/applications/templates/react/field-helpers-generator.ts +0 -106
  158. package/libs/instance-factories/applications/templates/react/package-json-generator.ts +0 -57
  159. package/libs/instance-factories/applications/templates/react/pattern-adapter-generator.ts +0 -179
  160. package/libs/instance-factories/applications/templates/react/react-pattern-adapter.tsx +0 -1347
  161. package/libs/instance-factories/applications/templates/react/relationship-field-generator.ts +0 -150
  162. package/libs/instance-factories/applications/templates/react/tailwind-adapter-generator.ts +0 -704
  163. package/libs/instance-factories/applications/templates/react/tailwind-adapter-wrapper-generator.ts +0 -84
  164. package/libs/instance-factories/applications/templates/react/view-dashboard-generator.ts +0 -150
  165. package/libs/instance-factories/applications/templates/react/view-detail-generator.ts +0 -150
  166. package/libs/instance-factories/applications/templates/react/view-form-generator.ts +0 -362
  167. package/libs/instance-factories/applications/templates/react/view-list-generator.ts +0 -98
  168. package/libs/instance-factories/applications/templates/react/view-router-generator.ts +0 -89
  169. package/libs/instance-factories/views/README.md +0 -62
  170. package/libs/instance-factories/views/index.d.ts +0 -13
  171. package/libs/instance-factories/views/index.d.ts.map +0 -1
  172. package/libs/instance-factories/views/index.js +0 -18
  173. package/libs/instance-factories/views/index.js.map +0 -1
  174. package/libs/instance-factories/views/index.ts +0 -45
  175. package/libs/instance-factories/views/react-components.yaml +0 -129
  176. package/libs/instance-factories/views/templates/ARCHITECTURE.md +0 -198
  177. package/libs/instance-factories/views/templates/react/adapters/antd-adapter.ts +0 -869
  178. package/libs/instance-factories/views/templates/react/adapters/mui-adapter.ts +0 -953
  179. package/libs/instance-factories/views/templates/react/adapters/shadcn-adapter.ts +0 -806
  180. package/libs/instance-factories/views/templates/react/app-generator.ts +0 -55
  181. package/libs/instance-factories/views/templates/react/components-generator.ts +0 -938
  182. package/libs/instance-factories/views/templates/react/forms-generator.ts +0 -325
  183. package/libs/instance-factories/views/templates/react/frontend-package-json-generator.ts +0 -57
  184. package/libs/instance-factories/views/templates/react/hooks-generator.ts +0 -106
  185. package/libs/instance-factories/views/templates/react/index-css-generator.ts +0 -14
  186. package/libs/instance-factories/views/templates/react/index-html-generator.ts +0 -34
  187. package/libs/instance-factories/views/templates/react/main-tsx-generator.ts +0 -29
  188. package/libs/instance-factories/views/templates/react/react-component-generator.d.ts +0 -152
  189. package/libs/instance-factories/views/templates/react/react-component-generator.d.ts.map +0 -1
  190. package/libs/instance-factories/views/templates/react/react-component-generator.js +0 -398
  191. package/libs/instance-factories/views/templates/react/react-component-generator.js.map +0 -1
  192. package/libs/instance-factories/views/templates/react/react-component-generator.ts +0 -533
  193. package/libs/instance-factories/views/templates/react/router-generator.ts +0 -197
  194. package/libs/instance-factories/views/templates/react/router-generic-generator.ts +0 -132
  195. package/libs/instance-factories/views/templates/react/shared-utils-generator.ts +0 -196
  196. package/libs/instance-factories/views/templates/react/spec-json-generator.ts +0 -17
  197. package/libs/instance-factories/views/templates/react/types-generator.ts +0 -76
  198. package/libs/instance-factories/views/templates/react/views-metadata-generator.ts +0 -42
  199. package/libs/instance-factories/views/templates/react/vite-config-generator.ts +0 -38
  200. package/libs/instance-factories/views/templates/runtime/runtime-view-renderer.d.ts.map +0 -1
  201. package/libs/instance-factories/views/templates/runtime/runtime-view-renderer.js.map +0 -1
  202. package/libs/instance-factories/views/templates/runtime/runtime-view-renderer.ts +0 -474
  203. package/libs/instance-factories/views/templates/shared/__tests__/composite-patterns.test.ts +0 -242
  204. package/libs/instance-factories/views/templates/shared/adapter-types.d.ts +0 -77
  205. package/libs/instance-factories/views/templates/shared/adapter-types.d.ts.map +0 -1
  206. package/libs/instance-factories/views/templates/shared/adapter-types.js +0 -47
  207. package/libs/instance-factories/views/templates/shared/adapter-types.js.map +0 -1
  208. package/libs/instance-factories/views/templates/shared/adapter-types.ts +0 -142
  209. package/libs/instance-factories/views/templates/shared/atomic-components-registry.d.ts +0 -63
  210. package/libs/instance-factories/views/templates/shared/atomic-components-registry.d.ts.map +0 -1
  211. package/libs/instance-factories/views/templates/shared/atomic-components-registry.js +0 -822
  212. package/libs/instance-factories/views/templates/shared/atomic-components-registry.js.map +0 -1
  213. package/libs/instance-factories/views/templates/shared/atomic-components-registry.ts +0 -908
  214. package/libs/instance-factories/views/templates/shared/base-generator.d.ts +0 -247
  215. package/libs/instance-factories/views/templates/shared/base-generator.d.ts.map +0 -1
  216. package/libs/instance-factories/views/templates/shared/base-generator.js +0 -363
  217. package/libs/instance-factories/views/templates/shared/base-generator.js.map +0 -1
  218. package/libs/instance-factories/views/templates/shared/base-generator.ts +0 -608
  219. package/libs/instance-factories/views/templates/shared/component-metadata.d.ts +0 -254
  220. package/libs/instance-factories/views/templates/shared/component-metadata.d.ts.map +0 -1
  221. package/libs/instance-factories/views/templates/shared/component-metadata.js +0 -602
  222. package/libs/instance-factories/views/templates/shared/component-metadata.js.map +0 -1
  223. package/libs/instance-factories/views/templates/shared/component-metadata.ts +0 -803
  224. package/libs/instance-factories/views/templates/shared/composite-pattern-types.ts +0 -250
  225. package/libs/instance-factories/views/templates/shared/composite-patterns.ts +0 -535
  226. package/libs/instance-factories/views/templates/shared/index.ts +0 -68
  227. package/libs/instance-factories/views/templates/shared/pattern-validator.ts +0 -279
  228. package/libs/instance-factories/views/templates/shared/property-mapper.d.ts +0 -149
  229. package/libs/instance-factories/views/templates/shared/property-mapper.d.ts.map +0 -1
  230. package/libs/instance-factories/views/templates/shared/property-mapper.js +0 -580
  231. package/libs/instance-factories/views/templates/shared/property-mapper.js.map +0 -1
  232. package/libs/instance-factories/views/templates/shared/property-mapper.ts +0 -700
  233. package/libs/instance-factories/views/templates/shared/syntax-mapper.d.ts +0 -143
  234. package/libs/instance-factories/views/templates/shared/syntax-mapper.d.ts.map +0 -1
  235. package/libs/instance-factories/views/templates/shared/syntax-mapper.js +0 -420
  236. package/libs/instance-factories/views/templates/shared/syntax-mapper.js.map +0 -1
  237. package/libs/instance-factories/views/templates/shared/syntax-mapper.ts +0 -539
@@ -0,0 +1,190 @@
1
+ import { METADATA_FIELDS } from "@specverse/runtime/views/core";
2
+ const AUTO_GENERATED_FIELD_NAMES = new Set(METADATA_FIELDS);
3
+ function composeFormBody(context) {
4
+ const belongsTo = extractBelongsTo(context.model);
5
+ const shadowedFKs = new Set(belongsTo.map((rel) => `${rel}Id`));
6
+ const fields = selectFormFields(context.model, shadowedFKs);
7
+ const lines = [];
8
+ lines.push('<div className="space-y-4">');
9
+ if (fields.length === 0 && belongsTo.length === 0) {
10
+ lines.push(' <p className="text-sm text-gray-400">No editable fields for this model.</p>');
11
+ lines.push("</div>");
12
+ return lines.join("\n");
13
+ }
14
+ for (const field of fields) {
15
+ lines.push(...renderInput(field));
16
+ }
17
+ if (belongsTo.length > 0) {
18
+ lines.push("");
19
+ lines.push(" {/* TODO: swap these text inputs for <select>s that load the related");
20
+ lines.push(" entities. See useEntitiesQuery(TargetModel) in the runtime hooks. */}");
21
+ for (const rel of belongsTo) {
22
+ lines.push(...renderInput({
23
+ name: `${rel}Id`,
24
+ label: humanize(rel),
25
+ type: "String",
26
+ required: true
27
+ }));
28
+ }
29
+ }
30
+ lines.push("</div>");
31
+ return lines.join("\n");
32
+ }
33
+ function selectFormFields(model, skip = /* @__PURE__ */ new Set()) {
34
+ const out = [];
35
+ const attrs = model.attributes ?? {};
36
+ for (const [name, rawDef] of Object.entries(attrs)) {
37
+ if (AUTO_GENERATED_FIELD_NAMES.has(name)) continue;
38
+ if (skip.has(name)) continue;
39
+ const def = rawDef;
40
+ if (def?.auto) continue;
41
+ const type = def?.type ?? parseTypeFromConvention(def) ?? "String";
42
+ const required = def?.required === true || hasConventionFlag(def, "required");
43
+ const values = def?.values;
44
+ const maxLength = def?.maxLength ?? def?.max;
45
+ const isLongString = typeof maxLength === "number" && maxLength > 100;
46
+ out.push({
47
+ name,
48
+ label: humanize(name),
49
+ type: normalizeType(type),
50
+ required,
51
+ values,
52
+ isLongString
53
+ });
54
+ }
55
+ return out;
56
+ }
57
+ function parseTypeFromConvention(def) {
58
+ if (typeof def === "string") {
59
+ const first = def.trim().split(/\s+/)[0];
60
+ return first;
61
+ }
62
+ return void 0;
63
+ }
64
+ function hasConventionFlag(def, flag) {
65
+ if (typeof def === "string") {
66
+ return def.split(/\s+/).includes(flag);
67
+ }
68
+ return false;
69
+ }
70
+ function normalizeType(type) {
71
+ return type.replace(/[^A-Za-z].*$/, "");
72
+ }
73
+ function extractBelongsTo(model) {
74
+ const rels = model.relationships ?? {};
75
+ const out = [];
76
+ for (const [name, def] of Object.entries(rels)) {
77
+ if (def?.type === "belongsTo") out.push(name);
78
+ }
79
+ return out;
80
+ }
81
+ 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";
82
+ const LABEL_CLS = "block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1";
83
+ function renderInput(field) {
84
+ const requiredMark = field.required ? " *" : "";
85
+ const reqAttr = field.required ? " required" : "";
86
+ if (field.values && field.values.length > 0) {
87
+ const options = field.values.map((v) => ` <option value="${escapeAttr(v)}">${escapeText(v)}</option>`).join("\n");
88
+ return [
89
+ " <div>",
90
+ ` <label className="${LABEL_CLS}" htmlFor="${field.name}">${field.label}${requiredMark}</label>`,
91
+ ` <select`,
92
+ ` id="${field.name}"`,
93
+ ` className="${INPUT_CLS}"`,
94
+ ` value={String((formData as any).${field.name} ?? '')}`,
95
+ ` onChange={e => handleChange('${field.name}', e.target.value)}${reqAttr}`,
96
+ ` >`,
97
+ ` <option value="">\u2014 choose \u2014</option>`,
98
+ options,
99
+ ` </select>`,
100
+ " </div>"
101
+ ];
102
+ }
103
+ if (field.type === "Text" || field.isLongString) {
104
+ return [
105
+ " <div>",
106
+ ` <label className="${LABEL_CLS}" htmlFor="${field.name}">${field.label}${requiredMark}</label>`,
107
+ ` <textarea`,
108
+ ` id="${field.name}"`,
109
+ ` className="${INPUT_CLS}"`,
110
+ ` rows={4}`,
111
+ ` value={String((formData as any).${field.name} ?? '')}`,
112
+ ` onChange={e => handleChange('${field.name}', e.target.value)}${reqAttr}`,
113
+ ` />`,
114
+ " </div>"
115
+ ];
116
+ }
117
+ if (field.type === "Boolean") {
118
+ return [
119
+ " <div>",
120
+ ` <label className="inline-flex items-center gap-2">`,
121
+ ` <input`,
122
+ ` id="${field.name}"`,
123
+ ` type="checkbox"`,
124
+ ` checked={Boolean((formData as any).${field.name})}`,
125
+ ` onChange={e => handleChange('${field.name}', e.target.checked)}`,
126
+ ` />`,
127
+ ` <span className="text-sm text-gray-700 dark:text-gray-300">${field.label}${requiredMark}</span>`,
128
+ ` </label>`,
129
+ " </div>"
130
+ ];
131
+ }
132
+ const inputType = mapInputType(field.type);
133
+ return [
134
+ " <div>",
135
+ ` <label className="${LABEL_CLS}" htmlFor="${field.name}">${field.label}${requiredMark}</label>`,
136
+ ` <input`,
137
+ ` id="${field.name}"`,
138
+ ` type="${inputType}"`,
139
+ ` className="${INPUT_CLS}"`,
140
+ ` value={String((formData as any).${field.name} ?? '')}`,
141
+ ` onChange={e => handleChange('${field.name}', ${coerceOnChange(field.type)})}${reqAttr}`,
142
+ ` />`,
143
+ " </div>"
144
+ ];
145
+ }
146
+ function mapInputType(type) {
147
+ switch (type) {
148
+ case "Integer":
149
+ case "Float":
150
+ case "Number":
151
+ case "Money":
152
+ return "number";
153
+ case "DateTime":
154
+ return "datetime-local";
155
+ case "Date":
156
+ return "date";
157
+ case "Email":
158
+ return "email";
159
+ case "URL":
160
+ return "url";
161
+ case "String":
162
+ case "UUID":
163
+ default:
164
+ return "text";
165
+ }
166
+ }
167
+ function coerceOnChange(type) {
168
+ switch (type) {
169
+ case "Integer":
170
+ case "Number":
171
+ return "e.target.value === '' ? undefined : Number(e.target.value)";
172
+ case "Float":
173
+ case "Money":
174
+ return "e.target.value === '' ? undefined : parseFloat(e.target.value)";
175
+ default:
176
+ return "e.target.value";
177
+ }
178
+ }
179
+ function humanize(name) {
180
+ return name.replace(/([A-Z])/g, " $1").replace(/^./, (c) => c.toUpperCase()).trim();
181
+ }
182
+ function escapeAttr(s) {
183
+ return s.replace(/"/g, "&quot;");
184
+ }
185
+ function escapeText(s) {
186
+ return s.replace(/</g, "&lt;").replace(/>/g, "&gt;");
187
+ }
188
+ export {
189
+ composeFormBody
190
+ };
@@ -0,0 +1,45 @@
1
+ function emitEntityDisplay() {
2
+ return `/**
3
+ * Pick the best human-readable label for an entity.
4
+ *
5
+ * Walks a prioritized list of candidate fields. Falls back to a
6
+ * truncated id if none match. Inlined into this project by
7
+ * @specverse/realize (ReactAppStarter); edit freely \u2014 nothing in
8
+ * the factory's regeneration will clobber your edits.
9
+ */
10
+ export function getEntityDisplayName(entity: Record<string, unknown> | null | undefined): string {
11
+ if (!entity) return '';
12
+
13
+ const candidates = ['name', 'title', 'displayName', 'label', 'username', 'email'];
14
+ for (const key of candidates) {
15
+ const value = entity[key];
16
+ if (typeof value === 'string' && value.trim().length > 0) {
17
+ return value;
18
+ }
19
+ }
20
+
21
+ const id = entity.id;
22
+ if (typeof id === 'string') {
23
+ return id.length > 8 ? id.slice(0, 8) + '\u2026' : id;
24
+ }
25
+ return id != null ? String(id) : '';
26
+ }
27
+
28
+ /**
29
+ * Map an entity id to the display name of the record with that id in
30
+ * a provided list. Handy for resolving belongsTo FK columns in
31
+ * list / detail views.
32
+ */
33
+ export function resolveEntityDisplayName(
34
+ id: unknown,
35
+ records: ReadonlyArray<Record<string, unknown>>
36
+ ): string {
37
+ if (id == null) return '';
38
+ const match = records.find(r => r.id === id);
39
+ return match ? getEntityDisplayName(match) : String(id);
40
+ }
41
+ `;
42
+ }
43
+ export {
44
+ emitEntityDisplay
45
+ };
@@ -0,0 +1,192 @@
1
+ const VOID_ELEMENTS = /* @__PURE__ */ new Set([
2
+ "area",
3
+ "base",
4
+ "br",
5
+ "col",
6
+ "embed",
7
+ "hr",
8
+ "img",
9
+ "input",
10
+ "link",
11
+ "meta",
12
+ "source",
13
+ "track",
14
+ "wbr"
15
+ ]);
16
+ const ATTRIBUTE_RENAMES = {
17
+ class: "className",
18
+ for: "htmlFor",
19
+ tabindex: "tabIndex",
20
+ readonly: "readOnly",
21
+ maxlength: "maxLength",
22
+ minlength: "minLength",
23
+ colspan: "colSpan",
24
+ rowspan: "rowSpan",
25
+ autofocus: "autoFocus",
26
+ autocomplete: "autoComplete",
27
+ cellpadding: "cellPadding",
28
+ cellspacing: "cellSpacing",
29
+ enctype: "encType",
30
+ usemap: "useMap",
31
+ accesskey: "accessKey",
32
+ contenteditable: "contentEditable",
33
+ spellcheck: "spellCheck",
34
+ crossorigin: "crossOrigin",
35
+ srcset: "srcSet",
36
+ srcdoc: "srcDoc"
37
+ };
38
+ function htmlToJsx(html) {
39
+ if (!html) return "";
40
+ const tokens = tokenize(html);
41
+ return tokens.map(transformToken).join("");
42
+ }
43
+ function tokenize(input) {
44
+ const tokens = [];
45
+ let i = 0;
46
+ const n = input.length;
47
+ while (i < n) {
48
+ const ch = input[i];
49
+ if (ch === "<") {
50
+ const tagStart = i;
51
+ i++;
52
+ let quote = null;
53
+ while (i < n) {
54
+ const c = input[i];
55
+ if (quote) {
56
+ if (c === quote) quote = null;
57
+ i++;
58
+ continue;
59
+ }
60
+ if (c === '"' || c === "'") {
61
+ quote = c;
62
+ i++;
63
+ continue;
64
+ }
65
+ if (c === ">") {
66
+ i++;
67
+ break;
68
+ }
69
+ i++;
70
+ }
71
+ const raw = input.slice(tagStart, i);
72
+ const isClosing = raw.startsWith("</");
73
+ const isSelfClosed = raw.endsWith("/>");
74
+ const tagName = parseTagName(raw);
75
+ tokens.push({ kind: "tag", raw, tagName, isClosing, isSelfClosed });
76
+ } else {
77
+ const textStart = i;
78
+ while (i < n && input[i] !== "<") i++;
79
+ tokens.push({ kind: "text", value: input.slice(textStart, i) });
80
+ }
81
+ }
82
+ return tokens;
83
+ }
84
+ function parseTagName(tagRaw) {
85
+ const m = tagRaw.match(/^<\/?([a-zA-Z][a-zA-Z0-9-]*)/);
86
+ return m ? m[1].toLowerCase() : "";
87
+ }
88
+ function transformToken(token) {
89
+ if (token.kind === "text") {
90
+ return token.value;
91
+ }
92
+ return transformTag(token.raw, token.tagName, token.isClosing, token.isSelfClosed);
93
+ }
94
+ function transformTag(raw, tagName, isClosing, isSelfClosed) {
95
+ if (isClosing) {
96
+ return raw;
97
+ }
98
+ const inner = isSelfClosed ? raw.slice(1, -2).trimEnd() : raw.slice(1, -1);
99
+ const nameMatch = inner.match(/^([a-zA-Z][a-zA-Z0-9-]*)(.*)$/s);
100
+ if (!nameMatch) return raw;
101
+ const elementName = nameMatch[1];
102
+ const attrsSection = nameMatch[2];
103
+ const attrs = parseAttributes(attrsSection);
104
+ const renamedAttrs = attrs.map(renameAttribute);
105
+ const rebuiltAttrs = serializeAttributes(renamedAttrs);
106
+ const shouldSelfClose = VOID_ELEMENTS.has(tagName) && !isSelfClosed;
107
+ if (shouldSelfClose || isSelfClosed) {
108
+ return `<${elementName}${rebuiltAttrs} />`;
109
+ }
110
+ return `<${elementName}${rebuiltAttrs}>`;
111
+ }
112
+ function parseAttributes(section) {
113
+ const attrs = [];
114
+ let i = 0;
115
+ const n = section.length;
116
+ while (i < n) {
117
+ while (i < n && /\s/.test(section[i])) i++;
118
+ if (i >= n) break;
119
+ const nameStart = i;
120
+ while (i < n && /[a-zA-Z0-9:_-]/.test(section[i])) i++;
121
+ if (i === nameStart) {
122
+ i++;
123
+ continue;
124
+ }
125
+ const name = section.slice(nameStart, i);
126
+ let j = i;
127
+ while (j < n && /\s/.test(section[j])) j++;
128
+ if (j >= n || section[j] !== "=") {
129
+ attrs.push({ name });
130
+ continue;
131
+ }
132
+ i = j + 1;
133
+ while (i < n && /\s/.test(section[i])) i++;
134
+ if (i >= n) {
135
+ attrs.push({ name });
136
+ break;
137
+ }
138
+ const vc = section[i];
139
+ if (vc === '"' || vc === "'") {
140
+ const quote = vc;
141
+ i++;
142
+ const valStart = i;
143
+ while (i < n && section[i] !== quote) i++;
144
+ const value = section.slice(valStart, i);
145
+ if (i < n) i++;
146
+ attrs.push({ name, value, quote });
147
+ } else {
148
+ const valStart = i;
149
+ while (i < n && !/[\s/>]/.test(section[i])) i++;
150
+ const value = section.slice(valStart, i);
151
+ attrs.push({ name, value });
152
+ }
153
+ }
154
+ return attrs;
155
+ }
156
+ function renameAttribute(attr) {
157
+ const lower = attr.name.toLowerCase();
158
+ const newName = ATTRIBUTE_RENAMES[lower] ?? attr.name;
159
+ if (lower === "style" && attr.value !== void 0) {
160
+ return { name: newName, value: cssStringToJsxObject(attr.value), quote: void 0 };
161
+ }
162
+ return { ...attr, name: newName };
163
+ }
164
+ function serializeAttributes(attrs) {
165
+ if (attrs.length === 0) return "";
166
+ const parts = attrs.map((attr) => {
167
+ if (attr.value === void 0) return ` ${attr.name}`;
168
+ if (attr.value.startsWith("{{") && attr.value.endsWith("}}")) {
169
+ return ` ${attr.name}=${attr.value}`;
170
+ }
171
+ const q = attr.quote ?? '"';
172
+ return ` ${attr.name}=${q}${attr.value}${q}`;
173
+ });
174
+ return parts.join("");
175
+ }
176
+ function cssStringToJsxObject(css) {
177
+ const declarations = css.split(";").map((d) => d.trim()).filter((d) => d.length > 0);
178
+ if (declarations.length === 0) return "{{}}";
179
+ const entries = declarations.map((decl) => {
180
+ const colonIdx = decl.indexOf(":");
181
+ if (colonIdx === -1) return null;
182
+ const prop = decl.slice(0, colonIdx).trim();
183
+ const value = decl.slice(colonIdx + 1).trim();
184
+ const camelProp = prop.replace(/-([a-z])/g, (_m, c) => c.toUpperCase());
185
+ const escapedValue = value.replace(/'/g, "\\'");
186
+ return `${camelProp}: '${escapedValue}'`;
187
+ }).filter((e) => e !== null);
188
+ return `{{ ${entries.join(", ")} }}`;
189
+ }
190
+ export {
191
+ htmlToJsx
192
+ };
@@ -0,0 +1,46 @@
1
+ import { createUniversalTailwindAdapter } from "@specverse/runtime/views/tailwind";
2
+ import { inferFieldsFromSchema } from "@specverse/runtime/views/core";
3
+ import { htmlToJsx } from "./html-to-jsx.js";
4
+ const TBODY_SENTINEL = "__SPECVERSE_TBODY_ROWS__";
5
+ function composeListBody(context) {
6
+ const columns = inferColumns(context);
7
+ const headers = columns.map(humanize);
8
+ const adapter = createUniversalTailwindAdapter({ darkMode: true });
9
+ const shellHtml = adapter.components.table.render({
10
+ properties: { columns: headers },
11
+ children: TBODY_SENTINEL
12
+ });
13
+ const shellJsx = htmlToJsx(shellHtml);
14
+ const rowMap = buildRowMap(columns);
15
+ if (!shellJsx.includes(TBODY_SENTINEL)) {
16
+ throw new Error(
17
+ "composeListBody: tbody sentinel not present in adapter output. The canonical Tailwind adapter may have changed its table rendering."
18
+ );
19
+ }
20
+ return shellJsx.replace(TBODY_SENTINEL, rowMap);
21
+ }
22
+ function inferColumns(context) {
23
+ return inferFieldsFromSchema(context.modelSchemas, context.model.name);
24
+ }
25
+ function humanize(name) {
26
+ return name.replace(/([A-Z])/g, " $1").replace(/^./, (c) => c.toUpperCase()).trim();
27
+ }
28
+ function buildRowMap(columns) {
29
+ const cells = columns.map(
30
+ (col) => ` <td className="px-6 py-4 text-sm text-gray-700 dark:text-gray-300">{String((item as any).${col} ?? '')}</td>`
31
+ ).join("\n");
32
+ return [
33
+ "{filtered.map((item, idx) => (",
34
+ " <tr",
35
+ " key={idx}",
36
+ " onClick={() => onSelect?.(item)}",
37
+ ' className="hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer"',
38
+ " >",
39
+ cells,
40
+ " </tr>",
41
+ "))}"
42
+ ].join("\n");
43
+ }
44
+ export {
45
+ composeListBody
46
+ };
@@ -0,0 +1,30 @@
1
+ import { generate as generateViews } from "./views-generator.js";
2
+ import { generate as generateAppTsx } from "./app-tsx-generator.js";
3
+ import { generate as generatePackageJson } from "./package-json-generator.js";
4
+ import {
5
+ loadHashManifest,
6
+ reconcileWrites,
7
+ summarize,
8
+ HASHES_DIR,
9
+ HASHES_FILE
10
+ } from "./regen-safety.js";
11
+ async function generate(context) {
12
+ const { spec, projectRoot } = context;
13
+ const proposed = {};
14
+ const viewFiles = await generateViews({ spec });
15
+ Object.assign(proposed, viewFiles);
16
+ proposed["src/App.tsx"] = await generateAppTsx({ spec });
17
+ proposed["package.json"] = await generatePackageJson({ spec });
18
+ const prevManifest = loadHashManifest(projectRoot);
19
+ const result = reconcileWrites(projectRoot, proposed, prevManifest);
20
+ console.log(summarize(result, projectRoot));
21
+ const manifestPath = `${HASHES_DIR}/${HASHES_FILE}`;
22
+ const out = {
23
+ ...result.approvedWrites,
24
+ [manifestPath]: JSON.stringify(result.manifest, null, 2) + "\n"
25
+ };
26
+ return out;
27
+ }
28
+ export {
29
+ generate
30
+ };
@@ -0,0 +1,38 @@
1
+ async function generate(context) {
2
+ const appName = context.spec?.metadata?.name ?? context.spec?.name ?? "specverse-starter-app";
3
+ const packageName = slugify(appName);
4
+ const pkg = {
5
+ name: packageName,
6
+ private: true,
7
+ version: "0.1.0",
8
+ type: "module",
9
+ scripts: {
10
+ dev: "vite",
11
+ build: "tsc && vite build",
12
+ preview: "vite preview",
13
+ typecheck: "tsc --noEmit"
14
+ },
15
+ dependencies: {
16
+ react: "^18.2.0",
17
+ "react-dom": "^18.2.0",
18
+ "@tanstack/react-query": "^5.0.0"
19
+ },
20
+ devDependencies: {
21
+ "@types/react": "^18.2.0",
22
+ "@types/react-dom": "^18.2.0",
23
+ "@vitejs/plugin-react": "^4.2.0",
24
+ autoprefixer: "^10.4.20",
25
+ postcss: "^8.4.47",
26
+ tailwindcss: "^3.4.13",
27
+ typescript: "^5.2.0",
28
+ vite: "^6.0.0"
29
+ }
30
+ };
31
+ return JSON.stringify(pkg, null, 2) + "\n";
32
+ }
33
+ function slugify(s) {
34
+ return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "specverse-starter-app";
35
+ }
36
+ export {
37
+ generate
38
+ };
@@ -0,0 +1,89 @@
1
+ import { createHash } from "crypto";
2
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
3
+ import { join, relative } from "path";
4
+ const HASHES_DIR = ".specverse-gen";
5
+ const HASHES_FILE = "hashes.json";
6
+ function sha256(s) {
7
+ return createHash("sha256").update(s, "utf8").digest("hex");
8
+ }
9
+ function loadHashManifest(projectRoot) {
10
+ const path = join(projectRoot, HASHES_DIR, HASHES_FILE);
11
+ if (!existsSync(path)) return {};
12
+ try {
13
+ const raw = readFileSync(path, "utf8");
14
+ const parsed = JSON.parse(raw);
15
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
16
+ const out = {};
17
+ for (const [k, v] of Object.entries(parsed)) {
18
+ if (typeof v === "string") out[k] = v;
19
+ }
20
+ return out;
21
+ }
22
+ } catch {
23
+ }
24
+ return {};
25
+ }
26
+ function saveHashManifest(projectRoot, manifest) {
27
+ const dir = join(projectRoot, HASHES_DIR);
28
+ mkdirSync(dir, { recursive: true });
29
+ const path = join(dir, HASHES_FILE);
30
+ writeFileSync(path, JSON.stringify(manifest, null, 2) + "\n", "utf8");
31
+ }
32
+ function reconcileWrites(projectRoot, proposed, prevManifest) {
33
+ const manifest = { ...prevManifest };
34
+ const approvedWrites = {};
35
+ const skipped = [];
36
+ for (const [relPath, content] of Object.entries(proposed)) {
37
+ const abs = join(projectRoot, relPath);
38
+ const newHash = sha256(content);
39
+ if (!existsSync(abs)) {
40
+ approvedWrites[relPath] = content;
41
+ manifest[relPath] = newHash;
42
+ continue;
43
+ }
44
+ const currentContent = readFileSync(abs, "utf8");
45
+ const currentHash = sha256(currentContent);
46
+ const recordedHash = prevManifest[relPath];
47
+ if (recordedHash == null) {
48
+ skipped.push({
49
+ path: relPath,
50
+ reason: "no prior hash recorded \u2014 cannot confirm this file was generated by us"
51
+ });
52
+ continue;
53
+ }
54
+ if (currentHash === recordedHash) {
55
+ approvedWrites[relPath] = content;
56
+ manifest[relPath] = newHash;
57
+ continue;
58
+ }
59
+ skipped.push({
60
+ path: relPath,
61
+ reason: "file has been edited since last generation"
62
+ });
63
+ }
64
+ return { approvedWrites, skipped, manifest };
65
+ }
66
+ function summarize(result, projectRoot) {
67
+ const lines = [];
68
+ const writeCount = Object.keys(result.approvedWrites).length;
69
+ lines.push(`[ReactAppStarter] Approved ${writeCount} file(s) for writing.`);
70
+ if (result.skipped.length > 0) {
71
+ lines.push(`[ReactAppStarter] Skipped ${result.skipped.length} user-edited file(s):`);
72
+ for (const { path, reason } of result.skipped) {
73
+ lines.push(` - ${path} (${reason})`);
74
+ }
75
+ lines.push(
76
+ `To accept upstream regeneration for a skipped file, delete it (\`rm ${relative(process.cwd(), projectRoot)}/PATH\`) and re-run \`spv realize\`.`
77
+ );
78
+ }
79
+ return lines.join("\n");
80
+ }
81
+ export {
82
+ HASHES_DIR,
83
+ HASHES_FILE,
84
+ loadHashManifest,
85
+ reconcileWrites,
86
+ saveHashManifest,
87
+ sha256,
88
+ summarize
89
+ };
@@ -0,0 +1,56 @@
1
+ import { readFileSync } from "fs";
2
+ import { join, dirname } from "path";
3
+ import { fileURLToPath } from "url";
4
+ function emitView(context) {
5
+ const skeleton = loadSkeleton(context.view.type);
6
+ const bodyJsx = context.renderBody(context);
7
+ const substitutions = buildSubstitutions(context, bodyJsx);
8
+ return applySubstitutions(skeleton, substitutions);
9
+ }
10
+ const SKELETON_BY_VIEW_TYPE = {
11
+ list: "list.tsx.template",
12
+ detail: "detail.tsx.template",
13
+ form: "form.tsx.template",
14
+ dashboard: "dashboard.tsx.template"
15
+ // Specialist types (board, timeline, calendar, analytics, workflow,
16
+ // wizard, comparison, settings, map, feed, profile) come online in
17
+ // Phase 2e.
18
+ };
19
+ function loadSkeleton(viewType) {
20
+ const filename = SKELETON_BY_VIEW_TYPE[viewType.toLowerCase()];
21
+ if (!filename) {
22
+ throw new Error(
23
+ `No skeleton registered for view type "${viewType}". Known types: ${Object.keys(SKELETON_BY_VIEW_TYPE).join(", ")}.`
24
+ );
25
+ }
26
+ const here = dirname(fileURLToPath(import.meta.url));
27
+ const path = join(here, "skeletons", filename);
28
+ return readFileSync(path, "utf8");
29
+ }
30
+ function buildSubstitutions(context, body) {
31
+ const modelName = context.model.name;
32
+ const pluralModel = pluralize(modelName);
33
+ return {
34
+ MODEL_NAME: modelName,
35
+ PLURAL_MODEL: pluralModel,
36
+ PLURAL_LOWER: pluralModel.toLowerCase(),
37
+ SINGULAR_LOWER: modelName.toLowerCase(),
38
+ BODY: body
39
+ };
40
+ }
41
+ function applySubstitutions(template, subs) {
42
+ let out = template;
43
+ for (const [key, value] of Object.entries(subs)) {
44
+ const pattern = new RegExp(`\\{\\{${key}\\}\\}`, "g");
45
+ out = out.replace(pattern, value);
46
+ }
47
+ return out;
48
+ }
49
+ function pluralize(s) {
50
+ if (/[^aeiou]y$/i.test(s)) return s.slice(0, -1) + "ies";
51
+ if (/(s|x|z|ch|sh)$/i.test(s)) return s + "es";
52
+ return s + "s";
53
+ }
54
+ export {
55
+ emitView
56
+ };