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
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({
|
|
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: ${
|
|
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
|
|