@topogram/cli 0.3.34
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/ARCHITECTURE.md +67 -0
- package/CHANGELOG.md +240 -0
- package/README.md +223 -0
- package/package.json +51 -0
- package/src/adoption/index.js +3 -0
- package/src/adoption/plan.js +702 -0
- package/src/adoption/reporting.js +464 -0
- package/src/adoption/review-groups.js +313 -0
- package/src/agent-ops/query-builders.js +5012 -0
- package/src/archive/archive.js +141 -0
- package/src/archive/compact.js +26 -0
- package/src/archive/jsonl.js +70 -0
- package/src/archive/resolver-bridge.js +82 -0
- package/src/archive/schema.js +87 -0
- package/src/archive/unarchive.js +108 -0
- package/src/catalog.js +752 -0
- package/src/cli/catalog-alias.js +166 -0
- package/src/cli.js +9738 -0
- package/src/component-behavior.js +173 -0
- package/src/example-implementation.js +91 -0
- package/src/format.js +19 -0
- package/src/generator/adapters.d.ts +4 -0
- package/src/generator/adapters.js +325 -0
- package/src/generator/api.d.ts +1 -0
- package/src/generator/api.js +1196 -0
- package/src/generator/check.js +355 -0
- package/src/generator/component-conformance.js +767 -0
- package/src/generator/components.js +39 -0
- package/src/generator/context/bundle.js +291 -0
- package/src/generator/context/diff.js +256 -0
- package/src/generator/context/digest.js +182 -0
- package/src/generator/context/domain-coverage.js +94 -0
- package/src/generator/context/domain-page.js +137 -0
- package/src/generator/context/index.js +42 -0
- package/src/generator/context/report.js +121 -0
- package/src/generator/context/shared.js +1397 -0
- package/src/generator/context/slice.js +703 -0
- package/src/generator/context/task-mode.js +466 -0
- package/src/generator/docs.js +327 -0
- package/src/generator/index.js +161 -0
- package/src/generator/native/parity-bundle.js +311 -0
- package/src/generator/output.js +300 -0
- package/src/generator/registry.js +482 -0
- package/src/generator/runtime/app-bundle.js +456 -0
- package/src/generator/runtime/bundle-shared.js +166 -0
- package/src/generator/runtime/compile-check.js +163 -0
- package/src/generator/runtime/deployment.js +287 -0
- package/src/generator/runtime/environment.js +635 -0
- package/src/generator/runtime/index.js +32 -0
- package/src/generator/runtime/runtime-check.js +554 -0
- package/src/generator/runtime/shared.js +515 -0
- package/src/generator/runtime/smoke.js +219 -0
- package/src/generator/schema.js +204 -0
- package/src/generator/sdlc/board.js +66 -0
- package/src/generator/sdlc/doc-page.js +53 -0
- package/src/generator/sdlc/index.js +23 -0
- package/src/generator/sdlc/release-notes.js +62 -0
- package/src/generator/sdlc/traceability-matrix.js +65 -0
- package/src/generator/shared.js +29 -0
- package/src/generator/surfaces/contracts.js +146 -0
- package/src/generator/surfaces/databases/contract.js +40 -0
- package/src/generator/surfaces/databases/index.js +84 -0
- package/src/generator/surfaces/databases/lifecycle-shared.d.ts +1 -0
- package/src/generator/surfaces/databases/lifecycle-shared.js +612 -0
- package/src/generator/surfaces/databases/migration-plan.js +281 -0
- package/src/generator/surfaces/databases/postgres/capabilities.js +14 -0
- package/src/generator/surfaces/databases/postgres/drizzle.js +99 -0
- package/src/generator/surfaces/databases/postgres/index.js +9 -0
- package/src/generator/surfaces/databases/postgres/lifecycle.js +16 -0
- package/src/generator/surfaces/databases/postgres/prisma.js +159 -0
- package/src/generator/surfaces/databases/postgres/sql-migration.js +102 -0
- package/src/generator/surfaces/databases/postgres/sql-schema.js +34 -0
- package/src/generator/surfaces/databases/shared.d.ts +1 -0
- package/src/generator/surfaces/databases/shared.js +350 -0
- package/src/generator/surfaces/databases/snapshot.js +96 -0
- package/src/generator/surfaces/databases/sqlite/capabilities.js +14 -0
- package/src/generator/surfaces/databases/sqlite/index.js +8 -0
- package/src/generator/surfaces/databases/sqlite/lifecycle.js +16 -0
- package/src/generator/surfaces/databases/sqlite/prisma.js +143 -0
- package/src/generator/surfaces/databases/sqlite/sql-migration.js +65 -0
- package/src/generator/surfaces/databases/sqlite/sql-schema.js +27 -0
- package/src/generator/surfaces/index.js +25 -0
- package/src/generator/surfaces/native/swiftui-app.js +38 -0
- package/src/generator/surfaces/native/swiftui-templates/Package.swift.txt +20 -0
- package/src/generator/surfaces/native/swiftui-templates/README.generated.md +26 -0
- package/src/generator/surfaces/native/swiftui-templates/runtime/DynamicScreens.swift +682 -0
- package/src/generator/surfaces/native/swiftui-templates/runtime/TodoAPIClient.swift +156 -0
- package/src/generator/surfaces/native/swiftui-templates/runtime/TodoSwiftUIApp.swift +44 -0
- package/src/generator/surfaces/native/swiftui-templates/runtime/Visibility.swift +183 -0
- package/src/generator/surfaces/services/express.d.ts +1 -0
- package/src/generator/surfaces/services/express.js +766 -0
- package/src/generator/surfaces/services/hono.d.ts +1 -0
- package/src/generator/surfaces/services/hono.js +204 -0
- package/src/generator/surfaces/services/index.js +42 -0
- package/src/generator/surfaces/services/persistence-wiring.js +240 -0
- package/src/generator/surfaces/services/runtime-helpers.js +631 -0
- package/src/generator/surfaces/services/server-contract.js +80 -0
- package/src/generator/surfaces/services/stateless.d.ts +1 -0
- package/src/generator/surfaces/services/stateless.js +97 -0
- package/src/generator/surfaces/shared.js +64 -0
- package/src/generator/surfaces/web/api-client.js +1 -0
- package/src/generator/surfaces/web/forms.js +1 -0
- package/src/generator/surfaces/web/index.d.ts +2 -0
- package/src/generator/surfaces/web/index.js +53 -0
- package/src/generator/surfaces/web/react-components.js +248 -0
- package/src/generator/surfaces/web/react.js +538 -0
- package/src/generator/surfaces/web/routes.js +1 -0
- package/src/generator/surfaces/web/screens.js +1 -0
- package/src/generator/surfaces/web/shared.js +369 -0
- package/src/generator/surfaces/web/sveltekit-actions.js +28 -0
- package/src/generator/surfaces/web/sveltekit-components.js +234 -0
- package/src/generator/surfaces/web/sveltekit.js +426 -0
- package/src/generator/surfaces/web/ui-web-contract.js +65 -0
- package/src/generator/surfaces/web/vanilla.js +239 -0
- package/src/generator/verification.js +84 -0
- package/src/generator.js +1 -0
- package/src/import/core/context.js +52 -0
- package/src/import/core/contracts.js +23 -0
- package/src/import/core/registry.js +81 -0
- package/src/import/core/runner.js +646 -0
- package/src/import/core/shared.js +910 -0
- package/src/import/enrichers/auth-session.js +18 -0
- package/src/import/enrichers/django-rest.js +226 -0
- package/src/import/enrichers/doc-linking.js +20 -0
- package/src/import/enrichers/rails-controllers.js +246 -0
- package/src/import/enrichers/rails-models.js +130 -0
- package/src/import/enrichers/workflow-target-state.js +10 -0
- package/src/import/extractors/api/aspnet-core.js +304 -0
- package/src/import/extractors/api/django-routes.js +318 -0
- package/src/import/extractors/api/express.js +154 -0
- package/src/import/extractors/api/fastify.js +371 -0
- package/src/import/extractors/api/flutter-dio.js +135 -0
- package/src/import/extractors/api/generic-route-fallback.js +90 -0
- package/src/import/extractors/api/graphql-code-first.js +565 -0
- package/src/import/extractors/api/graphql-sdl.js +309 -0
- package/src/import/extractors/api/jaxrs.js +303 -0
- package/src/import/extractors/api/micronaut.js +213 -0
- package/src/import/extractors/api/next-route.js +50 -0
- package/src/import/extractors/api/next-server-action.js +51 -0
- package/src/import/extractors/api/nextauth.js +52 -0
- package/src/import/extractors/api/openapi-code.js +242 -0
- package/src/import/extractors/api/openapi.js +232 -0
- package/src/import/extractors/api/rails-routes.js +230 -0
- package/src/import/extractors/api/react-native-repository.js +128 -0
- package/src/import/extractors/api/retrofit.js +103 -0
- package/src/import/extractors/api/spring-web.js +372 -0
- package/src/import/extractors/api/swift-webapi.js +116 -0
- package/src/import/extractors/api/trpc.js +212 -0
- package/src/import/extractors/db/django-models.js +232 -0
- package/src/import/extractors/db/dotnet-models.js +93 -0
- package/src/import/extractors/db/drizzle.js +242 -0
- package/src/import/extractors/db/ef-core.js +221 -0
- package/src/import/extractors/db/flutter-entities.js +120 -0
- package/src/import/extractors/db/jpa.js +120 -0
- package/src/import/extractors/db/liquibase.js +180 -0
- package/src/import/extractors/db/mybatis-xml.js +145 -0
- package/src/import/extractors/db/prisma.js +185 -0
- package/src/import/extractors/db/rails-schema.js +175 -0
- package/src/import/extractors/db/react-native-entities.js +95 -0
- package/src/import/extractors/db/room.js +193 -0
- package/src/import/extractors/db/snapshot.js +112 -0
- package/src/import/extractors/db/sql.js +180 -0
- package/src/import/extractors/db/swiftdata.js +137 -0
- package/src/import/extractors/ui/android-compose.js +230 -0
- package/src/import/extractors/ui/backend-only.js +70 -0
- package/src/import/extractors/ui/blazor.js +227 -0
- package/src/import/extractors/ui/flutter-screens.js +152 -0
- package/src/import/extractors/ui/maui-xaml.js +135 -0
- package/src/import/extractors/ui/next-app-router.js +83 -0
- package/src/import/extractors/ui/next-pages-router.js +141 -0
- package/src/import/extractors/ui/razor-pages.js +181 -0
- package/src/import/extractors/ui/react-native-screens.js +166 -0
- package/src/import/extractors/ui/react-router.js +139 -0
- package/src/import/extractors/ui/sveltekit.js +123 -0
- package/src/import/extractors/ui/swiftui.js +193 -0
- package/src/import/extractors/ui/uikit.js +175 -0
- package/src/import/extractors/verification/generic.js +290 -0
- package/src/import/extractors/workflows/generic.js +137 -0
- package/src/import/index.js +7 -0
- package/src/import/provenance.js +158 -0
- package/src/new-project.js +2107 -0
- package/src/parser.js +439 -0
- package/src/policy/review-boundaries.js +165 -0
- package/src/project-config.js +535 -0
- package/src/proofs/backend-parity.js +19 -0
- package/src/proofs/contract-audit.js +220 -0
- package/src/proofs/ios-parity.js +7 -0
- package/src/proofs/issues-parity.js +10 -0
- package/src/proofs/web-parity.js +50 -0
- package/src/realization/api/build-api-realization.js +5 -0
- package/src/realization/api/index.js +1 -0
- package/src/realization/backend/build-backend-runtime-realization.js +82 -0
- package/src/realization/backend/index.d.ts +1 -0
- package/src/realization/backend/index.js +4 -0
- package/src/realization/db/build-db-realization.js +17 -0
- package/src/realization/db/index.js +3 -0
- package/src/realization/db/migration-plan.js +5 -0
- package/src/realization/db/snapshot.js +5 -0
- package/src/realization/ui/build-ui-shared-realization.js +305 -0
- package/src/realization/ui/build-web-realization.js +189 -0
- package/src/realization/ui/index.js +2 -0
- package/src/reconcile/docs.js +280 -0
- package/src/reconcile/index.js +3 -0
- package/src/reconcile/journeys.js +441 -0
- package/src/resolver/docs.js +1 -0
- package/src/resolver/enrich/acceptance-criterion.js +14 -0
- package/src/resolver/enrich/bug.js +12 -0
- package/src/resolver/enrich/component.js +2 -0
- package/src/resolver/enrich/index.js +1 -0
- package/src/resolver/enrich/pitch.js +18 -0
- package/src/resolver/enrich/requirement.js +20 -0
- package/src/resolver/enrich/task.js +16 -0
- package/src/resolver/expressions.js +1 -0
- package/src/resolver/index.js +2422 -0
- package/src/resolver/normalize.js +1 -0
- package/src/resolver.js +1 -0
- package/src/sdlc/adopt.js +65 -0
- package/src/sdlc/check.js +86 -0
- package/src/sdlc/dod/acceptance-criterion.js +22 -0
- package/src/sdlc/dod/bug.js +26 -0
- package/src/sdlc/dod/document.js +23 -0
- package/src/sdlc/dod/index.js +25 -0
- package/src/sdlc/dod/pitch.js +23 -0
- package/src/sdlc/dod/requirement.js +34 -0
- package/src/sdlc/dod/task.js +39 -0
- package/src/sdlc/explain.js +116 -0
- package/src/sdlc/history.js +80 -0
- package/src/sdlc/paths.js +11 -0
- package/src/sdlc/release.js +106 -0
- package/src/sdlc/scaffold.js +89 -0
- package/src/sdlc/status-filter.js +54 -0
- package/src/sdlc/transition.js +112 -0
- package/src/sdlc/transitions/acceptance-criterion.js +28 -0
- package/src/sdlc/transitions/bug.js +31 -0
- package/src/sdlc/transitions/document.js +29 -0
- package/src/sdlc/transitions/index.js +56 -0
- package/src/sdlc/transitions/pitch.js +34 -0
- package/src/sdlc/transitions/requirement.js +31 -0
- package/src/sdlc/transitions/task.js +34 -0
- package/src/template-trust.js +597 -0
- package/src/validator/expressions.js +1 -0
- package/src/validator/index.js +3424 -0
- package/src/validator/kinds.js +346 -0
- package/src/validator/per-kind/acceptance-criterion.js +91 -0
- package/src/validator/per-kind/bug.js +77 -0
- package/src/validator/per-kind/component.js +274 -0
- package/src/validator/per-kind/domain.js +205 -0
- package/src/validator/per-kind/pitch.js +101 -0
- package/src/validator/per-kind/requirement.js +75 -0
- package/src/validator/per-kind/task.js +96 -0
- package/src/validator/registry.js +1 -0
- package/src/validator/utils.js +12 -0
- package/src/validator.js +1 -0
- package/src/workflows.js +7597 -0
- package/src/workspace-docs.js +265 -0
- package/template-helpers/react.js +5 -0
- package/template-helpers/sveltekit.js +5 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
function asArray(value) {
|
|
2
|
+
if (Array.isArray(value)) {
|
|
3
|
+
return value.filter(Boolean);
|
|
4
|
+
}
|
|
5
|
+
return value == null ? [] : [value];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function propByName(contract) {
|
|
9
|
+
return new Map((contract?.props || []).map((prop) => [prop.name, prop]));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function bindingSourceForProp(usage, propName) {
|
|
13
|
+
return (usage?.dataBindings || []).find((binding) => binding.prop === propName)?.source || null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function effectFromEventBinding(binding) {
|
|
17
|
+
if (binding.action === "navigate") {
|
|
18
|
+
return {
|
|
19
|
+
type: "navigation",
|
|
20
|
+
event: binding.event || null,
|
|
21
|
+
target: binding.target || null
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
if (binding.action === "action") {
|
|
25
|
+
return {
|
|
26
|
+
type: "command",
|
|
27
|
+
event: binding.event || null,
|
|
28
|
+
capability: binding.target || null
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
type: "unknown",
|
|
33
|
+
event: binding.event || null,
|
|
34
|
+
action: binding.action || null,
|
|
35
|
+
target: binding.target || null
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function uniqueEffects(effects) {
|
|
40
|
+
const seen = new Set();
|
|
41
|
+
const output = [];
|
|
42
|
+
for (const effect of effects) {
|
|
43
|
+
const key = JSON.stringify(effect);
|
|
44
|
+
if (seen.has(key)) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
seen.add(key);
|
|
48
|
+
output.push(effect);
|
|
49
|
+
}
|
|
50
|
+
return output;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function eventRealizations(usage, eventNames) {
|
|
54
|
+
const bindings = usage?.eventBindings || [];
|
|
55
|
+
return eventNames.map((eventName) => {
|
|
56
|
+
const matchingBindings = bindings.filter((binding) => binding.event === eventName);
|
|
57
|
+
return {
|
|
58
|
+
event: eventName,
|
|
59
|
+
bound: matchingBindings.length > 0,
|
|
60
|
+
bindings: matchingBindings.map((binding) => ({
|
|
61
|
+
event: binding.event || null,
|
|
62
|
+
action: binding.action || null,
|
|
63
|
+
target: binding.target || null
|
|
64
|
+
})),
|
|
65
|
+
effects: matchingBindings.map(effectFromEventBinding)
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function capabilityActionRealization(usage, capabilityId) {
|
|
71
|
+
const bindings = (usage?.eventBindings || [])
|
|
72
|
+
.filter((binding) =>
|
|
73
|
+
binding.action === "action" &&
|
|
74
|
+
binding.target?.kind === "capability" &&
|
|
75
|
+
binding.target?.id === capabilityId
|
|
76
|
+
);
|
|
77
|
+
const capability = {
|
|
78
|
+
id: capabilityId,
|
|
79
|
+
kind: "capability"
|
|
80
|
+
};
|
|
81
|
+
return {
|
|
82
|
+
event: null,
|
|
83
|
+
capability,
|
|
84
|
+
bound: bindings.length > 0,
|
|
85
|
+
bindings: bindings.map((binding) => ({
|
|
86
|
+
event: binding.event || null,
|
|
87
|
+
action: binding.action || null,
|
|
88
|
+
target: binding.target || null
|
|
89
|
+
})),
|
|
90
|
+
effects: bindings.length > 0
|
|
91
|
+
? bindings.map(effectFromEventBinding)
|
|
92
|
+
: [{
|
|
93
|
+
type: "command",
|
|
94
|
+
event: null,
|
|
95
|
+
capability,
|
|
96
|
+
source: "behavior"
|
|
97
|
+
}]
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function actionRealizations(usage, actionNames, eventNames) {
|
|
102
|
+
return actionNames.map((actionName) => {
|
|
103
|
+
if (eventNames.has(actionName)) {
|
|
104
|
+
return eventRealizations(usage, [actionName])[0];
|
|
105
|
+
}
|
|
106
|
+
return capabilityActionRealization(usage, actionName);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function behaviorStatus({ hasDirectives, state, emits }) {
|
|
111
|
+
const hasMissingEventBinding = emits.some((entry) => !entry.bound);
|
|
112
|
+
if (hasMissingEventBinding) {
|
|
113
|
+
return "partial";
|
|
114
|
+
}
|
|
115
|
+
if (state?.requiredness === "required" && !state.bound) {
|
|
116
|
+
return "partial";
|
|
117
|
+
}
|
|
118
|
+
return hasDirectives ? "realized" : "declared";
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Build projection-specific realizations for component behavior contracts.
|
|
123
|
+
*
|
|
124
|
+
* Components declare reusable behavior capabilities; projection ui_components
|
|
125
|
+
* bindings provide concrete data/event outcomes. This derived contract is the
|
|
126
|
+
* normalized bridge agents and generators can use without inferring behavior
|
|
127
|
+
* from stack code.
|
|
128
|
+
*
|
|
129
|
+
* @param {Record<string, any>|null} contract
|
|
130
|
+
* @param {Record<string, any>} usage
|
|
131
|
+
* @returns {Array<Record<string, any>>}
|
|
132
|
+
*/
|
|
133
|
+
export function buildComponentBehaviorRealizations(contract, usage) {
|
|
134
|
+
const props = propByName(contract);
|
|
135
|
+
const eventNames = new Set((contract?.events || []).map((event) => event.id).filter(Boolean));
|
|
136
|
+
return (contract?.behaviors || []).map((behavior) => {
|
|
137
|
+
const directives = behavior.directives || {};
|
|
138
|
+
const statePropName = directives.state || null;
|
|
139
|
+
const stateProp = statePropName ? props.get(statePropName) || null : null;
|
|
140
|
+
const state = statePropName
|
|
141
|
+
? {
|
|
142
|
+
prop: statePropName,
|
|
143
|
+
requiredness: stateProp?.requiredness || null,
|
|
144
|
+
bound: Boolean(bindingSourceForProp(usage, statePropName)),
|
|
145
|
+
source: bindingSourceForProp(usage, statePropName),
|
|
146
|
+
defaultValue: stateProp?.defaultValue ?? null
|
|
147
|
+
}
|
|
148
|
+
: null;
|
|
149
|
+
const emits = eventRealizations(usage, asArray(directives.emits));
|
|
150
|
+
const actions = actionRealizations(usage, [
|
|
151
|
+
...asArray(directives.actions),
|
|
152
|
+
...asArray(directives.submit)
|
|
153
|
+
], eventNames);
|
|
154
|
+
const dataDependencies = (usage?.dataBindings || []).map((binding) => ({
|
|
155
|
+
prop: binding.prop || null,
|
|
156
|
+
source: binding.source || null
|
|
157
|
+
}));
|
|
158
|
+
const effects = uniqueEffects([...emits, ...actions].flatMap((entry) => entry.effects));
|
|
159
|
+
const hasDirectives = Object.keys(directives).length > 0;
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
kind: behavior.kind || null,
|
|
163
|
+
source: behavior.source || null,
|
|
164
|
+
directives: { ...directives },
|
|
165
|
+
state,
|
|
166
|
+
emits,
|
|
167
|
+
actions,
|
|
168
|
+
dataDependencies,
|
|
169
|
+
effects,
|
|
170
|
+
status: behaviorStatus({ hasDirectives, state, emits: [...emits, ...actions] })
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
findLegacyImplementationConfig,
|
|
7
|
+
findProjectConfig
|
|
8
|
+
} from "./project-config.js";
|
|
9
|
+
import { assertTrustedImplementation } from "./template-trust.js";
|
|
10
|
+
|
|
11
|
+
function normalizeRoot(root) {
|
|
12
|
+
return String(root || "").replace(/\\/g, "/");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function normalizeSearchRoot(root) {
|
|
16
|
+
if (!root) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
const absolute = path.resolve(root);
|
|
20
|
+
try {
|
|
21
|
+
return fs.realpathSync(absolute);
|
|
22
|
+
} catch {
|
|
23
|
+
return absolute;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function implementationProviderError(root) {
|
|
28
|
+
const suffix = root ? ` for ${normalizeRoot(root)}` : "";
|
|
29
|
+
return new Error(
|
|
30
|
+
`Topogram app/runtime generation requires an explicit implementation provider${suffix}. ` +
|
|
31
|
+
"Add topogram.project.json with implementation.module, or pass an implementation provider in generator options."
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function findImplementationConfig(root) {
|
|
36
|
+
const projectConfig = findProjectConfig(root);
|
|
37
|
+
if (projectConfig?.config?.implementation) {
|
|
38
|
+
return {
|
|
39
|
+
config: projectConfig.config.implementation,
|
|
40
|
+
configPath: projectConfig.configPath,
|
|
41
|
+
configDir: projectConfig.configDir
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
return findLegacyImplementationConfig(root);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function getExampleImplementation(graph, options = {}) {
|
|
48
|
+
const implementation = options.implementation || options.implementationProvider || null;
|
|
49
|
+
if (implementation) {
|
|
50
|
+
return implementation;
|
|
51
|
+
}
|
|
52
|
+
throw implementationProviderError(graph?.root);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function loadImplementationProvider(root) {
|
|
56
|
+
const found = findImplementationConfig(root);
|
|
57
|
+
if (!found) {
|
|
58
|
+
throw implementationProviderError(root);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const { config, configPath, configDir } = found;
|
|
62
|
+
const projectConfig = findProjectConfig(root)?.config || null;
|
|
63
|
+
assertTrustedImplementation(found, projectConfig);
|
|
64
|
+
const implementationModule = config.implementation_module || config.module;
|
|
65
|
+
if (!implementationModule) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`Topogram implementation config ${normalizeRoot(configPath)} is missing implementation module.`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const modulePath = path.resolve(configDir, implementationModule);
|
|
72
|
+
const module = await import(pathToFileURL(modulePath).href);
|
|
73
|
+
const exportName = config.implementation_export || config.export || "default";
|
|
74
|
+
const implementation = module[exportName];
|
|
75
|
+
if (!implementation) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`Topogram implementation module ${normalizeRoot(modulePath)} does not export '${exportName}'.`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
if (
|
|
81
|
+
(config.implementation_id || config.id) &&
|
|
82
|
+
implementation.exampleId &&
|
|
83
|
+
implementation.exampleId !== (config.implementation_id || config.id)
|
|
84
|
+
) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`Topogram implementation config requested '${config.implementation_id || config.id}', ` +
|
|
87
|
+
`but provider exported '${implementation.exampleId}'.`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
return implementation;
|
|
91
|
+
}
|
package/src/format.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function stableStringify(value) {
|
|
2
|
+
return JSON.stringify(sortValue(value), null, 2);
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function sortValue(value) {
|
|
6
|
+
if (Array.isArray(value)) {
|
|
7
|
+
return value.map(sortValue);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (value && typeof value === "object") {
|
|
11
|
+
const sorted = {};
|
|
12
|
+
for (const key of Object.keys(value).sort()) {
|
|
13
|
+
sorted[key] = sortValue(value[key]);
|
|
14
|
+
}
|
|
15
|
+
return sorted;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export const BUNDLED_GENERATOR_ADAPTERS: any[];
|
|
2
|
+
export function getBundledGeneratorAdapter(generatorId: string): any;
|
|
3
|
+
export function resolveGeneratorForComponent(component: any, options?: { rootDir?: string | null; configDir?: string | null }): { manifest: any; adapter: any };
|
|
4
|
+
export function generateWithComponentGenerator(context: any): { files: Record<string, string>; artifacts?: any; diagnostics?: any[] };
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
|
|
6
|
+
import { generateApiContractGraph } from "./api.js";
|
|
7
|
+
import {
|
|
8
|
+
getGeneratorManifest,
|
|
9
|
+
packageGeneratorInstallHint,
|
|
10
|
+
resolveGeneratorManifestForBinding,
|
|
11
|
+
validateGeneratorManifest
|
|
12
|
+
} from "./registry.js";
|
|
13
|
+
import { generateDbContractGraph } from "./surfaces/databases/contract.js";
|
|
14
|
+
import { generateDbLifecyclePlan } from "./surfaces/databases/lifecycle-shared.js";
|
|
15
|
+
import {
|
|
16
|
+
generatePostgresDbLifecycleBundle
|
|
17
|
+
} from "./surfaces/databases/postgres/index.js";
|
|
18
|
+
import {
|
|
19
|
+
generateSqliteDbLifecycleBundle
|
|
20
|
+
} from "./surfaces/databases/sqlite/index.js";
|
|
21
|
+
import { generateSwiftUiApp } from "./surfaces/native/swiftui-app.js";
|
|
22
|
+
import { generateExpressServer } from "./surfaces/services/express.js";
|
|
23
|
+
import { generateHonoServer } from "./surfaces/services/hono.js";
|
|
24
|
+
import { generateServerContract } from "./surfaces/services/server-contract.js";
|
|
25
|
+
import { generateStatelessServer } from "./surfaces/services/stateless.js";
|
|
26
|
+
import { generateReactApp } from "./surfaces/web/react.js";
|
|
27
|
+
import { generateSvelteKitApp } from "./surfaces/web/sveltekit.js";
|
|
28
|
+
import { generateUiWebContract } from "./surfaces/web/ui-web-contract.js";
|
|
29
|
+
import { generateVanillaWebApp } from "./surfaces/web/vanilla.js";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @typedef {import("./registry.js").GeneratorManifest} GeneratorManifest
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @typedef {Object} GeneratorContext
|
|
37
|
+
* @property {Record<string, any>} graph
|
|
38
|
+
* @property {Record<string, any>} projection
|
|
39
|
+
* @property {Record<string, any>} component
|
|
40
|
+
* @property {Record<string, any>|null} [topology]
|
|
41
|
+
* @property {Record<string, any>} [contracts]
|
|
42
|
+
* @property {Record<string, any>|null} [implementation]
|
|
43
|
+
* @property {Record<string, any>} [options]
|
|
44
|
+
* @property {GeneratorManifest} [manifest]
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @typedef {Object} GeneratorResult
|
|
49
|
+
* @property {Record<string, string>} files
|
|
50
|
+
* @property {Record<string, any>} [artifacts]
|
|
51
|
+
* @property {Array<Record<string, any>>} [diagnostics]
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @typedef {Object} GeneratorAdapter
|
|
56
|
+
* @property {GeneratorManifest} manifest
|
|
57
|
+
* @property {(context: GeneratorContext) => GeneratorResult} generate
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @param {Record<string, string>} files
|
|
62
|
+
* @param {Record<string, any>} [artifacts]
|
|
63
|
+
* @returns {GeneratorResult}
|
|
64
|
+
*/
|
|
65
|
+
function fileResult(files, artifacts = {}) {
|
|
66
|
+
return { files, artifacts, diagnostics: [] };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @param {string} generatorId
|
|
71
|
+
* @returns {GeneratorManifest}
|
|
72
|
+
*/
|
|
73
|
+
function requiredManifest(generatorId) {
|
|
74
|
+
const manifest = getGeneratorManifest(generatorId);
|
|
75
|
+
if (!manifest) {
|
|
76
|
+
throw new Error(`Bundled generator '${generatorId}' is missing its manifest.`);
|
|
77
|
+
}
|
|
78
|
+
return manifest;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @param {GeneratorContext} context
|
|
83
|
+
* @returns {string}
|
|
84
|
+
*/
|
|
85
|
+
function projectionIdFor(context) {
|
|
86
|
+
return context.projection?.id || context.component?.projection?.id || context.options?.projectionId;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @param {GeneratorContext} context
|
|
91
|
+
* @param {string} profile
|
|
92
|
+
* @returns {Record<string, any>}
|
|
93
|
+
*/
|
|
94
|
+
function serverOptions(context, profile) {
|
|
95
|
+
const projectionId = projectionIdFor(context);
|
|
96
|
+
const dbProjectionId = context.component?.databaseComponent?.projection?.id || context.options?.dbProjectionId;
|
|
97
|
+
return {
|
|
98
|
+
...(context.options || {}),
|
|
99
|
+
projectionId,
|
|
100
|
+
dbProjectionId,
|
|
101
|
+
component: context.component,
|
|
102
|
+
topology: context.topology,
|
|
103
|
+
contracts: context.contracts,
|
|
104
|
+
implementation: context.implementation,
|
|
105
|
+
profile
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* @param {GeneratorContext} context
|
|
111
|
+
* @returns {Record<string, any>}
|
|
112
|
+
*/
|
|
113
|
+
function commonOptions(context) {
|
|
114
|
+
return {
|
|
115
|
+
...(context.options || {}),
|
|
116
|
+
projectionId: projectionIdFor(context),
|
|
117
|
+
component: context.component,
|
|
118
|
+
topology: context.topology,
|
|
119
|
+
contracts: context.contracts,
|
|
120
|
+
implementation: context.implementation
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* @param {GeneratorContext} context
|
|
126
|
+
* @returns {Record<string, any>}
|
|
127
|
+
*/
|
|
128
|
+
function buildContractsForContext(context) {
|
|
129
|
+
const projectionId = projectionIdFor(context);
|
|
130
|
+
if (context.component.type === "web") {
|
|
131
|
+
return {
|
|
132
|
+
uiWeb: generateUiWebContract(context.graph, { ...(context.options || {}), projectionId })
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
if (context.component.type === "api") {
|
|
136
|
+
return {
|
|
137
|
+
server: generateServerContract(context.graph, { ...(context.options || {}), projectionId }),
|
|
138
|
+
api: generateApiContractGraph(context.graph, {})
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
if (context.component.type === "database") {
|
|
142
|
+
return {
|
|
143
|
+
db: generateDbContractGraph(context.graph, { ...(context.options || {}), projectionId }),
|
|
144
|
+
lifecyclePlan: generateDbLifecyclePlan(context.graph, { ...(context.options || {}), projectionId })
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
if (context.component.type === "native") {
|
|
148
|
+
return {
|
|
149
|
+
uiWeb: generateUiWebContract(context.graph, { ...(context.options || {}), projectionId })
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
return {};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** @type {GeneratorAdapter[]} */
|
|
156
|
+
export const BUNDLED_GENERATOR_ADAPTERS = [
|
|
157
|
+
{
|
|
158
|
+
manifest: requiredManifest("topogram/hono"),
|
|
159
|
+
generate(context) {
|
|
160
|
+
if (context.component && !context.component.databaseComponent) {
|
|
161
|
+
return fileResult(generateStatelessServer(context.graph, serverOptions(context, "hono")));
|
|
162
|
+
}
|
|
163
|
+
return fileResult(generateHonoServer(context.graph, serverOptions(context, "hono")));
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
manifest: requiredManifest("topogram/express"),
|
|
168
|
+
generate(context) {
|
|
169
|
+
if (context.component && !context.component.databaseComponent) {
|
|
170
|
+
return fileResult(generateStatelessServer(context.graph, serverOptions(context, "express")));
|
|
171
|
+
}
|
|
172
|
+
return fileResult(generateExpressServer(context.graph, serverOptions(context, "express")));
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
manifest: requiredManifest("topogram/vanilla-web"),
|
|
177
|
+
generate(context) {
|
|
178
|
+
return fileResult(generateVanillaWebApp(context.graph, commonOptions(context)));
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
manifest: requiredManifest("topogram/sveltekit"),
|
|
183
|
+
generate(context) {
|
|
184
|
+
return fileResult(generateSvelteKitApp(context.graph, commonOptions(context)));
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
manifest: requiredManifest("topogram/react"),
|
|
189
|
+
generate(context) {
|
|
190
|
+
return fileResult(generateReactApp(context.graph, commonOptions(context)));
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
manifest: requiredManifest("topogram/swiftui"),
|
|
195
|
+
generate(context) {
|
|
196
|
+
return fileResult(generateSwiftUiApp(context.graph, commonOptions(context)));
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
manifest: requiredManifest("topogram/postgres"),
|
|
201
|
+
generate(context) {
|
|
202
|
+
return fileResult(generatePostgresDbLifecycleBundle(context.graph, commonOptions(context)));
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
manifest: requiredManifest("topogram/sqlite"),
|
|
207
|
+
generate(context) {
|
|
208
|
+
return fileResult(generateSqliteDbLifecycleBundle(context.graph, commonOptions(context)));
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
];
|
|
212
|
+
|
|
213
|
+
const ADAPTER_BY_ID = new Map(BUNDLED_GENERATOR_ADAPTERS.map((adapter) => [adapter.manifest.id, adapter]));
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* @param {string} generatorId
|
|
217
|
+
* @returns {GeneratorAdapter|null}
|
|
218
|
+
*/
|
|
219
|
+
export function getBundledGeneratorAdapter(generatorId) {
|
|
220
|
+
return ADAPTER_BY_ID.get(generatorId) || null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* @param {string|null|undefined} rootDir
|
|
225
|
+
* @returns {any}
|
|
226
|
+
*/
|
|
227
|
+
function requireFromProject(rootDir) {
|
|
228
|
+
return createRequire(path.join(rootDir || process.cwd(), "package.json"));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* @param {any} moduleValue
|
|
233
|
+
* @param {string|null|undefined} exportName
|
|
234
|
+
* @returns {any}
|
|
235
|
+
*/
|
|
236
|
+
function selectPackageExport(moduleValue, exportName) {
|
|
237
|
+
if (exportName) {
|
|
238
|
+
return moduleValue?.[exportName] || moduleValue?.default?.[exportName] || null;
|
|
239
|
+
}
|
|
240
|
+
return moduleValue?.default || moduleValue;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* @param {GeneratorManifest} manifest
|
|
245
|
+
* @param {Record<string, any>} component
|
|
246
|
+
* @param {{ rootDir?: string|null, configDir?: string|null }} [options]
|
|
247
|
+
* @returns {GeneratorAdapter}
|
|
248
|
+
*/
|
|
249
|
+
function loadPackageGeneratorAdapter(manifest, component, options = {}) {
|
|
250
|
+
const packageName = manifest.package || component?.generator?.package;
|
|
251
|
+
if (!packageName) {
|
|
252
|
+
throw new Error(`Component '${component?.id || "unknown"}' generator '${manifest.id}@${manifest.version}' is package-backed but does not declare a package.`);
|
|
253
|
+
}
|
|
254
|
+
const rootDir = options.configDir || options.rootDir || process.cwd();
|
|
255
|
+
let moduleValue;
|
|
256
|
+
try {
|
|
257
|
+
moduleValue = requireFromProject(rootDir)(packageName);
|
|
258
|
+
} catch (error) {
|
|
259
|
+
const installHint = packageGeneratorInstallHint(packageName);
|
|
260
|
+
throw new Error(`Component '${component?.id || "unknown"}' generator package '${packageName}' could not be loaded from '${rootDir}': ${error instanceof Error ? error.message : String(error)}${installHint ? `. ${installHint}` : ""}`);
|
|
261
|
+
}
|
|
262
|
+
const adapter = selectPackageExport(moduleValue, manifest.export);
|
|
263
|
+
if (!adapter || typeof adapter.generate !== "function") {
|
|
264
|
+
throw new Error(`Component '${component?.id || "unknown"}' generator package '${packageName}' must export an adapter with a generate(context) function.`);
|
|
265
|
+
}
|
|
266
|
+
const adapterManifest = adapter.manifest || manifest;
|
|
267
|
+
if (adapterManifest.id !== manifest.id || adapterManifest.version !== manifest.version) {
|
|
268
|
+
throw new Error(`Component '${component?.id || "unknown"}' generator package '${packageName}' adapter manifest '${adapterManifest.id}@${adapterManifest.version}' does not match '${manifest.id}@${manifest.version}'.`);
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
manifest,
|
|
272
|
+
generate(context) {
|
|
273
|
+
return adapter.generate(context);
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* @param {Record<string, any>} component
|
|
280
|
+
* @param {{ rootDir?: string|null, configDir?: string|null }} [options]
|
|
281
|
+
* @returns {{ manifest: GeneratorManifest, adapter: GeneratorAdapter }}
|
|
282
|
+
*/
|
|
283
|
+
export function resolveGeneratorForComponent(component, options = {}) {
|
|
284
|
+
const generatorId = component?.generator?.id;
|
|
285
|
+
const resolved = resolveGeneratorManifestForBinding(component?.generator, options);
|
|
286
|
+
const manifest = resolved.manifest || getGeneratorManifest(generatorId);
|
|
287
|
+
if (!manifest) {
|
|
288
|
+
const detail = resolved.errors.length > 0 ? ` ${resolved.errors.join(" ")}` : "";
|
|
289
|
+
throw new Error(`Component '${component?.id || "unknown"}' uses unknown generator '${generatorId || "unknown"}'.${detail}`);
|
|
290
|
+
}
|
|
291
|
+
if (manifest.planned) {
|
|
292
|
+
throw new Error(`Component '${component?.id || "unknown"}' uses planned generator '${manifest.id}', which is not implemented yet.`);
|
|
293
|
+
}
|
|
294
|
+
if (component.generator?.version !== manifest.version) {
|
|
295
|
+
throw new Error(`Component '${component?.id || "unknown"}' generator '${manifest.id}' version '${component.generator?.version}' is unsupported; expected '${manifest.version}'.`);
|
|
296
|
+
}
|
|
297
|
+
const manifestValidation = validateGeneratorManifest(manifest);
|
|
298
|
+
if (!manifestValidation.ok) {
|
|
299
|
+
throw new Error(manifestValidation.errors.join("\n"));
|
|
300
|
+
}
|
|
301
|
+
if (manifest.source === "package") {
|
|
302
|
+
return { manifest, adapter: loadPackageGeneratorAdapter(manifest, component, options) };
|
|
303
|
+
}
|
|
304
|
+
const adapter = getBundledGeneratorAdapter(manifest.id);
|
|
305
|
+
if (!adapter) {
|
|
306
|
+
const installHint = packageGeneratorInstallHint(component?.generator?.package || manifest.package);
|
|
307
|
+
throw new Error(`Component '${component?.id || "unknown"}' generator '${manifest.id}@${manifest.version}' is not available. Package-backed generators must be installed before generation.${installHint ? ` ${installHint}` : ""}`);
|
|
308
|
+
}
|
|
309
|
+
return { manifest, adapter };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* @param {Omit<GeneratorContext, "contracts"> & { contracts?: Record<string, any> }} context
|
|
314
|
+
* @returns {GeneratorResult}
|
|
315
|
+
*/
|
|
316
|
+
export function generateWithComponentGenerator(context) {
|
|
317
|
+
const { manifest, adapter } = resolveGeneratorForComponent(context.component, context.options || {});
|
|
318
|
+
const contracts = context.contracts || buildContractsForContext(context);
|
|
319
|
+
return adapter.generate({
|
|
320
|
+
...context,
|
|
321
|
+
projection: context.projection,
|
|
322
|
+
manifest,
|
|
323
|
+
contracts
|
|
324
|
+
});
|
|
325
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function generateApiContractGraph(graph: any, options?: any): any;
|