@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.
- package/assets/examples/manifests/frontend-only.yaml +3 -6
- package/assets/examples/manifests/fullstack-app.yaml +5 -7
- package/assets/examples/manifests/fullstack-monorepo.yaml +3 -6
- package/dist/inference/comprehensive-engine.d.ts.map +1 -1
- package/dist/inference/comprehensive-engine.js +3 -19
- package/dist/inference/comprehensive-engine.js.map +1 -1
- package/dist/inference/core/rule-engine.d.ts +31 -0
- package/dist/inference/core/rule-engine.d.ts.map +1 -1
- package/dist/inference/core/rule-engine.js +117 -33
- package/dist/inference/core/rule-engine.js.map +1 -1
- package/dist/inference/core/rule-file-types.d.ts +0 -2
- package/dist/inference/core/rule-file-types.d.ts.map +1 -1
- package/dist/inference/core/rule-file-types.js +3 -6
- package/dist/inference/core/rule-file-types.js.map +1 -1
- package/dist/inference/core/rule-loader.d.ts +5 -15
- package/dist/inference/core/rule-loader.d.ts.map +1 -1
- package/dist/inference/core/rule-loader.js +43 -132
- package/dist/inference/core/rule-loader.js.map +1 -1
- package/dist/inference/core/types.d.ts +0 -6
- package/dist/inference/core/types.d.ts.map +1 -1
- package/dist/inference/core/types.js +0 -4
- package/dist/inference/core/types.js.map +1 -1
- package/dist/inference/logical/generators/component-type-resolver.d.ts +0 -26
- package/dist/inference/logical/generators/component-type-resolver.d.ts.map +1 -1
- package/dist/inference/logical/generators/component-type-resolver.js +0 -19
- package/dist/inference/logical/generators/component-type-resolver.js.map +1 -1
- package/dist/inference/logical/generators/specialist-view-expander.d.ts +1 -17
- package/dist/inference/logical/generators/specialist-view-expander.d.ts.map +1 -1
- package/dist/inference/logical/generators/specialist-view-expander.js +0 -15
- package/dist/inference/logical/generators/specialist-view-expander.js.map +1 -1
- package/dist/inference/logical/generators/view-generator.d.ts +4 -14
- package/dist/inference/logical/generators/view-generator.d.ts.map +1 -1
- package/dist/inference/logical/generators/view-generator.js +6 -26
- package/dist/inference/logical/generators/view-generator.js.map +1 -1
- package/dist/inference/logical/index.d.ts +2 -2
- package/dist/inference/logical/index.d.ts.map +1 -1
- package/dist/inference/logical/logical-engine.d.ts.map +1 -1
- package/dist/inference/logical/logical-engine.js +17 -80
- package/dist/inference/logical/logical-engine.js.map +1 -1
- package/dist/inference/quint-transpiler.d.ts +5 -3
- package/dist/inference/quint-transpiler.d.ts.map +1 -1
- package/dist/inference/quint-transpiler.js +11 -6
- package/dist/inference/quint-transpiler.js.map +1 -1
- package/dist/libs/instance-factories/applications/templates/generic/main-generator.js +3 -3
- package/dist/libs/instance-factories/applications/templates/react/api-client-generator.js +16 -6
- package/dist/libs/instance-factories/applications/templates/react-starter/app-tsx-generator.js +110 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/dashboard-body-composer.js +121 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/detail-body-composer.js +78 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/form-body-composer.js +190 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/helpers-emitter.js +45 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/html-to-jsx.js +192 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/list-body-composer.js +46 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/orchestrator.js +30 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/package-json-generator.js +38 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/regen-safety.js +89 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/view-emitter.js +56 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/views-generator.js +66 -0
- package/dist/libs/instance-factories/cli/templates/commander/command-generator.js +14 -11
- package/dist/libs/instance-factories/controllers/templates/fastify/server-generator.js +11 -3
- package/dist/libs/instance-factories/services/templates/prisma/controller-generator.js +27 -17
- package/dist/libs/instance-factories/shared/path-resolver.js +1 -1
- package/dist/realize/index.d.ts.map +1 -1
- package/dist/realize/index.js +15 -22
- package/dist/realize/index.js.map +1 -1
- package/dist/registry/utils/manifest-adapter.d.ts +8 -1
- package/dist/registry/utils/manifest-adapter.d.ts.map +1 -1
- package/dist/registry/utils/manifest-adapter.js +8 -1
- package/dist/registry/utils/manifest-adapter.js.map +1 -1
- package/libs/instance-factories/applications/react-app-starter.yaml +150 -0
- package/libs/instance-factories/applications/templates/generic/main-generator.ts +3 -3
- package/libs/instance-factories/applications/templates/react/api-client-generator.ts +16 -6
- package/libs/instance-factories/applications/templates/react-starter/README.md +211 -0
- package/libs/instance-factories/applications/templates/react-starter/__tests__/dashboard-body-composer.test.ts +153 -0
- package/libs/instance-factories/applications/templates/react-starter/__tests__/detail-body-composer.test.ts +145 -0
- package/libs/instance-factories/applications/templates/react-starter/__tests__/form-body-composer.test.ts +175 -0
- package/libs/instance-factories/applications/templates/react-starter/__tests__/helpers-emitter.test.ts +55 -0
- package/libs/instance-factories/applications/templates/react-starter/__tests__/html-to-jsx.test.ts +140 -0
- package/libs/instance-factories/applications/templates/react-starter/__tests__/list-body-composer.test.ts +146 -0
- package/libs/instance-factories/applications/templates/react-starter/__tests__/orchestrator.test.ts +163 -0
- package/libs/instance-factories/applications/templates/react-starter/__tests__/parity-p2-factory-imports.test.ts +116 -0
- package/libs/instance-factories/applications/templates/react-starter/__tests__/parity-p3-rendered-output.test.ts +183 -0
- package/libs/instance-factories/applications/templates/react-starter/__tests__/regen-safety.test.ts +144 -0
- package/libs/instance-factories/applications/templates/react-starter/__tests__/starter-generators.test.ts +114 -0
- package/libs/instance-factories/applications/templates/react-starter/__tests__/view-emitter.test.ts +107 -0
- package/libs/instance-factories/applications/templates/react-starter/__tests__/views-generator.test.ts +139 -0
- package/libs/instance-factories/applications/templates/react-starter/app-tsx-generator.ts +141 -0
- package/libs/instance-factories/applications/templates/react-starter/dashboard-body-composer.ts +174 -0
- package/libs/instance-factories/applications/templates/react-starter/detail-body-composer.ts +135 -0
- package/libs/instance-factories/applications/templates/react-starter/form-body-composer.ts +306 -0
- package/libs/instance-factories/applications/templates/react-starter/helpers-emitter.ts +60 -0
- package/libs/instance-factories/applications/templates/react-starter/html-to-jsx.ts +334 -0
- package/libs/instance-factories/applications/templates/react-starter/list-body-composer.ts +120 -0
- package/libs/instance-factories/applications/templates/react-starter/orchestrator.ts +80 -0
- package/libs/instance-factories/applications/templates/react-starter/package-json-generator.ts +57 -0
- package/libs/instance-factories/applications/templates/react-starter/regen-safety.ts +157 -0
- package/libs/instance-factories/applications/templates/react-starter/skeletons/dashboard.tsx.template +47 -0
- package/libs/instance-factories/applications/templates/react-starter/skeletons/detail.tsx.template +94 -0
- package/libs/instance-factories/applications/templates/react-starter/skeletons/form.tsx.template +114 -0
- package/libs/instance-factories/applications/templates/react-starter/skeletons/list.tsx.template +72 -0
- package/libs/instance-factories/applications/templates/react-starter/view-emitter.ts +151 -0
- package/libs/instance-factories/applications/templates/react-starter/views-generator.ts +137 -0
- package/libs/instance-factories/cli/templates/commander/command-generator.ts +14 -11
- package/libs/instance-factories/controllers/templates/fastify/server-generator.ts +11 -3
- package/libs/instance-factories/services/templates/prisma/controller-generator.ts +27 -17
- package/libs/instance-factories/shared/path-resolver.ts +8 -2
- package/package.json +3 -3
- package/dist/libs/instance-factories/applications/templates/react/_view-components-source.js +0 -530
- package/dist/libs/instance-factories/applications/templates/react/app-tsx-generator.js +0 -73
- package/dist/libs/instance-factories/applications/templates/react/field-helpers-generator.js +0 -99
- package/dist/libs/instance-factories/applications/templates/react/package-json-generator.js +0 -49
- package/dist/libs/instance-factories/applications/templates/react/pattern-adapter-generator.js +0 -156
- package/dist/libs/instance-factories/applications/templates/react/react-pattern-adapter.js +0 -935
- package/dist/libs/instance-factories/applications/templates/react/relationship-field-generator.js +0 -143
- package/dist/libs/instance-factories/applications/templates/react/tailwind-adapter-generator.js +0 -646
- package/dist/libs/instance-factories/applications/templates/react/tailwind-adapter-wrapper-generator.js +0 -65
- package/dist/libs/instance-factories/applications/templates/react/view-dashboard-generator.js +0 -143
- package/dist/libs/instance-factories/applications/templates/react/view-detail-generator.js +0 -143
- package/dist/libs/instance-factories/applications/templates/react/view-form-generator.js +0 -355
- package/dist/libs/instance-factories/applications/templates/react/view-list-generator.js +0 -91
- package/dist/libs/instance-factories/applications/templates/react/view-router-generator.js +0 -79
- package/dist/libs/instance-factories/tools/templates/vscode/vscode-extension-generator.js.bak +0 -244
- package/dist/libs/instance-factories/views/index.js +0 -48
- package/dist/libs/instance-factories/views/templates/react/adapters/antd-adapter.js +0 -742
- package/dist/libs/instance-factories/views/templates/react/adapters/mui-adapter.js +0 -824
- package/dist/libs/instance-factories/views/templates/react/adapters/shadcn-adapter.js +0 -719
- package/dist/libs/instance-factories/views/templates/react/app-generator.js +0 -45
- package/dist/libs/instance-factories/views/templates/react/components-generator.js +0 -820
- package/dist/libs/instance-factories/views/templates/react/forms-generator.js +0 -275
- package/dist/libs/instance-factories/views/templates/react/frontend-package-json-generator.js +0 -46
- package/dist/libs/instance-factories/views/templates/react/hooks-generator.js +0 -81
- package/dist/libs/instance-factories/views/templates/react/index-css-generator.js +0 -9
- package/dist/libs/instance-factories/views/templates/react/index-html-generator.js +0 -23
- package/dist/libs/instance-factories/views/templates/react/main-tsx-generator.js +0 -21
- package/dist/libs/instance-factories/views/templates/react/react-component-generator.js +0 -299
- package/dist/libs/instance-factories/views/templates/react/router-generator.js +0 -136
- package/dist/libs/instance-factories/views/templates/react/router-generic-generator.js +0 -107
- package/dist/libs/instance-factories/views/templates/react/shared-utils-generator.js +0 -187
- package/dist/libs/instance-factories/views/templates/react/spec-json-generator.js +0 -7
- package/dist/libs/instance-factories/views/templates/react/types-generator.js +0 -56
- package/dist/libs/instance-factories/views/templates/react/views-metadata-generator.js +0 -27
- package/dist/libs/instance-factories/views/templates/react/vite-config-generator.js +0 -29
- package/dist/libs/instance-factories/views/templates/runtime/runtime-view-renderer.js +0 -261
- package/dist/libs/instance-factories/views/templates/shared/adapter-types.js +0 -34
- package/dist/libs/instance-factories/views/templates/shared/atomic-components-registry.js +0 -800
- package/dist/libs/instance-factories/views/templates/shared/base-generator.js +0 -305
- package/dist/libs/instance-factories/views/templates/shared/component-metadata.js +0 -517
- package/dist/libs/instance-factories/views/templates/shared/composite-pattern-types.js +0 -0
- package/dist/libs/instance-factories/views/templates/shared/composite-patterns.js +0 -445
- package/dist/libs/instance-factories/views/templates/shared/index.js +0 -80
- package/dist/libs/instance-factories/views/templates/shared/pattern-validator.js +0 -210
- package/dist/libs/instance-factories/views/templates/shared/property-mapper.js +0 -492
- package/dist/libs/instance-factories/views/templates/shared/syntax-mapper.js +0 -321
- package/dist/realize/index.js.bak +0 -758
- package/libs/instance-factories/applications/react-app.yaml +0 -186
- package/libs/instance-factories/applications/templates/react/_view-components-source.ts +0 -555
- package/libs/instance-factories/applications/templates/react/app-tsx-generator.ts +0 -94
- package/libs/instance-factories/applications/templates/react/field-helpers-generator.ts +0 -106
- package/libs/instance-factories/applications/templates/react/package-json-generator.ts +0 -57
- package/libs/instance-factories/applications/templates/react/pattern-adapter-generator.ts +0 -179
- package/libs/instance-factories/applications/templates/react/react-pattern-adapter.tsx +0 -1347
- package/libs/instance-factories/applications/templates/react/relationship-field-generator.ts +0 -150
- package/libs/instance-factories/applications/templates/react/tailwind-adapter-generator.ts +0 -704
- package/libs/instance-factories/applications/templates/react/tailwind-adapter-wrapper-generator.ts +0 -84
- package/libs/instance-factories/applications/templates/react/view-dashboard-generator.ts +0 -150
- package/libs/instance-factories/applications/templates/react/view-detail-generator.ts +0 -150
- package/libs/instance-factories/applications/templates/react/view-form-generator.ts +0 -362
- package/libs/instance-factories/applications/templates/react/view-list-generator.ts +0 -98
- package/libs/instance-factories/applications/templates/react/view-router-generator.ts +0 -89
- package/libs/instance-factories/views/README.md +0 -62
- package/libs/instance-factories/views/index.d.ts +0 -13
- package/libs/instance-factories/views/index.d.ts.map +0 -1
- package/libs/instance-factories/views/index.js +0 -18
- package/libs/instance-factories/views/index.js.map +0 -1
- package/libs/instance-factories/views/index.ts +0 -45
- package/libs/instance-factories/views/react-components.yaml +0 -129
- package/libs/instance-factories/views/templates/ARCHITECTURE.md +0 -198
- package/libs/instance-factories/views/templates/react/adapters/antd-adapter.ts +0 -869
- package/libs/instance-factories/views/templates/react/adapters/mui-adapter.ts +0 -953
- package/libs/instance-factories/views/templates/react/adapters/shadcn-adapter.ts +0 -806
- package/libs/instance-factories/views/templates/react/app-generator.ts +0 -55
- package/libs/instance-factories/views/templates/react/components-generator.ts +0 -938
- package/libs/instance-factories/views/templates/react/forms-generator.ts +0 -325
- package/libs/instance-factories/views/templates/react/frontend-package-json-generator.ts +0 -57
- package/libs/instance-factories/views/templates/react/hooks-generator.ts +0 -106
- package/libs/instance-factories/views/templates/react/index-css-generator.ts +0 -14
- package/libs/instance-factories/views/templates/react/index-html-generator.ts +0 -34
- package/libs/instance-factories/views/templates/react/main-tsx-generator.ts +0 -29
- package/libs/instance-factories/views/templates/react/react-component-generator.d.ts +0 -152
- package/libs/instance-factories/views/templates/react/react-component-generator.d.ts.map +0 -1
- package/libs/instance-factories/views/templates/react/react-component-generator.js +0 -398
- package/libs/instance-factories/views/templates/react/react-component-generator.js.map +0 -1
- package/libs/instance-factories/views/templates/react/react-component-generator.ts +0 -533
- package/libs/instance-factories/views/templates/react/router-generator.ts +0 -197
- package/libs/instance-factories/views/templates/react/router-generic-generator.ts +0 -132
- package/libs/instance-factories/views/templates/react/shared-utils-generator.ts +0 -196
- package/libs/instance-factories/views/templates/react/spec-json-generator.ts +0 -17
- package/libs/instance-factories/views/templates/react/types-generator.ts +0 -76
- package/libs/instance-factories/views/templates/react/views-metadata-generator.ts +0 -42
- package/libs/instance-factories/views/templates/react/vite-config-generator.ts +0 -38
- package/libs/instance-factories/views/templates/runtime/runtime-view-renderer.d.ts.map +0 -1
- package/libs/instance-factories/views/templates/runtime/runtime-view-renderer.js.map +0 -1
- package/libs/instance-factories/views/templates/runtime/runtime-view-renderer.ts +0 -474
- package/libs/instance-factories/views/templates/shared/__tests__/composite-patterns.test.ts +0 -242
- package/libs/instance-factories/views/templates/shared/adapter-types.d.ts +0 -77
- package/libs/instance-factories/views/templates/shared/adapter-types.d.ts.map +0 -1
- package/libs/instance-factories/views/templates/shared/adapter-types.js +0 -47
- package/libs/instance-factories/views/templates/shared/adapter-types.js.map +0 -1
- package/libs/instance-factories/views/templates/shared/adapter-types.ts +0 -142
- package/libs/instance-factories/views/templates/shared/atomic-components-registry.d.ts +0 -63
- package/libs/instance-factories/views/templates/shared/atomic-components-registry.d.ts.map +0 -1
- package/libs/instance-factories/views/templates/shared/atomic-components-registry.js +0 -822
- package/libs/instance-factories/views/templates/shared/atomic-components-registry.js.map +0 -1
- package/libs/instance-factories/views/templates/shared/atomic-components-registry.ts +0 -908
- package/libs/instance-factories/views/templates/shared/base-generator.d.ts +0 -247
- package/libs/instance-factories/views/templates/shared/base-generator.d.ts.map +0 -1
- package/libs/instance-factories/views/templates/shared/base-generator.js +0 -363
- package/libs/instance-factories/views/templates/shared/base-generator.js.map +0 -1
- package/libs/instance-factories/views/templates/shared/base-generator.ts +0 -608
- package/libs/instance-factories/views/templates/shared/component-metadata.d.ts +0 -254
- package/libs/instance-factories/views/templates/shared/component-metadata.d.ts.map +0 -1
- package/libs/instance-factories/views/templates/shared/component-metadata.js +0 -602
- package/libs/instance-factories/views/templates/shared/component-metadata.js.map +0 -1
- package/libs/instance-factories/views/templates/shared/component-metadata.ts +0 -803
- package/libs/instance-factories/views/templates/shared/composite-pattern-types.ts +0 -250
- package/libs/instance-factories/views/templates/shared/composite-patterns.ts +0 -535
- package/libs/instance-factories/views/templates/shared/index.ts +0 -68
- package/libs/instance-factories/views/templates/shared/pattern-validator.ts +0 -279
- package/libs/instance-factories/views/templates/shared/property-mapper.d.ts +0 -149
- package/libs/instance-factories/views/templates/shared/property-mapper.d.ts.map +0 -1
- package/libs/instance-factories/views/templates/shared/property-mapper.js +0 -580
- package/libs/instance-factories/views/templates/shared/property-mapper.js.map +0 -1
- package/libs/instance-factories/views/templates/shared/property-mapper.ts +0 -700
- package/libs/instance-factories/views/templates/shared/syntax-mapper.d.ts +0 -143
- package/libs/instance-factories/views/templates/shared/syntax-mapper.d.ts.map +0 -1
- package/libs/instance-factories/views/templates/shared/syntax-mapper.js +0 -420
- package/libs/instance-factories/views/templates/shared/syntax-mapper.js.map +0 -1
- package/libs/instance-factories/views/templates/shared/syntax-mapper.ts +0 -539
package/dist/libs/instance-factories/applications/templates/react-starter/form-body-composer.js
ADDED
|
@@ -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, """);
|
|
184
|
+
}
|
|
185
|
+
function escapeText(s) {
|
|
186
|
+
return s.replace(/</g, "<").replace(/>/g, ">");
|
|
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
|
+
};
|
package/dist/libs/instance-factories/applications/templates/react-starter/list-body-composer.js
ADDED
|
@@ -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
|
+
};
|
package/dist/libs/instance-factories/applications/templates/react-starter/package-json-generator.js
ADDED
|
@@ -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
|
+
};
|