create-bw-app 0.6.0 → 0.8.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/README.md CHANGED
@@ -1,22 +1,26 @@
1
1
  # create-bw-app
2
2
 
3
- Create a new BrightWeb app from either the platform or site starter.
3
+ Scaffold a new BrightWeb app from either the `platform` or `site` starter.
4
4
 
5
- ## Local workspace usage
5
+ ## Workspace usage
6
6
 
7
7
  From the BrightWeb platform repo root:
8
8
 
9
9
  ```bash
10
10
  pnpm create:client
11
+ pnpm create:client -- --help
11
12
  pnpm create:client -- --template site
12
13
  ```
13
14
 
14
- ## Future published usage
15
+ The workspace wrapper delegates to this package with `workspace:*` dependency wiring and BrightWeb-specific output rules.
16
+
17
+ ## Published usage
15
18
 
16
19
  Once this package is published to npm:
17
20
 
18
21
  ```bash
19
22
  pnpm dlx create-bw-app
23
+ pnpm dlx create-bw-app --template site
20
24
  npm create bw-app@latest
21
25
  ```
22
26
 
@@ -24,11 +28,30 @@ npm create bw-app@latest
24
28
 
25
29
  - prompts for app type: `platform` or `site`
26
30
  - prompts for project name
27
- - prompts for optional modules with a checkbox list when using the platform template
31
+ - prompts for optional platform modules: `admin`, `crm`, and `projects`
28
32
  - prompts to install dependencies immediately
29
- - copies a clean Next.js starter template
30
- - platform apps include BrightWeb auth, shell, and optional business modules
31
- - site apps include Next.js, Tailwind CSS, and local shadcn-style component primitives
33
+ - copies a clean Next.js App Router starter template
34
+ - platform apps include BrightWeb auth, shell wiring, and optional module starter surfaces
35
+ - site apps include Next.js, Tailwind CSS v4, and local component primitives
32
36
  - writes `package.json`, `next.config.ts`, `.gitignore`, and `README.md` for both templates
33
- - platform apps also write `.env.example` and `.env.local`
37
+ - platform apps also write `.env.example`, generated config files, and module feature flags
34
38
  - supports repo-local `workspace:*` wiring and future published dependency wiring
39
+
40
+ ## Workspace mode extras
41
+
42
+ When this package runs in BrightWeb workspace mode, it can:
43
+
44
+ - write the new app under `apps/<slug>`
45
+ - keep internal dependencies on `workspace:*`
46
+ - create `supabase/clients/<slug>/stack.json`
47
+ - create a client-only migrations folder so database planning stays aligned with scaffolded modules
48
+
49
+ Platform mode always resolves to the `Core + Admin` database baseline. Selecting `admin` affects the Admin starter UI and package wiring, not whether the Admin database layer exists.
50
+
51
+ ## Related references
52
+
53
+ - `packages/create-bw-app/src/generator.mjs`
54
+ - `packages/create-bw-app/src/constants.mjs`
55
+ - `packages/create-bw-app/template/base`
56
+ - `packages/create-bw-app/template/site/base`
57
+ - `packages/create-bw-app/template/modules`
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.8.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("Platform always resolves to the Core + Admin database baseline; selecting Admin only controls whether the Admin starter UI and package wiring are scaffolded.");
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,20 +352,23 @@ 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
  ? [
248
- "1. Review `.env.example`, then fill `.env.local` with real service credentials.",
366
+ "1. Review `.env.example`, then copy it to `.env.local` and fill in real service credentials.",
249
367
  "2. Run `pnpm install` from the BrightWeb workspace root.",
250
368
  `3. Run \`pnpm --filter ${slug} dev\`.`,
251
369
  ]
252
370
  : [
253
- "1. Review `.env.example`, then fill `.env.local` with real service credentials.",
371
+ "1. Review `.env.example`, then copy it to `.env.local` and fill in real service credentials.",
254
372
  `2. Run \`${packageManager} install\`.`,
255
373
  `3. Run \`${packageManager} dev\`.`,
256
374
  ];
@@ -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
+ "Platform always resolves to the Core + Admin database baseline; selecting Admin only controls whether the Admin starter UI and package wiring are scaffolded.",
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);
@@ -703,7 +904,6 @@ async function scaffoldPlatformProject({
703
904
  const envFileContent = createEnvFileContent({ slug: answers.slug, brandValues, moduleFlags });
704
905
 
705
906
  await fs.writeFile(path.join(targetDir, ".env.example"), envFileContent);
706
- await fs.writeFile(path.join(targetDir, ".env.local"), envFileContent);
707
907
  await fs.writeFile(path.join(targetDir, ".gitignore"), createGitignore());
708
908
  await fs.writeFile(
709
909
  path.join(targetDir, "README.md"),
@@ -712,8 +912,13 @@ async function scaffoldPlatformProject({
712
912
  selectedModules,
713
913
  workspaceMode,
714
914
  packageManager,
915
+ dbInstallPlan,
715
916
  }),
716
917
  );
918
+
919
+ if (workspaceMode) {
920
+ await writeWorkspaceClientStack(workspaceRoot, answers.slug, selectedModules);
921
+ }
717
922
  }
718
923
 
719
924
  async function scaffoldSiteProject({
@@ -799,6 +1004,12 @@ export async function createBrightwebClientApp(argvOptions, runtimeOptions = {})
799
1004
  }
800
1005
 
801
1006
  const versionMap = await getVersionMap(workspaceRoot);
1007
+ const dbModuleRegistry = await getDbModuleRegistry(workspaceRoot);
1008
+ const dbInstallPlan = createDbInstallPlan({
1009
+ selectedModules: answers.selectedModules,
1010
+ workspaceMode,
1011
+ registry: dbModuleRegistry,
1012
+ });
802
1013
  const install = answers.install && !argvOptions.dryRun;
803
1014
 
804
1015
  if (argvOptions.dryRun) {
@@ -810,6 +1021,7 @@ export async function createBrightwebClientApp(argvOptions, runtimeOptions = {})
810
1021
  workspaceMode,
811
1022
  install: answers.install,
812
1023
  template: answers.template,
1024
+ dbInstallPlan,
813
1025
  })}\n\n`);
814
1026
  return {
815
1027
  answers,
@@ -821,6 +1033,17 @@ export async function createBrightwebClientApp(argvOptions, runtimeOptions = {})
821
1033
  };
822
1034
  }
823
1035
 
1036
+ output.write(`${renderPlanSummary({
1037
+ targetDir,
1038
+ dependencyMode,
1039
+ selectedModules: answers.selectedModules,
1040
+ packageManager,
1041
+ workspaceMode,
1042
+ install: answers.install,
1043
+ template: answers.template,
1044
+ dbInstallPlan,
1045
+ })}\n\n`);
1046
+
824
1047
  if (answers.template === "site") {
825
1048
  await scaffoldSiteProject({
826
1049
  targetDir,
@@ -838,7 +1061,9 @@ export async function createBrightwebClientApp(argvOptions, runtimeOptions = {})
838
1061
  dependencyMode,
839
1062
  packageManager,
840
1063
  workspaceMode,
1064
+ workspaceRoot,
841
1065
  answers,
1066
+ dbInstallPlan,
842
1067
  });
843
1068
  }
844
1069
 
@@ -115,7 +115,7 @@ export default function HomePage() {
115
115
  <li>`config/brand.ts` for client identity and contact details.</li>
116
116
  <li>`config/modules.ts` for enabled platform modules.</li>
117
117
  <li>`config/env.ts` for infra requirements and readiness checks.</li>
118
- <li>`.env.local` from `.env.example` for per-client secrets and flags.</li>
118
+ <li>`.env.example` for starter defaults; copy it to `.env.local` for per-client secrets and flags.</li>
119
119
  </ul>
120
120
  </div>
121
121
  </article>
@@ -69,7 +69,7 @@ export function getStarterBootstrapChecklist() {
69
69
  {
70
70
  label: "Create per-client environment variables",
71
71
  done: config.envReadiness.allReady,
72
- detail: "Copy `.env.example` into `.env.local` and fill the real values.",
72
+ detail: "Copy `.env.example` to `.env.local` and fill the real values.",
73
73
  },
74
74
  ],
75
75
  };
@@ -20,7 +20,7 @@ export default async function AdminPlaygroundPage() {
20
20
  <p className="eyebrow">Admin Module</p>
21
21
  <h1>Admin users and roles playground</h1>
22
22
  <p className="muted">
23
- This route previews the shared admin governance module without relying on the BeGreen client app.
23
+ This route previews the shared admin governance module without relying on any client-specific app.
24
24
  </p>
25
25
  </div>
26
26
  </article>