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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/generator.mjs +228 -2
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "create-bw-app",
3
3
  "private": false,
4
- "version": "0.6.0",
4
+ "version": "0.7.0",
5
5
  "type": "module",
6
6
  "bin": "bin/create-bw-app.mjs",
7
7
  "files": [
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({ targetDir, dependencyMode, selectedModules, packageManager, workspaceMode, install, template }) {
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: ${template === "platform" ? (selectedModules.length > 0 ? selectedModules.join(", ") : "none") : "n/a"}`,
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