@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
package/libs/instance-factories/applications/templates/react-starter/__tests__/orchestrator.test.ts
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync, existsSync } from 'fs';
|
|
3
|
+
import { tmpdir } from 'os';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import * as ts from 'typescript';
|
|
6
|
+
import { generate } from '../orchestrator.js';
|
|
7
|
+
import { sha256, HASHES_DIR, HASHES_FILE } from '../regen-safety.js';
|
|
8
|
+
|
|
9
|
+
let projectRoot: string;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
projectRoot = mkdtempSync(join(tmpdir(), 'react-starter-orch-'));
|
|
13
|
+
// Suppress the orchestrator's console.log summary during tests.
|
|
14
|
+
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
rmSync(projectRoot, { recursive: true, force: true });
|
|
19
|
+
vi.restoreAllMocks();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
function makeSpec() {
|
|
23
|
+
return {
|
|
24
|
+
metadata: { name: 'My App' },
|
|
25
|
+
models: {
|
|
26
|
+
Post: {
|
|
27
|
+
name: 'Post',
|
|
28
|
+
attributes: {
|
|
29
|
+
id: { type: 'UUID', auto: 'uuid4' },
|
|
30
|
+
title: { type: 'String', required: true },
|
|
31
|
+
body: { type: 'Text' },
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
views: {},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function assertValidTsx(source: string, label: string): void {
|
|
40
|
+
const result = ts.transpileModule(source, {
|
|
41
|
+
compilerOptions: {
|
|
42
|
+
jsx: ts.JsxEmit.Preserve,
|
|
43
|
+
target: ts.ScriptTarget.ES2022,
|
|
44
|
+
module: ts.ModuleKind.ESNext,
|
|
45
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
46
|
+
strict: false,
|
|
47
|
+
},
|
|
48
|
+
reportDiagnostics: true,
|
|
49
|
+
});
|
|
50
|
+
const errors = result.diagnostics?.filter(d => d.category === ts.DiagnosticCategory.Error) ?? [];
|
|
51
|
+
if (errors.length > 0) {
|
|
52
|
+
const msg = errors.map(d => ts.flattenDiagnosticMessageText(d.messageText, '\n')).join('\n');
|
|
53
|
+
throw new Error(`${label} failed to parse:\n${msg}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Drive the orchestrator with the per-test tmp dir as the effective
|
|
59
|
+
* frontend root. `frontendDir='.'` collapses the
|
|
60
|
+
* `${outputDir}/${frontendDir}` join so files land directly under
|
|
61
|
+
* `projectRoot`, which keeps test assertions terse.
|
|
62
|
+
*/
|
|
63
|
+
async function runGenerate(spec = makeSpec()) {
|
|
64
|
+
return generate({ spec, outputDir: projectRoot, frontendDir: '.' });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** List every file the orchestrator wrote under projectRoot. */
|
|
68
|
+
function walk(dir: string, prefix = ''): string[] {
|
|
69
|
+
const { readdirSync, statSync } = require('fs');
|
|
70
|
+
const out: string[] = [];
|
|
71
|
+
for (const entry of readdirSync(dir)) {
|
|
72
|
+
const abs = join(dir, entry);
|
|
73
|
+
const rel = prefix ? `${prefix}/${entry}` : entry;
|
|
74
|
+
if (statSync(abs).isDirectory()) out.push(...walk(abs, rel));
|
|
75
|
+
else out.push(rel);
|
|
76
|
+
}
|
|
77
|
+
return out.sort();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
describe('orchestrator — first-run (empty project)', () => {
|
|
81
|
+
it('writes the full set of files: views + helpers + App.tsx + package.json + hash manifest', async () => {
|
|
82
|
+
const ret = await runGenerate();
|
|
83
|
+
// Contract: returns '' (realize's single-file writeOutput skips it).
|
|
84
|
+
expect(ret).toBe('');
|
|
85
|
+
|
|
86
|
+
const written = walk(projectRoot);
|
|
87
|
+
expect(written).toEqual([
|
|
88
|
+
`${HASHES_DIR}/${HASHES_FILE}`,
|
|
89
|
+
'package.json',
|
|
90
|
+
'src/App.tsx',
|
|
91
|
+
'src/lib/entity-display.ts',
|
|
92
|
+
'src/views/PostDashboardView.tsx',
|
|
93
|
+
'src/views/PostDetailView.tsx',
|
|
94
|
+
'src/views/PostFormView.tsx',
|
|
95
|
+
'src/views/PostListView.tsx',
|
|
96
|
+
]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('every emitted .tsx parses as valid TSX', async () => {
|
|
100
|
+
await runGenerate();
|
|
101
|
+
for (const path of walk(projectRoot)) {
|
|
102
|
+
if (path.endsWith('.tsx')) {
|
|
103
|
+
const source = readFileSync(join(projectRoot, path), 'utf8');
|
|
104
|
+
assertValidTsx(source, path);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('package.json has no @specverse/runtime dependency', async () => {
|
|
110
|
+
await runGenerate();
|
|
111
|
+
const pkg = JSON.parse(readFileSync(join(projectRoot, 'package.json'), 'utf8'));
|
|
112
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
113
|
+
expect(deps['@specverse/runtime']).toBeUndefined();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('the emitted hash manifest records every approved file', async () => {
|
|
117
|
+
await runGenerate();
|
|
118
|
+
const manifestPath = join(projectRoot, HASHES_DIR, HASHES_FILE);
|
|
119
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
120
|
+
for (const path of walk(projectRoot)) {
|
|
121
|
+
if (path === `${HASHES_DIR}/${HASHES_FILE}`) continue;
|
|
122
|
+
const content = readFileSync(join(projectRoot, path), 'utf8');
|
|
123
|
+
expect(manifest[path]).toBe(sha256(content));
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe('orchestrator — regeneration safety', () => {
|
|
129
|
+
it('re-approves pristine files on a second run', async () => {
|
|
130
|
+
await runGenerate();
|
|
131
|
+
|
|
132
|
+
// Record the first-run hash of one of the view files.
|
|
133
|
+
const listPath = 'src/views/PostListView.tsx';
|
|
134
|
+
const listAbs = join(projectRoot, listPath);
|
|
135
|
+
const firstContent = readFileSync(listAbs, 'utf8');
|
|
136
|
+
|
|
137
|
+
// Second run. Pristine files should be rewritten (the orchestrator
|
|
138
|
+
// writes them, possibly with identical bytes).
|
|
139
|
+
await runGenerate();
|
|
140
|
+
expect(existsSync(listAbs)).toBe(true);
|
|
141
|
+
expect(readFileSync(listAbs, 'utf8')).toBe(firstContent);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('preserves a user-edited file across regeneration', async () => {
|
|
145
|
+
await runGenerate();
|
|
146
|
+
|
|
147
|
+
const editedPath = 'src/views/PostListView.tsx';
|
|
148
|
+
const editedAbs = join(projectRoot, editedPath);
|
|
149
|
+
const userEdit = '/* user edit */';
|
|
150
|
+
writeFileSync(editedAbs, userEdit, 'utf8');
|
|
151
|
+
|
|
152
|
+
await runGenerate();
|
|
153
|
+
|
|
154
|
+
// File on disk is unchanged — user edit survived.
|
|
155
|
+
expect(readFileSync(editedAbs, 'utf8')).toBe(userEdit);
|
|
156
|
+
// Manifest still records the ORIGINAL (pre-edit) hash so a future
|
|
157
|
+
// un-edit can re-sync cleanly.
|
|
158
|
+
const manifest = JSON.parse(
|
|
159
|
+
readFileSync(join(projectRoot, HASHES_DIR, HASHES_FILE), 'utf8')
|
|
160
|
+
);
|
|
161
|
+
expect(manifest[editedPath]).toBeDefined();
|
|
162
|
+
expect(manifest[editedPath]).not.toBe(sha256(userEdit));
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('is cautious when the user deletes the hash manifest', async () => {
|
|
166
|
+
await runGenerate();
|
|
167
|
+
|
|
168
|
+
// User deletes the hash manifest but keeps the view files.
|
|
169
|
+
rmSync(join(projectRoot, HASHES_DIR), { recursive: true, force: true });
|
|
170
|
+
|
|
171
|
+
// Record the existing file contents (may be identical to what the
|
|
172
|
+
// orchestrator would emit, but we're treating them as unknown origin).
|
|
173
|
+
const listPath = join(projectRoot, 'src/views/PostListView.tsx');
|
|
174
|
+
const before = readFileSync(listPath, 'utf8');
|
|
175
|
+
|
|
176
|
+
await runGenerate();
|
|
177
|
+
|
|
178
|
+
// No hash manifest means we can't tell user edits from originals, so
|
|
179
|
+
// existing files are skipped. Content is unchanged.
|
|
180
|
+
expect(readFileSync(listPath, 'utf8')).toBe(before);
|
|
181
|
+
// A fresh hash manifest was written.
|
|
182
|
+
expect(existsSync(join(projectRoot, HASHES_DIR, HASHES_FILE))).toBe(true);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* P2 — Factory generators consume the pattern library, not forks
|
|
3
|
+
*
|
|
4
|
+
* Architectural invariant I5 from VIEW-RENDERING-ARCHITECTURE.md:
|
|
5
|
+
* "Factory generators are consumers of the pattern library, not
|
|
6
|
+
* reimplementations of it."
|
|
7
|
+
*
|
|
8
|
+
* Every composer / body-generator under react-starter must import
|
|
9
|
+
* from `@specverse/runtime/views/core` or `@specverse/runtime/views/tailwind`.
|
|
10
|
+
* A generator that doesn't is a reimplementation — it's drift-prone
|
|
11
|
+
* and violates the "one pattern library, three consumers" contract.
|
|
12
|
+
*
|
|
13
|
+
* Scope: files matching *-composer.ts or view-emitter.ts / generator
|
|
14
|
+
* files that actually produce view-layout code. Skeletons (.tsx.template)
|
|
15
|
+
* are content-only and exempt. helpers-emitter / html-to-jsx are
|
|
16
|
+
* transformation utilities, exempt — they don't consume pattern data.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { describe, it, expect } from 'vitest';
|
|
20
|
+
import { readdirSync, readFileSync } from 'fs';
|
|
21
|
+
import { join, resolve } from 'path';
|
|
22
|
+
import { fileURLToPath } from 'url';
|
|
23
|
+
|
|
24
|
+
const HERE = fileURLToPath(import.meta.url);
|
|
25
|
+
const STARTER_DIR = resolve(HERE, '..', '..');
|
|
26
|
+
|
|
27
|
+
const RUNTIME_IMPORT_PATTERN =
|
|
28
|
+
/from\s+['"]@specverse\/runtime\/views\/(core|tailwind)['"]/;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Files required to import from @specverse/runtime/views/core or
|
|
32
|
+
* /views/tailwind. The filenames match the convention of any generator
|
|
33
|
+
* that emits view layout or composes pattern data.
|
|
34
|
+
*/
|
|
35
|
+
const MUST_IMPORT_FROM_RUNTIME = (filename: string): boolean => {
|
|
36
|
+
// Every body composer (list / detail / form / dashboard).
|
|
37
|
+
if (/-body-composer\.ts$/.test(filename)) return true;
|
|
38
|
+
return false;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Files explicitly exempt from P2 — orchestrators, transformers, and
|
|
43
|
+
* utilities that don't consume pattern data. Each exemption carries
|
|
44
|
+
* a short justification as an inline comment near the import list.
|
|
45
|
+
*/
|
|
46
|
+
const EXEMPT_FILES = new Set([
|
|
47
|
+
// Orchestrator: dispatches to composers (which do import runtime).
|
|
48
|
+
'views-generator.ts',
|
|
49
|
+
'view-emitter.ts',
|
|
50
|
+
'orchestrator.ts',
|
|
51
|
+
// Pure string transformer, no pattern data.
|
|
52
|
+
'html-to-jsx.ts',
|
|
53
|
+
// Emits inline source code; no adapter calls needed.
|
|
54
|
+
'helpers-emitter.ts',
|
|
55
|
+
'app-tsx-generator.ts',
|
|
56
|
+
'package-json-generator.ts',
|
|
57
|
+
// Pure utility for content-hashing, no pattern consumption.
|
|
58
|
+
'regen-safety.ts',
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
function collectTypeScriptFiles(dir: string, prefix = ''): string[] {
|
|
62
|
+
const out: string[] = [];
|
|
63
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
64
|
+
for (const entry of entries) {
|
|
65
|
+
if (entry.name === '__tests__' || entry.name === 'skeletons') continue;
|
|
66
|
+
const full = join(dir, entry.name);
|
|
67
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
68
|
+
if (entry.isDirectory()) {
|
|
69
|
+
out.push(...collectTypeScriptFiles(full, rel));
|
|
70
|
+
} else if (entry.isFile() && entry.name.endsWith('.ts') && !entry.name.endsWith('.test.ts')) {
|
|
71
|
+
out.push(rel);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
describe('P2 — factory generators consume the pattern library', () => {
|
|
78
|
+
const files = collectTypeScriptFiles(STARTER_DIR);
|
|
79
|
+
|
|
80
|
+
it('produces a non-empty file list (sanity)', () => {
|
|
81
|
+
expect(files.length).toBeGreaterThan(0);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
for (const relPath of files) {
|
|
85
|
+
const filename = relPath.split('/').pop()!;
|
|
86
|
+
|
|
87
|
+
if (EXEMPT_FILES.has(filename)) continue;
|
|
88
|
+
if (!MUST_IMPORT_FROM_RUNTIME(filename)) continue;
|
|
89
|
+
|
|
90
|
+
it(`${relPath} imports from @specverse/runtime/views/{core|tailwind}`, () => {
|
|
91
|
+
const content = readFileSync(join(STARTER_DIR, relPath), 'utf8');
|
|
92
|
+
const imports = RUNTIME_IMPORT_PATTERN.test(content);
|
|
93
|
+
if (!imports) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`${relPath} must import from @specverse/runtime/views/core or ` +
|
|
96
|
+
`@specverse/runtime/views/tailwind to satisfy invariant I5. ` +
|
|
97
|
+
`If it genuinely doesn't need pattern data, add it to the ` +
|
|
98
|
+
`EXEMPT_FILES set with a justification. Generators that emit ` +
|
|
99
|
+
`view-layout code inevitably use pattern constants (field ` +
|
|
100
|
+
`classification, atomic components) — sourcing those from ` +
|
|
101
|
+
`runtime is what keeps the three consumers in sync.`
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
it('every composer is covered by an explicit rule (no silent skips)', () => {
|
|
108
|
+
const composers = files.filter(f => f.endsWith('-body-composer.ts'));
|
|
109
|
+
expect(composers.length).toBeGreaterThanOrEqual(4); // list/detail/form/dashboard at minimum
|
|
110
|
+
for (const c of composers) {
|
|
111
|
+
const filename = c.split('/').pop()!;
|
|
112
|
+
expect(MUST_IMPORT_FROM_RUNTIME(filename)).toBe(true);
|
|
113
|
+
expect(EXEMPT_FILES.has(filename)).toBe(false);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* P3 — Rendered-output equivalence
|
|
3
|
+
*
|
|
4
|
+
* Architectural invariant: the same (spec, view, model) must produce
|
|
5
|
+
* semantically equivalent output in app-demo (runtime React adapter)
|
|
6
|
+
* and in ReactAppStarter (Factory B emitter). They differ only in
|
|
7
|
+
* execution model — one renders in the browser, the other emits
|
|
8
|
+
* static JSX — but the visible structure must match.
|
|
9
|
+
*
|
|
10
|
+
* This file tests the LIST VIEW path, where parity is most
|
|
11
|
+
* tractable because both consumers call the same Tailwind adapter
|
|
12
|
+
* for the table shell. The test covers:
|
|
13
|
+
*
|
|
14
|
+
* 1. Column selection — both use `inferFieldsFromSchema` from
|
|
15
|
+
* the canonical pattern engine. Drift in either composer's
|
|
16
|
+
* column-filter logic would diverge here.
|
|
17
|
+
* 2. Table shell HTML — the Tailwind adapter renders the same
|
|
18
|
+
* shell for both paths; ensured by Factory B importing
|
|
19
|
+
* `createUniversalTailwindAdapter` directly.
|
|
20
|
+
* 3. Field humanisation — `camelCase` → `Title Case` for headers.
|
|
21
|
+
*
|
|
22
|
+
* Not yet covered (future extensions):
|
|
23
|
+
* - Detail / form / dashboard views — would need a reference
|
|
24
|
+
* React-adapter render path captured as an HTML string, then
|
|
25
|
+
* diffed against the Factory B emitted JSX modulo structural
|
|
26
|
+
* differences. Doable but bigger test machinery.
|
|
27
|
+
* - FK display-name resolution — blocked on Factory B's TODO to
|
|
28
|
+
* replace plain-FK inputs with dropdowns.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { describe, it, expect } from 'vitest';
|
|
32
|
+
import { inferFieldsFromSchema } from '@specverse/runtime/views/core';
|
|
33
|
+
import { createUniversalTailwindAdapter } from '@specverse/runtime/views/tailwind';
|
|
34
|
+
import { composeListBody } from '../list-body-composer.js';
|
|
35
|
+
import type { EmitContext, ModelSpec } from '../view-emitter.js';
|
|
36
|
+
|
|
37
|
+
// ───────────────────────────────────────────────────────────────────────
|
|
38
|
+
// Reference model + view-spec setup
|
|
39
|
+
// ───────────────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
function makeReferenceModel(): ModelSpec {
|
|
42
|
+
return {
|
|
43
|
+
name: 'Article',
|
|
44
|
+
attributes: {
|
|
45
|
+
id: { type: 'UUID', required: true, auto: 'uuid4' }, // excluded
|
|
46
|
+
title: { type: 'String', required: true }, // included
|
|
47
|
+
slug: { type: 'String', required: true, unique: true }, // included
|
|
48
|
+
content: { type: 'Text', required: false }, // included
|
|
49
|
+
status: { type: 'String', values: ['draft', 'published'] },// included
|
|
50
|
+
authorId: { type: 'UUID', required: true }, // included (FK)
|
|
51
|
+
createdAt: { type: 'DateTime', auto: 'now' }, // excluded (auto)
|
|
52
|
+
updatedAt: { type: 'DateTime', auto: 'now' }, // excluded (auto)
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function makeContext(): EmitContext {
|
|
58
|
+
const model = makeReferenceModel();
|
|
59
|
+
return {
|
|
60
|
+
view: { type: 'list', model: 'Article' },
|
|
61
|
+
viewName: 'ArticleListView',
|
|
62
|
+
model,
|
|
63
|
+
modelSchemas: { Article: model },
|
|
64
|
+
renderBody: composeListBody,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function humanize(s: string): string {
|
|
69
|
+
return s.replace(/([A-Z])/g, ' $1').replace(/^./, c => c.toUpperCase()).trim();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ───────────────────────────────────────────────────────────────────────
|
|
73
|
+
// Parity assertions
|
|
74
|
+
// ───────────────────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
describe('P3 — list view column inference parity', () => {
|
|
77
|
+
it('runtime inferFieldsFromSchema and Factory B composer see the same columns', () => {
|
|
78
|
+
const ctx = makeContext();
|
|
79
|
+
const runtimeColumns = inferFieldsFromSchema(ctx.modelSchemas, ctx.model.name);
|
|
80
|
+
|
|
81
|
+
// We can extract Factory B's column choices by looking at what
|
|
82
|
+
// headers it emits into the rendered output.
|
|
83
|
+
const body = composeListBody(ctx);
|
|
84
|
+
|
|
85
|
+
for (const col of runtimeColumns) {
|
|
86
|
+
const label = humanize(col);
|
|
87
|
+
expect(body, `runtime column "${col}" (label: "${label}") must appear in Factory B output`)
|
|
88
|
+
.toContain(`>${label}<`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// And nothing EXTRA from the model that runtime excludes:
|
|
92
|
+
const runtimeSet = new Set(runtimeColumns);
|
|
93
|
+
const allAttrs = Object.keys(ctx.model.attributes);
|
|
94
|
+
const runtimeExcluded = allAttrs.filter(a => !runtimeSet.has(a));
|
|
95
|
+
for (const excluded of runtimeExcluded) {
|
|
96
|
+
const label = humanize(excluded);
|
|
97
|
+
// Factory B must NOT emit a <th> for this field.
|
|
98
|
+
const headerPattern = new RegExp(`<th\\b[^>]*>${label}</th>`);
|
|
99
|
+
expect(body).not.toMatch(headerPattern);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('matches the expected column set for the reference model', () => {
|
|
104
|
+
const cols = inferFieldsFromSchema(makeContext().modelSchemas, 'Article');
|
|
105
|
+
// Canonical expectation for this reference spec.
|
|
106
|
+
expect(cols).toEqual(['title', 'slug', 'content', 'status', 'authorId']);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('P3 — list view table shell parity', () => {
|
|
111
|
+
it('emits a table shell byte-identical to a direct Tailwind adapter call', () => {
|
|
112
|
+
// The composer calls createUniversalTailwindAdapter() + table.render()
|
|
113
|
+
// with a known columns list and a sentinel in children. If we call
|
|
114
|
+
// the adapter directly with the same args, we should get the same
|
|
115
|
+
// shell HTML (modulo the sentinel vs the Factory B JSX injection).
|
|
116
|
+
const ctx = makeContext();
|
|
117
|
+
const runtimeColumns = inferFieldsFromSchema(ctx.modelSchemas, ctx.model.name);
|
|
118
|
+
const headers = runtimeColumns.map(humanize);
|
|
119
|
+
|
|
120
|
+
const adapter = createUniversalTailwindAdapter({ darkMode: true });
|
|
121
|
+
const SENTINEL = 'SENTINEL';
|
|
122
|
+
const shellHtml = adapter.components.table.render({
|
|
123
|
+
properties: { columns: headers },
|
|
124
|
+
children: SENTINEL,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Every column header appears in the shell, in order.
|
|
128
|
+
let lastIdx = -1;
|
|
129
|
+
for (const header of headers) {
|
|
130
|
+
const idx = shellHtml.indexOf(`>${header}</th>`);
|
|
131
|
+
expect(idx, `header "${header}" must appear in Tailwind adapter output`).toBeGreaterThan(-1);
|
|
132
|
+
expect(idx, `header "${header}" must appear AFTER the previous header`).toBeGreaterThan(lastIdx);
|
|
133
|
+
lastIdx = idx;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Factory B's composer output must preserve the same header order.
|
|
137
|
+
const body = composeListBody(ctx);
|
|
138
|
+
let lastBodyIdx = -1;
|
|
139
|
+
for (const header of headers) {
|
|
140
|
+
const idx = body.indexOf(`>${header}</th>`);
|
|
141
|
+
expect(idx).toBeGreaterThan(-1);
|
|
142
|
+
expect(idx).toBeGreaterThan(lastBodyIdx);
|
|
143
|
+
lastBodyIdx = idx;
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('Factory B output carries Tailwind classes from the canonical adapter shell', () => {
|
|
148
|
+
const body = composeListBody(makeContext());
|
|
149
|
+
// A sampling of classes that come from the adapter's table template.
|
|
150
|
+
// If the adapter changes its classes, both consumers update together.
|
|
151
|
+
expect(body).toContain('overflow-x-auto');
|
|
152
|
+
expect(body).toContain('min-w-full');
|
|
153
|
+
expect(body).toContain('divide-y divide-gray-200');
|
|
154
|
+
expect(body).toContain('bg-gray-50 dark:bg-gray-800');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('rows are synthesized by Factory B (the adapter emits placeholder for react mount)', () => {
|
|
158
|
+
const body = composeListBody(makeContext());
|
|
159
|
+
// The body should contain a real JSX .map() — Factory B's
|
|
160
|
+
// contribution to the shared shell.
|
|
161
|
+
expect(body).toContain('filtered.map((item, idx) =>');
|
|
162
|
+
expect(body).toContain('onClick={() => onSelect?.(item)}');
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe('P3 — field humanisation parity', () => {
|
|
167
|
+
it('both paths humanise column names the same way (camelCase → Title Case)', () => {
|
|
168
|
+
// Factory B's humanize is inlined in the composer. The runtime
|
|
169
|
+
// adapter humanises via the same-shaped regex in pattern-engine /
|
|
170
|
+
// react-pattern-adapter. This test documents the contract by
|
|
171
|
+
// comparing against a fixture.
|
|
172
|
+
const cases: [string, string][] = [
|
|
173
|
+
['title', 'Title'],
|
|
174
|
+
['authorId', 'Author Id'],
|
|
175
|
+
['createdAt', 'Created At'],
|
|
176
|
+
['slugifiedURL', 'Slugified U R L'],
|
|
177
|
+
['name', 'Name'],
|
|
178
|
+
];
|
|
179
|
+
for (const [input, expected] of cases) {
|
|
180
|
+
expect(humanize(input), `humanize("${input}")`).toBe(expected);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
});
|
package/libs/instance-factories/applications/templates/react-starter/__tests__/regen-safety.test.ts
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync, mkdirSync } from 'fs';
|
|
3
|
+
import { tmpdir } from 'os';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import {
|
|
6
|
+
loadHashManifest,
|
|
7
|
+
saveHashManifest,
|
|
8
|
+
reconcileWrites,
|
|
9
|
+
sha256,
|
|
10
|
+
HASHES_DIR,
|
|
11
|
+
HASHES_FILE,
|
|
12
|
+
} from '../regen-safety.js';
|
|
13
|
+
|
|
14
|
+
let projectRoot: string;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
projectRoot = mkdtempSync(join(tmpdir(), 'specverse-regen-safety-'));
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
rmSync(projectRoot, { recursive: true, force: true });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('hash manifest persistence', () => {
|
|
25
|
+
it('loadHashManifest returns {} when the file is missing', () => {
|
|
26
|
+
expect(loadHashManifest(projectRoot)).toEqual({});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('round-trips through save → load', () => {
|
|
30
|
+
saveHashManifest(projectRoot, { 'src/a.ts': 'abc', 'src/b.ts': 'def' });
|
|
31
|
+
const loaded = loadHashManifest(projectRoot);
|
|
32
|
+
expect(loaded).toEqual({ 'src/a.ts': 'abc', 'src/b.ts': 'def' });
|
|
33
|
+
expect(existsSync(join(projectRoot, HASHES_DIR, HASHES_FILE))).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('tolerates a malformed manifest file by returning {}', () => {
|
|
37
|
+
const dir = join(projectRoot, HASHES_DIR);
|
|
38
|
+
mkdirSync(dir, { recursive: true });
|
|
39
|
+
writeFileSync(join(dir, HASHES_FILE), '{ not valid JSON', 'utf8');
|
|
40
|
+
expect(loadHashManifest(projectRoot)).toEqual({});
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('reconcileWrites — pure planning (no I/O writes)', () => {
|
|
45
|
+
it('approves brand-new files and records their hashes', () => {
|
|
46
|
+
const proposed = { 'src/views/PostListView.tsx': 'content-a' };
|
|
47
|
+
const result = reconcileWrites(projectRoot, proposed, {});
|
|
48
|
+
expect(result.approvedWrites).toEqual(proposed);
|
|
49
|
+
expect(result.skipped).toEqual([]);
|
|
50
|
+
expect(existsSync(join(projectRoot, 'src/views/PostListView.tsx'))).toBe(false);
|
|
51
|
+
expect(result.manifest['src/views/PostListView.tsx']).toBe(sha256('content-a'));
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('approves overwriting a pristine file and updates its hash', () => {
|
|
55
|
+
const path = 'src/a.ts';
|
|
56
|
+
const absPath = join(projectRoot, path);
|
|
57
|
+
const originalContent = 'v1';
|
|
58
|
+
const newContent = 'v2';
|
|
59
|
+
|
|
60
|
+
mkdirSync(join(projectRoot, 'src'), { recursive: true });
|
|
61
|
+
writeFileSync(absPath, originalContent, 'utf8');
|
|
62
|
+
const prevManifest = { [path]: sha256(originalContent) };
|
|
63
|
+
|
|
64
|
+
const result = reconcileWrites(projectRoot, { [path]: newContent }, prevManifest);
|
|
65
|
+
|
|
66
|
+
expect(result.approvedWrites[path]).toBe(newContent);
|
|
67
|
+
expect(result.skipped).toEqual([]);
|
|
68
|
+
// reconcileWrites is pure planning — filesystem unchanged
|
|
69
|
+
expect(readFileSync(absPath, 'utf8')).toBe(originalContent);
|
|
70
|
+
expect(result.manifest[path]).toBe(sha256(newContent));
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('skips a user-edited file and keeps the old hash record', () => {
|
|
74
|
+
const path = 'src/edited.ts';
|
|
75
|
+
const absPath = join(projectRoot, path);
|
|
76
|
+
const originalContent = 'v1';
|
|
77
|
+
const editedContent = 'v1 + user edits';
|
|
78
|
+
const newContent = 'v2';
|
|
79
|
+
|
|
80
|
+
mkdirSync(join(projectRoot, 'src'), { recursive: true });
|
|
81
|
+
writeFileSync(absPath, editedContent, 'utf8');
|
|
82
|
+
const prevManifest = { [path]: sha256(originalContent) };
|
|
83
|
+
|
|
84
|
+
const result = reconcileWrites(projectRoot, { [path]: newContent }, prevManifest);
|
|
85
|
+
|
|
86
|
+
expect(result.approvedWrites[path]).toBeUndefined();
|
|
87
|
+
expect(result.skipped).toHaveLength(1);
|
|
88
|
+
expect(result.skipped[0]).toMatchObject({
|
|
89
|
+
path,
|
|
90
|
+
reason: expect.stringContaining('edited'),
|
|
91
|
+
});
|
|
92
|
+
expect(readFileSync(absPath, 'utf8')).toBe(editedContent);
|
|
93
|
+
expect(result.manifest[path]).toBe(prevManifest[path]);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('skips files that exist but have no prior hash record', () => {
|
|
97
|
+
const path = 'src/unknown.ts';
|
|
98
|
+
const absPath = join(projectRoot, path);
|
|
99
|
+
|
|
100
|
+
mkdirSync(join(projectRoot, 'src'), { recursive: true });
|
|
101
|
+
writeFileSync(absPath, 'manually placed', 'utf8');
|
|
102
|
+
|
|
103
|
+
const result = reconcileWrites(projectRoot, { [path]: 'would-overwrite' }, {});
|
|
104
|
+
|
|
105
|
+
expect(result.approvedWrites[path]).toBeUndefined();
|
|
106
|
+
expect(result.skipped).toHaveLength(1);
|
|
107
|
+
expect(result.skipped[0].reason).toContain('no prior hash recorded');
|
|
108
|
+
expect(readFileSync(absPath, 'utf8')).toBe('manually placed');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('does not mutate the input prevManifest', () => {
|
|
112
|
+
const prev = { 'a.ts': sha256('old') };
|
|
113
|
+
const prevSnapshot = { ...prev };
|
|
114
|
+
reconcileWrites(projectRoot, { 'a.ts': 'new', 'b.ts': 'also new' }, prev);
|
|
115
|
+
expect(prev).toEqual(prevSnapshot);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('handles a batch of mixed outcomes correctly', () => {
|
|
119
|
+
mkdirSync(join(projectRoot, 'src'), { recursive: true });
|
|
120
|
+
writeFileSync(join(projectRoot, 'src/pristine.ts'), 'v1', 'utf8');
|
|
121
|
+
writeFileSync(join(projectRoot, 'src/edited.ts'), 'v1 + user edit', 'utf8');
|
|
122
|
+
const prev = {
|
|
123
|
+
'src/pristine.ts': sha256('v1'),
|
|
124
|
+
'src/edited.ts': sha256('v1'),
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const proposed = {
|
|
128
|
+
'src/pristine.ts': 'v2',
|
|
129
|
+
'src/edited.ts': 'v2',
|
|
130
|
+
'src/new.ts': 'brand new',
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const result = reconcileWrites(projectRoot, proposed, prev);
|
|
134
|
+
|
|
135
|
+
expect(Object.keys(result.approvedWrites).sort()).toEqual(['src/new.ts', 'src/pristine.ts']);
|
|
136
|
+
expect(result.skipped.map(s => s.path)).toEqual(['src/edited.ts']);
|
|
137
|
+
// No writes happened
|
|
138
|
+
expect(readFileSync(join(projectRoot, 'src/pristine.ts'), 'utf8')).toBe('v1');
|
|
139
|
+
expect(readFileSync(join(projectRoot, 'src/edited.ts'), 'utf8')).toBe('v1 + user edit');
|
|
140
|
+
expect(result.manifest['src/pristine.ts']).toBe(sha256('v2'));
|
|
141
|
+
expect(result.manifest['src/edited.ts']).toBe(prev['src/edited.ts']);
|
|
142
|
+
expect(result.manifest['src/new.ts']).toBe(sha256('brand new'));
|
|
143
|
+
});
|
|
144
|
+
});
|