@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
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regeneration safety for ReactAppStarter
|
|
3
|
+
*
|
|
4
|
+
* When realize runs Factory B, generated files can end up in two states:
|
|
5
|
+
*
|
|
6
|
+
* 1. Pristine — the file on disk is byte-identical to what we last
|
|
7
|
+
* wrote. Safe to overwrite with a new version.
|
|
8
|
+
*
|
|
9
|
+
* 2. User-edited — the user modified the file. We must NOT clobber
|
|
10
|
+
* their work.
|
|
11
|
+
*
|
|
12
|
+
* The mechanism is an SHA-256 hash recorded per file in
|
|
13
|
+
* `.specverse-gen/hashes.json` at write time. At regeneration, we
|
|
14
|
+
* compare the on-disk hash against the recorded one. Mismatch → skip
|
|
15
|
+
* with a warning. Match → overwrite and update the hash.
|
|
16
|
+
*
|
|
17
|
+
* The factory orchestrator wraps realize's write calls through
|
|
18
|
+
* `reconcileWrites` below, which handles all three outcomes:
|
|
19
|
+
* - brand-new file → write, record hash
|
|
20
|
+
* - pristine existing → overwrite, update hash
|
|
21
|
+
* - user-edited existing → skip, leave hash unchanged
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { createHash } from 'crypto';
|
|
25
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
26
|
+
import { join, relative } from 'path';
|
|
27
|
+
|
|
28
|
+
/** Relative path (from project root) → SHA-256 hex digest. */
|
|
29
|
+
export type HashManifest = Record<string, string>;
|
|
30
|
+
|
|
31
|
+
export const HASHES_DIR = '.specverse-gen';
|
|
32
|
+
export const HASHES_FILE = 'hashes.json';
|
|
33
|
+
|
|
34
|
+
export function sha256(s: string): string {
|
|
35
|
+
return createHash('sha256').update(s, 'utf8').digest('hex');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Load a `.specverse-gen/hashes.json` from the project root. Returns
|
|
40
|
+
* an empty manifest if the file is missing or malformed — the
|
|
41
|
+
* calling convention is that a missing manifest means "no record of
|
|
42
|
+
* prior generation, treat everything as new."
|
|
43
|
+
*/
|
|
44
|
+
export function loadHashManifest(projectRoot: string): HashManifest {
|
|
45
|
+
const path = join(projectRoot, HASHES_DIR, HASHES_FILE);
|
|
46
|
+
if (!existsSync(path)) return {};
|
|
47
|
+
try {
|
|
48
|
+
const raw = readFileSync(path, 'utf8');
|
|
49
|
+
const parsed = JSON.parse(raw);
|
|
50
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
51
|
+
const out: HashManifest = {};
|
|
52
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
53
|
+
if (typeof v === 'string') out[k] = v;
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
// Malformed — treat as missing. Next generate pass will overwrite.
|
|
59
|
+
}
|
|
60
|
+
return {};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Write the manifest back to `.specverse-gen/hashes.json`. */
|
|
64
|
+
export function saveHashManifest(projectRoot: string, manifest: HashManifest): void {
|
|
65
|
+
const dir = join(projectRoot, HASHES_DIR);
|
|
66
|
+
mkdirSync(dir, { recursive: true });
|
|
67
|
+
const path = join(dir, HASHES_FILE);
|
|
68
|
+
writeFileSync(path, JSON.stringify(manifest, null, 2) + '\n', 'utf8');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface ReconcileResult {
|
|
72
|
+
/** Relative path → content for every file approved for writing. */
|
|
73
|
+
approvedWrites: Record<string, string>;
|
|
74
|
+
/** Files we skipped because the user edited them (or couldn't confirm origin). */
|
|
75
|
+
skipped: { path: string; reason: string }[];
|
|
76
|
+
/** The updated hash manifest reflecting all approved writes. */
|
|
77
|
+
manifest: HashManifest;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Pure-planning triage. For each proposed file, decides whether the
|
|
82
|
+
* write is safe:
|
|
83
|
+
* - Path doesn't exist → APPROVE, record hash.
|
|
84
|
+
* - Path exists + on-disk hash matches recorded hash → APPROVE
|
|
85
|
+
* (pristine overwrite), update hash.
|
|
86
|
+
* - Path exists + hash mismatch → SKIP (user-edited), preserve old
|
|
87
|
+
* hash record.
|
|
88
|
+
* - Path exists + no recorded hash → SKIP (cautious default).
|
|
89
|
+
*
|
|
90
|
+
* Reads the filesystem but doesn't write. Returns the approved writes
|
|
91
|
+
* as a map the caller can pass to the realize write pipeline.
|
|
92
|
+
*/
|
|
93
|
+
export function reconcileWrites(
|
|
94
|
+
projectRoot: string,
|
|
95
|
+
proposed: Record<string, string>,
|
|
96
|
+
prevManifest: HashManifest
|
|
97
|
+
): ReconcileResult {
|
|
98
|
+
const manifest: HashManifest = { ...prevManifest };
|
|
99
|
+
const approvedWrites: Record<string, string> = {};
|
|
100
|
+
const skipped: { path: string; reason: string }[] = [];
|
|
101
|
+
|
|
102
|
+
for (const [relPath, content] of Object.entries(proposed)) {
|
|
103
|
+
const abs = join(projectRoot, relPath);
|
|
104
|
+
const newHash = sha256(content);
|
|
105
|
+
|
|
106
|
+
if (!existsSync(abs)) {
|
|
107
|
+
approvedWrites[relPath] = content;
|
|
108
|
+
manifest[relPath] = newHash;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const currentContent = readFileSync(abs, 'utf8');
|
|
113
|
+
const currentHash = sha256(currentContent);
|
|
114
|
+
const recordedHash = prevManifest[relPath];
|
|
115
|
+
|
|
116
|
+
if (recordedHash == null) {
|
|
117
|
+
skipped.push({
|
|
118
|
+
path: relPath,
|
|
119
|
+
reason: 'no prior hash recorded — cannot confirm this file was generated by us',
|
|
120
|
+
});
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (currentHash === recordedHash) {
|
|
125
|
+
approvedWrites[relPath] = content;
|
|
126
|
+
manifest[relPath] = newHash;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
skipped.push({
|
|
131
|
+
path: relPath,
|
|
132
|
+
reason: 'file has been edited since last generation',
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return { approvedWrites, skipped, manifest };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Human-readable summary of a ReconcileResult. Used by the factory
|
|
141
|
+
* orchestrator to log after a run.
|
|
142
|
+
*/
|
|
143
|
+
export function summarize(result: ReconcileResult, projectRoot: string): string {
|
|
144
|
+
const lines: string[] = [];
|
|
145
|
+
const writeCount = Object.keys(result.approvedWrites).length;
|
|
146
|
+
lines.push(`[ReactAppStarter] Approved ${writeCount} file(s) for writing.`);
|
|
147
|
+
if (result.skipped.length > 0) {
|
|
148
|
+
lines.push(`[ReactAppStarter] Skipped ${result.skipped.length} user-edited file(s):`);
|
|
149
|
+
for (const { path, reason } of result.skipped) {
|
|
150
|
+
lines.push(` - ${path} (${reason})`);
|
|
151
|
+
}
|
|
152
|
+
lines.push(
|
|
153
|
+
`To accept upstream regeneration for a skipped file, delete it (\`rm ${relative(process.cwd(), projectRoot)}/PATH\`) and re-run \`spv realize\`.`
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
return lines.join('\n');
|
|
157
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* {{MODEL_NAME}}DashboardView — generated by @specverse/realize (ReactAppStarter)
|
|
3
|
+
*
|
|
4
|
+
* Safe to edit. Edits are preserved across regeneration via content
|
|
5
|
+
* hashing (see .specverse-gen/hashes.json). To accept an upstream
|
|
6
|
+
* regeneration of this file, delete it first, then run `spv realize`.
|
|
7
|
+
*
|
|
8
|
+
* A minimal dashboard: summary counts derived from the list query,
|
|
9
|
+
* plus a compact preview of recent records. Charts and aggregation
|
|
10
|
+
* metrics are deferred — add them as the backend grows suitable
|
|
11
|
+
* endpoints.
|
|
12
|
+
*/
|
|
13
|
+
import { useMemo } from 'react';
|
|
14
|
+
import { use{{PLURAL_MODEL}}Query } from '../hooks/useApi';
|
|
15
|
+
import type { {{MODEL_NAME}} } from '../types/api';
|
|
16
|
+
|
|
17
|
+
interface {{MODEL_NAME}}DashboardViewProps {
|
|
18
|
+
/** Number of recent records to preview. Default 5. */
|
|
19
|
+
previewLimit?: number;
|
|
20
|
+
onSelect?: (item: {{MODEL_NAME}}) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function {{MODEL_NAME}}DashboardView({
|
|
24
|
+
previewLimit = 5,
|
|
25
|
+
onSelect,
|
|
26
|
+
}: {{MODEL_NAME}}DashboardViewProps) {
|
|
27
|
+
const { data: items = [], isLoading, error } = use{{PLURAL_MODEL}}Query();
|
|
28
|
+
|
|
29
|
+
const preview = useMemo(
|
|
30
|
+
() => items.slice(0, previewLimit),
|
|
31
|
+
[items, previewLimit]
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
if (isLoading) return <div className="p-4 text-gray-500">Loading dashboard…</div>;
|
|
35
|
+
if (error) return <div className="p-4 text-red-600">Error loading {{PLURAL_LOWER}}: {String(error)}</div>;
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div className="p-6 space-y-6">
|
|
39
|
+
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
|
40
|
+
{{MODEL_NAME}} dashboard
|
|
41
|
+
</h2>
|
|
42
|
+
|
|
43
|
+
{/* Pattern-rendered dashboard body (metrics + preview). Edit freely. */}
|
|
44
|
+
{{BODY}}
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|
package/libs/instance-factories/applications/templates/react-starter/skeletons/detail.tsx.template
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* {{MODEL_NAME}}DetailView — generated by @specverse/realize (ReactAppStarter)
|
|
3
|
+
*
|
|
4
|
+
* Safe to edit. Edits are preserved across regeneration via content
|
|
5
|
+
* hashing (see .specverse-gen/hashes.json). To accept an upstream
|
|
6
|
+
* regeneration of this file, delete it first, then run `spv realize`.
|
|
7
|
+
*/
|
|
8
|
+
import { use{{PLURAL_MODEL}}Query, useDelete{{MODEL_NAME}}Mutation } from '../hooks/useApi';
|
|
9
|
+
import type { {{MODEL_NAME}} } from '../types/api';
|
|
10
|
+
|
|
11
|
+
interface {{MODEL_NAME}}DetailViewProps {
|
|
12
|
+
entityId: string | number;
|
|
13
|
+
onEdit?: (item: {{MODEL_NAME}}) => void;
|
|
14
|
+
onBack?: () => void;
|
|
15
|
+
/** Called after the delete mutation succeeds. */
|
|
16
|
+
onDeleted?: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function {{MODEL_NAME}}DetailView({
|
|
20
|
+
entityId,
|
|
21
|
+
onEdit,
|
|
22
|
+
onBack,
|
|
23
|
+
onDeleted,
|
|
24
|
+
}: {{MODEL_NAME}}DetailViewProps) {
|
|
25
|
+
const { data: items = [], isLoading, error } = use{{PLURAL_MODEL}}Query();
|
|
26
|
+
const deleteItem = useDelete{{MODEL_NAME}}Mutation();
|
|
27
|
+
|
|
28
|
+
const item = items.find(
|
|
29
|
+
(x: {{MODEL_NAME}}) => (x as any).id === entityId
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
if (isLoading) return <div className="p-4 text-gray-500">Loading…</div>;
|
|
33
|
+
if (error) return <div className="p-4 text-red-600">Error loading {{PLURAL_LOWER}}: {String(error)}</div>;
|
|
34
|
+
if (!item) return <div className="p-4 text-gray-400">No {{SINGULAR_LOWER}} matching id {String(entityId)}.</div>;
|
|
35
|
+
|
|
36
|
+
const handleDelete = async () => {
|
|
37
|
+
if (!confirm('Delete this {{SINGULAR_LOWER}}?')) return;
|
|
38
|
+
try {
|
|
39
|
+
await deleteItem.mutateAsync((item as any).id);
|
|
40
|
+
onDeleted?.();
|
|
41
|
+
} catch {
|
|
42
|
+
// deleteItem.error is surfaced below
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div className="p-6 space-y-4">
|
|
48
|
+
<div className="flex items-center justify-between">
|
|
49
|
+
<div className="flex items-center gap-3">
|
|
50
|
+
{onBack && (
|
|
51
|
+
<button
|
|
52
|
+
type="button"
|
|
53
|
+
onClick={onBack}
|
|
54
|
+
className="text-sm text-gray-500 hover:text-gray-700"
|
|
55
|
+
>
|
|
56
|
+
← Back
|
|
57
|
+
</button>
|
|
58
|
+
)}
|
|
59
|
+
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
|
60
|
+
{{MODEL_NAME}} detail
|
|
61
|
+
</h2>
|
|
62
|
+
</div>
|
|
63
|
+
<div className="flex gap-2">
|
|
64
|
+
{onEdit && (
|
|
65
|
+
<button
|
|
66
|
+
type="button"
|
|
67
|
+
onClick={() => onEdit(item)}
|
|
68
|
+
className="rounded bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
|
|
69
|
+
>
|
|
70
|
+
Edit
|
|
71
|
+
</button>
|
|
72
|
+
)}
|
|
73
|
+
<button
|
|
74
|
+
type="button"
|
|
75
|
+
onClick={handleDelete}
|
|
76
|
+
disabled={deleteItem.isPending}
|
|
77
|
+
className="rounded bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700 disabled:opacity-60"
|
|
78
|
+
>
|
|
79
|
+
{deleteItem.isPending ? 'Deleting…' : 'Delete'}
|
|
80
|
+
</button>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
{/* Pattern-rendered detail body (fields). Edit freely. */}
|
|
85
|
+
{{BODY}}
|
|
86
|
+
|
|
87
|
+
{deleteItem.isError && (
|
|
88
|
+
<div className="p-2 text-sm text-red-600">
|
|
89
|
+
Delete failed: {String(deleteItem.error)}
|
|
90
|
+
</div>
|
|
91
|
+
)}
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
}
|
package/libs/instance-factories/applications/templates/react-starter/skeletons/form.tsx.template
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* {{MODEL_NAME}}FormView — generated by @specverse/realize (ReactAppStarter)
|
|
3
|
+
*
|
|
4
|
+
* Safe to edit. Edits are preserved across regeneration via content
|
|
5
|
+
* hashing (see .specverse-gen/hashes.json). To accept an upstream
|
|
6
|
+
* regeneration of this file, delete it first, then run `spv realize`.
|
|
7
|
+
*
|
|
8
|
+
* Controlled form. One field per non-auto-generated attribute.
|
|
9
|
+
* Mode: 'create' (default) or 'update' — passed as a prop.
|
|
10
|
+
*/
|
|
11
|
+
import { useEffect, useState } from 'react';
|
|
12
|
+
import {
|
|
13
|
+
use{{PLURAL_MODEL}}Query,
|
|
14
|
+
useCreate{{MODEL_NAME}}Mutation,
|
|
15
|
+
useUpdate{{MODEL_NAME}}Mutation,
|
|
16
|
+
} from '../hooks/useApi';
|
|
17
|
+
import type { {{MODEL_NAME}} } from '../types/api';
|
|
18
|
+
|
|
19
|
+
type FormMode = 'create' | 'update';
|
|
20
|
+
|
|
21
|
+
interface {{MODEL_NAME}}FormViewProps {
|
|
22
|
+
mode?: FormMode;
|
|
23
|
+
/** Required in update mode. */
|
|
24
|
+
entityId?: string | number;
|
|
25
|
+
onSuccess?: (item: {{MODEL_NAME}}) => void;
|
|
26
|
+
onCancel?: () => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function {{MODEL_NAME}}FormView({
|
|
30
|
+
mode = 'create',
|
|
31
|
+
entityId,
|
|
32
|
+
onSuccess,
|
|
33
|
+
onCancel,
|
|
34
|
+
}: {{MODEL_NAME}}FormViewProps) {
|
|
35
|
+
const { data: items = [] } = use{{PLURAL_MODEL}}Query();
|
|
36
|
+
const createItem = useCreate{{MODEL_NAME}}Mutation();
|
|
37
|
+
const updateItem = useUpdate{{MODEL_NAME}}Mutation();
|
|
38
|
+
|
|
39
|
+
const existing =
|
|
40
|
+
mode === 'update'
|
|
41
|
+
? items.find((x: {{MODEL_NAME}}) => (x as any).id === entityId)
|
|
42
|
+
: undefined;
|
|
43
|
+
|
|
44
|
+
const [formData, setFormData] = useState<Partial<{{MODEL_NAME}}>>(existing ?? {});
|
|
45
|
+
|
|
46
|
+
// When the fetched list lands after initial render (update mode),
|
|
47
|
+
// hydrate the form with the loaded entity's data.
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (existing) setFormData(existing);
|
|
50
|
+
}, [existing]);
|
|
51
|
+
|
|
52
|
+
const handleChange = (field: string, value: unknown) => {
|
|
53
|
+
setFormData(prev => ({ ...prev, [field]: value }) as Partial<{{MODEL_NAME}}>);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const handleSubmit = async (event: React.FormEvent) => {
|
|
57
|
+
event.preventDefault();
|
|
58
|
+
try {
|
|
59
|
+
const result =
|
|
60
|
+
mode === 'create'
|
|
61
|
+
? await createItem.mutateAsync(formData as {{MODEL_NAME}})
|
|
62
|
+
: await updateItem.mutateAsync({
|
|
63
|
+
id: entityId as any,
|
|
64
|
+
data: formData as Partial<{{MODEL_NAME}}>,
|
|
65
|
+
});
|
|
66
|
+
onSuccess?.(result as {{MODEL_NAME}});
|
|
67
|
+
} catch {
|
|
68
|
+
// Mutation errors are surfaced below via createItem / updateItem.
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const mutation = mode === 'create' ? createItem : updateItem;
|
|
73
|
+
const submitLabel =
|
|
74
|
+
mode === 'create'
|
|
75
|
+
? mutation.isPending ? 'Creating…' : 'Create {{MODEL_NAME}}'
|
|
76
|
+
: mutation.isPending ? 'Updating…' : 'Update {{MODEL_NAME}}';
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<form onSubmit={handleSubmit} className="p-6 space-y-6">
|
|
80
|
+
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
|
81
|
+
{mode === 'create' ? 'New {{MODEL_NAME}}' : 'Edit {{MODEL_NAME}}'}
|
|
82
|
+
</h2>
|
|
83
|
+
|
|
84
|
+
{/* Pattern-rendered form fields. Edit freely. */}
|
|
85
|
+
{{BODY}}
|
|
86
|
+
|
|
87
|
+
<div className="flex gap-2 border-t border-gray-200 dark:border-gray-700 pt-4">
|
|
88
|
+
<button
|
|
89
|
+
type="submit"
|
|
90
|
+
disabled={mutation.isPending}
|
|
91
|
+
className="rounded bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-60"
|
|
92
|
+
>
|
|
93
|
+
{submitLabel}
|
|
94
|
+
</button>
|
|
95
|
+
{onCancel && (
|
|
96
|
+
<button
|
|
97
|
+
type="button"
|
|
98
|
+
onClick={onCancel}
|
|
99
|
+
className="rounded border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-200 dark:hover:bg-gray-800"
|
|
100
|
+
>
|
|
101
|
+
Cancel
|
|
102
|
+
</button>
|
|
103
|
+
)}
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
{mutation.isError && (
|
|
107
|
+
<div className="rounded border border-red-200 bg-red-50 p-3 text-sm text-red-700 dark:border-red-800 dark:bg-red-950 dark:text-red-300">
|
|
108
|
+
{mode === 'create' ? 'Create failed: ' : 'Update failed: '}
|
|
109
|
+
{String(mutation.error)}
|
|
110
|
+
</div>
|
|
111
|
+
)}
|
|
112
|
+
</form>
|
|
113
|
+
);
|
|
114
|
+
}
|
package/libs/instance-factories/applications/templates/react-starter/skeletons/list.tsx.template
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* {{MODEL_NAME}}ListView — generated by @specverse/realize (ReactAppStarter)
|
|
3
|
+
*
|
|
4
|
+
* Safe to edit. Edits are preserved across regeneration via content
|
|
5
|
+
* hashing (see .specverse-gen/hashes.json). To accept an upstream
|
|
6
|
+
* regeneration of this file, delete it first, then run `spv realize`.
|
|
7
|
+
*/
|
|
8
|
+
import { useState, useMemo } from 'react';
|
|
9
|
+
import { use{{PLURAL_MODEL}}Query, useDelete{{MODEL_NAME}}Mutation } from '../hooks/useApi';
|
|
10
|
+
import type { {{MODEL_NAME}} } from '../types/api';
|
|
11
|
+
|
|
12
|
+
interface {{MODEL_NAME}}ListViewProps {
|
|
13
|
+
onSelect?: (item: {{MODEL_NAME}}) => void;
|
|
14
|
+
onCreate?: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function {{MODEL_NAME}}ListView({ onSelect, onCreate }: {{MODEL_NAME}}ListViewProps) {
|
|
18
|
+
const { data: items = [], isLoading, error } = use{{PLURAL_MODEL}}Query();
|
|
19
|
+
const deleteItem = useDelete{{MODEL_NAME}}Mutation();
|
|
20
|
+
const [searchTerm, setSearchTerm] = useState('');
|
|
21
|
+
|
|
22
|
+
const filtered = useMemo(
|
|
23
|
+
() =>
|
|
24
|
+
items.filter((item: {{MODEL_NAME}}) =>
|
|
25
|
+
// Cast to `any` inside JSX context — `Record<string, unknown>`
|
|
26
|
+
// looks like a JSX opening tag to the TSX parser when this
|
|
27
|
+
// expression ends up inside JSX. `any` is safe here: we only
|
|
28
|
+
// read values for substring-matching against the search term.
|
|
29
|
+
Object.values(item as any).some(v =>
|
|
30
|
+
String(v ?? '').toLowerCase().includes(searchTerm.toLowerCase())
|
|
31
|
+
)
|
|
32
|
+
),
|
|
33
|
+
[items, searchTerm]
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
if (isLoading) return <div className="p-4 text-gray-500">Loading {{PLURAL_LOWER}}…</div>;
|
|
37
|
+
if (error) return <div className="p-4 text-red-600">Error loading {{PLURAL_LOWER}}: {String(error)}</div>;
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div className="p-6 space-y-4">
|
|
41
|
+
<div className="flex items-center justify-between">
|
|
42
|
+
<input
|
|
43
|
+
type="search"
|
|
44
|
+
placeholder="Search {{PLURAL_LOWER}}…"
|
|
45
|
+
value={searchTerm}
|
|
46
|
+
onChange={e => setSearchTerm(e.target.value)}
|
|
47
|
+
className="w-64 rounded border border-gray-300 px-3 py-2 text-sm"
|
|
48
|
+
/>
|
|
49
|
+
<button
|
|
50
|
+
type="button"
|
|
51
|
+
onClick={onCreate}
|
|
52
|
+
className="rounded bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
|
|
53
|
+
>
|
|
54
|
+
+ New {{MODEL_NAME}}
|
|
55
|
+
</button>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
{/* Pattern-rendered list body (table + rows). Edit freely. */}
|
|
59
|
+
{{BODY}}
|
|
60
|
+
|
|
61
|
+
{filtered.length === 0 && (
|
|
62
|
+
<div className="p-8 text-center text-gray-400">No {{PLURAL_LOWER}} yet.</div>
|
|
63
|
+
)}
|
|
64
|
+
|
|
65
|
+
{deleteItem.isError && (
|
|
66
|
+
<div className="p-2 text-sm text-red-600">
|
|
67
|
+
Delete failed: {String(deleteItem.error)}
|
|
68
|
+
</div>
|
|
69
|
+
)}
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ReactAppStarter view emitter
|
|
3
|
+
*
|
|
4
|
+
* Given a view spec + model spec + expanded spec, emits a complete
|
|
5
|
+
* idiomatic React component source file as a string.
|
|
6
|
+
*
|
|
7
|
+
* Strategy 3 ("skeleton + rendered interior"): a hand-written
|
|
8
|
+
* skeleton template provides the idiomatic React outer structure
|
|
9
|
+
* (imports, hooks, props, layout). The canonical Tailwind adapter
|
|
10
|
+
* renders the pattern's interior as HTML. The html-to-jsx transformer
|
|
11
|
+
* converts the HTML to JSX. The emitter composes these.
|
|
12
|
+
*
|
|
13
|
+
* See README.md in this directory for the architecture.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { readFileSync } from 'fs';
|
|
17
|
+
import { join, dirname } from 'path';
|
|
18
|
+
import { fileURLToPath } from 'url';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The minimal shape of a view spec this emitter needs. Matches the
|
|
22
|
+
* inferred spec format — `view.type` drives skeleton selection,
|
|
23
|
+
* `view.model` identifies the primary model, `view.uiComponents` (if
|
|
24
|
+
* present) overrides inference defaults.
|
|
25
|
+
*/
|
|
26
|
+
export interface ViewSpec {
|
|
27
|
+
type: string;
|
|
28
|
+
model?: string;
|
|
29
|
+
uiComponents?: Record<string, unknown>;
|
|
30
|
+
[key: string]: unknown;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The minimal shape of a model spec this emitter needs. Field details
|
|
35
|
+
* flow through to the Tailwind adapter which decides column selection.
|
|
36
|
+
*/
|
|
37
|
+
export interface ModelSpec {
|
|
38
|
+
name: string;
|
|
39
|
+
attributes: Record<string, unknown>;
|
|
40
|
+
relationships?: Record<string, unknown>;
|
|
41
|
+
lifecycles?: Record<string, unknown>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface EmitContext {
|
|
45
|
+
view: ViewSpec;
|
|
46
|
+
viewName: string; // e.g. "PostListView"
|
|
47
|
+
model: ModelSpec;
|
|
48
|
+
modelSchemas: Record<string, ModelSpec>;
|
|
49
|
+
/** Pluggable so we can stub the renderer in unit tests. */
|
|
50
|
+
renderBody: RenderBodyFn;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Render the body of a view as JSX-safe source — the exact string
|
|
55
|
+
* that is dropped at `{{BODY}}` in the skeleton.
|
|
56
|
+
*
|
|
57
|
+
* The renderBody implementation is responsible for the whole body
|
|
58
|
+
* pipeline: invoke the canonical Tailwind adapter for static shell
|
|
59
|
+
* rendering, run that HTML through `htmlToJsx`, and inject any JSX
|
|
60
|
+
* expressions (`.map()`, event handlers). Keeping it here means
|
|
61
|
+
* view-emitter stays a pure orchestrator and can't accidentally
|
|
62
|
+
* double-transform JSX that's already JSX.
|
|
63
|
+
*/
|
|
64
|
+
export type RenderBodyFn = (context: EmitContext) => string;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Emit a complete .tsx file for a single view. Pure function — all
|
|
68
|
+
* inputs come in via the context argument so it's easy to test.
|
|
69
|
+
*/
|
|
70
|
+
export function emitView(context: EmitContext): string {
|
|
71
|
+
const skeleton = loadSkeleton(context.view.type);
|
|
72
|
+
const bodyJsx = context.renderBody(context);
|
|
73
|
+
|
|
74
|
+
const substitutions = buildSubstitutions(context, bodyJsx);
|
|
75
|
+
return applySubstitutions(skeleton, substitutions);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
79
|
+
// Skeleton loading
|
|
80
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
const SKELETON_BY_VIEW_TYPE: Record<string, string> = {
|
|
83
|
+
list: 'list.tsx.template',
|
|
84
|
+
detail: 'detail.tsx.template',
|
|
85
|
+
form: 'form.tsx.template',
|
|
86
|
+
dashboard: 'dashboard.tsx.template',
|
|
87
|
+
// Specialist types (board, timeline, calendar, analytics, workflow,
|
|
88
|
+
// wizard, comparison, settings, map, feed, profile) come online in
|
|
89
|
+
// Phase 2e.
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
function loadSkeleton(viewType: string): string {
|
|
93
|
+
const filename = SKELETON_BY_VIEW_TYPE[viewType.toLowerCase()];
|
|
94
|
+
if (!filename) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`No skeleton registered for view type "${viewType}". ` +
|
|
97
|
+
`Known types: ${Object.keys(SKELETON_BY_VIEW_TYPE).join(', ')}.`
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
102
|
+
const path = join(here, 'skeletons', filename);
|
|
103
|
+
return readFileSync(path, 'utf8');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
107
|
+
// Substitution
|
|
108
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
interface Substitutions {
|
|
111
|
+
MODEL_NAME: string;
|
|
112
|
+
PLURAL_MODEL: string;
|
|
113
|
+
PLURAL_LOWER: string;
|
|
114
|
+
SINGULAR_LOWER: string;
|
|
115
|
+
BODY: string;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function buildSubstitutions(context: EmitContext, body: string): Substitutions {
|
|
119
|
+
const modelName = context.model.name;
|
|
120
|
+
const pluralModel = pluralize(modelName);
|
|
121
|
+
return {
|
|
122
|
+
MODEL_NAME: modelName,
|
|
123
|
+
PLURAL_MODEL: pluralModel,
|
|
124
|
+
PLURAL_LOWER: pluralModel.toLowerCase(),
|
|
125
|
+
SINGULAR_LOWER: modelName.toLowerCase(),
|
|
126
|
+
BODY: body,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function applySubstitutions(template: string, subs: Substitutions): string {
|
|
131
|
+
let out = template;
|
|
132
|
+
for (const [key, value] of Object.entries(subs)) {
|
|
133
|
+
// Replace every occurrence of {{KEY}} — plain string substitution,
|
|
134
|
+
// no expression evaluation. Escape regex special chars in the key
|
|
135
|
+
// (defensive; keys are ASCII constants).
|
|
136
|
+
const pattern = new RegExp(`\\{\\{${key}\\}\\}`, 'g');
|
|
137
|
+
out = out.replace(pattern, value);
|
|
138
|
+
}
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Minimal English pluralizer matching the adapter's conventions.
|
|
144
|
+
* Good enough for generated code — users can edit the output if they
|
|
145
|
+
* want a different pluralization.
|
|
146
|
+
*/
|
|
147
|
+
function pluralize(s: string): string {
|
|
148
|
+
if (/[^aeiou]y$/i.test(s)) return s.slice(0, -1) + 'ies';
|
|
149
|
+
if (/(s|x|z|ch|sh)$/i.test(s)) return s + 'es';
|
|
150
|
+
return s + 's';
|
|
151
|
+
}
|