@topogram/cli 0.3.51 → 0.3.52
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 +4 -4
- package/CHANGELOG.md +11 -11
- package/package.json +1 -1
- package/src/adoption/plan.js +2 -2
- package/src/agent-ops/query-builders.js +42 -33
- package/src/cli.js +174 -129
- package/src/generator/adapters.d.ts +1 -0
- package/src/generator/adapters.js +64 -39
- package/src/generator/check.js +19 -12
- package/src/generator/context/diff.js +9 -9
- package/src/generator/context/domain-coverage.js +11 -10
- package/src/generator/context/domain-page.js +6 -6
- package/src/generator/context/shared.js +37 -21
- package/src/generator/context/slice.js +70 -65
- package/src/generator/index.js +12 -12
- package/src/generator/output.js +21 -20
- package/src/generator/registry.js +61 -49
- package/src/generator/runtime/app-bundle.js +15 -15
- package/src/generator/runtime/compile-check.js +7 -7
- package/src/generator/runtime/deployment.js +9 -9
- package/src/generator/runtime/environment.js +39 -39
- package/src/generator/runtime/runtime-check.js +5 -5
- package/src/generator/runtime/shared.js +40 -38
- package/src/generator/runtime/smoke.js +5 -5
- package/src/generator/surfaces/databases/contract.js +1 -1
- package/src/generator/surfaces/databases/lifecycle-shared.js +6 -5
- package/src/generator/surfaces/databases/postgres/drizzle.js +3 -2
- package/src/generator/surfaces/databases/postgres/prisma.js +3 -2
- package/src/generator/surfaces/databases/shared.js +3 -2
- package/src/generator/surfaces/databases/snapshot.js +1 -1
- package/src/generator/surfaces/databases/sqlite/prisma.js +3 -2
- package/src/generator/surfaces/native/swiftui-app.js +3 -3
- package/src/generator/surfaces/native/swiftui-templates/Package.swift.txt +1 -1
- package/src/generator/surfaces/native/swiftui-templates/README.generated.md +3 -3
- package/src/generator/surfaces/native/swiftui-templates/runtime/DynamicScreens.swift +3 -3
- package/src/generator/surfaces/services/persistence-wiring.js +3 -2
- package/src/generator/surfaces/services/server-contract.js +4 -4
- package/src/generator/surfaces/shared.js +2 -2
- package/src/generator/surfaces/web/design-intent.js +1 -1
- package/src/generator/surfaces/web/index.js +7 -7
- package/src/generator/surfaces/web/{react-components.js → react-widgets.js} +53 -53
- package/src/generator/surfaces/web/react.js +36 -36
- package/src/generator/surfaces/web/{sveltekit-components.js → sveltekit-widgets.js} +53 -53
- package/src/generator/surfaces/web/sveltekit.js +34 -34
- package/src/generator/surfaces/web/{ui-web-contract.js → ui-surface-contract.js} +8 -8
- package/src/generator/surfaces/web/vanilla.js +6 -6
- package/src/generator/{component-conformance.js → widget-conformance.js} +129 -128
- package/src/generator/widgets.js +40 -0
- package/src/generator-policy.js +10 -12
- package/src/import/core/runner.js +34 -34
- package/src/import/core/shared.js +1 -1
- package/src/import/extractors/ui/android-compose.js +1 -1
- package/src/import/extractors/ui/blazor.js +1 -1
- package/src/import/extractors/ui/razor-pages.js +1 -1
- package/src/import/extractors/ui/react-router.js +4 -4
- package/src/import/extractors/ui/sveltekit.js +4 -4
- package/src/import/extractors/ui/swiftui.js +1 -1
- package/src/import/extractors/ui/uikit.js +1 -1
- package/src/new-project.js +19 -18
- package/src/project-config.js +92 -42
- package/src/proofs/contract-audit.js +1 -1
- package/src/proofs/ios-parity.js +1 -1
- package/src/proofs/issues-parity.js +1 -1
- package/src/realization/backend/build-backend-runtime-realization.js +2 -2
- package/src/realization/ui/build-ui-shared-realization.js +33 -33
- package/src/realization/ui/build-web-realization.js +23 -20
- package/src/reconcile/journeys.js +1 -1
- package/src/resolver/index.js +148 -65
- package/src/validator/index.js +473 -423
- package/src/validator/kinds.js +36 -36
- package/src/validator/per-kind/{component.js → widget.js} +47 -47
- package/src/{component-behavior.js → widget-behavior.js} +3 -3
- package/src/workflows.js +39 -38
- package/template-helpers/react.js +4 -4
- package/template-helpers/sveltekit.js +4 -4
- package/src/generator/components.js +0 -39
- /package/src/resolver/enrich/{component.js → widget.js} +0 -0
|
@@ -10,13 +10,13 @@ import { UI_GENERATOR_RENDERED_COMPONENT_PATTERNS } from "../ui/taxonomy.js";
|
|
|
10
10
|
* @property {string} id
|
|
11
11
|
* @property {string} version
|
|
12
12
|
* @property {"api"|"web"|"database"|"native"} surface
|
|
13
|
-
* @property {
|
|
14
|
-
* @property {string[]}
|
|
13
|
+
* @property {string[]} runtimeKinds
|
|
14
|
+
* @property {string[]} projectionTypes
|
|
15
15
|
* @property {string[]} inputs
|
|
16
16
|
* @property {string[]} outputs
|
|
17
17
|
* @property {Record<string, string>} stack
|
|
18
18
|
* @property {Record<string, boolean>} capabilities
|
|
19
|
-
* @property {{ patterns?: string[], behaviors?: string[], unsupported?: "error"|"warning"|"contract-only" }} [
|
|
19
|
+
* @property {{ patterns?: string[], behaviors?: string[], unsupported?: "error"|"warning"|"contract-only" }} [widgetSupport]
|
|
20
20
|
* @property {"bundled"|"package"} source
|
|
21
21
|
* @property {string} [profile]
|
|
22
22
|
* @property {string} [package]
|
|
@@ -32,8 +32,8 @@ export const GENERATOR_MANIFESTS = [
|
|
|
32
32
|
id: "topogram/hono",
|
|
33
33
|
version: "1",
|
|
34
34
|
surface: "api",
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
runtimeKinds: ["api_service"],
|
|
36
|
+
projectionTypes: ["api_contract"],
|
|
37
37
|
inputs: ["server-contract", "api-contracts"],
|
|
38
38
|
outputs: ["api-service"],
|
|
39
39
|
stack: { runtime: "node", framework: "hono", language: "typescript" },
|
|
@@ -45,8 +45,8 @@ export const GENERATOR_MANIFESTS = [
|
|
|
45
45
|
id: "topogram/express",
|
|
46
46
|
version: "1",
|
|
47
47
|
surface: "api",
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
runtimeKinds: ["api_service"],
|
|
49
|
+
projectionTypes: ["api_contract"],
|
|
50
50
|
inputs: ["server-contract", "api-contracts"],
|
|
51
51
|
outputs: ["api-service"],
|
|
52
52
|
stack: { runtime: "node", framework: "express", language: "typescript" },
|
|
@@ -58,13 +58,13 @@ export const GENERATOR_MANIFESTS = [
|
|
|
58
58
|
id: "topogram/vanilla-web",
|
|
59
59
|
version: "1",
|
|
60
60
|
surface: "web",
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
inputs: ["ui-
|
|
61
|
+
runtimeKinds: ["web_surface"],
|
|
62
|
+
projectionTypes: ["web_surface"],
|
|
63
|
+
inputs: ["ui-surface-contract"],
|
|
64
64
|
outputs: ["web-app", "generation-coverage"],
|
|
65
65
|
stack: { runtime: "browser", framework: "vanilla", language: "javascript" },
|
|
66
66
|
capabilities: { routes: true, components: false, coverage: true },
|
|
67
|
-
|
|
67
|
+
widgetSupport: { patterns: [], behaviors: [], unsupported: "contract-only" },
|
|
68
68
|
source: "bundled",
|
|
69
69
|
profile: "vanilla"
|
|
70
70
|
},
|
|
@@ -72,13 +72,13 @@ export const GENERATOR_MANIFESTS = [
|
|
|
72
72
|
id: "topogram/sveltekit",
|
|
73
73
|
version: "1",
|
|
74
74
|
surface: "web",
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
inputs: ["ui-
|
|
75
|
+
runtimeKinds: ["web_surface"],
|
|
76
|
+
projectionTypes: ["web_surface"],
|
|
77
|
+
inputs: ["ui-surface-contract", "api-contracts"],
|
|
78
78
|
outputs: ["web-app", "generation-coverage"],
|
|
79
79
|
stack: { runtime: "node", framework: "sveltekit", language: "typescript" },
|
|
80
80
|
capabilities: { routes: true, components: true, coverage: true },
|
|
81
|
-
|
|
81
|
+
widgetSupport: {
|
|
82
82
|
patterns: RENDERED_COMPONENT_PATTERNS,
|
|
83
83
|
behaviors: ["selection", "sorting", "filtering", "search", "pagination", "bulk_action", "optimistic_update"],
|
|
84
84
|
unsupported: "warning"
|
|
@@ -90,13 +90,13 @@ export const GENERATOR_MANIFESTS = [
|
|
|
90
90
|
id: "topogram/react",
|
|
91
91
|
version: "1",
|
|
92
92
|
surface: "web",
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
inputs: ["ui-
|
|
93
|
+
runtimeKinds: ["web_surface"],
|
|
94
|
+
projectionTypes: ["web_surface"],
|
|
95
|
+
inputs: ["ui-surface-contract", "api-contracts"],
|
|
96
96
|
outputs: ["web-app", "generation-coverage"],
|
|
97
97
|
stack: { runtime: "browser", framework: "react", language: "typescript" },
|
|
98
98
|
capabilities: { routes: true, components: true, coverage: true },
|
|
99
|
-
|
|
99
|
+
widgetSupport: {
|
|
100
100
|
patterns: RENDERED_COMPONENT_PATTERNS,
|
|
101
101
|
behaviors: ["selection", "sorting", "filtering", "search", "pagination", "bulk_action", "optimistic_update"],
|
|
102
102
|
unsupported: "warning"
|
|
@@ -108,13 +108,13 @@ export const GENERATOR_MANIFESTS = [
|
|
|
108
108
|
id: "topogram/swiftui",
|
|
109
109
|
version: "1",
|
|
110
110
|
surface: "native",
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
inputs: ["ui-
|
|
111
|
+
runtimeKinds: ["ios_surface"],
|
|
112
|
+
projectionTypes: ["ios_surface"],
|
|
113
|
+
inputs: ["ui-surface-contract", "api-contracts"],
|
|
114
114
|
outputs: ["native-app"],
|
|
115
115
|
stack: { platform: "ios", framework: "swiftui", language: "swift" },
|
|
116
116
|
capabilities: { routes: true, components: false, coverage: false },
|
|
117
|
-
|
|
117
|
+
widgetSupport: { patterns: [], behaviors: [], unsupported: "contract-only" },
|
|
118
118
|
source: "bundled",
|
|
119
119
|
profile: "swiftui"
|
|
120
120
|
},
|
|
@@ -122,8 +122,8 @@ export const GENERATOR_MANIFESTS = [
|
|
|
122
122
|
id: "topogram/postgres",
|
|
123
123
|
version: "1",
|
|
124
124
|
surface: "database",
|
|
125
|
-
|
|
126
|
-
|
|
125
|
+
runtimeKinds: ["database"],
|
|
126
|
+
projectionTypes: ["db_contract"],
|
|
127
127
|
inputs: ["db-contract", "db-lifecycle-plan"],
|
|
128
128
|
outputs: ["db-lifecycle-bundle", "sql-schema", "sql-migration", "prisma-schema", "drizzle-schema"],
|
|
129
129
|
stack: { database: "postgres", language: "sql" },
|
|
@@ -135,8 +135,8 @@ export const GENERATOR_MANIFESTS = [
|
|
|
135
135
|
id: "topogram/sqlite",
|
|
136
136
|
version: "1",
|
|
137
137
|
surface: "database",
|
|
138
|
-
|
|
139
|
-
|
|
138
|
+
runtimeKinds: ["database"],
|
|
139
|
+
projectionTypes: ["db_contract"],
|
|
140
140
|
inputs: ["db-contract", "db-lifecycle-plan"],
|
|
141
141
|
outputs: ["db-lifecycle-bundle", "sql-schema", "sql-migration", "prisma-schema"],
|
|
142
142
|
stack: { database: "sqlite", language: "sql" },
|
|
@@ -148,13 +148,13 @@ export const GENERATOR_MANIFESTS = [
|
|
|
148
148
|
id: "topogram/android-compose",
|
|
149
149
|
version: "1",
|
|
150
150
|
surface: "native",
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
inputs: ["ui-
|
|
151
|
+
runtimeKinds: ["android_surface"],
|
|
152
|
+
projectionTypes: ["android_surface"],
|
|
153
|
+
inputs: ["ui-surface-contract", "api-contracts"],
|
|
154
154
|
outputs: ["native-app"],
|
|
155
155
|
stack: { platform: "android", framework: "compose", language: "kotlin" },
|
|
156
156
|
capabilities: { routes: true, components: false, coverage: false },
|
|
157
|
-
|
|
157
|
+
widgetSupport: { patterns: [], behaviors: [], unsupported: "contract-only" },
|
|
158
158
|
source: "bundled",
|
|
159
159
|
profile: "compose",
|
|
160
160
|
planned: true
|
|
@@ -392,8 +392,17 @@ export function validateGeneratorManifest(manifest) {
|
|
|
392
392
|
if (!["api", "web", "database", "native"].includes(manifest.surface)) {
|
|
393
393
|
errors.push(`${label} surface must be api, web, database, or native`);
|
|
394
394
|
}
|
|
395
|
-
if (
|
|
396
|
-
errors.push(`${label}
|
|
395
|
+
if (manifest.targetKind != null) {
|
|
396
|
+
errors.push(`${label} targetKind was renamed to runtimeKinds`);
|
|
397
|
+
}
|
|
398
|
+
if (!isStringArray(manifest.runtimeKinds, true)) {
|
|
399
|
+
errors.push(`${label} runtimeKinds must be a non-empty string array`);
|
|
400
|
+
}
|
|
401
|
+
if (manifest["projectionPlatforms"] != null) {
|
|
402
|
+
errors.push(`${label} projectionPlatforms was renamed to projectionTypes`);
|
|
403
|
+
}
|
|
404
|
+
if (!isStringArray(manifest.projectionTypes, true)) {
|
|
405
|
+
errors.push(`${label} projectionTypes must be a non-empty string array`);
|
|
397
406
|
}
|
|
398
407
|
if (!isStringArray(manifest.inputs)) {
|
|
399
408
|
errors.push(`${label} inputs must be a string array`);
|
|
@@ -407,21 +416,24 @@ export function validateGeneratorManifest(manifest) {
|
|
|
407
416
|
if (!manifest.capabilities || typeof manifest.capabilities !== "object" || Array.isArray(manifest.capabilities)) {
|
|
408
417
|
errors.push(`${label} capabilities must be an object`);
|
|
409
418
|
}
|
|
410
|
-
if (manifest
|
|
411
|
-
|
|
412
|
-
|
|
419
|
+
if (manifest["componentSupport"] != null) {
|
|
420
|
+
errors.push(`${label} componentSupport was renamed to widgetSupport`);
|
|
421
|
+
}
|
|
422
|
+
if (manifest.widgetSupport != null) {
|
|
423
|
+
if (typeof manifest.widgetSupport !== "object" || Array.isArray(manifest.widgetSupport)) {
|
|
424
|
+
errors.push(`${label} widgetSupport must be an object when present`);
|
|
413
425
|
} else {
|
|
414
|
-
if (manifest.
|
|
415
|
-
errors.push(`${label}
|
|
426
|
+
if (manifest.widgetSupport.patterns != null && !isStringArray(manifest.widgetSupport.patterns)) {
|
|
427
|
+
errors.push(`${label} widgetSupport.patterns must be a string array`);
|
|
416
428
|
}
|
|
417
|
-
if (manifest.
|
|
418
|
-
errors.push(`${label}
|
|
429
|
+
if (manifest.widgetSupport.behaviors != null && !isStringArray(manifest.widgetSupport.behaviors)) {
|
|
430
|
+
errors.push(`${label} widgetSupport.behaviors must be a string array`);
|
|
419
431
|
}
|
|
420
432
|
if (
|
|
421
|
-
manifest.
|
|
422
|
-
!["error", "warning", "contract-only"].includes(manifest.
|
|
433
|
+
manifest.widgetSupport.unsupported != null &&
|
|
434
|
+
!["error", "warning", "contract-only"].includes(manifest.widgetSupport.unsupported)
|
|
423
435
|
) {
|
|
424
|
-
errors.push(`${label}
|
|
436
|
+
errors.push(`${label} widgetSupport.unsupported must be error, warning, or contract-only`);
|
|
425
437
|
}
|
|
426
438
|
}
|
|
427
439
|
}
|
|
@@ -466,20 +478,20 @@ export function isApiProjection(projection) {
|
|
|
466
478
|
*/
|
|
467
479
|
export function projectionCompatibilityKey(projection) {
|
|
468
480
|
if (isApiProjection(projection)) {
|
|
469
|
-
return "
|
|
481
|
+
return "api_contract";
|
|
470
482
|
}
|
|
471
|
-
return projection?.platform || "";
|
|
483
|
+
return projection?.type || projection?.platform || "";
|
|
472
484
|
}
|
|
473
485
|
|
|
474
486
|
/**
|
|
475
487
|
* @param {GeneratorManifest|null|undefined} manifest
|
|
476
|
-
* @param {string}
|
|
488
|
+
* @param {string} runtimeKind
|
|
477
489
|
* @param {Record<string, any>|null|undefined} projection
|
|
478
490
|
* @returns {boolean}
|
|
479
491
|
*/
|
|
480
|
-
export function isGeneratorCompatible(manifest,
|
|
481
|
-
if (!manifest || manifest.planned || manifest.
|
|
492
|
+
export function isGeneratorCompatible(manifest, runtimeKind, projection) {
|
|
493
|
+
if (!manifest || manifest.planned || !manifest.runtimeKinds.includes(runtimeKind)) {
|
|
482
494
|
return false;
|
|
483
495
|
}
|
|
484
|
-
return manifest.
|
|
496
|
+
return manifest.projectionTypes.includes(projectionCompatibilityKey(projection));
|
|
485
497
|
}
|
|
@@ -66,14 +66,14 @@ function buildAppBundlePlan(graph, options = {}) {
|
|
|
66
66
|
db: dbProjection?.id || null
|
|
67
67
|
},
|
|
68
68
|
topology: {
|
|
69
|
-
|
|
70
|
-
id:
|
|
71
|
-
|
|
72
|
-
projection:
|
|
73
|
-
generator:
|
|
74
|
-
port:
|
|
75
|
-
|
|
76
|
-
|
|
69
|
+
runtimes: topology.runtimes.map((runtime) => ({
|
|
70
|
+
id: runtime.id,
|
|
71
|
+
kind: runtime.kind,
|
|
72
|
+
projection: runtime.projection.id,
|
|
73
|
+
generator: runtime.generator,
|
|
74
|
+
port: runtime.port ?? null,
|
|
75
|
+
uses_api: runtime.api || null,
|
|
76
|
+
uses_database: runtime.database || null
|
|
77
77
|
}))
|
|
78
78
|
},
|
|
79
79
|
runtimeReference,
|
|
@@ -108,8 +108,8 @@ function renderAppBundleEnvExample(plan) {
|
|
|
108
108
|
const demoUserId = runtimeDemoUserId(plan.runtimeReference);
|
|
109
109
|
const databaseName = plan.runtimeReference.environment.databaseName || "topogram_app";
|
|
110
110
|
const topology = {
|
|
111
|
-
primaryApi: { port: plan.topology.
|
|
112
|
-
primaryWeb: { port: plan.topology.
|
|
111
|
+
primaryApi: { port: plan.topology.runtimes.find((runtime) => runtime.kind === "api_service")?.port },
|
|
112
|
+
primaryWeb: { port: plan.topology.runtimes.find((runtime) => runtime.kind === "web_surface")?.port }
|
|
113
113
|
};
|
|
114
114
|
const ports = runtimePorts(plan.runtimeReference, topology);
|
|
115
115
|
const urls = runtimeUrls(plan.runtimeReference, topology);
|
|
@@ -126,7 +126,7 @@ ${plan.runtimeReference.environment.envExample || ""}
|
|
|
126
126
|
# Smoke-test defaults
|
|
127
127
|
${plan.projections.api ? `TOPOGRAM_API_BASE_URL=${urls.api}\n` : ""}${plan.projections.ui ? `TOPOGRAM_WEB_BASE_URL=${urls.web}\n` : ""}`;
|
|
128
128
|
}
|
|
129
|
-
if (plan.projections.dbPlatform === "
|
|
129
|
+
if (plan.projections.dbPlatform === "db_contract") {
|
|
130
130
|
return `# App bundle defaults
|
|
131
131
|
TOPOGRAM_ENVIRONMENT_PROFILE=${plan.profiles.environment}
|
|
132
132
|
TOPOGRAM_DEPLOY_PROFILE=${plan.profiles.deployment}
|
|
@@ -173,8 +173,8 @@ TOPOGRAM_WEB_BASE_URL=${urls.web}
|
|
|
173
173
|
|
|
174
174
|
function renderAppBundleReadme(plan) {
|
|
175
175
|
const urls = runtimeUrls(plan.runtimeReference, {
|
|
176
|
-
primaryApi: { port: plan.topology.
|
|
177
|
-
primaryWeb: { port: plan.topology.
|
|
176
|
+
primaryApi: { port: plan.topology.runtimes.find((runtime) => runtime.kind === "api_service")?.port },
|
|
177
|
+
primaryWeb: { port: plan.topology.runtimes.find((runtime) => runtime.kind === "web_surface")?.port }
|
|
178
178
|
});
|
|
179
179
|
return `# ${plan.name}
|
|
180
180
|
|
|
@@ -311,8 +311,8 @@ bash "$SCRIPT_DIR/runtime-check.sh"
|
|
|
311
311
|
|
|
312
312
|
function renderAppBundleWaitForStackScript(plan) {
|
|
313
313
|
const topology = {
|
|
314
|
-
primaryApi: { port: plan.topology.
|
|
315
|
-
primaryWeb: { port: plan.topology.
|
|
314
|
+
primaryApi: { port: plan.topology.runtimes.find((runtime) => runtime.kind === "api_service")?.port },
|
|
315
|
+
primaryWeb: { port: plan.topology.runtimes.find((runtime) => runtime.kind === "web_surface")?.port }
|
|
316
316
|
};
|
|
317
317
|
const ports = runtimePorts(plan.runtimeReference, topology);
|
|
318
318
|
const urls = runtimeUrls(plan.runtimeReference, topology);
|
|
@@ -57,11 +57,11 @@ function buildCompileCheckPlan(graph, options = {}) {
|
|
|
57
57
|
db: dbProjection?.id || null
|
|
58
58
|
},
|
|
59
59
|
topology: {
|
|
60
|
-
|
|
61
|
-
id:
|
|
62
|
-
|
|
63
|
-
projection:
|
|
64
|
-
generator:
|
|
60
|
+
runtimes: topology.runtimes.map((runtime) => ({
|
|
61
|
+
id: runtime.id,
|
|
62
|
+
kind: runtime.kind,
|
|
63
|
+
projection: runtime.projection.id,
|
|
64
|
+
generator: runtime.generator
|
|
65
65
|
}))
|
|
66
66
|
},
|
|
67
67
|
checks: [...apiChecks, ...webChecks]
|
|
@@ -73,7 +73,7 @@ function renderCompileCheckEnvExample(graph, options = {}) {
|
|
|
73
73
|
const topology = resolveRuntimeTopology(graph, options);
|
|
74
74
|
const { dbProjection } = getDefaultEnvironmentProjections(graph, options);
|
|
75
75
|
const urls = runtimeUrls(runtimeReference, topology);
|
|
76
|
-
if (dbProjection?.platform === "
|
|
76
|
+
if (dbProjection?.platform === "db_contract") {
|
|
77
77
|
return `DATABASE_URL=./var/${runtimeReference.environment.databaseName || "topogram_app"}.sqlite
|
|
78
78
|
PUBLIC_TOPOGRAM_API_BASE_URL=${urls.api}
|
|
79
79
|
PUBLIC_TOPOGRAM_DEMO_USER_ID=${runtimeDemoUserId(runtimeReference)}
|
|
@@ -122,7 +122,7 @@ function renderCompileCheckScript(plan) {
|
|
|
122
122
|
""
|
|
123
123
|
];
|
|
124
124
|
if (plan.checks.length === 0) {
|
|
125
|
-
lines.push('echo "No API or web
|
|
125
|
+
lines.push('echo "No API or web runtimes are configured; compile check is a no-op."');
|
|
126
126
|
}
|
|
127
127
|
for (const check of plan.checks) {
|
|
128
128
|
const label = check.id.includes("web")
|
|
@@ -33,7 +33,7 @@ function buildDeploymentPlan(graph, options = {}) {
|
|
|
33
33
|
const profile = options.profileId || "fly_io";
|
|
34
34
|
const supportedProfiles = ["fly_io", "railway"];
|
|
35
35
|
const webProfile = manifestGeneratorProfile(topology.primaryWeb?.generator?.id, null) || projectionHintProfile(uiProjection, "sveltekit");
|
|
36
|
-
const databaseTarget = dbProjection.platform === "
|
|
36
|
+
const databaseTarget = dbProjection.platform === "db_contract"
|
|
37
37
|
? "sqlite_file"
|
|
38
38
|
: profile === "fly_io"
|
|
39
39
|
? "managed_postgres"
|
|
@@ -54,14 +54,14 @@ function buildDeploymentPlan(graph, options = {}) {
|
|
|
54
54
|
db: dbProjection.id
|
|
55
55
|
},
|
|
56
56
|
topology: {
|
|
57
|
-
|
|
58
|
-
id:
|
|
59
|
-
|
|
60
|
-
projection:
|
|
61
|
-
generator:
|
|
62
|
-
port:
|
|
63
|
-
|
|
64
|
-
|
|
57
|
+
runtimes: topology.runtimes.map((runtime) => ({
|
|
58
|
+
id: runtime.id,
|
|
59
|
+
kind: runtime.kind,
|
|
60
|
+
projection: runtime.projection.id,
|
|
61
|
+
generator: runtime.generator,
|
|
62
|
+
port: runtime.port ?? null,
|
|
63
|
+
uses_api: runtime.api || null,
|
|
64
|
+
uses_database: runtime.database || null
|
|
65
65
|
}))
|
|
66
66
|
},
|
|
67
67
|
directories: {
|
|
@@ -43,10 +43,10 @@ function buildEnvironmentPlan(graph, options = {}) {
|
|
|
43
43
|
const topology = resolveRuntimeTopology(graph, options);
|
|
44
44
|
const { apiProjection, uiProjection, dbProjection } = getDefaultEnvironmentProjections(graph, options);
|
|
45
45
|
const dbLifecycle = dbProjection ? generateDbLifecyclePlan(graph, { ...options, projectionId: dbProjection.id }) : null;
|
|
46
|
-
const profile = options.profileId || (dbProjection?.platform === "
|
|
46
|
+
const profile = options.profileId || (dbProjection?.platform === "db_contract" || !dbProjection ? "local_process" : "local_docker");
|
|
47
47
|
const usesDocker = profile === "local_docker";
|
|
48
48
|
const webProfile = manifestGeneratorProfile(topology.primaryWeb?.generator?.id, null) || projectionHintProfile(uiProjection, "sveltekit");
|
|
49
|
-
const isSqlite = dbProjection?.platform === "
|
|
49
|
+
const isSqlite = dbProjection?.platform === "db_contract";
|
|
50
50
|
const ports = runtimePorts(runtimeReference, topology);
|
|
51
51
|
|
|
52
52
|
return {
|
|
@@ -58,14 +58,14 @@ function buildEnvironmentPlan(graph, options = {}) {
|
|
|
58
58
|
profile
|
|
59
59
|
},
|
|
60
60
|
topology: {
|
|
61
|
-
|
|
62
|
-
id:
|
|
63
|
-
|
|
64
|
-
projection:
|
|
65
|
-
generator:
|
|
66
|
-
port:
|
|
67
|
-
|
|
68
|
-
|
|
61
|
+
runtimes: topology.runtimes.map((runtime) => ({
|
|
62
|
+
id: runtime.id,
|
|
63
|
+
kind: runtime.kind,
|
|
64
|
+
projection: runtime.projection.id,
|
|
65
|
+
generator: runtime.generator,
|
|
66
|
+
port: runtime.port ?? null,
|
|
67
|
+
uses_api: runtime.api || null,
|
|
68
|
+
uses_database: runtime.database || null
|
|
69
69
|
}))
|
|
70
70
|
},
|
|
71
71
|
projections: {
|
|
@@ -110,7 +110,7 @@ function buildEnvironmentPlan(graph, options = {}) {
|
|
|
110
110
|
db: topology.primaryDb ? topology.dbDir(topology.primaryDb) : null,
|
|
111
111
|
scripts: "scripts"
|
|
112
112
|
},
|
|
113
|
-
|
|
113
|
+
runtimes: {
|
|
114
114
|
apis: topology.apiComponents.map((component) => ({
|
|
115
115
|
id: component.id,
|
|
116
116
|
projection: component.projection.id,
|
|
@@ -165,11 +165,11 @@ function renderEnvironmentEnvExample(plan) {
|
|
|
165
165
|
primaryApi: { port: plan.ports.server },
|
|
166
166
|
primaryWeb: { port: plan.ports.web }
|
|
167
167
|
});
|
|
168
|
-
const extraDatabaseLines = plan.
|
|
169
|
-
.filter((component) => component.id !== plan.
|
|
168
|
+
const extraDatabaseLines = plan.runtimes.databases
|
|
169
|
+
.filter((component) => component.id !== plan.runtimes.databases[0]?.id)
|
|
170
170
|
.map((component) => {
|
|
171
171
|
const fallbackName = `${databaseName}_${component.id}`;
|
|
172
|
-
if (component.platform === "
|
|
172
|
+
if (component.platform === "db_contract") {
|
|
173
173
|
return `${component.env.databaseUrl}=file:./var/${component.id}.sqlite`;
|
|
174
174
|
}
|
|
175
175
|
return [
|
|
@@ -185,7 +185,7 @@ function renderEnvironmentEnvExample(plan) {
|
|
|
185
185
|
TOPOGRAM_ENVIRONMENT_PROFILE=${plan.environment.profile}
|
|
186
186
|
|
|
187
187
|
# Local stack ports
|
|
188
|
-
${plan.
|
|
188
|
+
${plan.runtimes.apis.length ? `SERVER_PORT=${plan.ports.server}\n` : ""}${plan.runtimes.webs.length ? `WEB_PORT=${plan.ports.web}\n` : ""}${plan.runtimes.webs.length && plan.runtimes.apis.length ? `PUBLIC_TOPOGRAM_API_BASE_URL=${urls.api}\n` : ""}${plan.runtimes.webs.length ? `TOPOGRAM_CORS_ORIGINS=${urls.web},http://127.0.0.1:${plan.ports.web}\n` : ""}PUBLIC_TOPOGRAM_DEMO_USER_ID=${demoUserId}
|
|
189
189
|
TOPOGRAM_DEMO_USER_ID=${demoUserId}
|
|
190
190
|
${plan.runtimeReference.environment.envExample || ""}
|
|
191
191
|
TOPOGRAM_SEED_DEMO=true
|
|
@@ -193,7 +193,7 @@ TOPOGRAM_SEED_DEMO=true
|
|
|
193
193
|
if (!plan.projections.db.platform) {
|
|
194
194
|
return commonLines;
|
|
195
195
|
}
|
|
196
|
-
if (plan.projections.db.platform === "
|
|
196
|
+
if (plan.projections.db.platform === "db_contract") {
|
|
197
197
|
return `# Environment profile
|
|
198
198
|
TOPOGRAM_ENVIRONMENT_PROFILE=${plan.environment.profile}
|
|
199
199
|
|
|
@@ -267,12 +267,12 @@ function apiDatabaseExportLines(component) {
|
|
|
267
267
|
}
|
|
268
268
|
|
|
269
269
|
function renderEnvironmentReadme(plan) {
|
|
270
|
-
const hasDb = plan.
|
|
271
|
-
const hasApi = plan.
|
|
272
|
-
const hasWeb = plan.
|
|
270
|
+
const hasDb = plan.runtimes.databases.length > 0;
|
|
271
|
+
const hasApi = plan.runtimes.apis.length > 0;
|
|
272
|
+
const hasWeb = plan.runtimes.webs.length > 0;
|
|
273
273
|
const localProcessNotes = !hasDb
|
|
274
274
|
? "- This bundle has no generated database surface."
|
|
275
|
-
: plan.projections.db.platform === "
|
|
275
|
+
: plan.projections.db.platform === "db_contract"
|
|
276
276
|
? "- SQLite is file-backed for this bundle; no separate DB server is required."
|
|
277
277
|
: `- Make sure the Postgres server is already running before \`${plan.commands.bootstrapDb}\`.\n- \`DATABASE_URL\` and \`DATABASE_ADMIN_URL\` should point at your local or managed Postgres instance.`;
|
|
278
278
|
const dockerSection = plan.orchestration.usesDocker
|
|
@@ -291,13 +291,13 @@ ${localProcessNotes}
|
|
|
291
291
|
|
|
292
292
|
This bundle packages the generated runtime into one local environment:
|
|
293
293
|
|
|
294
|
-
${hasApi ? "- `services/<api-id>/`: generated API service scaffolds\n" : ""}${hasWeb ? `- \`web/<web-id>/\`: generated ${plan.runtimeProfiles.web === "react" ? "Vite + React Router" : plan.runtimeProfiles.web === "vanilla" ? "vanilla HTML/CSS/JS" : "SvelteKit"} web scaffolds\n` : ""}${hasDb ? "- `db/<db-id>/`: generated DB lifecycle bundles\n" : ""}${plan.files.dockerCompose ? `- \`${plan.files.dockerCompose}\`: local Postgres container` : hasDb ? (plan.projections.db.platform === "
|
|
294
|
+
${hasApi ? "- `services/<api-id>/`: generated API service scaffolds\n" : ""}${hasWeb ? `- \`web/<web-id>/\`: generated ${plan.runtimeProfiles.web === "react" ? "Vite + React Router" : plan.runtimeProfiles.web === "vanilla" ? "vanilla HTML/CSS/JS" : "SvelteKit"} web scaffolds\n` : ""}${hasDb ? "- `db/<db-id>/`: generated DB lifecycle bundles\n" : ""}${plan.files.dockerCompose ? `- \`${plan.files.dockerCompose}\`: local Postgres container` : hasDb ? (plan.projections.db.platform === "db_contract" ? "- local SQLite file orchestration (no Docker files generated)" : "- local-process Postgres orchestration (no Docker files generated)") : "- no DB orchestration is generated"}
|
|
295
295
|
|
|
296
296
|
## Quick Start
|
|
297
297
|
|
|
298
298
|
1. Copy \`.env.example\` to \`.env\` if you want to customize defaults
|
|
299
299
|
2. Start the database:
|
|
300
|
-
- ${!hasDb ? "not applicable" : plan.projections.db.platform === "
|
|
300
|
+
- ${!hasDb ? "not applicable" : plan.projections.db.platform === "db_contract" ? "no separate DB service is required" : plan.orchestration.usesDocker ? `\`${plan.commands.dockerDb}\`` : "use your local Postgres service"}
|
|
301
301
|
3. Bootstrap or migrate the database:
|
|
302
302
|
- \`${plan.commands.bootstrapDb}\`
|
|
303
303
|
4. Start the stack:
|
|
@@ -313,7 +313,7 @@ ${dockerSection}
|
|
|
313
313
|
|
|
314
314
|
## Notes
|
|
315
315
|
|
|
316
|
-
- ${hasApi && hasDb ? `The generated server expects ${plan.projections.db.platform === "
|
|
316
|
+
- ${hasApi && hasDb ? `The generated server expects ${plan.projections.db.platform === "db_contract" ? "SQLite plus Prisma." : "Postgres plus Prisma."}` : hasApi ? "The generated server is stateless." : "No server surface is generated."}
|
|
317
317
|
- ${hasWeb && hasApi ? "The generated web app talks to `PUBLIC_TOPOGRAM_API_BASE_URL`." : hasWeb ? "The generated web app is standalone." : "No web surface is generated."}
|
|
318
318
|
- If \`.env\` is missing, generated scripts fall back to \`.env.example\`.
|
|
319
319
|
- The DB lifecycle scripts remain the source of truth for greenfield bootstrap and brownfield migration.
|
|
@@ -325,9 +325,9 @@ function renderEnvironmentLoadEnvScript() {
|
|
|
325
325
|
}
|
|
326
326
|
|
|
327
327
|
function renderEnvironmentBootstrapDbScript(plan) {
|
|
328
|
-
const dbBootstrapLines = plan.
|
|
328
|
+
const dbBootstrapLines = plan.runtimes.databases.map((component) => {
|
|
329
329
|
const env = component.env;
|
|
330
|
-
const runtimeApi = plan.
|
|
330
|
+
const runtimeApi = plan.runtimes.apis.find((apiComponent) => apiComponent.database === component.id);
|
|
331
331
|
const assignments = [
|
|
332
332
|
`DATABASE_URL="\${${env.databaseUrl}:-}"`,
|
|
333
333
|
`DATABASE_ADMIN_URL="\${${env.databaseAdminUrl}:-}"`,
|
|
@@ -335,10 +335,10 @@ function renderEnvironmentBootstrapDbScript(plan) {
|
|
|
335
335
|
].filter(Boolean).join(" ");
|
|
336
336
|
return `(cd "$ROOT_DIR/${component.dir}" && TOPOGRAM_ENV_FILE=/dev/null ${assignments} bash ./scripts/db-bootstrap-or-migrate.sh)`;
|
|
337
337
|
});
|
|
338
|
-
const primaryApi = plan.
|
|
339
|
-
if (plan.
|
|
338
|
+
const primaryApi = plan.runtimes.apis[0];
|
|
339
|
+
if (plan.runtimes.databases.length === 0) {
|
|
340
340
|
return renderEnvAwareShellScript([
|
|
341
|
-
'echo "No database
|
|
341
|
+
'echo "No database runtimes are configured; skipping DB bootstrap."'
|
|
342
342
|
]);
|
|
343
343
|
}
|
|
344
344
|
return renderEnvAwareShellScript([
|
|
@@ -367,9 +367,9 @@ function componentScriptOptions() {
|
|
|
367
367
|
};
|
|
368
368
|
}
|
|
369
369
|
|
|
370
|
-
function renderEnvironmentServerDevScript(plan, component = plan.
|
|
370
|
+
function renderEnvironmentServerDevScript(plan, component = plan.runtimes.apis[0], options = {}) {
|
|
371
371
|
if (!component) {
|
|
372
|
-
return renderEnvAwareShellScript(['echo "No API
|
|
372
|
+
return renderEnvAwareShellScript(['echo "No API runtimes are configured."']);
|
|
373
373
|
}
|
|
374
374
|
const guardPortsScript = options.componentScript ? '"$ROOT_DIR/scripts/guard-ports.mjs"' : '"$SCRIPT_DIR/guard-ports.mjs"';
|
|
375
375
|
return renderEnvAwareShellScript([
|
|
@@ -386,11 +386,11 @@ function renderEnvironmentServerDevScript(plan, component = plan.components.apis
|
|
|
386
386
|
], options.componentScript ? componentScriptOptions() : {});
|
|
387
387
|
}
|
|
388
388
|
|
|
389
|
-
function renderEnvironmentWebDevScript(plan, component = plan.
|
|
389
|
+
function renderEnvironmentWebDevScript(plan, component = plan.runtimes.webs[0], options = {}) {
|
|
390
390
|
if (!component) {
|
|
391
|
-
return renderEnvAwareShellScript(['echo "No web
|
|
391
|
+
return renderEnvAwareShellScript(['echo "No web runtimes are configured."']);
|
|
392
392
|
}
|
|
393
|
-
const apiComponent = plan.
|
|
393
|
+
const apiComponent = plan.runtimes.apis.find((entry) => entry.id === component.api) || plan.runtimes.apis[0];
|
|
394
394
|
const guardPortsScript = options.componentScript ? '"$ROOT_DIR/scripts/guard-ports.mjs"' : '"$SCRIPT_DIR/guard-ports.mjs"';
|
|
395
395
|
return renderEnvAwareShellScript([
|
|
396
396
|
`node ${guardPortsScript} web`,
|
|
@@ -406,8 +406,8 @@ function renderEnvironmentWebDevScript(plan, component = plan.components.webs[0]
|
|
|
406
406
|
|
|
407
407
|
function renderEnvironmentStackDevScript(plan) {
|
|
408
408
|
const startLines = [
|
|
409
|
-
...plan.
|
|
410
|
-
...plan.
|
|
409
|
+
...plan.runtimes.apis.map((component) => `bash "$SCRIPT_DIR/services/${component.id}-dev.sh" &\nPIDS+=($!)`),
|
|
410
|
+
...plan.runtimes.webs.map((component) => `bash "$SCRIPT_DIR/web/${component.id}-dev.sh" &\nPIDS+=($!)`)
|
|
411
411
|
];
|
|
412
412
|
return `#!/usr/bin/env bash
|
|
413
413
|
set -euo pipefail
|
|
@@ -447,8 +447,8 @@ ${startLines.length ? "wait" : ""}
|
|
|
447
447
|
|
|
448
448
|
function renderEnvironmentGuardPortsScript(plan) {
|
|
449
449
|
const ports = [
|
|
450
|
-
...plan.
|
|
451
|
-
...plan.
|
|
450
|
+
...plan.runtimes.apis.map((component) => ({ id: component.id, type: "api", env: `${component.id.toUpperCase()}_PORT`, fallbackEnv: "SERVER_PORT", port: component.port })),
|
|
451
|
+
...plan.runtimes.webs.map((component) => ({ id: component.id, type: "web", env: `${component.id.toUpperCase()}_PORT`, fallbackEnv: "WEB_PORT", port: component.port }))
|
|
452
452
|
];
|
|
453
453
|
return `#!/usr/bin/env node
|
|
454
454
|
import net from "node:net";
|
|
@@ -596,10 +596,10 @@ export function generateEnvironmentBundle(graph, options = {}) {
|
|
|
596
596
|
"scripts/guard-ports.mjs": renderEnvironmentGuardPortsScript(plan)
|
|
597
597
|
};
|
|
598
598
|
|
|
599
|
-
for (const component of plan.
|
|
599
|
+
for (const component of plan.runtimes.apis) {
|
|
600
600
|
files[`scripts/services/${component.id}-dev.sh`] = renderEnvironmentServerDevScript(plan, component, { componentScript: true });
|
|
601
601
|
}
|
|
602
|
-
for (const component of plan.
|
|
602
|
+
for (const component of plan.runtimes.webs) {
|
|
603
603
|
files[`scripts/web/${component.id}-dev.sh`] = renderEnvironmentWebDevScript(plan, component, { componentScript: true });
|
|
604
604
|
}
|
|
605
605
|
|
|
@@ -38,11 +38,11 @@ function buildRuntimeCheckPlan(graph, options = {}) {
|
|
|
38
38
|
db: dbProjection.id
|
|
39
39
|
},
|
|
40
40
|
topology: {
|
|
41
|
-
|
|
42
|
-
id:
|
|
43
|
-
|
|
44
|
-
projection:
|
|
45
|
-
generator:
|
|
41
|
+
runtimes: topology.runtimes.map((runtime) => ({
|
|
42
|
+
id: runtime.id,
|
|
43
|
+
kind: runtime.kind,
|
|
44
|
+
projection: runtime.projection.id,
|
|
45
|
+
generator: runtime.generator
|
|
46
46
|
}))
|
|
47
47
|
},
|
|
48
48
|
...(verification ? { verification } : {}),
|