create-bw-app 0.6.0 → 0.7.0
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/package.json +1 -1
- package/src/generator.mjs +228 -2
package/package.json
CHANGED
package/src/generator.mjs
CHANGED
|
@@ -19,6 +19,14 @@ import {
|
|
|
19
19
|
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
20
20
|
const TEMPLATE_ROOT = path.join(PACKAGE_ROOT, "template");
|
|
21
21
|
const TEMPLATE_KEY_SET = new Set(TEMPLATE_OPTIONS.map((templateOption) => templateOption.key));
|
|
22
|
+
const DEFAULT_DB_MODULE_REGISTRY = {
|
|
23
|
+
modules: {
|
|
24
|
+
core: { label: "Core", dependsOn: [] },
|
|
25
|
+
admin: { label: "Admin", dependsOn: ["core"] },
|
|
26
|
+
crm: { label: "CRM", dependsOn: ["core", "admin"] },
|
|
27
|
+
projects: { label: "Projects", dependsOn: ["core", "admin", "crm"] },
|
|
28
|
+
},
|
|
29
|
+
};
|
|
22
30
|
|
|
23
31
|
function slugify(value) {
|
|
24
32
|
return value
|
|
@@ -103,6 +111,113 @@ async function readJsonIfPresent(filePath) {
|
|
|
103
111
|
return JSON.parse(await fs.readFile(filePath, "utf8"));
|
|
104
112
|
}
|
|
105
113
|
|
|
114
|
+
async function getDbModuleRegistry(workspaceRoot) {
|
|
115
|
+
if (!workspaceRoot) {
|
|
116
|
+
return DEFAULT_DB_MODULE_REGISTRY;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const registryPath = path.join(workspaceRoot, "supabase", "module-registry.json");
|
|
120
|
+
return (await readJsonIfPresent(registryPath)) || DEFAULT_DB_MODULE_REGISTRY;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function resolveModuleOrder(registry, enabledModules) {
|
|
124
|
+
const resolved = [];
|
|
125
|
+
const visited = new Set();
|
|
126
|
+
const visiting = new Set();
|
|
127
|
+
|
|
128
|
+
function visit(moduleKey) {
|
|
129
|
+
if (visited.has(moduleKey)) return;
|
|
130
|
+
if (visiting.has(moduleKey)) {
|
|
131
|
+
throw new Error(`Circular module dependency detected at "${moduleKey}".`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const moduleConfig = registry.modules?.[moduleKey];
|
|
135
|
+
if (!moduleConfig) {
|
|
136
|
+
throw new Error(`Unknown database module "${moduleKey}".`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
visiting.add(moduleKey);
|
|
140
|
+
for (const dependency of moduleConfig.dependsOn || []) {
|
|
141
|
+
visit(dependency);
|
|
142
|
+
}
|
|
143
|
+
visiting.delete(moduleKey);
|
|
144
|
+
visited.add(moduleKey);
|
|
145
|
+
resolved.push(moduleKey);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
for (const moduleKey of enabledModules) {
|
|
149
|
+
visit(moduleKey);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return resolved;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function getSelectedModuleLabels(selectedModules) {
|
|
156
|
+
return selectedModules.map((moduleKey) => {
|
|
157
|
+
return getModuleLabel(moduleKey);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function getModuleLabel(moduleKey) {
|
|
162
|
+
const moduleDefinition = SELECTABLE_MODULES.find((candidate) => candidate.key === moduleKey);
|
|
163
|
+
if (moduleDefinition) {
|
|
164
|
+
return moduleDefinition.label;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (moduleKey === "core") {
|
|
168
|
+
return "Core";
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return titleizeSlug(moduleKey);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function createDbInstallPlan({ selectedModules, workspaceMode, registry }) {
|
|
175
|
+
if (!workspaceMode) {
|
|
176
|
+
return {
|
|
177
|
+
selectedLabels: getSelectedModuleLabels(selectedModules),
|
|
178
|
+
resolvedOrder: [],
|
|
179
|
+
notes: [],
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const requestedModules = Array.from(new Set(["core", ...selectedModules]));
|
|
184
|
+
if (!requestedModules.includes("admin")) {
|
|
185
|
+
requestedModules.push("admin");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const resolvedOrder = resolveModuleOrder(registry, requestedModules);
|
|
189
|
+
const notes = [];
|
|
190
|
+
|
|
191
|
+
if (!selectedModules.includes("admin") && resolvedOrder.includes("admin")) {
|
|
192
|
+
notes.push("Admin governance stays enabled for platform auth/RBAC even if the admin UI module is not selected.");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
for (const moduleKey of resolvedOrder) {
|
|
196
|
+
if (moduleKey === "core" || selectedModules.includes(moduleKey) || moduleKey === "admin") {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const dependents = resolvedOrder.filter((candidateKey) => {
|
|
201
|
+
const dependencyList = registry.modules?.[candidateKey]?.dependsOn || [];
|
|
202
|
+
return dependencyList.includes(moduleKey);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
if (dependents.length === 0) continue;
|
|
206
|
+
|
|
207
|
+
const dependentLabels = dependents
|
|
208
|
+
.map((candidateKey) => registry.modules?.[candidateKey]?.label || getModuleLabel(candidateKey))
|
|
209
|
+
.join(", ");
|
|
210
|
+
const moduleLabel = registry.modules?.[moduleKey]?.label || getModuleLabel(moduleKey);
|
|
211
|
+
notes.push(`${moduleLabel} is included because ${dependentLabels} depends on it.`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
selectedLabels: getSelectedModuleLabels(selectedModules),
|
|
216
|
+
resolvedOrder,
|
|
217
|
+
notes,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
106
221
|
async function getVersionMap(workspaceRoot) {
|
|
107
222
|
const versionMap = {
|
|
108
223
|
...APP_DEPENDENCY_DEFAULTS,
|
|
@@ -237,11 +352,14 @@ function createPlatformReadme({
|
|
|
237
352
|
selectedModules,
|
|
238
353
|
workspaceMode,
|
|
239
354
|
packageManager,
|
|
355
|
+
dbInstallPlan,
|
|
240
356
|
}) {
|
|
241
357
|
const moduleLines = SELECTABLE_MODULES.map((moduleDefinition) => {
|
|
242
358
|
const enabled = selectedModules.includes(moduleDefinition.key);
|
|
243
359
|
return `- ${moduleDefinition.key}: ${enabled ? "enabled" : "disabled"}`;
|
|
244
360
|
});
|
|
361
|
+
const resolvedDbStackLines = dbInstallPlan.resolvedOrder.map((moduleKey) => `- ${getModuleLabel(moduleKey)}`);
|
|
362
|
+
const dependencyNotes = dbInstallPlan.notes.map((note) => `- ${note}`);
|
|
245
363
|
|
|
246
364
|
const localSteps = workspaceMode
|
|
247
365
|
? [
|
|
@@ -268,6 +386,22 @@ function createPlatformReadme({
|
|
|
268
386
|
"",
|
|
269
387
|
...moduleLines,
|
|
270
388
|
"",
|
|
389
|
+
...(workspaceMode
|
|
390
|
+
? [
|
|
391
|
+
"## Resolved database stack",
|
|
392
|
+
"",
|
|
393
|
+
...resolvedDbStackLines,
|
|
394
|
+
"",
|
|
395
|
+
...(dependencyNotes.length > 0
|
|
396
|
+
? [
|
|
397
|
+
"## Dependency notes",
|
|
398
|
+
"",
|
|
399
|
+
...dependencyNotes,
|
|
400
|
+
"",
|
|
401
|
+
]
|
|
402
|
+
: []),
|
|
403
|
+
]
|
|
404
|
+
: []),
|
|
271
405
|
"## Starter routes",
|
|
272
406
|
"",
|
|
273
407
|
"- `/`",
|
|
@@ -548,6 +682,50 @@ async function ensureDirectory(targetDir) {
|
|
|
548
682
|
await fs.mkdir(targetDir, { recursive: true });
|
|
549
683
|
}
|
|
550
684
|
|
|
685
|
+
async function writeWorkspaceClientStack(workspaceRoot, slug, selectedModules) {
|
|
686
|
+
const clientDir = path.join(workspaceRoot, "supabase", "clients", slug);
|
|
687
|
+
const stackPath = path.join(clientDir, "stack.json");
|
|
688
|
+
const migrationsDir = path.join(clientDir, "migrations");
|
|
689
|
+
const registry = await getDbModuleRegistry(workspaceRoot);
|
|
690
|
+
const dbInstallPlan = createDbInstallPlan({
|
|
691
|
+
selectedModules,
|
|
692
|
+
workspaceMode: true,
|
|
693
|
+
registry,
|
|
694
|
+
});
|
|
695
|
+
const enabledModules = dbInstallPlan.resolvedOrder;
|
|
696
|
+
|
|
697
|
+
if (await pathExists(stackPath)) {
|
|
698
|
+
throw new Error(`Client stack already exists: ${stackPath}`);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
await ensureDirectory(migrationsDir);
|
|
702
|
+
await fs.writeFile(path.join(migrationsDir, ".gitkeep"), "", "utf8");
|
|
703
|
+
await fs.writeFile(
|
|
704
|
+
stackPath,
|
|
705
|
+
`${JSON.stringify(
|
|
706
|
+
{
|
|
707
|
+
client: {
|
|
708
|
+
slug,
|
|
709
|
+
label: titleizeSlug(slug),
|
|
710
|
+
},
|
|
711
|
+
historyMode: "greenfield-modular",
|
|
712
|
+
futureMode: "forward-only-modular",
|
|
713
|
+
enabledModules,
|
|
714
|
+
clientMigrationPath: `supabase/clients/${slug}/migrations`,
|
|
715
|
+
notes: [
|
|
716
|
+
"Generated by create-bw-app in workspace mode.",
|
|
717
|
+
`Selected app modules: ${dbInstallPlan.selectedLabels.length > 0 ? dbInstallPlan.selectedLabels.join(", ") : "none"}.`,
|
|
718
|
+
`Resolved database stack: ${enabledModules.map((moduleKey) => getModuleLabel(moduleKey)).join(" -> ")}.`,
|
|
719
|
+
"Admin governance stays enabled for platform auth/RBAC even if the admin UI module is not selected.",
|
|
720
|
+
"The database install order is resolved from supabase/module-registry.json.",
|
|
721
|
+
],
|
|
722
|
+
},
|
|
723
|
+
null,
|
|
724
|
+
2,
|
|
725
|
+
)}\n`,
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
|
|
551
729
|
async function runInstall(command, cwd) {
|
|
552
730
|
return new Promise((resolve, reject) => {
|
|
553
731
|
const child = spawn(command, ["install"], {
|
|
@@ -568,15 +746,36 @@ async function runInstall(command, cwd) {
|
|
|
568
746
|
});
|
|
569
747
|
}
|
|
570
748
|
|
|
571
|
-
function renderPlanSummary({
|
|
749
|
+
function renderPlanSummary({
|
|
750
|
+
targetDir,
|
|
751
|
+
dependencyMode,
|
|
752
|
+
selectedModules,
|
|
753
|
+
packageManager,
|
|
754
|
+
workspaceMode,
|
|
755
|
+
install,
|
|
756
|
+
template,
|
|
757
|
+
dbInstallPlan,
|
|
758
|
+
}) {
|
|
572
759
|
const installLocation = workspaceMode ? "workspace root" : "project directory";
|
|
573
760
|
const templateLabel = TEMPLATE_OPTIONS.find((templateOption) => templateOption.key === template)?.label || template;
|
|
761
|
+
const selectedModuleSummary =
|
|
762
|
+
template === "platform"
|
|
763
|
+
? dbInstallPlan.selectedLabels.length > 0
|
|
764
|
+
? dbInstallPlan.selectedLabels.join(", ")
|
|
765
|
+
: "none"
|
|
766
|
+
: "n/a";
|
|
574
767
|
|
|
575
768
|
return [
|
|
576
769
|
`Template: ${templateLabel}`,
|
|
577
770
|
`Target directory: ${targetDir}`,
|
|
578
771
|
`Dependency mode: ${dependencyMode}`,
|
|
579
|
-
`Selected modules: ${
|
|
772
|
+
`Selected modules: ${selectedModuleSummary}`,
|
|
773
|
+
...(template === "platform" && workspaceMode
|
|
774
|
+
? [
|
|
775
|
+
`Resolved database stack: ${dbInstallPlan.resolvedOrder.map((moduleKey) => getModuleLabel(moduleKey)).join(" -> ")}`,
|
|
776
|
+
...dbInstallPlan.notes.map((note) => `- ${note}`),
|
|
777
|
+
]
|
|
778
|
+
: []),
|
|
580
779
|
`Install dependencies: ${install ? `yes (${packageManager} in ${installLocation})` : "no"}`,
|
|
581
780
|
].join("\n");
|
|
582
781
|
}
|
|
@@ -668,7 +867,9 @@ async function scaffoldPlatformProject({
|
|
|
668
867
|
dependencyMode,
|
|
669
868
|
packageManager,
|
|
670
869
|
workspaceMode,
|
|
870
|
+
workspaceRoot,
|
|
671
871
|
answers,
|
|
872
|
+
dbInstallPlan,
|
|
672
873
|
}) {
|
|
673
874
|
const moduleFlags = createModuleFlags(selectedModules);
|
|
674
875
|
const brandValues = createDerivedBrandValues(answers.slug);
|
|
@@ -712,8 +913,13 @@ async function scaffoldPlatformProject({
|
|
|
712
913
|
selectedModules,
|
|
713
914
|
workspaceMode,
|
|
714
915
|
packageManager,
|
|
916
|
+
dbInstallPlan,
|
|
715
917
|
}),
|
|
716
918
|
);
|
|
919
|
+
|
|
920
|
+
if (workspaceMode) {
|
|
921
|
+
await writeWorkspaceClientStack(workspaceRoot, answers.slug, selectedModules);
|
|
922
|
+
}
|
|
717
923
|
}
|
|
718
924
|
|
|
719
925
|
async function scaffoldSiteProject({
|
|
@@ -799,6 +1005,12 @@ export async function createBrightwebClientApp(argvOptions, runtimeOptions = {})
|
|
|
799
1005
|
}
|
|
800
1006
|
|
|
801
1007
|
const versionMap = await getVersionMap(workspaceRoot);
|
|
1008
|
+
const dbModuleRegistry = await getDbModuleRegistry(workspaceRoot);
|
|
1009
|
+
const dbInstallPlan = createDbInstallPlan({
|
|
1010
|
+
selectedModules: answers.selectedModules,
|
|
1011
|
+
workspaceMode,
|
|
1012
|
+
registry: dbModuleRegistry,
|
|
1013
|
+
});
|
|
802
1014
|
const install = answers.install && !argvOptions.dryRun;
|
|
803
1015
|
|
|
804
1016
|
if (argvOptions.dryRun) {
|
|
@@ -810,6 +1022,7 @@ export async function createBrightwebClientApp(argvOptions, runtimeOptions = {})
|
|
|
810
1022
|
workspaceMode,
|
|
811
1023
|
install: answers.install,
|
|
812
1024
|
template: answers.template,
|
|
1025
|
+
dbInstallPlan,
|
|
813
1026
|
})}\n\n`);
|
|
814
1027
|
return {
|
|
815
1028
|
answers,
|
|
@@ -821,6 +1034,17 @@ export async function createBrightwebClientApp(argvOptions, runtimeOptions = {})
|
|
|
821
1034
|
};
|
|
822
1035
|
}
|
|
823
1036
|
|
|
1037
|
+
output.write(`${renderPlanSummary({
|
|
1038
|
+
targetDir,
|
|
1039
|
+
dependencyMode,
|
|
1040
|
+
selectedModules: answers.selectedModules,
|
|
1041
|
+
packageManager,
|
|
1042
|
+
workspaceMode,
|
|
1043
|
+
install: answers.install,
|
|
1044
|
+
template: answers.template,
|
|
1045
|
+
dbInstallPlan,
|
|
1046
|
+
})}\n\n`);
|
|
1047
|
+
|
|
824
1048
|
if (answers.template === "site") {
|
|
825
1049
|
await scaffoldSiteProject({
|
|
826
1050
|
targetDir,
|
|
@@ -838,7 +1062,9 @@ export async function createBrightwebClientApp(argvOptions, runtimeOptions = {})
|
|
|
838
1062
|
dependencyMode,
|
|
839
1063
|
packageManager,
|
|
840
1064
|
workspaceMode,
|
|
1065
|
+
workspaceRoot,
|
|
841
1066
|
answers,
|
|
1067
|
+
dbInstallPlan,
|
|
842
1068
|
});
|
|
843
1069
|
}
|
|
844
1070
|
|