create-bw-app 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "create-bw-app",
3
3
  "private": false,
4
- "version": "0.5.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
  "- `/`",
@@ -384,9 +518,11 @@ function createPackageJson({
384
518
  },
385
519
  dependencies: sortObjectKeys(dependencies),
386
520
  devDependencies: sortObjectKeys({
521
+ "@tailwindcss/postcss": versionMap["@tailwindcss/postcss"],
387
522
  "@types/node": versionMap["@types/node"],
388
523
  "@types/react": versionMap["@types/react"],
389
524
  "@types/react-dom": versionMap["@types/react-dom"],
525
+ tailwindcss: versionMap.tailwindcss,
390
526
  typescript: versionMap.typescript,
391
527
  }),
392
528
  };
@@ -546,6 +682,50 @@ async function ensureDirectory(targetDir) {
546
682
  await fs.mkdir(targetDir, { recursive: true });
547
683
  }
548
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
+
549
729
  async function runInstall(command, cwd) {
550
730
  return new Promise((resolve, reject) => {
551
731
  const child = spawn(command, ["install"], {
@@ -566,15 +746,36 @@ async function runInstall(command, cwd) {
566
746
  });
567
747
  }
568
748
 
569
- 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
+ }) {
570
759
  const installLocation = workspaceMode ? "workspace root" : "project directory";
571
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";
572
767
 
573
768
  return [
574
769
  `Template: ${templateLabel}`,
575
770
  `Target directory: ${targetDir}`,
576
771
  `Dependency mode: ${dependencyMode}`,
577
- `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
+ : []),
578
779
  `Install dependencies: ${install ? `yes (${packageManager} in ${installLocation})` : "no"}`,
579
780
  ].join("\n");
580
781
  }
@@ -666,7 +867,9 @@ async function scaffoldPlatformProject({
666
867
  dependencyMode,
667
868
  packageManager,
668
869
  workspaceMode,
870
+ workspaceRoot,
669
871
  answers,
872
+ dbInstallPlan,
670
873
  }) {
671
874
  const moduleFlags = createModuleFlags(selectedModules);
672
875
  const brandValues = createDerivedBrandValues(answers.slug);
@@ -710,8 +913,13 @@ async function scaffoldPlatformProject({
710
913
  selectedModules,
711
914
  workspaceMode,
712
915
  packageManager,
916
+ dbInstallPlan,
713
917
  }),
714
918
  );
919
+
920
+ if (workspaceMode) {
921
+ await writeWorkspaceClientStack(workspaceRoot, answers.slug, selectedModules);
922
+ }
715
923
  }
716
924
 
717
925
  async function scaffoldSiteProject({
@@ -797,6 +1005,12 @@ export async function createBrightwebClientApp(argvOptions, runtimeOptions = {})
797
1005
  }
798
1006
 
799
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
+ });
800
1014
  const install = answers.install && !argvOptions.dryRun;
801
1015
 
802
1016
  if (argvOptions.dryRun) {
@@ -808,6 +1022,7 @@ export async function createBrightwebClientApp(argvOptions, runtimeOptions = {})
808
1022
  workspaceMode,
809
1023
  install: answers.install,
810
1024
  template: answers.template,
1025
+ dbInstallPlan,
811
1026
  })}\n\n`);
812
1027
  return {
813
1028
  answers,
@@ -819,6 +1034,17 @@ export async function createBrightwebClientApp(argvOptions, runtimeOptions = {})
819
1034
  };
820
1035
  }
821
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
+
822
1048
  if (answers.template === "site") {
823
1049
  await scaffoldSiteProject({
824
1050
  targetDir,
@@ -836,7 +1062,9 @@ export async function createBrightwebClientApp(argvOptions, runtimeOptions = {})
836
1062
  dependencyMode,
837
1063
  packageManager,
838
1064
  workspaceMode,
1065
+ workspaceRoot,
839
1066
  answers,
1067
+ dbInstallPlan,
840
1068
  });
841
1069
  }
842
1070
 
@@ -1,3 +1,5 @@
1
+ @import "tailwindcss";
2
+
1
3
  :root {
2
4
  color-scheme: light;
3
5
  --font-sans: "IBM Plex Sans", "Segoe UI", sans-serif;
@@ -0,0 +1,7 @@
1
+ const config = {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ };
6
+
7
+ export default config;