@specverse/engines 4.1.21 → 4.1.23
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/dist/inference/ui-contracts/index.d.ts +38 -0
- package/dist/inference/ui-contracts/index.d.ts.map +1 -0
- package/dist/inference/ui-contracts/index.js +212 -0
- package/dist/inference/ui-contracts/index.js.map +1 -0
- package/dist/inference/ui-contracts/rules/_shared.d.ts +32 -0
- package/dist/inference/ui-contracts/rules/_shared.d.ts.map +1 -0
- package/dist/inference/ui-contracts/rules/_shared.js +103 -0
- package/dist/inference/ui-contracts/rules/_shared.js.map +1 -0
- package/dist/inference/ui-contracts/rules/action-buttons-present.d.ts +21 -0
- package/dist/inference/ui-contracts/rules/action-buttons-present.d.ts.map +1 -0
- package/dist/inference/ui-contracts/rules/action-buttons-present.js +62 -0
- package/dist/inference/ui-contracts/rules/action-buttons-present.js.map +1 -0
- package/dist/inference/ui-contracts/rules/create-reflects-in-list.d.ts +22 -0
- package/dist/inference/ui-contracts/rules/create-reflects-in-list.d.ts.map +1 -0
- package/dist/inference/ui-contracts/rules/create-reflects-in-list.js +48 -0
- package/dist/inference/ui-contracts/rules/create-reflects-in-list.js.map +1 -0
- package/dist/inference/ui-contracts/rules/delete-reflects-in-list.d.ts +22 -0
- package/dist/inference/ui-contracts/rules/delete-reflects-in-list.d.ts.map +1 -0
- package/dist/inference/ui-contracts/rules/delete-reflects-in-list.js +50 -0
- package/dist/inference/ui-contracts/rules/delete-reflects-in-list.js.map +1 -0
- package/dist/inference/ui-contracts/rules/detail-view-renders.d.ts +24 -0
- package/dist/inference/ui-contracts/rules/detail-view-renders.d.ts.map +1 -0
- package/dist/inference/ui-contracts/rules/detail-view-renders.js +34 -0
- package/dist/inference/ui-contracts/rules/detail-view-renders.js.map +1 -0
- package/dist/inference/ui-contracts/rules/evolve-reflects-in-list.d.ts +21 -0
- package/dist/inference/ui-contracts/rules/evolve-reflects-in-list.d.ts.map +1 -0
- package/dist/inference/ui-contracts/rules/evolve-reflects-in-list.js +53 -0
- package/dist/inference/ui-contracts/rules/evolve-reflects-in-list.js.map +1 -0
- package/dist/inference/ui-contracts/rules/form-shows-required-indicators.d.ts +15 -0
- package/dist/inference/ui-contracts/rules/form-shows-required-indicators.d.ts.map +1 -0
- package/dist/inference/ui-contracts/rules/form-shows-required-indicators.js +38 -0
- package/dist/inference/ui-contracts/rules/form-shows-required-indicators.js.map +1 -0
- package/dist/inference/ui-contracts/rules/hasmany-shows-children-in-detail.d.ts +17 -0
- package/dist/inference/ui-contracts/rules/hasmany-shows-children-in-detail.d.ts.map +1 -0
- package/dist/inference/ui-contracts/rules/hasmany-shows-children-in-detail.js +39 -0
- package/dist/inference/ui-contracts/rules/hasmany-shows-children-in-detail.js.map +1 -0
- package/dist/inference/ui-contracts/rules/lifecycle-state-visible-in-detail.d.ts +25 -0
- package/dist/inference/ui-contracts/rules/lifecycle-state-visible-in-detail.d.ts.map +1 -0
- package/dist/inference/ui-contracts/rules/lifecycle-state-visible-in-detail.js +66 -0
- package/dist/inference/ui-contracts/rules/lifecycle-state-visible-in-detail.js.map +1 -0
- package/dist/inference/ui-contracts/rules/list-shows-business-columns.d.ts +17 -0
- package/dist/inference/ui-contracts/rules/list-shows-business-columns.d.ts.map +1 -0
- package/dist/inference/ui-contracts/rules/list-shows-business-columns.js +39 -0
- package/dist/inference/ui-contracts/rules/list-shows-business-columns.js.map +1 -0
- package/dist/inference/ui-contracts/rules/list-view-renders.d.ts +19 -0
- package/dist/inference/ui-contracts/rules/list-view-renders.d.ts.map +1 -0
- package/dist/inference/ui-contracts/rules/list-view-renders.js +29 -0
- package/dist/inference/ui-contracts/rules/list-view-renders.js.map +1 -0
- package/dist/inference/ui-contracts/rules/nav-has-model-entries.d.ts +20 -0
- package/dist/inference/ui-contracts/rules/nav-has-model-entries.d.ts.map +1 -0
- package/dist/inference/ui-contracts/rules/nav-has-model-entries.js +29 -0
- package/dist/inference/ui-contracts/rules/nav-has-model-entries.js.map +1 -0
- package/dist/inference/ui-contracts/test-case-types.d.ts +126 -0
- package/dist/inference/ui-contracts/test-case-types.d.ts.map +1 -0
- package/dist/inference/ui-contracts/test-case-types.js +14 -0
- package/dist/inference/ui-contracts/test-case-types.js.map +1 -0
- package/dist/inference/ui-contracts/translator.d.ts +17 -0
- package/dist/inference/ui-contracts/translator.d.ts.map +1 -0
- package/dist/inference/ui-contracts/translator.js +127 -0
- package/dist/inference/ui-contracts/translator.js.map +1 -0
- package/dist/libs/instance-factories/applications/templates/react/api-client-generator.js +37 -2
- package/dist/libs/instance-factories/applications/templates/react/runtime-app-tsx-generator.js +41 -4
- package/dist/libs/instance-factories/applications/templates/react/use-api-hooks-generator.js +27 -7
- package/dist/libs/instance-factories/scaffolding/templates/generic/package-json-generator.js +10 -0
- package/dist/libs/instance-factories/views/templates/react/components-generator.js +34 -23
- package/dist/libs/instance-factories/views/templates/react/hooks-generator.js +51 -81
- package/dist/realize/index.d.ts.map +1 -1
- package/dist/realize/index.js +204 -0
- package/dist/realize/index.js.map +1 -1
- package/libs/instance-factories/applications/templates/react/api-client-generator.ts +37 -2
- package/libs/instance-factories/applications/templates/react/runtime-app-tsx-generator.ts +41 -4
- package/libs/instance-factories/applications/templates/react/use-api-hooks-generator.ts +27 -7
- package/libs/instance-factories/scaffolding/templates/generic/package-json-generator.ts +13 -0
- package/libs/instance-factories/views/templates/react/components-generator.ts +34 -23
- package/libs/instance-factories/views/templates/react/hooks-generator.ts +72 -88
- package/package.json +1 -1
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TestCase — declarative description of a UI contract test.
|
|
3
|
+
*
|
|
4
|
+
* Rules (pure functions) produce TestCase[]. The translator renders
|
|
5
|
+
* each TestCase into a Playwright `.spec.ts` file that imports helpers
|
|
6
|
+
* from `@specverse/runtime/test-harness`. Neither rules nor test cases
|
|
7
|
+
* know anything about React Query, cache invalidation, or any
|
|
8
|
+
* implementation detail — they speak only spec language.
|
|
9
|
+
*
|
|
10
|
+
* v1 steps cover navigation + assertion. Later phases add create /
|
|
11
|
+
* delete / evolve / expect-absent for state-sync rules.
|
|
12
|
+
*/
|
|
13
|
+
export type TestStep = {
|
|
14
|
+
action: 'bootRuntime';
|
|
15
|
+
} | {
|
|
16
|
+
action: 'expectNavHasModel';
|
|
17
|
+
modelName: string;
|
|
18
|
+
} | {
|
|
19
|
+
action: 'navigateToModel';
|
|
20
|
+
modelName: string;
|
|
21
|
+
} | {
|
|
22
|
+
action: 'expectViewRenders';
|
|
23
|
+
} | {
|
|
24
|
+
action: 'expectListColumn';
|
|
25
|
+
modelName: string;
|
|
26
|
+
attributeName: string;
|
|
27
|
+
} | {
|
|
28
|
+
action: 'expectFormInput';
|
|
29
|
+
modelName: string;
|
|
30
|
+
attributeName: string;
|
|
31
|
+
required?: boolean;
|
|
32
|
+
} | {
|
|
33
|
+
action: 'expectLifecycleState';
|
|
34
|
+
modelName: string;
|
|
35
|
+
stateName: string;
|
|
36
|
+
} | {
|
|
37
|
+
action: 'expectRelationshipSection';
|
|
38
|
+
modelName: string;
|
|
39
|
+
relationshipName: string;
|
|
40
|
+
} | {
|
|
41
|
+
action: 'expectActionButton';
|
|
42
|
+
modelName: string;
|
|
43
|
+
actionLabel: string;
|
|
44
|
+
} | {
|
|
45
|
+
action: 'createEntity';
|
|
46
|
+
modelName: string;
|
|
47
|
+
data: Record<string, unknown>;
|
|
48
|
+
bindAs?: string;
|
|
49
|
+
} | {
|
|
50
|
+
action: 'deleteEntity';
|
|
51
|
+
modelName: string;
|
|
52
|
+
idExpr: string;
|
|
53
|
+
} | {
|
|
54
|
+
action: 'evolveEntity';
|
|
55
|
+
modelName: string;
|
|
56
|
+
idExpr: string;
|
|
57
|
+
targetState: string;
|
|
58
|
+
} | {
|
|
59
|
+
action: 'expectEntityInList';
|
|
60
|
+
modelName: string;
|
|
61
|
+
displayValue: string;
|
|
62
|
+
} | {
|
|
63
|
+
action: 'expectEntityAbsent';
|
|
64
|
+
modelName: string;
|
|
65
|
+
displayValue: string;
|
|
66
|
+
};
|
|
67
|
+
export interface TestCase {
|
|
68
|
+
/** The rule that produced this test case. Used in test names for attribution. */
|
|
69
|
+
ruleId: string;
|
|
70
|
+
/** The spec element the case applies to (e.g. 'Poll', 'PollController.delete'). */
|
|
71
|
+
specElement: string;
|
|
72
|
+
/** Full test name — shown by Playwright reporter. */
|
|
73
|
+
name: string;
|
|
74
|
+
/** Ordered sequence of actions/assertions. */
|
|
75
|
+
steps: TestStep[];
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* A rule is a pure function from a normalized spec to a list of test
|
|
79
|
+
* cases. Rules never touch the filesystem, the network, or any mutable
|
|
80
|
+
* state — they just describe what should be tested.
|
|
81
|
+
*/
|
|
82
|
+
export type UiContractRule = (spec: NormalizedSpec) => TestCase[];
|
|
83
|
+
/**
|
|
84
|
+
* Minimal normalized spec shape the rules operate on. The realize
|
|
85
|
+
* pipeline converts raw spec output into this shape before running
|
|
86
|
+
* rules. Kept deliberately small — rules grow their own helpers as
|
|
87
|
+
* they need richer data.
|
|
88
|
+
*/
|
|
89
|
+
export interface NormalizedSpec {
|
|
90
|
+
models: ModelInfo[];
|
|
91
|
+
controllers?: ControllerInfo[];
|
|
92
|
+
services?: ServiceInfo[];
|
|
93
|
+
views?: ViewInfo[];
|
|
94
|
+
}
|
|
95
|
+
export interface ModelInfo {
|
|
96
|
+
name: string;
|
|
97
|
+
attributes?: AttributeInfo[];
|
|
98
|
+
lifecycles?: Record<string, unknown>;
|
|
99
|
+
relationships?: RelationshipInfo[];
|
|
100
|
+
}
|
|
101
|
+
export interface AttributeInfo {
|
|
102
|
+
name: string;
|
|
103
|
+
type?: string;
|
|
104
|
+
required?: boolean;
|
|
105
|
+
category?: 'business' | 'metadata' | 'relationship';
|
|
106
|
+
}
|
|
107
|
+
export interface RelationshipInfo {
|
|
108
|
+
name: string;
|
|
109
|
+
type: string;
|
|
110
|
+
target?: string;
|
|
111
|
+
}
|
|
112
|
+
export interface ControllerInfo {
|
|
113
|
+
name: string;
|
|
114
|
+
model?: string;
|
|
115
|
+
cured?: Record<string, unknown>;
|
|
116
|
+
}
|
|
117
|
+
export interface ServiceInfo {
|
|
118
|
+
name: string;
|
|
119
|
+
operations?: Record<string, unknown>;
|
|
120
|
+
}
|
|
121
|
+
export interface ViewInfo {
|
|
122
|
+
name: string;
|
|
123
|
+
type: string;
|
|
124
|
+
model?: string;
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=test-case-types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-case-types.d.ts","sourceRoot":"","sources":["../../../src/inference/ui-contracts/test-case-types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TestCase — declarative description of a UI contract test.
|
|
3
|
+
*
|
|
4
|
+
* Rules (pure functions) produce TestCase[]. The translator renders
|
|
5
|
+
* each TestCase into a Playwright `.spec.ts` file that imports helpers
|
|
6
|
+
* from `@specverse/runtime/test-harness`. Neither rules nor test cases
|
|
7
|
+
* know anything about React Query, cache invalidation, or any
|
|
8
|
+
* implementation detail — they speak only spec language.
|
|
9
|
+
*
|
|
10
|
+
* v1 steps cover navigation + assertion. Later phases add create /
|
|
11
|
+
* delete / evolve / expect-absent for state-sync rules.
|
|
12
|
+
*/
|
|
13
|
+
export {};
|
|
14
|
+
//# sourceMappingURL=test-case-types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-case-types.js","sourceRoot":"","sources":["../../../src/inference/ui-contracts/test-case-types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TestCase → Playwright .spec.ts translator.
|
|
3
|
+
*
|
|
4
|
+
* Renders a list of TestCase objects into a single Playwright spec
|
|
5
|
+
* file. The output file imports from `@specverse/runtime/test-harness`
|
|
6
|
+
* and nothing else from the runtime — all logic lives in the harness
|
|
7
|
+
* helpers. The translator is intentionally dumb: each step maps to a
|
|
8
|
+
* helper call, nothing more.
|
|
9
|
+
*/
|
|
10
|
+
import type { TestCase } from './test-case-types.js';
|
|
11
|
+
/**
|
|
12
|
+
* Render a full `.spec.ts` file for one or more test cases sharing
|
|
13
|
+
* the same rule. One file per rule keeps failures easy to attribute
|
|
14
|
+
* and lets `npx playwright test` filter by rule ID via file glob.
|
|
15
|
+
*/
|
|
16
|
+
export declare function renderSpecFile(ruleId: string, testCases: TestCase[]): string;
|
|
17
|
+
//# sourceMappingURL=translator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"translator.d.ts","sourceRoot":"","sources":["../../../src/inference/ui-contracts/translator.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAY,MAAM,sBAAsB,CAAC;AAwF/D;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,MAAM,CA6B5E"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TestCase → Playwright .spec.ts translator.
|
|
3
|
+
*
|
|
4
|
+
* Renders a list of TestCase objects into a single Playwright spec
|
|
5
|
+
* file. The output file imports from `@specverse/runtime/test-harness`
|
|
6
|
+
* and nothing else from the runtime — all logic lives in the harness
|
|
7
|
+
* helpers. The translator is intentionally dumb: each step maps to a
|
|
8
|
+
* helper call, nothing more.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Render a single step as a line of test body code.
|
|
12
|
+
*/
|
|
13
|
+
function renderStep(step) {
|
|
14
|
+
switch (step.action) {
|
|
15
|
+
case 'bootRuntime':
|
|
16
|
+
// Assign `page` once per test body. Multiple bootRuntime steps
|
|
17
|
+
// inside one test aren't supported and would shadow the binding.
|
|
18
|
+
return ` const { page } = await bootRuntime(browser);`;
|
|
19
|
+
case 'expectNavHasModel':
|
|
20
|
+
return ` await expectNavHasModel(page, ${JSON.stringify(step.modelName)});`;
|
|
21
|
+
case 'navigateToModel':
|
|
22
|
+
return ` await navigateToModel(page, ${JSON.stringify(step.modelName)});`;
|
|
23
|
+
case 'expectViewRenders':
|
|
24
|
+
return ` await expectViewRenders(page);`;
|
|
25
|
+
case 'expectListColumn':
|
|
26
|
+
return ` await expectListColumn(page, ${JSON.stringify(step.modelName)}, ${JSON.stringify(step.attributeName)});`;
|
|
27
|
+
case 'expectFormInput': {
|
|
28
|
+
const opts = step.required ? `, { required: true }` : ``;
|
|
29
|
+
return ` await expectFormInput(page, ${JSON.stringify(step.modelName)}, ${JSON.stringify(step.attributeName)}${opts});`;
|
|
30
|
+
}
|
|
31
|
+
case 'expectLifecycleState':
|
|
32
|
+
return ` await expectLifecycleState(page, ${JSON.stringify(step.modelName)}, ${JSON.stringify(step.stateName)});`;
|
|
33
|
+
case 'expectRelationshipSection':
|
|
34
|
+
return ` await expectRelationshipSection(page, ${JSON.stringify(step.modelName)}, ${JSON.stringify(step.relationshipName)});`;
|
|
35
|
+
case 'expectActionButton':
|
|
36
|
+
return ` await expectActionButton(page, ${JSON.stringify(step.modelName)}, ${JSON.stringify(step.actionLabel)});`;
|
|
37
|
+
case 'createEntity': {
|
|
38
|
+
const varName = step.bindAs || '_entityId';
|
|
39
|
+
const dataLit = JSON.stringify(step.data);
|
|
40
|
+
return ` const ${varName} = await createEntity(page, ${JSON.stringify(step.modelName)}, ${dataLit});`;
|
|
41
|
+
}
|
|
42
|
+
case 'deleteEntity':
|
|
43
|
+
return ` await deleteEntity(page, ${JSON.stringify(step.modelName)}, ${step.idExpr});`;
|
|
44
|
+
case 'evolveEntity':
|
|
45
|
+
return ` await evolveEntity(page, ${JSON.stringify(step.modelName)}, ${step.idExpr}, ${JSON.stringify(step.targetState)});`;
|
|
46
|
+
case 'expectEntityInList':
|
|
47
|
+
return ` await expectEntityInList(page, ${JSON.stringify(step.modelName)}, ${JSON.stringify(step.displayValue)});`;
|
|
48
|
+
case 'expectEntityAbsent':
|
|
49
|
+
return ` await expectEntityAbsent(page, ${JSON.stringify(step.modelName)}, ${JSON.stringify(step.displayValue)});`;
|
|
50
|
+
default: {
|
|
51
|
+
const _exhaustive = step;
|
|
52
|
+
throw new Error(`translator: unknown step action: ${JSON.stringify(_exhaustive)}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Collect the set of harness helpers a test case depends on so the
|
|
58
|
+
* import list only contains what's actually used.
|
|
59
|
+
*/
|
|
60
|
+
function helpersFor(step) {
|
|
61
|
+
switch (step.action) {
|
|
62
|
+
case 'bootRuntime':
|
|
63
|
+
return ['bootRuntime'];
|
|
64
|
+
case 'expectNavHasModel':
|
|
65
|
+
return ['expectNavHasModel'];
|
|
66
|
+
case 'navigateToModel':
|
|
67
|
+
return ['navigateToModel'];
|
|
68
|
+
case 'expectViewRenders':
|
|
69
|
+
return ['expectViewRenders'];
|
|
70
|
+
case 'expectListColumn':
|
|
71
|
+
return ['expectListColumn'];
|
|
72
|
+
case 'expectFormInput':
|
|
73
|
+
return ['expectFormInput'];
|
|
74
|
+
case 'expectLifecycleState':
|
|
75
|
+
return ['expectLifecycleState'];
|
|
76
|
+
case 'expectRelationshipSection':
|
|
77
|
+
return ['expectRelationshipSection'];
|
|
78
|
+
case 'expectActionButton':
|
|
79
|
+
return ['expectActionButton'];
|
|
80
|
+
case 'createEntity':
|
|
81
|
+
return ['createEntity'];
|
|
82
|
+
case 'deleteEntity':
|
|
83
|
+
return ['deleteEntity'];
|
|
84
|
+
case 'evolveEntity':
|
|
85
|
+
return ['evolveEntity'];
|
|
86
|
+
case 'expectEntityInList':
|
|
87
|
+
return ['expectEntityInList'];
|
|
88
|
+
case 'expectEntityAbsent':
|
|
89
|
+
return ['expectEntityAbsent'];
|
|
90
|
+
default:
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Render a full `.spec.ts` file for one or more test cases sharing
|
|
96
|
+
* the same rule. One file per rule keeps failures easy to attribute
|
|
97
|
+
* and lets `npx playwright test` filter by rule ID via file glob.
|
|
98
|
+
*/
|
|
99
|
+
export function renderSpecFile(ruleId, testCases) {
|
|
100
|
+
const helpers = new Set();
|
|
101
|
+
for (const tc of testCases) {
|
|
102
|
+
for (const step of tc.steps) {
|
|
103
|
+
for (const h of helpersFor(step))
|
|
104
|
+
helpers.add(h);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const importList = [...helpers].sort().join(', ');
|
|
108
|
+
const header = `/**
|
|
109
|
+
* Auto-generated UI contract test file.
|
|
110
|
+
* Rule: ${ruleId}
|
|
111
|
+
* DO NOT EDIT — regenerated every \`spv realize\` run. Hand edits are
|
|
112
|
+
* lost on the next regenerate. Change the rule or the spec instead.
|
|
113
|
+
*/
|
|
114
|
+
|
|
115
|
+
import { test } from '@playwright/test';
|
|
116
|
+
import { ${importList} } from '@specverse/runtime/test-harness';
|
|
117
|
+
`;
|
|
118
|
+
const body = testCases.map(tc => {
|
|
119
|
+
const stepLines = tc.steps.map(renderStep).join('\n');
|
|
120
|
+
return `
|
|
121
|
+
test(${JSON.stringify(tc.name)}, async ({ browser }) => {
|
|
122
|
+
${stepLines}
|
|
123
|
+
});`;
|
|
124
|
+
}).join('\n');
|
|
125
|
+
return header + body + '\n';
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=translator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"translator.js","sourceRoot":"","sources":["../../../src/inference/ui-contracts/translator.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH;;GAEG;AACH,SAAS,UAAU,CAAC,IAAc;IAChC,QAAQ,IAAI,CAAC,MAAM,EAAE,CAAC;QACpB,KAAK,aAAa;YAChB,+DAA+D;YAC/D,iEAAiE;YACjE,OAAO,gDAAgD,CAAC;QAC1D,KAAK,mBAAmB;YACtB,OAAO,mCAAmC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;QAC/E,KAAK,iBAAiB;YACpB,OAAO,iCAAiC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;QAC7E,KAAK,mBAAmB;YACtB,OAAO,kCAAkC,CAAC;QAC5C,KAAK,kBAAkB;YACrB,OAAO,kCAAkC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC;QACrH,KAAK,iBAAiB,CAAC,CAAC,CAAC;YACvB,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,EAAE,CAAC;YACzD,OAAO,iCAAiC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,aAAa,CAAC,GAAG,IAAI,IAAI,CAAC;QAC3H,CAAC;QACD,KAAK,sBAAsB;YACzB,OAAO,sCAAsC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;QACrH,KAAK,2BAA2B;YAC9B,OAAO,2CAA2C,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC;QACjI,KAAK,oBAAoB;YACvB,OAAO,oCAAoC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC;QACrH,KAAK,cAAc,CAAC,CAAC,CAAC;YACpB,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,IAAI,WAAW,CAAC;YAC3C,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC1C,OAAO,WAAW,OAAO,+BAA+B,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,OAAO,IAAI,CAAC;QACzG,CAAC;QACD,KAAK,cAAc;YACjB,OAAO,8BAA8B,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,IAAI,CAAC,MAAM,IAAI,CAAC;QAC1F,KAAK,cAAc;YACjB,OAAO,8BAA8B,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC;QAC/H,KAAK,oBAAoB;YACvB,OAAO,oCAAoC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC;QACtH,KAAK,oBAAoB;YACvB,OAAO,oCAAoC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC;QACtH,OAAO,CAAC,CAAC,CAAC;YACR,MAAM,WAAW,GAAU,IAAI,CAAC;YAChC,MAAM,IAAI,KAAK,CAAC,oCAAoC,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;QACrF,CAAC;IACH,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,SAAS,UAAU,CAAC,IAAc;IAChC,QAAQ,IAAI,CAAC,MAAM,EAAE,CAAC;QACpB,KAAK,aAAa;YAChB,OAAO,CAAC,aAAa,CAAC,CAAC;QACzB,KAAK,mBAAmB;YACtB,OAAO,CAAC,mBAAmB,CAAC,CAAC;QAC/B,KAAK,iBAAiB;YACpB,OAAO,CAAC,iBAAiB,CAAC,CAAC;QAC7B,KAAK,mBAAmB;YACtB,OAAO,CAAC,mBAAmB,CAAC,CAAC;QAC/B,KAAK,kBAAkB;YACrB,OAAO,CAAC,kBAAkB,CAAC,CAAC;QAC9B,KAAK,iBAAiB;YACpB,OAAO,CAAC,iBAAiB,CAAC,CAAC;QAC7B,KAAK,sBAAsB;YACzB,OAAO,CAAC,sBAAsB,CAAC,CAAC;QAClC,KAAK,2BAA2B;YAC9B,OAAO,CAAC,2BAA2B,CAAC,CAAC;QACvC,KAAK,oBAAoB;YACvB,OAAO,CAAC,oBAAoB,CAAC,CAAC;QAChC,KAAK,cAAc;YACjB,OAAO,CAAC,cAAc,CAAC,CAAC;QAC1B,KAAK,cAAc;YACjB,OAAO,CAAC,cAAc,CAAC,CAAC;QAC1B,KAAK,cAAc;YACjB,OAAO,CAAC,cAAc,CAAC,CAAC;QAC1B,KAAK,oBAAoB;YACvB,OAAO,CAAC,oBAAoB,CAAC,CAAC;QAChC,KAAK,oBAAoB;YACvB,OAAO,CAAC,oBAAoB,CAAC,CAAC;QAChC;YACE,OAAO,EAAE,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,MAAc,EAAE,SAAqB;IAClE,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;IAClC,KAAK,MAAM,EAAE,IAAI,SAAS,EAAE,CAAC;QAC3B,KAAK,MAAM,IAAI,IAAI,EAAE,CAAC,KAAK,EAAE,CAAC;YAC5B,KAAK,MAAM,CAAC,IAAI,UAAU,CAAC,IAAI,CAAC;gBAAE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QACnD,CAAC;IACH,CAAC;IACD,MAAM,UAAU,GAAG,CAAC,GAAG,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAElD,MAAM,MAAM,GAAG;;WAEN,MAAM;;;;;;WAMN,UAAU;CACpB,CAAC;IAEA,MAAM,IAAI,GAAG,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE;QAC9B,MAAM,SAAS,GAAG,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACtD,OAAO;OACJ,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,IAAI,CAAC;EAC5B,SAAS;IACP,CAAC;IACH,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAEd,OAAO,MAAM,GAAG,IAAI,GAAG,IAAI,CAAC;AAC9B,CAAC"}
|
|
@@ -43,9 +43,12 @@ export const WS_URL = apiUrl
|
|
|
43
43
|
: \`\${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}/\${window.location.host}/ws\`;
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
|
-
* Generic API request helper
|
|
46
|
+
* Generic API request helper \u2014 exported so view components can
|
|
47
|
+
* use it as a drop-in replacement for raw fetch calls. Always
|
|
48
|
+
* routes through this module so the conformance check
|
|
49
|
+
* \`no-raw-fetch-in-views\` stays green.
|
|
47
50
|
*/
|
|
48
|
-
async function apiRequest<T = any>(
|
|
51
|
+
export async function apiRequest<T = any>(
|
|
49
52
|
method: string,
|
|
50
53
|
path: string,
|
|
51
54
|
body: any = null
|
|
@@ -304,6 +307,22 @@ export async function executeOperation(
|
|
|
304
307
|
return apiRequest<ApiResponse>(method, path, Object.keys(params).length > 0 ? params : null);
|
|
305
308
|
}
|
|
306
309
|
|
|
310
|
+
/**
|
|
311
|
+
* Execute a service operation
|
|
312
|
+
* Services are RPC-style under /services/{serviceName}/{operationName}
|
|
313
|
+
*/
|
|
314
|
+
export async function executeServiceOperation(
|
|
315
|
+
serviceName: string,
|
|
316
|
+
operationName: string,
|
|
317
|
+
params: Record<string, any>
|
|
318
|
+
): Promise<ApiResponse> {
|
|
319
|
+
return apiRequest<ApiResponse>(
|
|
320
|
+
'POST',
|
|
321
|
+
\`/services/\${serviceName}/\${operationName}\`,
|
|
322
|
+
params
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
307
326
|
/**
|
|
308
327
|
* Transition entity lifecycle state
|
|
309
328
|
*/
|
|
@@ -395,6 +414,22 @@ export async function executeOperation(
|
|
|
395
414
|
);
|
|
396
415
|
}
|
|
397
416
|
|
|
417
|
+
/**
|
|
418
|
+
* Execute a service operation
|
|
419
|
+
* Services are RPC-style under /services/{serviceName}/{operationName}
|
|
420
|
+
*/
|
|
421
|
+
export async function executeServiceOperation(
|
|
422
|
+
serviceName: string,
|
|
423
|
+
operationName: string,
|
|
424
|
+
params: Record<string, any>
|
|
425
|
+
): Promise<ApiResponse> {
|
|
426
|
+
return apiRequest<ApiResponse>(
|
|
427
|
+
'POST',
|
|
428
|
+
\`/services/\${serviceName}/\${operationName}\`,
|
|
429
|
+
params
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
|
|
398
433
|
/**
|
|
399
434
|
* Transition entity lifecycle state
|
|
400
435
|
*/
|
package/dist/libs/instance-factories/applications/templates/react/runtime-app-tsx-generator.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
function generateRuntimeAppTsx(context) {
|
|
2
2
|
const { spec } = context;
|
|
3
3
|
const appName = spec?.metadata?.name || spec?.name || "SpecVerse App";
|
|
4
|
-
return `import { useState, useEffect, useMemo } from 'react';
|
|
5
|
-
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
4
|
+
return `import { useState, useEffect, useMemo, useCallback } from 'react';
|
|
5
|
+
import { QueryClient, QueryClientProvider, useQueryClient } from '@tanstack/react-query';
|
|
6
6
|
import yaml from 'js-yaml';
|
|
7
|
-
import { RuntimeViewProvider } from '@specverse/runtime/views/react';
|
|
7
|
+
import { RuntimeViewProvider, useEntitySync } from '@specverse/runtime/views/react';
|
|
8
8
|
import { DevShell } from '@specverse/runtime/views/react';
|
|
9
9
|
import {
|
|
10
10
|
useEntitiesQuery,
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
useExecuteOperationMutation,
|
|
13
13
|
useTransitionStateMutation,
|
|
14
14
|
} from './hooks/useApi';
|
|
15
|
+
import { listEntities, getRuntimeInfo } from './lib/apiClient';
|
|
15
16
|
import devSpecRaw from './dev.specly?raw';
|
|
16
17
|
|
|
17
18
|
// Parse YAML spec
|
|
@@ -64,6 +65,40 @@ function AppContent() {
|
|
|
64
65
|
.catch(() => {});
|
|
65
66
|
}, []);
|
|
66
67
|
|
|
68
|
+
// State-sync contract: WebSocket entity events \u2192 React Query cache
|
|
69
|
+
// invalidation. This is what guarantees "delete in one view is visible
|
|
70
|
+
// in every other view within one WS round-trip" \u2014 it runs at the App
|
|
71
|
+
// level so it's always on, regardless of which tab is active.
|
|
72
|
+
const queryClient = useQueryClient();
|
|
73
|
+
const invalidateEntities = useCallback((modelName: string) => {
|
|
74
|
+
queryClient.invalidateQueries({ queryKey: ['entities', modelName] });
|
|
75
|
+
}, [queryClient]);
|
|
76
|
+
useEntitySync({ invalidateEntities, apiBaseUrl: '/api' });
|
|
77
|
+
|
|
78
|
+
// Name resolver for OperationResultView. Kept in the host so runtime
|
|
79
|
+
// view components never issue raw fetch() calls \u2014 all HTTP goes
|
|
80
|
+
// through apiClient's canonical layer.
|
|
81
|
+
const resolveEntityNames = useCallback(async (ids: string[]): Promise<Record<string, string>> => {
|
|
82
|
+
const names: Record<string, string> = {};
|
|
83
|
+
try {
|
|
84
|
+
const info = await getRuntimeInfo();
|
|
85
|
+
const models = info?.models || [];
|
|
86
|
+
await Promise.all(models.map(async (model: string) => {
|
|
87
|
+
try {
|
|
88
|
+
const entities = await listEntities(model + 'Controller');
|
|
89
|
+
for (const entity of entities) {
|
|
90
|
+
const id = (entity as any)?.id;
|
|
91
|
+
const dataId = (entity as any)?.data?.id;
|
|
92
|
+
const display = (entity as any)?.name || (entity as any)?.title || (entity as any)?.label || id;
|
|
93
|
+
if (id && ids.includes(id)) names[id] = String(display);
|
|
94
|
+
if (dataId && ids.includes(dataId)) names[dataId] = String(display);
|
|
95
|
+
}
|
|
96
|
+
} catch { /* skip this model */ }
|
|
97
|
+
}));
|
|
98
|
+
} catch { /* ignore \u2014 caller falls back to raw IDs */ }
|
|
99
|
+
return names;
|
|
100
|
+
}, []);
|
|
101
|
+
|
|
67
102
|
const runtimeValue = useMemo(() => ({
|
|
68
103
|
useEntitiesQuery,
|
|
69
104
|
useModelSchemaQuery,
|
|
@@ -74,7 +109,9 @@ function AppContent() {
|
|
|
74
109
|
views: [],
|
|
75
110
|
spec: appSpec,
|
|
76
111
|
apiBaseUrl: '/api',
|
|
77
|
-
|
|
112
|
+
invalidateEntities,
|
|
113
|
+
resolveEntityNames,
|
|
114
|
+
}), [appSpec, invalidateEntities, resolveEntityNames]);
|
|
78
115
|
|
|
79
116
|
return (
|
|
80
117
|
<RuntimeViewProvider value={runtimeValue}>
|
package/dist/libs/instance-factories/applications/templates/react/use-api-hooks-generator.js
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
getModelSchema,
|
|
12
12
|
listEntities,
|
|
13
13
|
executeOperation,
|
|
14
|
+
executeServiceOperation,
|
|
14
15
|
transitionState
|
|
15
16
|
} from '../lib/apiClient';
|
|
16
17
|
import type { ModelSchema, Entity, ApiResponse } from '../types/api';
|
|
@@ -49,9 +50,14 @@ export function useEntitiesQuery(controllerName: string | null, modelName: strin
|
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
/**
|
|
52
|
-
* Mutation hook for executing operations
|
|
53
|
-
*
|
|
54
|
-
*
|
|
53
|
+
* Mutation hook for executing operations \u2014 handles both controllers
|
|
54
|
+
* and services. Pass either \`controllerName\` (for CRUD or custom
|
|
55
|
+
* controller actions) or \`serviceName\` (for RPC-style service ops).
|
|
56
|
+
*
|
|
57
|
+
* Matches RuntimeViewProviderValue.useExecuteOperationMutation contract.
|
|
58
|
+
* Service operations invalidate the \`services\` query key; controller
|
|
59
|
+
* operations invalidate the corresponding model's \`entities\` key so
|
|
60
|
+
* all mounted views refetch automatically.
|
|
55
61
|
*/
|
|
56
62
|
export function useExecuteOperationMutation() {
|
|
57
63
|
const queryClient = useQueryClient();
|
|
@@ -59,12 +65,14 @@ export function useExecuteOperationMutation() {
|
|
|
59
65
|
return useMutation({
|
|
60
66
|
mutationFn: ({
|
|
61
67
|
controllerName,
|
|
68
|
+
serviceName,
|
|
62
69
|
operationName,
|
|
63
70
|
data,
|
|
64
71
|
params,
|
|
65
72
|
entityId
|
|
66
73
|
}: {
|
|
67
|
-
controllerName
|
|
74
|
+
controllerName?: string;
|
|
75
|
+
serviceName?: string;
|
|
68
76
|
operationName: string;
|
|
69
77
|
data?: Record<string, any>;
|
|
70
78
|
params?: Record<string, any>;
|
|
@@ -73,11 +81,23 @@ export function useExecuteOperationMutation() {
|
|
|
73
81
|
// Merge: accept both 'data' and 'params' for compatibility
|
|
74
82
|
const mergedParams = { ...(data || params || {}) };
|
|
75
83
|
if (entityId) mergedParams.id = entityId;
|
|
76
|
-
|
|
84
|
+
if (serviceName) {
|
|
85
|
+
return executeServiceOperation(serviceName, operationName, mergedParams);
|
|
86
|
+
}
|
|
87
|
+
if (controllerName) {
|
|
88
|
+
return executeOperation(controllerName, operationName, mergedParams);
|
|
89
|
+
}
|
|
90
|
+
throw new Error('useExecuteOperationMutation: either controllerName or serviceName is required');
|
|
77
91
|
},
|
|
78
92
|
onSuccess: (_data, variables) => {
|
|
79
|
-
|
|
80
|
-
|
|
93
|
+
if (variables.controllerName) {
|
|
94
|
+
const modelName = variables.controllerName.replace(/Controller$/, '');
|
|
95
|
+
queryClient.invalidateQueries({ queryKey: ['entities', modelName] });
|
|
96
|
+
}
|
|
97
|
+
// Service operations aren't tied to a single entity type; no
|
|
98
|
+
// cache key to invalidate here. If a service mutation affects
|
|
99
|
+
// entities, the backend should publish an event that
|
|
100
|
+
// useEntitySync picks up via WebSocket.
|
|
81
101
|
}
|
|
82
102
|
});
|
|
83
103
|
}
|
package/dist/libs/instance-factories/scaffolding/templates/generic/package-json-generator.js
CHANGED
|
@@ -29,6 +29,13 @@ function generatePackageJson(context) {
|
|
|
29
29
|
"test": "npm test --workspaces",
|
|
30
30
|
"test:backend": `npm test --workspace=${backendDir}`,
|
|
31
31
|
"test:frontend": `npm test --workspace=${frontendDir}`,
|
|
32
|
+
// UI contract tests — auto-generated from spec inference rules.
|
|
33
|
+
// Assumes backend + frontend are already running (see docs/proposals/UI-CONTRACT-INFERENCE.md).
|
|
34
|
+
"test:contract": "playwright test --config=playwright.contract.config.ts",
|
|
35
|
+
// Implementation conformance — static grep-level checks that
|
|
36
|
+
// verify the runtime implementation has no escape hatches
|
|
37
|
+
// violating the design contract. Fast (millis), no browser.
|
|
38
|
+
"test:conformance": "node scripts/test-conformance.mjs",
|
|
32
39
|
// Database management (backend)
|
|
33
40
|
"db:setup": `npm run db:setup --workspace=${backendDir}`,
|
|
34
41
|
"db:generate": `npm run db:generate --workspace=${backendDir}`,
|
|
@@ -39,6 +46,9 @@ function generatePackageJson(context) {
|
|
|
39
46
|
"lint": "npm run lint --workspaces",
|
|
40
47
|
"lint:fix": "npm run lint:fix --workspaces"
|
|
41
48
|
};
|
|
49
|
+
pkg.devDependencies = {
|
|
50
|
+
"@playwright/test": "^1.40.0"
|
|
51
|
+
};
|
|
42
52
|
} else {
|
|
43
53
|
pkg.scripts = {
|
|
44
54
|
...aggregated.scripts,
|