@specverse/engines 4.1.30 → 4.2.1
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/assets/templates/default/specs/main.specly +65 -0
- 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/CURVED-INTERFACE.md +278 -0
- package/dist/libs/instance-factories/README.md +73 -0
- package/dist/libs/instance-factories/applications/README.md +51 -0
- package/dist/libs/instance-factories/applications/generic-app.yaml +52 -0
- package/{libs/instance-factories/applications/react-app.yaml → dist/libs/instance-factories/applications/react-app-runtime.yaml} +30 -77
- package/dist/libs/instance-factories/applications/react-app-starter.yaml +143 -0
- package/dist/libs/instance-factories/applications/templates/react/env-example-generator.js +24 -2
- package/dist/libs/instance-factories/applications/templates/react/vite-config-generator.js +54 -33
- package/dist/libs/instance-factories/applications/templates/react-starter/README.md +211 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/api-types-starter-generator.js +69 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/app-tsx-generator.js +110 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/belongs-to.js +40 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/dashboard-body-composer.js +129 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/detail-body-composer.js +80 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/form-body-composer.js +217 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/helpers-emitter.js +51 -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 +56 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/orchestrator.js +41 -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/skeletons/dashboard.tsx.template +49 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/skeletons/detail.tsx.template +96 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/skeletons/form.tsx.template +116 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/skeletons/list.tsx.template +74 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/use-api-hooks-starter-generator.js +95 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/view-emitter.js +81 -0
- package/dist/libs/instance-factories/applications/templates/react-starter/views-generator.js +66 -0
- package/dist/libs/instance-factories/archived/fastify-prisma.yaml +104 -0
- package/dist/libs/instance-factories/cli/README.md +43 -0
- package/dist/libs/instance-factories/cli/commander-js.yaml +55 -0
- package/dist/libs/instance-factories/cli/templates/commander/command-generator.js +63 -12
- package/dist/libs/instance-factories/communication/README.md +47 -0
- package/dist/libs/instance-factories/communication/event-emitter.yaml +60 -0
- package/dist/libs/instance-factories/communication/rabbitmq-events.yaml +87 -0
- package/dist/libs/instance-factories/controllers/README.md +42 -0
- package/dist/libs/instance-factories/controllers/fastify.yaml +139 -0
- package/dist/libs/instance-factories/controllers/templates/fastify/server-generator.js +29 -2
- package/dist/libs/instance-factories/infrastructure/README.md +29 -0
- package/dist/libs/instance-factories/infrastructure/docker-k8s.yaml +61 -0
- package/dist/libs/instance-factories/orms/README.md +54 -0
- package/dist/libs/instance-factories/orms/prisma.yaml +89 -0
- package/dist/libs/instance-factories/orms/templates/prisma/schema-generator.js +2 -2
- package/dist/libs/instance-factories/scaffolding/README.md +49 -0
- package/dist/libs/instance-factories/scaffolding/generic-scaffold.yaml +65 -0
- package/dist/libs/instance-factories/sdks/README.md +28 -0
- package/dist/libs/instance-factories/sdks/python-sdk.yaml +66 -0
- package/dist/libs/instance-factories/sdks/typescript-sdk.yaml +59 -0
- package/dist/libs/instance-factories/services/README.md +55 -0
- package/dist/libs/instance-factories/services/prisma-services.yaml +71 -0
- package/dist/libs/instance-factories/storage/README.md +34 -0
- package/dist/libs/instance-factories/storage/mongodb.yaml +79 -0
- package/dist/libs/instance-factories/storage/postgresql.yaml +75 -0
- package/dist/libs/instance-factories/storage/redis.yaml +79 -0
- package/dist/libs/instance-factories/testing/README.md +40 -0
- package/dist/libs/instance-factories/testing/vitest-tests.yaml +63 -0
- package/dist/libs/instance-factories/tools/README.md +70 -0
- package/dist/libs/instance-factories/tools/mcp.yaml +36 -0
- package/dist/libs/instance-factories/tools/vscode.yaml +35 -0
- package/dist/libs/instance-factories/validation/README.md +38 -0
- package/dist/libs/instance-factories/validation/zod.yaml +56 -0
- package/dist/realize/engines/code-generator.d.ts.map +1 -1
- package/dist/realize/engines/code-generator.js +3 -0
- package/dist/realize/engines/code-generator.js.map +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 +143 -0
- package/libs/instance-factories/applications/templates/react/env-example-generator.ts +24 -2
- package/libs/instance-factories/applications/templates/react/vite-config-generator.ts +54 -33
- 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 +146 -0
- package/libs/instance-factories/applications/templates/react-starter/__tests__/form-body-composer.test.ts +188 -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 +184 -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/api-types-starter-generator.ts +98 -0
- package/libs/instance-factories/applications/templates/react-starter/app-tsx-generator.ts +141 -0
- package/libs/instance-factories/applications/templates/react-starter/belongs-to.ts +82 -0
- package/libs/instance-factories/applications/templates/react-starter/dashboard-body-composer.ts +189 -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 +383 -0
- package/libs/instance-factories/applications/templates/react-starter/helpers-emitter.ts +66 -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 +146 -0
- package/libs/instance-factories/applications/templates/react-starter/orchestrator.ts +95 -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 +49 -0
- package/libs/instance-factories/applications/templates/react-starter/skeletons/detail.tsx.template +96 -0
- package/libs/instance-factories/applications/templates/react-starter/skeletons/form.tsx.template +116 -0
- package/libs/instance-factories/applications/templates/react-starter/skeletons/list.tsx.template +74 -0
- package/libs/instance-factories/applications/templates/react-starter/use-api-hooks-starter-generator.ts +124 -0
- package/libs/instance-factories/applications/templates/react-starter/view-emitter.ts +209 -0
- package/libs/instance-factories/applications/templates/react-starter/views-generator.ts +137 -0
- package/libs/instance-factories/cli/templates/commander/command-generator.ts +63 -12
- package/libs/instance-factories/controllers/fastify.yaml +7 -0
- package/libs/instance-factories/controllers/templates/fastify/server-generator.ts +36 -2
- package/libs/instance-factories/orms/templates/prisma/schema-generator.ts +11 -4
- 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/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/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,49 @@
|
|
|
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
|
+
{{RELATED_IMPORTS}}
|
|
16
|
+
import type { {{MODEL_NAME}} } from '../types/api';
|
|
17
|
+
|
|
18
|
+
interface {{MODEL_NAME}}DashboardViewProps {
|
|
19
|
+
/** Number of recent records to preview. Default 5. */
|
|
20
|
+
previewLimit?: number;
|
|
21
|
+
onSelect?: (item: {{MODEL_NAME}}) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function {{MODEL_NAME}}DashboardView({
|
|
25
|
+
previewLimit = 5,
|
|
26
|
+
onSelect,
|
|
27
|
+
}: {{MODEL_NAME}}DashboardViewProps) {
|
|
28
|
+
const { data: items = [], isLoading, error } = use{{PLURAL_MODEL}}Query();
|
|
29
|
+
{{RELATED_HOOKS}}
|
|
30
|
+
|
|
31
|
+
const preview = useMemo(
|
|
32
|
+
() => items.slice(0, previewLimit),
|
|
33
|
+
[items, previewLimit]
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
if (isLoading) return <div className="p-4 text-gray-500">Loading dashboard…</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-6">
|
|
41
|
+
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
|
42
|
+
{{MODEL_NAME}} dashboard
|
|
43
|
+
</h2>
|
|
44
|
+
|
|
45
|
+
{/* Pattern-rendered dashboard body (metrics + preview). Edit freely. */}
|
|
46
|
+
{{BODY}}
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
package/libs/instance-factories/applications/templates/react-starter/skeletons/detail.tsx.template
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
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
|
+
{{RELATED_IMPORTS}}
|
|
10
|
+
import type { {{MODEL_NAME}} } from '../types/api';
|
|
11
|
+
|
|
12
|
+
interface {{MODEL_NAME}}DetailViewProps {
|
|
13
|
+
entityId: string | number;
|
|
14
|
+
onEdit?: (item: {{MODEL_NAME}}) => void;
|
|
15
|
+
onBack?: () => void;
|
|
16
|
+
/** Called after the delete mutation succeeds. */
|
|
17
|
+
onDeleted?: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function {{MODEL_NAME}}DetailView({
|
|
21
|
+
entityId,
|
|
22
|
+
onEdit,
|
|
23
|
+
onBack,
|
|
24
|
+
onDeleted,
|
|
25
|
+
}: {{MODEL_NAME}}DetailViewProps) {
|
|
26
|
+
const { data: items = [], isLoading, error } = use{{PLURAL_MODEL}}Query();
|
|
27
|
+
const deleteItem = useDelete{{MODEL_NAME}}Mutation();
|
|
28
|
+
{{RELATED_HOOKS}}
|
|
29
|
+
|
|
30
|
+
const item = items.find(
|
|
31
|
+
(x: {{MODEL_NAME}}) => (x as any).id === entityId
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
if (isLoading) return <div className="p-4 text-gray-500">Loading…</div>;
|
|
35
|
+
if (error) return <div className="p-4 text-red-600">Error loading {{PLURAL_LOWER}}: {String(error)}</div>;
|
|
36
|
+
if (!item) return <div className="p-4 text-gray-400">No {{SINGULAR_LOWER}} matching id {String(entityId)}.</div>;
|
|
37
|
+
|
|
38
|
+
const handleDelete = async () => {
|
|
39
|
+
if (!confirm('Delete this {{SINGULAR_LOWER}}?')) return;
|
|
40
|
+
try {
|
|
41
|
+
await deleteItem.mutateAsync((item as any).id);
|
|
42
|
+
onDeleted?.();
|
|
43
|
+
} catch {
|
|
44
|
+
// deleteItem.error is surfaced below
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div className="p-6 space-y-4">
|
|
50
|
+
<div className="flex items-center justify-between">
|
|
51
|
+
<div className="flex items-center gap-3">
|
|
52
|
+
{onBack && (
|
|
53
|
+
<button
|
|
54
|
+
type="button"
|
|
55
|
+
onClick={onBack}
|
|
56
|
+
className="text-sm text-gray-500 hover:text-gray-700"
|
|
57
|
+
>
|
|
58
|
+
← Back
|
|
59
|
+
</button>
|
|
60
|
+
)}
|
|
61
|
+
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
|
62
|
+
{{MODEL_NAME}} detail
|
|
63
|
+
</h2>
|
|
64
|
+
</div>
|
|
65
|
+
<div className="flex gap-2">
|
|
66
|
+
{onEdit && (
|
|
67
|
+
<button
|
|
68
|
+
type="button"
|
|
69
|
+
onClick={() => onEdit(item)}
|
|
70
|
+
className="rounded bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
|
|
71
|
+
>
|
|
72
|
+
Edit
|
|
73
|
+
</button>
|
|
74
|
+
)}
|
|
75
|
+
<button
|
|
76
|
+
type="button"
|
|
77
|
+
onClick={handleDelete}
|
|
78
|
+
disabled={deleteItem.isPending}
|
|
79
|
+
className="rounded bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700 disabled:opacity-60"
|
|
80
|
+
>
|
|
81
|
+
{deleteItem.isPending ? 'Deleting…' : 'Delete'}
|
|
82
|
+
</button>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
{/* Pattern-rendered detail body (fields). Edit freely. */}
|
|
87
|
+
{{BODY}}
|
|
88
|
+
|
|
89
|
+
{deleteItem.isError && (
|
|
90
|
+
<div className="p-2 text-sm text-red-600">
|
|
91
|
+
Delete failed: {String(deleteItem.error)}
|
|
92
|
+
</div>
|
|
93
|
+
)}
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
}
|
package/libs/instance-factories/applications/templates/react-starter/skeletons/form.tsx.template
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
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
|
+
{{RELATED_IMPORTS}}
|
|
18
|
+
import type { {{MODEL_NAME}} } from '../types/api';
|
|
19
|
+
|
|
20
|
+
type FormMode = 'create' | 'update';
|
|
21
|
+
|
|
22
|
+
interface {{MODEL_NAME}}FormViewProps {
|
|
23
|
+
mode?: FormMode;
|
|
24
|
+
/** Required in update mode. */
|
|
25
|
+
entityId?: string | number;
|
|
26
|
+
onSuccess?: (item: {{MODEL_NAME}}) => void;
|
|
27
|
+
onCancel?: () => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function {{MODEL_NAME}}FormView({
|
|
31
|
+
mode = 'create',
|
|
32
|
+
entityId,
|
|
33
|
+
onSuccess,
|
|
34
|
+
onCancel,
|
|
35
|
+
}: {{MODEL_NAME}}FormViewProps) {
|
|
36
|
+
const { data: items = [] } = use{{PLURAL_MODEL}}Query();
|
|
37
|
+
const createItem = useCreate{{MODEL_NAME}}Mutation();
|
|
38
|
+
const updateItem = useUpdate{{MODEL_NAME}}Mutation();
|
|
39
|
+
{{RELATED_HOOKS}}
|
|
40
|
+
|
|
41
|
+
const existing =
|
|
42
|
+
mode === 'update'
|
|
43
|
+
? items.find((x: {{MODEL_NAME}}) => (x as any).id === entityId)
|
|
44
|
+
: undefined;
|
|
45
|
+
|
|
46
|
+
const [formData, setFormData] = useState<Partial<{{MODEL_NAME}}>>(existing ?? {});
|
|
47
|
+
|
|
48
|
+
// When the fetched list lands after initial render (update mode),
|
|
49
|
+
// hydrate the form with the loaded entity's data.
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (existing) setFormData(existing);
|
|
52
|
+
}, [existing]);
|
|
53
|
+
|
|
54
|
+
const handleChange = (field: string, value: unknown) => {
|
|
55
|
+
setFormData(prev => ({ ...prev, [field]: value }) as Partial<{{MODEL_NAME}}>);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const handleSubmit = async (event: React.FormEvent) => {
|
|
59
|
+
event.preventDefault();
|
|
60
|
+
try {
|
|
61
|
+
const result =
|
|
62
|
+
mode === 'create'
|
|
63
|
+
? await createItem.mutateAsync(formData as {{MODEL_NAME}})
|
|
64
|
+
: await updateItem.mutateAsync({
|
|
65
|
+
id: entityId as any,
|
|
66
|
+
data: formData as Partial<{{MODEL_NAME}}>,
|
|
67
|
+
});
|
|
68
|
+
onSuccess?.(result as {{MODEL_NAME}});
|
|
69
|
+
} catch {
|
|
70
|
+
// Mutation errors are surfaced below via createItem / updateItem.
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const mutation = mode === 'create' ? createItem : updateItem;
|
|
75
|
+
const submitLabel =
|
|
76
|
+
mode === 'create'
|
|
77
|
+
? mutation.isPending ? 'Creating…' : 'Create {{MODEL_NAME}}'
|
|
78
|
+
: mutation.isPending ? 'Updating…' : 'Update {{MODEL_NAME}}';
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<form onSubmit={handleSubmit} className="p-6 space-y-6">
|
|
82
|
+
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
|
83
|
+
{mode === 'create' ? 'New {{MODEL_NAME}}' : 'Edit {{MODEL_NAME}}'}
|
|
84
|
+
</h2>
|
|
85
|
+
|
|
86
|
+
{/* Pattern-rendered form fields. Edit freely. */}
|
|
87
|
+
{{BODY}}
|
|
88
|
+
|
|
89
|
+
<div className="flex gap-2 border-t border-gray-200 dark:border-gray-700 pt-4">
|
|
90
|
+
<button
|
|
91
|
+
type="submit"
|
|
92
|
+
disabled={mutation.isPending}
|
|
93
|
+
className="rounded bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-60"
|
|
94
|
+
>
|
|
95
|
+
{submitLabel}
|
|
96
|
+
</button>
|
|
97
|
+
{onCancel && (
|
|
98
|
+
<button
|
|
99
|
+
type="button"
|
|
100
|
+
onClick={onCancel}
|
|
101
|
+
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"
|
|
102
|
+
>
|
|
103
|
+
Cancel
|
|
104
|
+
</button>
|
|
105
|
+
)}
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
{mutation.isError && (
|
|
109
|
+
<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">
|
|
110
|
+
{mode === 'create' ? 'Create failed: ' : 'Update failed: '}
|
|
111
|
+
{String(mutation.error)}
|
|
112
|
+
</div>
|
|
113
|
+
)}
|
|
114
|
+
</form>
|
|
115
|
+
);
|
|
116
|
+
}
|
package/libs/instance-factories/applications/templates/react-starter/skeletons/list.tsx.template
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
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
|
+
{{RELATED_IMPORTS}}
|
|
11
|
+
import type { {{MODEL_NAME}} } from '../types/api';
|
|
12
|
+
|
|
13
|
+
interface {{MODEL_NAME}}ListViewProps {
|
|
14
|
+
onSelect?: (item: {{MODEL_NAME}}) => void;
|
|
15
|
+
onCreate?: () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function {{MODEL_NAME}}ListView({ onSelect, onCreate }: {{MODEL_NAME}}ListViewProps) {
|
|
19
|
+
const { data: items = [], isLoading, error } = use{{PLURAL_MODEL}}Query();
|
|
20
|
+
const deleteItem = useDelete{{MODEL_NAME}}Mutation();
|
|
21
|
+
{{RELATED_HOOKS}}
|
|
22
|
+
const [searchTerm, setSearchTerm] = useState('');
|
|
23
|
+
|
|
24
|
+
const filtered = useMemo(
|
|
25
|
+
() =>
|
|
26
|
+
items.filter((item: {{MODEL_NAME}}) =>
|
|
27
|
+
// Cast to `any` inside JSX context — `Record<string, unknown>`
|
|
28
|
+
// looks like a JSX opening tag to the TSX parser when this
|
|
29
|
+
// expression ends up inside JSX. `any` is safe here: we only
|
|
30
|
+
// read values for substring-matching against the search term.
|
|
31
|
+
Object.values(item as any).some(v =>
|
|
32
|
+
String(v ?? '').toLowerCase().includes(searchTerm.toLowerCase())
|
|
33
|
+
)
|
|
34
|
+
),
|
|
35
|
+
[items, searchTerm]
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
if (isLoading) return <div className="p-4 text-gray-500">Loading {{PLURAL_LOWER}}…</div>;
|
|
39
|
+
if (error) return <div className="p-4 text-red-600">Error loading {{PLURAL_LOWER}}: {String(error)}</div>;
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className="p-6 space-y-4">
|
|
43
|
+
<div className="flex items-center justify-between">
|
|
44
|
+
<input
|
|
45
|
+
type="search"
|
|
46
|
+
placeholder="Search {{PLURAL_LOWER}}…"
|
|
47
|
+
value={searchTerm}
|
|
48
|
+
onChange={e => setSearchTerm(e.target.value)}
|
|
49
|
+
className="w-64 rounded border border-gray-300 px-3 py-2 text-sm"
|
|
50
|
+
/>
|
|
51
|
+
<button
|
|
52
|
+
type="button"
|
|
53
|
+
onClick={onCreate}
|
|
54
|
+
className="rounded bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
|
|
55
|
+
>
|
|
56
|
+
+ New {{MODEL_NAME}}
|
|
57
|
+
</button>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
{/* Pattern-rendered list body (table + rows). Edit freely. */}
|
|
61
|
+
{{BODY}}
|
|
62
|
+
|
|
63
|
+
{filtered.length === 0 && (
|
|
64
|
+
<div className="p-8 text-center text-gray-400">No {{PLURAL_LOWER}} yet.</div>
|
|
65
|
+
)}
|
|
66
|
+
|
|
67
|
+
{deleteItem.isError && (
|
|
68
|
+
<div className="p-2 text-sm text-red-600">
|
|
69
|
+
Delete failed: {String(deleteItem.error)}
|
|
70
|
+
</div>
|
|
71
|
+
)}
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useApi hooks generator for ReactAppStarter
|
|
3
|
+
*
|
|
4
|
+
* Emits typed per-model React Query hooks that the starter's view
|
|
5
|
+
* skeletons import directly (`useTasksQuery`, `useCreateTaskMutation`,
|
|
6
|
+
* etc.). All hooks hit the realized backend's REST endpoints under
|
|
7
|
+
* `/api/{resource}` via the sibling `apiClient` helper.
|
|
8
|
+
*
|
|
9
|
+
* Why a separate generator from the runtime one: ReactAppRuntime
|
|
10
|
+
* ships a generic `useEntitiesQuery(controller, model)` because its
|
|
11
|
+
* views are rendered by @specverse/runtime using runtime dispatch.
|
|
12
|
+
* The starter wants plain typed hooks the user can read and edit.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Minimal English pluralizer. Matches the conventions used by
|
|
17
|
+
* view-emitter so the generated hooks pair cleanly with the generated
|
|
18
|
+
* view imports.
|
|
19
|
+
*/
|
|
20
|
+
function pluralize(s: string): string {
|
|
21
|
+
if (/y$/.test(s) && !/[aeiou]y$/.test(s)) return s.replace(/y$/, 'ies');
|
|
22
|
+
if (/(s|x|z|ch|sh)$/.test(s)) return s + 'es';
|
|
23
|
+
return s + 's';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface UseApiHooksStarterContext {
|
|
27
|
+
spec: {
|
|
28
|
+
models?: Record<string, any>;
|
|
29
|
+
};
|
|
30
|
+
manifest?: unknown;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function generate(context: UseApiHooksStarterContext): Promise<string> {
|
|
34
|
+
const models = Object.keys(context.spec.models ?? {});
|
|
35
|
+
|
|
36
|
+
const importsAndTypes = `/**
|
|
37
|
+
* useApi — typed per-model React Query hooks (ReactAppStarter)
|
|
38
|
+
*
|
|
39
|
+
* Safe to edit. Users who want to reshape their API client or add
|
|
40
|
+
* request/response interceptors should start here. Each hook calls
|
|
41
|
+
* the sibling \`apiClient\` which does the actual fetch; edit
|
|
42
|
+
* \`apiClient.ts\` to change transport concerns (headers, auth, etc.).
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
46
|
+
${models.map(m => `import type { ${m} } from '../types/api';`).join('\n')}
|
|
47
|
+
|
|
48
|
+
const API_BASE = import.meta.env.VITE_API_BASE_URL || '';
|
|
49
|
+
|
|
50
|
+
async function fetchJSON<T = unknown>(url: string, init?: RequestInit): Promise<T> {
|
|
51
|
+
const res = await fetch(url, {
|
|
52
|
+
headers: { 'Content-Type': 'application/json' },
|
|
53
|
+
...init,
|
|
54
|
+
});
|
|
55
|
+
if (!res.ok) {
|
|
56
|
+
throw new Error(\`\${res.status} \${res.statusText} — \${url}\`);
|
|
57
|
+
}
|
|
58
|
+
if (res.status === 204) return undefined as T;
|
|
59
|
+
return (await res.json()) as T;
|
|
60
|
+
}
|
|
61
|
+
`;
|
|
62
|
+
|
|
63
|
+
const hookBlocks = models.map(m => generateModelHooks(m)).join('\n\n');
|
|
64
|
+
|
|
65
|
+
return importsAndTypes + '\n' + hookBlocks + '\n';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function generateModelHooks(model: string): string {
|
|
69
|
+
const plural = pluralize(model);
|
|
70
|
+
const resource = plural.toLowerCase();
|
|
71
|
+
const modelLower = model.toLowerCase();
|
|
72
|
+
|
|
73
|
+
return `// ─── ${model} ───────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
export function use${plural}Query() {
|
|
76
|
+
return useQuery({
|
|
77
|
+
queryKey: ['${resource}'],
|
|
78
|
+
queryFn: () => fetchJSON<${model}[]>(\`\${API_BASE}/api/${resource}\`),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function use${model}Query(id: string | number | undefined) {
|
|
83
|
+
return useQuery({
|
|
84
|
+
queryKey: ['${resource}', id],
|
|
85
|
+
queryFn: () => fetchJSON<${model}>(\`\${API_BASE}/api/${resource}/\${id}\`),
|
|
86
|
+
enabled: id !== undefined && id !== null,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function useCreate${model}Mutation() {
|
|
91
|
+
const qc = useQueryClient();
|
|
92
|
+
return useMutation({
|
|
93
|
+
mutationFn: (data: Partial<${model}>) =>
|
|
94
|
+
fetchJSON<${model}>(\`\${API_BASE}/api/${resource}\`, {
|
|
95
|
+
method: 'POST',
|
|
96
|
+
body: JSON.stringify(data),
|
|
97
|
+
}),
|
|
98
|
+
onSuccess: () => qc.invalidateQueries({ queryKey: ['${resource}'] }),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function useUpdate${model}Mutation() {
|
|
103
|
+
const qc = useQueryClient();
|
|
104
|
+
return useMutation({
|
|
105
|
+
mutationFn: ({ id, data }: { id: string | number; data: Partial<${model}> }) =>
|
|
106
|
+
fetchJSON<${model}>(\`\${API_BASE}/api/${resource}/\${id}\`, {
|
|
107
|
+
method: 'PATCH',
|
|
108
|
+
body: JSON.stringify(data),
|
|
109
|
+
}),
|
|
110
|
+
onSuccess: () => qc.invalidateQueries({ queryKey: ['${resource}'] }),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function useDelete${model}Mutation() {
|
|
115
|
+
const qc = useQueryClient();
|
|
116
|
+
return useMutation({
|
|
117
|
+
mutationFn: (id: string | number) =>
|
|
118
|
+
fetchJSON<void>(\`\${API_BASE}/api/${resource}/\${id}\`, { method: 'DELETE' }),
|
|
119
|
+
onSuccess: () => qc.invalidateQueries({ queryKey: ['${resource}'] }),
|
|
120
|
+
});
|
|
121
|
+
}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export default generate;
|