@topogram/cli 0.3.54 → 0.3.56
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/CHANGELOG.md +18 -0
- package/package.json +1 -1
- package/src/adoption/review-groups.js +3 -3
- package/src/agent-ops/query-builders.js +34 -34
- package/src/archive/schema.js +1 -1
- package/src/cli.js +173 -20
- package/src/generator/adapters.js +23 -7
- package/src/generator/context/domain-coverage.js +2 -2
- package/src/generator/context/shared.js +6 -22
- package/src/generator/context/slice.js +10 -10
- package/src/generator/docs.js +1 -1
- package/src/generator/registry.js +1 -1
- package/src/generator/runtime/app-bundle.js +8 -6
- package/src/generator/runtime/compile-check.js +7 -5
- package/src/generator/runtime/deployment.js +6 -4
- package/src/generator/runtime/environment.js +31 -28
- package/src/generator/runtime/shared.js +53 -39
- package/src/generator/surfaces/contracts.js +1 -1
- package/src/generator/surfaces/databases/index.js +5 -3
- package/src/generator/surfaces/databases/lifecycle-shared.js +1 -1
- package/src/generator/surfaces/databases/postgres/drizzle.js +1 -1
- package/src/generator/surfaces/databases/postgres/prisma.js +1 -1
- package/src/generator/surfaces/databases/shared.js +3 -3
- package/src/generator/surfaces/databases/sqlite/prisma.js +1 -1
- package/src/generator/surfaces/index.js +5 -3
- package/src/generator/surfaces/services/index.js +10 -6
- package/src/generator/surfaces/services/persistence-wiring.js +1 -1
- package/src/generator/surfaces/services/server-contract.js +1 -1
- package/src/generator/surfaces/shared.js +1 -1
- package/src/generator/surfaces/web/index.js +5 -3
- package/src/generator/surfaces/web/ui-surface-contract.js +1 -1
- package/src/generator/widget-conformance.js +6 -6
- package/src/generator/widgets.js +1 -1
- package/src/generator-policy.js +10 -10
- package/src/import/core/runner.js +60 -50
- package/src/project-config.js +5 -42
- package/src/proofs/contract-audit.js +1 -1
- package/src/realization/backend/build-backend-runtime-realization.js +3 -3
- package/src/realization/ui/build-ui-shared-realization.js +9 -9
- package/src/realization/ui/build-web-realization.js +3 -3
- package/src/reconcile/journeys.js +1 -1
- package/src/resolver/enrich/widget.js +2 -2
- package/src/resolver/index.js +4 -23
- package/src/validator/index.js +10 -10
- package/src/workflows.js +49 -49
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
function projectionTypesFromProjections(projections) {
|
|
13
13
|
const projectionTypes = new Set();
|
|
14
14
|
for (const projection of projections) {
|
|
15
|
-
const projectionType = projection?.type || projection?.
|
|
15
|
+
const projectionType = projection?.type || projection?.type;
|
|
16
16
|
if (projectionType) {
|
|
17
17
|
projectionTypes.add(projectionType);
|
|
18
18
|
}
|
|
@@ -53,7 +53,7 @@ export function generateDomainCoverage(graph, options = {}) {
|
|
|
53
53
|
for (const projectionType of projectionTypes) {
|
|
54
54
|
const realized = projectionStatements.some(
|
|
55
55
|
(projection) =>
|
|
56
|
-
(projection.type || projection.
|
|
56
|
+
(projection.type || projection.type) === projectionType &&
|
|
57
57
|
(projection.realizes || []).some((entry) => entry.id === capabilityId)
|
|
58
58
|
);
|
|
59
59
|
matrix[capabilityId][projectionType] = realized;
|
|
@@ -58,11 +58,11 @@ function summarizeProjection(projection) {
|
|
|
58
58
|
kind: projection.kind,
|
|
59
59
|
name: projection.name || projection.id,
|
|
60
60
|
description: projection.description || null,
|
|
61
|
-
|
|
61
|
+
type: projection.type || null,
|
|
62
62
|
realizes: refIds(projection.realizes),
|
|
63
63
|
outputs: stableSortedStrings(projection.outputs || []),
|
|
64
64
|
uiScreens: stableSortedStrings((projection.uiScreens || []).map((screen) => screen.id)),
|
|
65
|
-
|
|
65
|
+
widgetBindings: stableSortedStrings((projection.widgetBindings || []).map((entry) => entry.widget?.id).filter(Boolean)),
|
|
66
66
|
dbTables: stableSortedStrings((projection.dbTables || []).map((table) => table.table || table.entity?.id)),
|
|
67
67
|
httpCapabilities: stableSortedStrings((projection.http || []).map((entry) => entry.capability?.id)),
|
|
68
68
|
reviewBoundary: reviewBoundaryForProjectionPolicy(projection),
|
|
@@ -397,11 +397,7 @@ export function relatedProjectionsForShape(graph, shapeId) {
|
|
|
397
397
|
}
|
|
398
398
|
|
|
399
399
|
export function widgetById(graph, widgetId) {
|
|
400
|
-
return (graph?.byKind?.widget ||
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
export function componentById(graph, componentId) {
|
|
404
|
-
return widgetById(graph, componentId);
|
|
400
|
+
return (graph?.byKind?.widget || []).find((widget) => widget.id === widgetId) || null;
|
|
405
401
|
}
|
|
406
402
|
|
|
407
403
|
function projectionById(graph, projectionId) {
|
|
@@ -435,7 +431,7 @@ function downstreamProjectionIds(graph, projectionIds) {
|
|
|
435
431
|
}
|
|
436
432
|
|
|
437
433
|
export function relatedWidgetsForProjection(graph, projection) {
|
|
438
|
-
const directIds = (projection?.
|
|
434
|
+
const directIds = (projection?.widgetBindings || []).map((entry) => entry.widget?.id).filter(Boolean);
|
|
439
435
|
const inheritedIds = realizedProjectionIds(projection)
|
|
440
436
|
.map((projectionId) => projectionById(graph, projectionId))
|
|
441
437
|
.filter(Boolean)
|
|
@@ -443,10 +439,6 @@ export function relatedWidgetsForProjection(graph, projection) {
|
|
|
443
439
|
return stableSortedStrings([...directIds, ...inheritedIds]);
|
|
444
440
|
}
|
|
445
441
|
|
|
446
|
-
export function relatedComponentsForProjection(graph, projection) {
|
|
447
|
-
return relatedWidgetsForProjection(graph, projection);
|
|
448
|
-
}
|
|
449
|
-
|
|
450
442
|
export function relatedShapesForWidget(widget) {
|
|
451
443
|
if (!widget) return [];
|
|
452
444
|
const ids = [
|
|
@@ -461,15 +453,11 @@ export function relatedShapesForWidget(widget) {
|
|
|
461
453
|
return stableSortedStrings(ids);
|
|
462
454
|
}
|
|
463
455
|
|
|
464
|
-
export function relatedShapesForComponent(component) {
|
|
465
|
-
return relatedShapesForWidget(component);
|
|
466
|
-
}
|
|
467
|
-
|
|
468
456
|
export function relatedProjectionsForWidget(graph, widgetId) {
|
|
469
457
|
const widget = widgetById(graph, widgetId);
|
|
470
458
|
if (!widget) return [];
|
|
471
459
|
const explicitProjectionIds = stableSortedStrings((graph?.byKind?.projection || [])
|
|
472
|
-
.filter((projection) => (projection.
|
|
460
|
+
.filter((projection) => (projection.widgetBindings || []).some((entry) => entry.widget?.id === widgetId))
|
|
473
461
|
.map((projection) => projection.id));
|
|
474
462
|
const dependencyProjectionIds = stableSortedStrings((widget.dependencies || []).flatMap((dependency) => {
|
|
475
463
|
const kind = referenceKind(dependency);
|
|
@@ -499,10 +487,6 @@ export function relatedProjectionsForWidget(graph, widgetId) {
|
|
|
499
487
|
return downstreamProjectionIds(graph, stableSortedStrings([...explicitProjectionIds, ...dependencyProjectionIds, ...viaUiRegions]));
|
|
500
488
|
}
|
|
501
489
|
|
|
502
|
-
export function relatedProjectionsForComponent(graph, componentId) {
|
|
503
|
-
return relatedProjectionsForWidget(graph, componentId);
|
|
504
|
-
}
|
|
505
|
-
|
|
506
490
|
function referenceKind(reference) {
|
|
507
491
|
if (reference?.target?.kind) {
|
|
508
492
|
return reference.target.kind;
|
|
@@ -555,7 +539,7 @@ export function workspaceInventory(graph) {
|
|
|
555
539
|
journeys: stableSortedStrings((graph.docs || []).filter((doc) => doc.kind === "journey").map((doc) => doc.id)),
|
|
556
540
|
entities: stableSortedStrings((graph.byKind.entity || []).map((item) => item.id)),
|
|
557
541
|
projections: stableSortedStrings((graph.byKind.projection || []).map((item) => item.id)),
|
|
558
|
-
widgets: stableSortedStrings((graph.byKind.widget ||
|
|
542
|
+
widgets: stableSortedStrings((graph.byKind.widget || []).map((item) => item.id)),
|
|
559
543
|
verifications: stableSortedStrings((graph.byKind.verification || []).map((item) => item.id)),
|
|
560
544
|
domains: stableSortedStrings((graph.byKind.domain || []).map((item) => item.id)),
|
|
561
545
|
pitches: stableSortedStrings((graph.byKind.pitch || []).map((item) => item.id)),
|
|
@@ -337,7 +337,7 @@ function widgetDependencyKind(dependency) {
|
|
|
337
337
|
}
|
|
338
338
|
|
|
339
339
|
function uiAgentPacketForProjection(graph, projection) {
|
|
340
|
-
const projectionType = projection.type || projection.
|
|
340
|
+
const projectionType = projection.type || projection.type || null;
|
|
341
341
|
if (projectionType !== "ui_contract" && !String(projectionType || "").endsWith("_surface")) {
|
|
342
342
|
return null;
|
|
343
343
|
}
|
|
@@ -368,7 +368,7 @@ function uiAgentPacketForProjection(graph, projection) {
|
|
|
368
368
|
screenId: route.screenId,
|
|
369
369
|
path: route.path
|
|
370
370
|
})),
|
|
371
|
-
widgets: (ownerProjection.
|
|
371
|
+
widgets: (ownerProjection.widgetBindings || []).map((usage) => widgetUsagePacket(usage)),
|
|
372
372
|
designTokens: designIntentPacket(ownerProjection),
|
|
373
373
|
requiredGates: uiRequiredGates(projection.id)
|
|
374
374
|
};
|
|
@@ -378,9 +378,9 @@ function uiAgentPacketForWidget(graph, widget, projectionIds) {
|
|
|
378
378
|
const projectionSet = new Set(projectionIds);
|
|
379
379
|
const sourceUsages = [];
|
|
380
380
|
for (const projection of graph.byKind.projection || []) {
|
|
381
|
-
for (const usage of projection.
|
|
382
|
-
if (usage.
|
|
383
|
-
const projectionType = projection.type || projection.
|
|
381
|
+
for (const usage of projection.widgetBindings || []) {
|
|
382
|
+
if (usage.widget?.id !== widget.id) continue;
|
|
383
|
+
const projectionType = projection.type || projection.type || null;
|
|
384
384
|
sourceUsages.push({
|
|
385
385
|
projection: {
|
|
386
386
|
id: projection.id,
|
|
@@ -406,9 +406,9 @@ function uiAgentPacketForWidget(graph, widget, projectionIds) {
|
|
|
406
406
|
id: widget.id,
|
|
407
407
|
name: widget.name || widget.id,
|
|
408
408
|
category: widget.category || null,
|
|
409
|
-
patterns: widget.widgetContract?.patterns ||
|
|
410
|
-
regions: widget.widgetContract?.regions ||
|
|
411
|
-
behaviors: widget.widgetContract?.behaviors ||
|
|
409
|
+
patterns: widget.widgetContract?.patterns || [],
|
|
410
|
+
regions: widget.widgetContract?.regions || [],
|
|
411
|
+
behaviors: widget.widgetContract?.behaviors || []
|
|
412
412
|
},
|
|
413
413
|
sourceUsages,
|
|
414
414
|
inheritedBy: [...projectionSet]
|
|
@@ -421,7 +421,7 @@ function uiAgentPacketForWidget(graph, widget, projectionIds) {
|
|
|
421
421
|
function sharedUiProjectionFor(graph, projection) {
|
|
422
422
|
for (const reference of projection.realizes || []) {
|
|
423
423
|
const candidate = (graph.byKind.projection || []).find((entry) => entry.id === reference.id);
|
|
424
|
-
if ((candidate?.type || candidate?.
|
|
424
|
+
if ((candidate?.type || candidate?.type) === "ui_contract") {
|
|
425
425
|
return candidate;
|
|
426
426
|
}
|
|
427
427
|
}
|
|
@@ -432,7 +432,7 @@ function widgetUsagePacket(usage) {
|
|
|
432
432
|
return {
|
|
433
433
|
screenId: usage.screenId || null,
|
|
434
434
|
region: usage.region || null,
|
|
435
|
-
widgetId: usage.
|
|
435
|
+
widgetId: usage.widget?.id || null,
|
|
436
436
|
dataBindings: (usage.dataBindings || []).map((binding) => ({
|
|
437
437
|
prop: binding.prop || null,
|
|
438
438
|
source: binding.source?.id || binding.source || null
|
package/src/generator/docs.js
CHANGED
|
@@ -229,7 +229,7 @@ export function generateDocs(graph) {
|
|
|
229
229
|
lines.push(projection.description);
|
|
230
230
|
lines.push("");
|
|
231
231
|
}
|
|
232
|
-
lines.push(`
|
|
232
|
+
lines.push(`Projection type: \`${projection.type}\``);
|
|
233
233
|
lines.push(`Realizes: ${refList(projection.realizes)}`);
|
|
234
234
|
lines.push(`Outputs: ${symbolList(projection.outputs)}`);
|
|
235
235
|
lines.push("");
|
|
@@ -490,7 +490,7 @@ export function projectionCompatibilityKey(projection) {
|
|
|
490
490
|
if (isApiProjection(projection)) {
|
|
491
491
|
return "api_contract";
|
|
492
492
|
}
|
|
493
|
-
return projection?.type || projection?.
|
|
493
|
+
return projection?.type || projection?.type || "";
|
|
494
494
|
}
|
|
495
495
|
|
|
496
496
|
/**
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
} from "./shared.js";
|
|
24
24
|
import { getExampleImplementation } from "../../example-implementation.js";
|
|
25
25
|
import { mergeNamedBundles, renderLoadEnvScript, renderNestedBundleShellScript } from "./bundle-shared.js";
|
|
26
|
+
import { generateDbLifecyclePlan } from "../surfaces/databases/lifecycle-shared.js";
|
|
26
27
|
|
|
27
28
|
function runtimeReferenceFor(graph, options = {}) {
|
|
28
29
|
try {
|
|
@@ -47,6 +48,7 @@ function buildAppBundlePlan(graph, options = {}) {
|
|
|
47
48
|
const runtimeReference = runtimeReferenceFor(graph, options);
|
|
48
49
|
const topology = resolveRuntimeTopology(graph, options);
|
|
49
50
|
const { apiProjection, uiProjection, dbProjection } = getDefaultEnvironmentProjections(graph, options);
|
|
51
|
+
const dbLifecycle = dbProjection ? generateDbLifecyclePlan(graph, { ...options, projectionId: dbProjection.id }) : null;
|
|
50
52
|
const environmentProfile = options.profileId || "local_process";
|
|
51
53
|
const deployProfile = options.deployProfileId || "fly_io";
|
|
52
54
|
const smokeVerification = buildVerificationSummary(graph, ["smoke", "journey"]);
|
|
@@ -63,7 +65,9 @@ function buildAppBundlePlan(graph, options = {}) {
|
|
|
63
65
|
projections: {
|
|
64
66
|
api: apiProjection?.id || null,
|
|
65
67
|
ui: uiProjection?.id || null,
|
|
66
|
-
db: dbProjection?.id || null
|
|
68
|
+
db: dbProjection?.id || null,
|
|
69
|
+
dbType: dbProjection?.type || null,
|
|
70
|
+
dbEngine: dbLifecycle?.engine || null
|
|
67
71
|
},
|
|
68
72
|
topology: {
|
|
69
73
|
runtimes: topology.runtimes.map((runtime) => ({
|
|
@@ -113,7 +117,7 @@ function renderAppBundleEnvExample(plan) {
|
|
|
113
117
|
};
|
|
114
118
|
const ports = runtimePorts(plan.runtimeReference, topology);
|
|
115
119
|
const urls = runtimeUrls(plan.runtimeReference, topology);
|
|
116
|
-
if (!plan.projections.
|
|
120
|
+
if (!plan.projections.dbType) {
|
|
117
121
|
return `# App bundle defaults
|
|
118
122
|
TOPOGRAM_ENVIRONMENT_PROFILE=${plan.profiles.environment}
|
|
119
123
|
TOPOGRAM_DEPLOY_PROFILE=${plan.profiles.deployment}
|
|
@@ -126,7 +130,7 @@ ${plan.runtimeReference.environment.envExample || ""}
|
|
|
126
130
|
# Smoke-test defaults
|
|
127
131
|
${plan.projections.api ? `TOPOGRAM_API_BASE_URL=${urls.api}\n` : ""}${plan.projections.ui ? `TOPOGRAM_WEB_BASE_URL=${urls.web}\n` : ""}`;
|
|
128
132
|
}
|
|
129
|
-
if (plan.projections.
|
|
133
|
+
if (plan.projections.dbEngine === "sqlite") {
|
|
130
134
|
return `# App bundle defaults
|
|
131
135
|
TOPOGRAM_ENVIRONMENT_PROFILE=${plan.profiles.environment}
|
|
132
136
|
TOPOGRAM_DEPLOY_PROFILE=${plan.profiles.deployment}
|
|
@@ -409,9 +413,7 @@ function noopBundle(name, message) {
|
|
|
409
413
|
export function generateAppBundle(graph, options = {}) {
|
|
410
414
|
const plan = buildAppBundlePlan(graph, options);
|
|
411
415
|
const topology = resolveRuntimeTopology(graph, options);
|
|
412
|
-
const
|
|
413
|
-
plan.projections.dbPlatform = projections.dbProjection?.platform || null;
|
|
414
|
-
const fullStack = topology.apiComponents.length > 0 && topology.webComponents.length > 0 && topology.dbComponents.length > 0;
|
|
416
|
+
const fullStack = topology.apiRuntimes.length > 0 && topology.webRuntimes.length > 0 && topology.dbRuntimes.length > 0;
|
|
415
417
|
const envBundle = generateEnvironmentBundle(graph, { ...options, profileId: plan.profiles.environment });
|
|
416
418
|
const deployBundle = fullStack
|
|
417
419
|
? generateDeploymentBundle(graph, { ...options, profileId: plan.profiles.deployment })
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
} from "./shared.js";
|
|
9
9
|
import { getExampleImplementation } from "../../example-implementation.js";
|
|
10
10
|
import { mergeBundleFiles } from "./bundle-shared.js";
|
|
11
|
+
import { generateDbLifecyclePlan } from "../surfaces/databases/lifecycle-shared.js";
|
|
11
12
|
|
|
12
13
|
function compileCheckName(graph, options = {}) {
|
|
13
14
|
try {
|
|
@@ -28,13 +29,13 @@ function runtimeReferenceFor(graph, options = {}) {
|
|
|
28
29
|
function buildCompileCheckPlan(graph, options = {}) {
|
|
29
30
|
const topology = resolveRuntimeTopology(graph, options);
|
|
30
31
|
const { apiProjection, uiProjection, dbProjection } = getDefaultEnvironmentProjections(graph, options);
|
|
31
|
-
const apiChecks = topology.
|
|
32
|
+
const apiChecks = topology.apiRuntimes.map((component, index) => ({
|
|
32
33
|
id: index === 0 ? "server_typecheck" : `server_typecheck_${component.id}`,
|
|
33
34
|
cwd: topology.serviceDir(component),
|
|
34
35
|
install: "npm install --no-audit --no-fund",
|
|
35
36
|
command: "npm run check"
|
|
36
37
|
}));
|
|
37
|
-
const webChecks = topology.
|
|
38
|
+
const webChecks = topology.webRuntimes.flatMap((component, index) => [
|
|
38
39
|
{
|
|
39
40
|
id: index === 0 ? "web_typecheck" : `web_typecheck_${component.id}`,
|
|
40
41
|
cwd: topology.webDir(component),
|
|
@@ -72,8 +73,9 @@ function renderCompileCheckEnvExample(graph, options = {}) {
|
|
|
72
73
|
const runtimeReference = runtimeReferenceFor(graph, options);
|
|
73
74
|
const topology = resolveRuntimeTopology(graph, options);
|
|
74
75
|
const { dbProjection } = getDefaultEnvironmentProjections(graph, options);
|
|
76
|
+
const dbLifecycle = dbProjection ? generateDbLifecyclePlan(graph, { ...options, projectionId: dbProjection.id }) : null;
|
|
75
77
|
const urls = runtimeUrls(runtimeReference, topology);
|
|
76
|
-
if (
|
|
78
|
+
if (dbLifecycle?.engine === "sqlite") {
|
|
77
79
|
return `DATABASE_URL=./var/${runtimeReference.environment.databaseName || "topogram_app"}.sqlite
|
|
78
80
|
PUBLIC_TOPOGRAM_API_BASE_URL=${urls.api}
|
|
79
81
|
PUBLIC_TOPOGRAM_DEMO_USER_ID=${runtimeDemoUserId(runtimeReference)}
|
|
@@ -148,11 +150,11 @@ export function generateCompileCheckBundle(graph, options = {}) {
|
|
|
148
150
|
"compile-check-plan.json": `${JSON.stringify(plan, null, 2)}\n`,
|
|
149
151
|
"scripts/check.sh": renderCompileCheckScript(plan)
|
|
150
152
|
};
|
|
151
|
-
for (const component of topology.
|
|
153
|
+
for (const component of topology.apiRuntimes) {
|
|
152
154
|
const serverBundle = generateServerBundle(graph, component.projection.id, { ...options, component });
|
|
153
155
|
mergeBundleFiles(files, topology.serviceDir(component), serverBundle);
|
|
154
156
|
}
|
|
155
|
-
for (const component of topology.
|
|
157
|
+
for (const component of topology.webRuntimes) {
|
|
156
158
|
const webBundle = generateWebBundle(graph, component.projection.id, { ...options, component });
|
|
157
159
|
mergeBundleFiles(files, topology.webDir(component), webBundle);
|
|
158
160
|
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
import { getExampleImplementation } from "../../example-implementation.js";
|
|
9
9
|
import { mergeNamedBundles, renderRootEnvFileShellScript, renderRootShellScript } from "./bundle-shared.js";
|
|
10
10
|
import { generatorProfile as manifestGeneratorProfile } from "../registry.js";
|
|
11
|
+
import { generateDbLifecyclePlan } from "../surfaces/databases/lifecycle-shared.js";
|
|
11
12
|
|
|
12
13
|
function projectionHintProfile(projection, fallback) {
|
|
13
14
|
for (const entry of projection.generatorDefaults || []) {
|
|
@@ -33,7 +34,8 @@ function buildDeploymentPlan(graph, options = {}) {
|
|
|
33
34
|
const profile = options.profileId || "fly_io";
|
|
34
35
|
const supportedProfiles = ["fly_io", "railway"];
|
|
35
36
|
const webProfile = manifestGeneratorProfile(topology.primaryWeb?.generator?.id, null) || projectionHintProfile(uiProjection, "sveltekit");
|
|
36
|
-
const
|
|
37
|
+
const dbLifecycle = dbProjection ? generateDbLifecyclePlan(graph, { ...options, projectionId: dbProjection.id }) : null;
|
|
38
|
+
const databaseTarget = dbLifecycle?.engine === "sqlite"
|
|
37
39
|
? "sqlite_file"
|
|
38
40
|
: profile === "fly_io"
|
|
39
41
|
? "managed_postgres"
|
|
@@ -260,19 +262,19 @@ export function generateDeploymentBundle(graph, options = {}) {
|
|
|
260
262
|
files["railway.json"] = renderRailwayJson(plan);
|
|
261
263
|
}
|
|
262
264
|
|
|
263
|
-
for (const component of topology.
|
|
265
|
+
for (const component of topology.apiRuntimes) {
|
|
264
266
|
const serverBundle = generateServerBundle(graph, component.projection.id, { ...options, component });
|
|
265
267
|
mergeNamedBundles(files, {
|
|
266
268
|
[topology.serviceDir(component)]: serverBundle
|
|
267
269
|
});
|
|
268
270
|
}
|
|
269
|
-
for (const component of topology.
|
|
271
|
+
for (const component of topology.webRuntimes) {
|
|
270
272
|
const webBundle = generateWebBundle(graph, component.projection.id, { ...options, component });
|
|
271
273
|
mergeNamedBundles(files, {
|
|
272
274
|
[topology.webDir(component)]: webBundle
|
|
273
275
|
});
|
|
274
276
|
}
|
|
275
|
-
for (const component of topology.
|
|
277
|
+
for (const component of topology.dbRuntimes) {
|
|
276
278
|
const dbBundle = generateDbBundle(graph, component.projection.id, { ...options, component });
|
|
277
279
|
mergeNamedBundles(files, {
|
|
278
280
|
[topology.dbDir(component)]: dbBundle
|
|
@@ -43,10 +43,11 @@ 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
|
|
46
|
+
const dbEngine = dbLifecycle?.engine || null;
|
|
47
|
+
const profile = options.profileId || (dbEngine === "sqlite" || !dbProjection ? "local_process" : "local_docker");
|
|
47
48
|
const usesDocker = profile === "local_docker";
|
|
48
49
|
const webProfile = manifestGeneratorProfile(topology.primaryWeb?.generator?.id, null) || projectionHintProfile(uiProjection, "sveltekit");
|
|
49
|
-
const isSqlite =
|
|
50
|
+
const isSqlite = dbEngine === "sqlite";
|
|
50
51
|
const ports = runtimePorts(runtimeReference, topology);
|
|
51
52
|
|
|
52
53
|
return {
|
|
@@ -71,15 +72,16 @@ function buildEnvironmentPlan(graph, options = {}) {
|
|
|
71
72
|
projections: {
|
|
72
73
|
api: {
|
|
73
74
|
id: apiProjection?.id || null,
|
|
74
|
-
|
|
75
|
+
type: apiProjection?.type || null
|
|
75
76
|
},
|
|
76
77
|
ui: {
|
|
77
78
|
id: uiProjection?.id || null,
|
|
78
|
-
|
|
79
|
+
type: uiProjection?.type || null
|
|
79
80
|
},
|
|
80
81
|
db: {
|
|
81
82
|
id: dbProjection?.id || null,
|
|
82
|
-
|
|
83
|
+
type: dbProjection?.type || null,
|
|
84
|
+
engine: dbEngine,
|
|
83
85
|
profile: dbProjection?.generatorDefaults?.profile || dbProjection?.profile || null
|
|
84
86
|
}
|
|
85
87
|
},
|
|
@@ -111,27 +113,28 @@ function buildEnvironmentPlan(graph, options = {}) {
|
|
|
111
113
|
scripts: "scripts"
|
|
112
114
|
},
|
|
113
115
|
runtimes: {
|
|
114
|
-
apis: topology.
|
|
116
|
+
apis: topology.apiRuntimes.map((component) => ({
|
|
115
117
|
id: component.id,
|
|
116
118
|
projection: component.projection.id,
|
|
117
119
|
port: component.port || ports.server,
|
|
118
120
|
dir: topology.serviceDir(component),
|
|
119
|
-
|
|
120
|
-
databaseEnv: component.
|
|
121
|
-
? dbEnvVarsForComponent(component.
|
|
121
|
+
uses_database: component.database,
|
|
122
|
+
databaseEnv: component.databaseRuntime
|
|
123
|
+
? dbEnvVarsForComponent(component.databaseRuntime, { primary: component.databaseRuntime?.id === topology.primaryDb?.id })
|
|
122
124
|
: null
|
|
123
125
|
})),
|
|
124
|
-
webs: topology.
|
|
126
|
+
webs: topology.webRuntimes.map((component) => ({
|
|
125
127
|
id: component.id,
|
|
126
128
|
projection: component.projection.id,
|
|
127
129
|
port: component.port || ports.web,
|
|
128
130
|
dir: topology.webDir(component),
|
|
129
|
-
|
|
131
|
+
uses_api: component.api
|
|
130
132
|
})),
|
|
131
|
-
databases: topology.
|
|
133
|
+
databases: topology.dbRuntimes.map((component) => ({
|
|
132
134
|
id: component.id,
|
|
133
135
|
projection: component.projection.id,
|
|
134
|
-
|
|
136
|
+
type: component.projection.type,
|
|
137
|
+
engine: component.id === topology.primaryDb?.id ? dbEngine : null,
|
|
135
138
|
port: component.port,
|
|
136
139
|
dir: topology.dbDir(component),
|
|
137
140
|
env: dbEnvVarsForComponent(component, { primary: component.id === topology.primaryDb?.id })
|
|
@@ -169,7 +172,7 @@ function renderEnvironmentEnvExample(plan) {
|
|
|
169
172
|
.filter((component) => component.id !== plan.runtimes.databases[0]?.id)
|
|
170
173
|
.map((component) => {
|
|
171
174
|
const fallbackName = `${databaseName}_${component.id}`;
|
|
172
|
-
if (component.
|
|
175
|
+
if (component.engine === "sqlite") {
|
|
173
176
|
return `${component.env.databaseUrl}=file:./var/${component.id}.sqlite`;
|
|
174
177
|
}
|
|
175
178
|
return [
|
|
@@ -190,10 +193,10 @@ TOPOGRAM_DEMO_USER_ID=${demoUserId}
|
|
|
190
193
|
${plan.runtimeReference.environment.envExample || ""}
|
|
191
194
|
TOPOGRAM_SEED_DEMO=true
|
|
192
195
|
`;
|
|
193
|
-
if (!plan.projections.db.
|
|
196
|
+
if (!plan.projections.db.type) {
|
|
194
197
|
return commonLines;
|
|
195
198
|
}
|
|
196
|
-
if (plan.projections.db.
|
|
199
|
+
if (plan.projections.db.engine === "sqlite") {
|
|
197
200
|
return `# Environment profile
|
|
198
201
|
TOPOGRAM_ENVIRONMENT_PROFILE=${plan.environment.profile}
|
|
199
202
|
|
|
@@ -272,7 +275,7 @@ function renderEnvironmentReadme(plan) {
|
|
|
272
275
|
const hasWeb = plan.runtimes.webs.length > 0;
|
|
273
276
|
const localProcessNotes = !hasDb
|
|
274
277
|
? "- This bundle has no generated database surface."
|
|
275
|
-
: plan.projections.db.
|
|
278
|
+
: plan.projections.db.engine === "sqlite"
|
|
276
279
|
? "- SQLite is file-backed for this bundle; no separate DB server is required."
|
|
277
280
|
: `- 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
281
|
const dockerSection = plan.orchestration.usesDocker
|
|
@@ -291,13 +294,13 @@ ${localProcessNotes}
|
|
|
291
294
|
|
|
292
295
|
This bundle packages the generated runtime into one local environment:
|
|
293
296
|
|
|
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.
|
|
297
|
+
${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.engine === "sqlite" ? "- local SQLite file orchestration (no Docker files generated)" : "- local-process Postgres orchestration (no Docker files generated)") : "- no DB orchestration is generated"}
|
|
295
298
|
|
|
296
299
|
## Quick Start
|
|
297
300
|
|
|
298
301
|
1. Copy \`.env.example\` to \`.env\` if you want to customize defaults
|
|
299
302
|
2. Start the database:
|
|
300
|
-
- ${!hasDb ? "not applicable" : plan.projections.db.
|
|
303
|
+
- ${!hasDb ? "not applicable" : plan.projections.db.engine === "sqlite" ? "no separate DB service is required" : plan.orchestration.usesDocker ? `\`${plan.commands.dockerDb}\`` : "use your local Postgres service"}
|
|
301
304
|
3. Bootstrap or migrate the database:
|
|
302
305
|
- \`${plan.commands.bootstrapDb}\`
|
|
303
306
|
4. Start the stack:
|
|
@@ -313,7 +316,7 @@ ${dockerSection}
|
|
|
313
316
|
|
|
314
317
|
## Notes
|
|
315
318
|
|
|
316
|
-
- ${hasApi && hasDb ? `The generated server expects ${plan.projections.db.
|
|
319
|
+
- ${hasApi && hasDb ? `The generated server expects ${plan.projections.db.engine === "sqlite" ? "SQLite plus Prisma." : "Postgres plus Prisma."}` : hasApi ? "The generated server is stateless." : "No server surface is generated."}
|
|
317
320
|
- ${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
321
|
- If \`.env\` is missing, generated scripts fall back to \`.env.example\`.
|
|
319
322
|
- The DB lifecycle scripts remain the source of truth for greenfield bootstrap and brownfield migration.
|
|
@@ -327,7 +330,7 @@ function renderEnvironmentLoadEnvScript() {
|
|
|
327
330
|
function renderEnvironmentBootstrapDbScript(plan) {
|
|
328
331
|
const dbBootstrapLines = plan.runtimes.databases.map((component) => {
|
|
329
332
|
const env = component.env;
|
|
330
|
-
const runtimeApi = plan.runtimes.apis.find((
|
|
333
|
+
const runtimeApi = plan.runtimes.apis.find((apiRuntime) => apiRuntime.uses_database === component.id);
|
|
331
334
|
const assignments = [
|
|
332
335
|
`DATABASE_URL="\${${env.databaseUrl}:-}"`,
|
|
333
336
|
`DATABASE_ADMIN_URL="\${${env.databaseAdminUrl}:-}"`,
|
|
@@ -353,7 +356,7 @@ function renderEnvironmentBootstrapDbScript(plan) {
|
|
|
353
356
|
...dbBootstrapLines,
|
|
354
357
|
'if [[ "${TOPOGRAM_SEED_DEMO:-true}" != "false" ]]; then',
|
|
355
358
|
...apiDatabaseExportLines(primaryApi),
|
|
356
|
-
...(primaryApi?.
|
|
359
|
+
...(primaryApi?.uses_database
|
|
357
360
|
? [`(cd "$ROOT_DIR/${primaryApi.dir}" && npm install && npm exec -- prisma generate --schema prisma/schema.prisma && npm exec -- prisma db push --schema prisma/schema.prisma --skip-generate && npm run seed:demo)`]
|
|
358
361
|
: ['echo "No DB-backed API component is configured; skipping demo seed."']),
|
|
359
362
|
"fi"
|
|
@@ -381,7 +384,7 @@ function renderEnvironmentServerDevScript(plan, component = plan.runtimes.apis[0
|
|
|
381
384
|
"",
|
|
382
385
|
`cd "$ROOT_DIR/${component.dir}"`,
|
|
383
386
|
"npm install",
|
|
384
|
-
...(component.
|
|
387
|
+
...(component.uses_database ? ["npm exec -- prisma generate --schema prisma/schema.prisma"] : []),
|
|
385
388
|
"npm run dev"
|
|
386
389
|
], options.componentScript ? componentScriptOptions() : {});
|
|
387
390
|
}
|
|
@@ -390,12 +393,12 @@ function renderEnvironmentWebDevScript(plan, component = plan.runtimes.webs[0],
|
|
|
390
393
|
if (!component) {
|
|
391
394
|
return renderEnvAwareShellScript(['echo "No web runtimes are configured."']);
|
|
392
395
|
}
|
|
393
|
-
const
|
|
396
|
+
const apiRuntime = plan.runtimes.apis.find((entry) => entry.id === component.uses_api) || plan.runtimes.apis[0];
|
|
394
397
|
const guardPortsScript = options.componentScript ? '"$ROOT_DIR/scripts/guard-ports.mjs"' : '"$SCRIPT_DIR/guard-ports.mjs"';
|
|
395
398
|
return renderEnvAwareShellScript([
|
|
396
399
|
`node ${guardPortsScript} web`,
|
|
397
400
|
"",
|
|
398
|
-
...(
|
|
401
|
+
...(apiRuntime ? [`export PUBLIC_TOPOGRAM_API_BASE_URL="\${PUBLIC_TOPOGRAM_API_BASE_URL:-http://localhost:\${${apiRuntime.id.toUpperCase()}_PORT:-\${SERVER_PORT:-${apiRuntime.port}}}}"`] : []),
|
|
399
402
|
`export TOPOGRAM_CORS_ORIGINS="\${TOPOGRAM_CORS_ORIGINS:-http://localhost:\${${component.id.toUpperCase()}_PORT:-\${WEB_PORT:-${component.port}}},http://127.0.0.1:\${${component.id.toUpperCase()}_PORT:-\${WEB_PORT:-${component.port}}}}"`,
|
|
400
403
|
"",
|
|
401
404
|
`cd "$ROOT_DIR/${component.dir}"`,
|
|
@@ -609,19 +612,19 @@ export function generateEnvironmentBundle(graph, options = {}) {
|
|
|
609
612
|
files["scripts/docker-stack.sh"] = renderEnvironmentDockerStackScript();
|
|
610
613
|
}
|
|
611
614
|
|
|
612
|
-
for (const component of topology.
|
|
615
|
+
for (const component of topology.apiRuntimes) {
|
|
613
616
|
const serverBundle = generateServerBundle(graph, component.projection.id, { ...options, component });
|
|
614
617
|
mergeNamedBundles(files, {
|
|
615
618
|
[topology.serviceDir(component)]: serverBundle
|
|
616
619
|
});
|
|
617
620
|
}
|
|
618
|
-
for (const component of topology.
|
|
621
|
+
for (const component of topology.webRuntimes) {
|
|
619
622
|
const webBundle = generateWebBundle(graph, component.projection.id, { ...options, component });
|
|
620
623
|
mergeNamedBundles(files, {
|
|
621
624
|
[topology.webDir(component)]: webBundle
|
|
622
625
|
});
|
|
623
626
|
}
|
|
624
|
-
for (const component of topology.
|
|
627
|
+
for (const component of topology.dbRuntimes) {
|
|
625
628
|
const dbBundle = generateDbBundle(graph, component.projection.id, { ...options, component });
|
|
626
629
|
mergeNamedBundles(files, {
|
|
627
630
|
[topology.dbDir(component)]: dbBundle
|