create-bw-app 0.3.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 +34 -0
- package/bin/create-bw-app.mjs +9 -0
- package/package.json +27 -0
- package/src/cli.mjs +78 -0
- package/src/constants.mjs +114 -0
- package/src/generator.mjs +821 -0
- package/template/base/app/bootstrap/page.tsx +75 -0
- package/template/base/app/globals.css +545 -0
- package/template/base/app/layout.tsx +16 -0
- package/template/base/app/page.tsx +144 -0
- package/template/base/app/playground/auth/auth-playground.tsx +112 -0
- package/template/base/app/playground/auth/page.tsx +5 -0
- package/template/base/app/playground/layout.tsx +41 -0
- package/template/base/app/preview/app-shell/page.tsx +11 -0
- package/template/base/app/preview/app-shell-preview.tsx +185 -0
- package/template/base/config/bootstrap.ts +125 -0
- package/template/base/config/brand.ts +21 -0
- package/template/base/config/client.ts +18 -0
- package/template/base/config/env.ts +64 -0
- package/template/base/config/modules.ts +62 -0
- package/template/base/next-env.d.ts +6 -0
- package/template/base/public/brand/logo-dark.svg +7 -0
- package/template/base/public/brand/logo-light.svg +7 -0
- package/template/base/public/brand/logo-mark.svg +5 -0
- package/template/base/tsconfig.json +36 -0
- package/template/modules/admin/app/api/admin/users/roles/route.ts +6 -0
- package/template/modules/admin/app/api/admin/users/route.ts +6 -0
- package/template/modules/admin/app/playground/admin/page.tsx +102 -0
- package/template/modules/crm/app/playground/crm/page.tsx +103 -0
- package/template/modules/projects/app/playground/projects/page.tsx +93 -0
- package/template/site/base/app/globals.css +68 -0
- package/template/site/base/app/layout.tsx +17 -0
- package/template/site/base/app/page.tsx +165 -0
- package/template/site/base/components/ui/badge.tsx +28 -0
- package/template/site/base/components/ui/button.tsx +52 -0
- package/template/site/base/components/ui/card.tsx +28 -0
- package/template/site/base/components.json +17 -0
- package/template/site/base/lib/utils.ts +6 -0
- package/template/site/base/next-env.d.ts +6 -0
- package/template/site/base/postcss.config.mjs +7 -0
- package/template/site/base/tsconfig.json +23 -0
|
@@ -0,0 +1,821 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { stdout as output } from "node:process";
|
|
6
|
+
import { checkbox, confirm, input as promptInput, select } from "@inquirer/prompts";
|
|
7
|
+
import {
|
|
8
|
+
APP_DEPENDENCY_DEFAULTS,
|
|
9
|
+
APP_DEV_DEPENDENCY_DEFAULTS,
|
|
10
|
+
CLI_DISPLAY_NAME,
|
|
11
|
+
CORE_PACKAGES,
|
|
12
|
+
DEFAULTS,
|
|
13
|
+
SELECTABLE_MODULES,
|
|
14
|
+
SITE_DEPENDENCY_DEFAULTS,
|
|
15
|
+
SITE_DEV_DEPENDENCY_DEFAULTS,
|
|
16
|
+
TEMPLATE_OPTIONS,
|
|
17
|
+
} from "./constants.mjs";
|
|
18
|
+
|
|
19
|
+
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
20
|
+
const TEMPLATE_ROOT = path.join(PACKAGE_ROOT, "template");
|
|
21
|
+
const TEMPLATE_KEY_SET = new Set(TEMPLATE_OPTIONS.map((templateOption) => templateOption.key));
|
|
22
|
+
|
|
23
|
+
function slugify(value) {
|
|
24
|
+
return value
|
|
25
|
+
.trim()
|
|
26
|
+
.toLowerCase()
|
|
27
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
28
|
+
.replace(/^-+|-+$/g, "")
|
|
29
|
+
.replace(/-{2,}/g, "-");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function titleizeSlug(value) {
|
|
33
|
+
return value
|
|
34
|
+
.split("-")
|
|
35
|
+
.filter(Boolean)
|
|
36
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
37
|
+
.join(" ");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parseModulesInput(rawValue) {
|
|
41
|
+
const normalized = rawValue.trim().toLowerCase();
|
|
42
|
+
if (!normalized || normalized === "all") {
|
|
43
|
+
return SELECTABLE_MODULES.map((moduleDefinition) => moduleDefinition.key);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (normalized === "none") {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const keys = normalized
|
|
51
|
+
.split(",")
|
|
52
|
+
.map((value) => value.trim())
|
|
53
|
+
.filter(Boolean);
|
|
54
|
+
|
|
55
|
+
const knownKeys = new Set(SELECTABLE_MODULES.map((moduleDefinition) => moduleDefinition.key));
|
|
56
|
+
const invalidKeys = keys.filter((key) => !knownKeys.has(key));
|
|
57
|
+
if (invalidKeys.length > 0) {
|
|
58
|
+
throw new Error(`Unknown module key(s): ${invalidKeys.join(", ")}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return Array.from(new Set(keys));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function parseTemplateInput(rawValue) {
|
|
65
|
+
const normalized = rawValue.trim().toLowerCase();
|
|
66
|
+
if (!normalized) return null;
|
|
67
|
+
if (normalized === "web") return "site";
|
|
68
|
+
if (TEMPLATE_KEY_SET.has(normalized)) return normalized;
|
|
69
|
+
throw new Error(`Unknown template: ${rawValue}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function detectPackageManager(explicitManager) {
|
|
73
|
+
if (explicitManager) return explicitManager;
|
|
74
|
+
|
|
75
|
+
const userAgent = process.env.npm_config_user_agent || "";
|
|
76
|
+
if (userAgent.startsWith("pnpm/")) return "pnpm";
|
|
77
|
+
if (userAgent.startsWith("npm/")) return "npm";
|
|
78
|
+
if (userAgent.startsWith("yarn/")) return "yarn";
|
|
79
|
+
if (userAgent.startsWith("bun/")) return "bun";
|
|
80
|
+
return "pnpm";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function sortObjectKeys(inputObject) {
|
|
84
|
+
return Object.fromEntries(
|
|
85
|
+
Object.entries(inputObject).sort(([leftKey], [rightKey]) => leftKey.localeCompare(rightKey)),
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function pathExists(targetPath) {
|
|
90
|
+
try {
|
|
91
|
+
await fs.access(targetPath);
|
|
92
|
+
return true;
|
|
93
|
+
} catch {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function readJsonIfPresent(filePath) {
|
|
99
|
+
if (!(await pathExists(filePath))) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return JSON.parse(await fs.readFile(filePath, "utf8"));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function getVersionMap(workspaceRoot) {
|
|
107
|
+
const versionMap = {
|
|
108
|
+
...APP_DEPENDENCY_DEFAULTS,
|
|
109
|
+
...APP_DEV_DEPENDENCY_DEFAULTS,
|
|
110
|
+
...SITE_DEPENDENCY_DEFAULTS,
|
|
111
|
+
...SITE_DEV_DEPENDENCY_DEFAULTS,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
if (!workspaceRoot) {
|
|
115
|
+
return versionMap;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const starterManifestPath = path.join(workspaceRoot, "apps", "starter-site", "package.json");
|
|
119
|
+
const starterManifest = await readJsonIfPresent(starterManifestPath);
|
|
120
|
+
if (starterManifest) {
|
|
121
|
+
Object.assign(versionMap, starterManifest.dependencies || {}, starterManifest.devDependencies || {});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
for (const packageName of [
|
|
125
|
+
"@brightweblabs/app-shell",
|
|
126
|
+
"@brightweblabs/core-auth",
|
|
127
|
+
"@brightweblabs/infra",
|
|
128
|
+
"@brightweblabs/module-admin",
|
|
129
|
+
"@brightweblabs/module-crm",
|
|
130
|
+
"@brightweblabs/module-projects",
|
|
131
|
+
"@brightweblabs/ui",
|
|
132
|
+
]) {
|
|
133
|
+
const folderName = packageName.replace("@brightweblabs/", "");
|
|
134
|
+
const packageManifestPath = path.join(workspaceRoot, "packages", folderName, "package.json");
|
|
135
|
+
const packageManifest = await readJsonIfPresent(packageManifestPath);
|
|
136
|
+
if (packageManifest?.version) {
|
|
137
|
+
versionMap[packageName] = `^${packageManifest.version}`;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return versionMap;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function createModuleFlags(selectedModules) {
|
|
145
|
+
const selected = new Set(selectedModules);
|
|
146
|
+
return Object.fromEntries(
|
|
147
|
+
SELECTABLE_MODULES.map((moduleDefinition) => [moduleDefinition.key, selected.has(moduleDefinition.key)]),
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function createDerivedBrandValues(slug) {
|
|
152
|
+
const projectName = titleizeSlug(slug);
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
companyName: projectName,
|
|
156
|
+
productName: `${projectName} ${DEFAULTS.productNameSuffix}`,
|
|
157
|
+
tagline: DEFAULTS.tagline,
|
|
158
|
+
contactEmail: DEFAULTS.contactEmail,
|
|
159
|
+
supportEmail: DEFAULTS.supportEmail,
|
|
160
|
+
primaryHex: DEFAULTS.primaryHex,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function createEnvFileContent({ slug, brandValues, moduleFlags }) {
|
|
165
|
+
return [
|
|
166
|
+
"NEXT_PUBLIC_APP_URL=http://localhost:3000",
|
|
167
|
+
"NEXT_PUBLIC_SUPABASE_URL=",
|
|
168
|
+
"NEXT_PUBLIC_SUPABASE_ANON_KEY=",
|
|
169
|
+
"SUPABASE_SERVICE_ROLE_KEY=",
|
|
170
|
+
"RESEND_API_KEY=",
|
|
171
|
+
"",
|
|
172
|
+
`NEXT_PUBLIC_CLIENT_COMPANY_NAME=${brandValues.companyName}`,
|
|
173
|
+
`NEXT_PUBLIC_CLIENT_PRODUCT_NAME=${brandValues.productName}`,
|
|
174
|
+
`NEXT_PUBLIC_CLIENT_SLUG=${slug}`,
|
|
175
|
+
`NEXT_PUBLIC_CLIENT_TAGLINE=${brandValues.tagline}`,
|
|
176
|
+
`NEXT_PUBLIC_CLIENT_CONTACT_EMAIL=${brandValues.contactEmail}`,
|
|
177
|
+
`NEXT_PUBLIC_CLIENT_SUPPORT_EMAIL=${brandValues.supportEmail}`,
|
|
178
|
+
`NEXT_PUBLIC_CLIENT_PRIMARY_HEX=${brandValues.primaryHex}`,
|
|
179
|
+
"",
|
|
180
|
+
`NEXT_PUBLIC_ENABLE_CRM=${String(moduleFlags.crm)}`,
|
|
181
|
+
`NEXT_PUBLIC_ENABLE_PROJECTS=${String(moduleFlags.projects)}`,
|
|
182
|
+
`NEXT_PUBLIC_ENABLE_ADMIN=${String(moduleFlags.admin)}`,
|
|
183
|
+
"",
|
|
184
|
+
].join("\n");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function createGitignore() {
|
|
188
|
+
return [".DS_Store", "node_modules", ".next", "dist", "coverage", "*.log", "*.tsbuildinfo", ".env*.local", "!.env.example", ""].join("\n");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function createPlatformReadme({
|
|
192
|
+
slug,
|
|
193
|
+
selectedModules,
|
|
194
|
+
workspaceMode,
|
|
195
|
+
packageManager,
|
|
196
|
+
}) {
|
|
197
|
+
const moduleLines = SELECTABLE_MODULES.map((moduleDefinition) => {
|
|
198
|
+
const enabled = selectedModules.includes(moduleDefinition.key);
|
|
199
|
+
return `- ${moduleDefinition.key}: ${enabled ? "enabled" : "disabled"}`;
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const localSteps = workspaceMode
|
|
203
|
+
? [
|
|
204
|
+
"1. Review `.env.example`, then fill `.env.local` with real service credentials.",
|
|
205
|
+
"2. Run `pnpm install` from the BrightWeb workspace root.",
|
|
206
|
+
`3. Run \`pnpm --filter ${slug} dev\`.`,
|
|
207
|
+
]
|
|
208
|
+
: [
|
|
209
|
+
"1. Review `.env.example`, then fill `.env.local` with real service credentials.",
|
|
210
|
+
`2. Run \`${packageManager} install\`.`,
|
|
211
|
+
`3. Run \`${packageManager} dev\`.`,
|
|
212
|
+
];
|
|
213
|
+
|
|
214
|
+
return [
|
|
215
|
+
`# ${titleizeSlug(slug)}`,
|
|
216
|
+
"",
|
|
217
|
+
`Project scaffolded with ${CLI_DISPLAY_NAME}.`,
|
|
218
|
+
"",
|
|
219
|
+
"## Local setup",
|
|
220
|
+
"",
|
|
221
|
+
...localSteps,
|
|
222
|
+
"",
|
|
223
|
+
"## Enabled modules",
|
|
224
|
+
"",
|
|
225
|
+
...moduleLines,
|
|
226
|
+
"",
|
|
227
|
+
"## Starter routes",
|
|
228
|
+
"",
|
|
229
|
+
"- `/`",
|
|
230
|
+
"- `/bootstrap`",
|
|
231
|
+
"- `/preview/app-shell`",
|
|
232
|
+
"- `/playground/auth`",
|
|
233
|
+
...selectedModules.map((moduleKey) => `- /playground/${moduleKey}`),
|
|
234
|
+
"",
|
|
235
|
+
].join("\n");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function createSiteReadme({ slug, workspaceMode, packageManager }) {
|
|
239
|
+
const localSteps = workspaceMode
|
|
240
|
+
? [
|
|
241
|
+
"1. Run `pnpm install` from the BrightWeb workspace root.",
|
|
242
|
+
`2. Run \`pnpm --filter ${slug} dev\`.`,
|
|
243
|
+
]
|
|
244
|
+
: [
|
|
245
|
+
`1. Run \`${packageManager} install\`.`,
|
|
246
|
+
`2. Run \`${packageManager} dev\`.`,
|
|
247
|
+
];
|
|
248
|
+
|
|
249
|
+
return [
|
|
250
|
+
`# ${titleizeSlug(slug)}`,
|
|
251
|
+
"",
|
|
252
|
+
`Site scaffolded with ${CLI_DISPLAY_NAME}.`,
|
|
253
|
+
"",
|
|
254
|
+
"## Stack",
|
|
255
|
+
"",
|
|
256
|
+
"- Next.js App Router",
|
|
257
|
+
"- Tailwind CSS v4",
|
|
258
|
+
"- Local shadcn-style component primitives",
|
|
259
|
+
"",
|
|
260
|
+
"## Local setup",
|
|
261
|
+
"",
|
|
262
|
+
...localSteps,
|
|
263
|
+
"",
|
|
264
|
+
"## Starter surfaces",
|
|
265
|
+
"",
|
|
266
|
+
"- `/`",
|
|
267
|
+
"",
|
|
268
|
+
"Edit `config/site.ts` to change the site name, copy, and public links.",
|
|
269
|
+
"",
|
|
270
|
+
].join("\n");
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function createPackageJson({
|
|
274
|
+
slug,
|
|
275
|
+
dependencyMode,
|
|
276
|
+
selectedModules,
|
|
277
|
+
versionMap,
|
|
278
|
+
template,
|
|
279
|
+
}) {
|
|
280
|
+
if (template === "site") {
|
|
281
|
+
return {
|
|
282
|
+
name: slug,
|
|
283
|
+
private: true,
|
|
284
|
+
version: "0.0.0",
|
|
285
|
+
scripts: {
|
|
286
|
+
dev: "next dev",
|
|
287
|
+
build: "next build",
|
|
288
|
+
start: "next start",
|
|
289
|
+
lint: "tsc --noEmit",
|
|
290
|
+
},
|
|
291
|
+
dependencies: sortObjectKeys({
|
|
292
|
+
"class-variance-authority": versionMap["class-variance-authority"],
|
|
293
|
+
"clsx": versionMap.clsx,
|
|
294
|
+
"lucide-react": versionMap["lucide-react"],
|
|
295
|
+
"next": versionMap.next,
|
|
296
|
+
"react": versionMap.react,
|
|
297
|
+
"react-dom": versionMap["react-dom"],
|
|
298
|
+
"tailwind-merge": versionMap["tailwind-merge"],
|
|
299
|
+
}),
|
|
300
|
+
devDependencies: sortObjectKeys({
|
|
301
|
+
"@tailwindcss/postcss": versionMap["@tailwindcss/postcss"],
|
|
302
|
+
"@types/node": versionMap["@types/node"],
|
|
303
|
+
"@types/react": versionMap["@types/react"],
|
|
304
|
+
"@types/react-dom": versionMap["@types/react-dom"],
|
|
305
|
+
"postcss": versionMap.postcss,
|
|
306
|
+
"tailwindcss": versionMap.tailwindcss,
|
|
307
|
+
"typescript": versionMap.typescript,
|
|
308
|
+
}),
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const internalDependencyVersion = (packageName) =>
|
|
313
|
+
dependencyMode === "workspace" ? "workspace:*" : versionMap[packageName];
|
|
314
|
+
|
|
315
|
+
const dependencies = {
|
|
316
|
+
"@brightweblabs/app-shell": internalDependencyVersion("@brightweblabs/app-shell"),
|
|
317
|
+
"@brightweblabs/core-auth": internalDependencyVersion("@brightweblabs/core-auth"),
|
|
318
|
+
"@brightweblabs/infra": internalDependencyVersion("@brightweblabs/infra"),
|
|
319
|
+
"@brightweblabs/ui": internalDependencyVersion("@brightweblabs/ui"),
|
|
320
|
+
"lucide-react": versionMap["lucide-react"],
|
|
321
|
+
"next": versionMap.next,
|
|
322
|
+
"react": versionMap.react,
|
|
323
|
+
"react-dom": versionMap["react-dom"],
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
for (const moduleDefinition of SELECTABLE_MODULES) {
|
|
327
|
+
if (selectedModules.includes(moduleDefinition.key)) {
|
|
328
|
+
dependencies[moduleDefinition.packageName] = internalDependencyVersion(moduleDefinition.packageName);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
name: slug,
|
|
334
|
+
private: true,
|
|
335
|
+
version: "0.0.0",
|
|
336
|
+
scripts: {
|
|
337
|
+
dev: "next dev",
|
|
338
|
+
build: "next build",
|
|
339
|
+
start: "next start",
|
|
340
|
+
lint: "tsc --noEmit",
|
|
341
|
+
},
|
|
342
|
+
dependencies: sortObjectKeys(dependencies),
|
|
343
|
+
devDependencies: sortObjectKeys({
|
|
344
|
+
"@types/node": versionMap["@types/node"],
|
|
345
|
+
"@types/react": versionMap["@types/react"],
|
|
346
|
+
"@types/react-dom": versionMap["@types/react-dom"],
|
|
347
|
+
typescript: versionMap.typescript,
|
|
348
|
+
}),
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function createNextConfig({ template, selectedModules }) {
|
|
353
|
+
if (template === "site") {
|
|
354
|
+
return [
|
|
355
|
+
'import type { NextConfig } from "next";',
|
|
356
|
+
"",
|
|
357
|
+
"const nextConfig: NextConfig = {};",
|
|
358
|
+
"",
|
|
359
|
+
"export default nextConfig;",
|
|
360
|
+
"",
|
|
361
|
+
].join("\n");
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const transpilePackages = [...CORE_PACKAGES];
|
|
365
|
+
|
|
366
|
+
for (const moduleDefinition of SELECTABLE_MODULES) {
|
|
367
|
+
if (selectedModules.includes(moduleDefinition.key)) {
|
|
368
|
+
transpilePackages.push(moduleDefinition.packageName);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return [
|
|
373
|
+
'import type { NextConfig } from "next";',
|
|
374
|
+
"",
|
|
375
|
+
"const nextConfig: NextConfig = {",
|
|
376
|
+
" transpilePackages: [",
|
|
377
|
+
...transpilePackages.map((packageName) => ` "${packageName}",`),
|
|
378
|
+
" ],",
|
|
379
|
+
"};",
|
|
380
|
+
"",
|
|
381
|
+
"export default nextConfig;",
|
|
382
|
+
"",
|
|
383
|
+
].join("\n");
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function createShellConfig(selectedModules) {
|
|
387
|
+
const importLines = [];
|
|
388
|
+
const registrationLines = [];
|
|
389
|
+
|
|
390
|
+
if (selectedModules.includes("admin")) {
|
|
391
|
+
importLines.push('import { adminModuleRegistration } from "@brightweblabs/module-admin/registration";');
|
|
392
|
+
registrationLines.push(' if (enabled.has("admin")) registrations.push(adminModuleRegistration);');
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (selectedModules.includes("crm")) {
|
|
396
|
+
importLines.push('import { crmModuleRegistration } from "@brightweblabs/module-crm/registration";');
|
|
397
|
+
registrationLines.push(' if (enabled.has("crm")) registrations.push(crmModuleRegistration);');
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (selectedModules.includes("projects")) {
|
|
401
|
+
importLines.push('import { projectsModuleRegistration } from "@brightweblabs/module-projects/registration";');
|
|
402
|
+
registrationLines.push(' if (enabled.has("projects")) registrations.push(projectsModuleRegistration);');
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return [
|
|
406
|
+
'import { LayoutDashboard, Wrench } from "lucide-react";',
|
|
407
|
+
"import {",
|
|
408
|
+
" buildClientAppShellRegistration,",
|
|
409
|
+
" resolveClientAppShellConfig,",
|
|
410
|
+
" type ClientAppShellRegistration,",
|
|
411
|
+
" type ShellContextualAction,",
|
|
412
|
+
" type ShellModuleRegistration,",
|
|
413
|
+
'} from "@brightweblabs/app-shell";',
|
|
414
|
+
...importLines,
|
|
415
|
+
'import { starterBrandConfig } from "./brand";',
|
|
416
|
+
'import { getEnabledStarterModules } from "./modules";',
|
|
417
|
+
"",
|
|
418
|
+
"const dashboardModuleRegistration: ShellModuleRegistration<ShellContextualAction> = {",
|
|
419
|
+
' key: "dashboard",',
|
|
420
|
+
' placement: "primary",',
|
|
421
|
+
' navItems: [{ href: "/dashboard", label: "Dashboard", icon: LayoutDashboard }],',
|
|
422
|
+
' toolbarRoutes: [{ surface: "dashboard", match: { exact: ["/dashboard"] } }],',
|
|
423
|
+
"};",
|
|
424
|
+
"",
|
|
425
|
+
"function getStarterModuleRegistrations() {",
|
|
426
|
+
' const enabled = new Set(getEnabledStarterModules().map((moduleConfig) => moduleConfig.key));',
|
|
427
|
+
" const registrations: ShellModuleRegistration<ShellContextualAction>[] = [dashboardModuleRegistration];",
|
|
428
|
+
"",
|
|
429
|
+
...registrationLines,
|
|
430
|
+
"",
|
|
431
|
+
" return registrations;",
|
|
432
|
+
"}",
|
|
433
|
+
"",
|
|
434
|
+
"export function getStarterShellConfig() {",
|
|
435
|
+
" const enabledModules = getEnabledStarterModules();",
|
|
436
|
+
" const shellRegistration: ClientAppShellRegistration<ShellContextualAction> = {",
|
|
437
|
+
" brand: {",
|
|
438
|
+
' href: "/",',
|
|
439
|
+
' ariaLabel: `${starterBrandConfig.companyName} public site`,',
|
|
440
|
+
" alt: starterBrandConfig.companyName,",
|
|
441
|
+
" collapsedLogo: {",
|
|
442
|
+
' src: "/brand/logo-mark.svg",',
|
|
443
|
+
" width: 48,",
|
|
444
|
+
" height: 48,",
|
|
445
|
+
" },",
|
|
446
|
+
" lightLogo: {",
|
|
447
|
+
' src: "/brand/logo-light.svg",',
|
|
448
|
+
" width: 176,",
|
|
449
|
+
" height: 44,",
|
|
450
|
+
" },",
|
|
451
|
+
" darkLogo: {",
|
|
452
|
+
' src: "/brand/logo-dark.svg",',
|
|
453
|
+
" width: 176,",
|
|
454
|
+
" height: 44,",
|
|
455
|
+
" },",
|
|
456
|
+
" },",
|
|
457
|
+
" toolsSection: {",
|
|
458
|
+
' key: "tools",',
|
|
459
|
+
' label: "Ferramentas",',
|
|
460
|
+
" icon: Wrench,",
|
|
461
|
+
' collapsedHref: enabledModules.find((moduleConfig) => moduleConfig.playgroundHref)?.playgroundHref || "/",',
|
|
462
|
+
" },",
|
|
463
|
+
" modules: getStarterModuleRegistrations(),",
|
|
464
|
+
" };",
|
|
465
|
+
"",
|
|
466
|
+
" const builtRegistration = buildClientAppShellRegistration(shellRegistration);",
|
|
467
|
+
" const shellPreview = resolveClientAppShellConfig(builtRegistration.shellConfig, {",
|
|
468
|
+
" isAdmin: true,",
|
|
469
|
+
" isStaff: true,",
|
|
470
|
+
" });",
|
|
471
|
+
"",
|
|
472
|
+
" return {",
|
|
473
|
+
" enabledModules,",
|
|
474
|
+
" shellConfig: builtRegistration.shellConfig,",
|
|
475
|
+
" shellPreview,",
|
|
476
|
+
" toolbarRoutes: builtRegistration.toolbarRoutes,",
|
|
477
|
+
" };",
|
|
478
|
+
"}",
|
|
479
|
+
"",
|
|
480
|
+
].join("\n");
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function createSiteConfigFile(slug) {
|
|
484
|
+
const siteName = titleizeSlug(slug);
|
|
485
|
+
|
|
486
|
+
return [
|
|
487
|
+
"export const siteConfig = {",
|
|
488
|
+
` name: "${siteName}",`,
|
|
489
|
+
' description: "A refined BrightWeb site starter built with Next.js and Tailwind CSS.",',
|
|
490
|
+
' eyebrow: "BrightWeb Site Starter",',
|
|
491
|
+
' primaryCta: { label: "Start a project", href: "mailto:hello@example.com" },',
|
|
492
|
+
' secondaryCta: { label: "See the build notes", href: "#process" },',
|
|
493
|
+
"};",
|
|
494
|
+
"",
|
|
495
|
+
].join("\n");
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
async function copyDirectory(sourceDir, targetDir) {
|
|
499
|
+
await fs.cp(sourceDir, targetDir, { recursive: true });
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
async function ensureDirectory(targetDir) {
|
|
503
|
+
await fs.mkdir(targetDir, { recursive: true });
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
async function runInstall(command, cwd) {
|
|
507
|
+
return new Promise((resolve, reject) => {
|
|
508
|
+
const child = spawn(command, ["install"], {
|
|
509
|
+
cwd,
|
|
510
|
+
stdio: "inherit",
|
|
511
|
+
shell: process.platform === "win32",
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
child.on("error", reject);
|
|
515
|
+
child.on("exit", (code) => {
|
|
516
|
+
if (code === 0) {
|
|
517
|
+
resolve();
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
reject(new Error(`Install command failed with exit code ${code ?? "unknown"}.`));
|
|
522
|
+
});
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function renderPlanSummary({ targetDir, dependencyMode, selectedModules, packageManager, workspaceMode, install, template }) {
|
|
527
|
+
const installLocation = workspaceMode ? "workspace root" : "project directory";
|
|
528
|
+
const templateLabel = TEMPLATE_OPTIONS.find((templateOption) => templateOption.key === template)?.label || template;
|
|
529
|
+
|
|
530
|
+
return [
|
|
531
|
+
`Template: ${templateLabel}`,
|
|
532
|
+
`Target directory: ${targetDir}`,
|
|
533
|
+
`Dependency mode: ${dependencyMode}`,
|
|
534
|
+
`Selected modules: ${template === "platform" ? (selectedModules.length > 0 ? selectedModules.join(", ") : "none") : "n/a"}`,
|
|
535
|
+
`Install dependencies: ${install ? `yes (${packageManager} in ${installLocation})` : "no"}`,
|
|
536
|
+
].join("\n");
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
async function collectAnswers(argvOptions, runtimeOptions) {
|
|
540
|
+
const slugFromFlag = argvOptions.name || argvOptions.slug;
|
|
541
|
+
const slug = slugify(slugFromFlag || "");
|
|
542
|
+
|
|
543
|
+
if (!slug && argvOptions.yes) {
|
|
544
|
+
throw new Error("`--yes` requires `--name` or `--slug`.");
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
output.write(`\n${runtimeOptions.banner || "BrightWeb app installer"}\n\n`);
|
|
548
|
+
|
|
549
|
+
const templateFromFlag = argvOptions.template ? parseTemplateInput(argvOptions.template) : null;
|
|
550
|
+
const template = templateFromFlag
|
|
551
|
+
|| (argvOptions.yes
|
|
552
|
+
? "platform"
|
|
553
|
+
: await select({
|
|
554
|
+
message: "What kind of app are you creating?",
|
|
555
|
+
default: "platform",
|
|
556
|
+
choices: TEMPLATE_OPTIONS.map((templateOption) => ({
|
|
557
|
+
value: templateOption.key,
|
|
558
|
+
name: templateOption.label,
|
|
559
|
+
description: templateOption.description,
|
|
560
|
+
})),
|
|
561
|
+
}));
|
|
562
|
+
|
|
563
|
+
const resolvedSlug = slug || slugify(await promptInput({ message: "What is your project named?" }));
|
|
564
|
+
if (!resolvedSlug) {
|
|
565
|
+
throw new Error("A valid project name is required.");
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
let selectedModules = [];
|
|
569
|
+
|
|
570
|
+
if (template === "platform") {
|
|
571
|
+
const askedModules = [];
|
|
572
|
+
if (!argvOptions.modules && !argvOptions.yes) {
|
|
573
|
+
const selectedByPrompt = await checkbox({
|
|
574
|
+
message: "Which modules should be installed?",
|
|
575
|
+
choices: SELECTABLE_MODULES.map((moduleDefinition) => ({
|
|
576
|
+
value: moduleDefinition.key,
|
|
577
|
+
name: moduleDefinition.label,
|
|
578
|
+
description: `Install ${moduleDefinition.label} routes, config wiring, and package dependencies.`,
|
|
579
|
+
})),
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
askedModules.push(...selectedByPrompt);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
selectedModules = argvOptions.modules
|
|
586
|
+
? parseModulesInput(argvOptions.modules)
|
|
587
|
+
: argvOptions.yes
|
|
588
|
+
? []
|
|
589
|
+
: askedModules;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const install =
|
|
593
|
+
typeof argvOptions.install === "boolean"
|
|
594
|
+
? argvOptions.install
|
|
595
|
+
: argvOptions.yes
|
|
596
|
+
? true
|
|
597
|
+
: await confirm({
|
|
598
|
+
message: "Install dependencies now?",
|
|
599
|
+
default: true,
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
return {
|
|
603
|
+
slug: resolvedSlug,
|
|
604
|
+
template,
|
|
605
|
+
selectedModules,
|
|
606
|
+
install,
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function resolveTargetDirectory(runtimeOptions, answers) {
|
|
611
|
+
if (runtimeOptions.targetDir) {
|
|
612
|
+
return path.resolve(runtimeOptions.targetDir);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const outputRoot = path.resolve(runtimeOptions.outputDir || process.cwd());
|
|
616
|
+
return path.join(outputRoot, answers.slug);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
async function scaffoldPlatformProject({
|
|
620
|
+
targetDir,
|
|
621
|
+
selectedModules,
|
|
622
|
+
versionMap,
|
|
623
|
+
dependencyMode,
|
|
624
|
+
packageManager,
|
|
625
|
+
workspaceMode,
|
|
626
|
+
answers,
|
|
627
|
+
}) {
|
|
628
|
+
const moduleFlags = createModuleFlags(selectedModules);
|
|
629
|
+
const brandValues = createDerivedBrandValues(answers.slug);
|
|
630
|
+
const baseTemplateDir = path.join(TEMPLATE_ROOT, "base");
|
|
631
|
+
|
|
632
|
+
await ensureDirectory(path.dirname(targetDir));
|
|
633
|
+
await copyDirectory(baseTemplateDir, targetDir);
|
|
634
|
+
|
|
635
|
+
for (const moduleDefinition of SELECTABLE_MODULES) {
|
|
636
|
+
if (!selectedModules.includes(moduleDefinition.key)) continue;
|
|
637
|
+
const moduleTemplateDir = path.join(TEMPLATE_ROOT, "modules", moduleDefinition.templateFolder);
|
|
638
|
+
await copyDirectory(moduleTemplateDir, targetDir);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
await fs.writeFile(
|
|
642
|
+
path.join(targetDir, "package.json"),
|
|
643
|
+
`${JSON.stringify(
|
|
644
|
+
createPackageJson({
|
|
645
|
+
slug: answers.slug,
|
|
646
|
+
dependencyMode,
|
|
647
|
+
selectedModules,
|
|
648
|
+
versionMap,
|
|
649
|
+
template: "platform",
|
|
650
|
+
}),
|
|
651
|
+
null,
|
|
652
|
+
2,
|
|
653
|
+
)}\n`,
|
|
654
|
+
);
|
|
655
|
+
await fs.writeFile(path.join(targetDir, "next.config.ts"), createNextConfig({ template: "platform", selectedModules }));
|
|
656
|
+
await fs.writeFile(path.join(targetDir, "config", "shell.ts"), createShellConfig(selectedModules));
|
|
657
|
+
|
|
658
|
+
const envFileContent = createEnvFileContent({ slug: answers.slug, brandValues, moduleFlags });
|
|
659
|
+
|
|
660
|
+
await fs.writeFile(path.join(targetDir, ".env.example"), envFileContent);
|
|
661
|
+
await fs.writeFile(path.join(targetDir, ".env.local"), envFileContent);
|
|
662
|
+
await fs.writeFile(path.join(targetDir, ".gitignore"), createGitignore());
|
|
663
|
+
await fs.writeFile(
|
|
664
|
+
path.join(targetDir, "README.md"),
|
|
665
|
+
createPlatformReadme({
|
|
666
|
+
slug: answers.slug,
|
|
667
|
+
selectedModules,
|
|
668
|
+
workspaceMode,
|
|
669
|
+
packageManager,
|
|
670
|
+
}),
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
async function scaffoldSiteProject({
|
|
675
|
+
targetDir,
|
|
676
|
+
versionMap,
|
|
677
|
+
dependencyMode,
|
|
678
|
+
packageManager,
|
|
679
|
+
workspaceMode,
|
|
680
|
+
answers,
|
|
681
|
+
}) {
|
|
682
|
+
void dependencyMode;
|
|
683
|
+
|
|
684
|
+
const baseTemplateDir = path.join(TEMPLATE_ROOT, "site", "base");
|
|
685
|
+
|
|
686
|
+
await ensureDirectory(path.dirname(targetDir));
|
|
687
|
+
await copyDirectory(baseTemplateDir, targetDir);
|
|
688
|
+
await ensureDirectory(path.join(targetDir, "config"));
|
|
689
|
+
|
|
690
|
+
await fs.writeFile(
|
|
691
|
+
path.join(targetDir, "package.json"),
|
|
692
|
+
`${JSON.stringify(
|
|
693
|
+
createPackageJson({
|
|
694
|
+
slug: answers.slug,
|
|
695
|
+
dependencyMode: "published",
|
|
696
|
+
selectedModules: [],
|
|
697
|
+
versionMap,
|
|
698
|
+
template: "site",
|
|
699
|
+
}),
|
|
700
|
+
null,
|
|
701
|
+
2,
|
|
702
|
+
)}\n`,
|
|
703
|
+
);
|
|
704
|
+
await fs.writeFile(path.join(targetDir, "next.config.ts"), createNextConfig({ template: "site", selectedModules: [] }));
|
|
705
|
+
await fs.writeFile(path.join(targetDir, ".gitignore"), createGitignore());
|
|
706
|
+
await fs.writeFile(path.join(targetDir, "config", "site.ts"), createSiteConfigFile(answers.slug));
|
|
707
|
+
await fs.writeFile(
|
|
708
|
+
path.join(targetDir, "README.md"),
|
|
709
|
+
createSiteReadme({
|
|
710
|
+
slug: answers.slug,
|
|
711
|
+
workspaceMode,
|
|
712
|
+
packageManager,
|
|
713
|
+
}),
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function printCompletionMessage({ targetDir, workspaceMode, slug, packageManager, install }) {
|
|
718
|
+
const relativeTargetDir = path.relative(process.cwd(), targetDir) || path.basename(targetDir);
|
|
719
|
+
|
|
720
|
+
output.write(`\nCreated app at ${relativeTargetDir}\n`);
|
|
721
|
+
if (!install) {
|
|
722
|
+
output.write("Dependencies were not installed.\n");
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
if (workspaceMode) {
|
|
726
|
+
output.write(`Next step: pnpm --filter ${slug} dev\n\n`);
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
output.write(`Next step: cd ${targetDir} && ${packageManager} dev\n\n`);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
export async function createBrightwebClientApp(argvOptions, runtimeOptions = {}) {
|
|
734
|
+
const dependencyMode = runtimeOptions.dependencyMode || argvOptions.dependencyMode || "published";
|
|
735
|
+
const workspaceRoot = runtimeOptions.workspaceRoot
|
|
736
|
+
? path.resolve(runtimeOptions.workspaceRoot)
|
|
737
|
+
: argvOptions.workspaceRoot
|
|
738
|
+
? path.resolve(argvOptions.workspaceRoot)
|
|
739
|
+
: null;
|
|
740
|
+
const workspaceMode = dependencyMode === "workspace";
|
|
741
|
+
const packageManager = detectPackageManager(workspaceMode ? "pnpm" : argvOptions.packageManager);
|
|
742
|
+
|
|
743
|
+
const answers = await collectAnswers(argvOptions, runtimeOptions);
|
|
744
|
+
const targetDir = resolveTargetDirectory(
|
|
745
|
+
{
|
|
746
|
+
outputDir: runtimeOptions.outputDir || argvOptions.outputDir,
|
|
747
|
+
targetDir: runtimeOptions.targetDir || argvOptions.targetDir,
|
|
748
|
+
},
|
|
749
|
+
answers,
|
|
750
|
+
);
|
|
751
|
+
|
|
752
|
+
if (await pathExists(targetDir)) {
|
|
753
|
+
throw new Error(`Target directory already exists: ${targetDir}`);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
const versionMap = await getVersionMap(workspaceRoot);
|
|
757
|
+
const install = answers.install && !argvOptions.dryRun;
|
|
758
|
+
|
|
759
|
+
if (argvOptions.dryRun) {
|
|
760
|
+
output.write(`${renderPlanSummary({
|
|
761
|
+
targetDir,
|
|
762
|
+
dependencyMode,
|
|
763
|
+
selectedModules: answers.selectedModules,
|
|
764
|
+
packageManager,
|
|
765
|
+
workspaceMode,
|
|
766
|
+
install: answers.install,
|
|
767
|
+
template: answers.template,
|
|
768
|
+
})}\n\n`);
|
|
769
|
+
return {
|
|
770
|
+
answers,
|
|
771
|
+
targetDir,
|
|
772
|
+
dependencyMode,
|
|
773
|
+
workspaceMode,
|
|
774
|
+
packageManager,
|
|
775
|
+
dryRun: true,
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
if (answers.template === "site") {
|
|
780
|
+
await scaffoldSiteProject({
|
|
781
|
+
targetDir,
|
|
782
|
+
versionMap,
|
|
783
|
+
dependencyMode,
|
|
784
|
+
packageManager,
|
|
785
|
+
workspaceMode,
|
|
786
|
+
answers,
|
|
787
|
+
});
|
|
788
|
+
} else {
|
|
789
|
+
await scaffoldPlatformProject({
|
|
790
|
+
targetDir,
|
|
791
|
+
selectedModules: answers.selectedModules,
|
|
792
|
+
versionMap,
|
|
793
|
+
dependencyMode,
|
|
794
|
+
packageManager,
|
|
795
|
+
workspaceMode,
|
|
796
|
+
answers,
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
if (install) {
|
|
801
|
+
const installCwd = workspaceMode ? workspaceRoot : targetDir;
|
|
802
|
+
await runInstall(packageManager, installCwd);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
printCompletionMessage({
|
|
806
|
+
targetDir,
|
|
807
|
+
workspaceMode,
|
|
808
|
+
slug: answers.slug,
|
|
809
|
+
packageManager,
|
|
810
|
+
install,
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
return {
|
|
814
|
+
answers,
|
|
815
|
+
targetDir,
|
|
816
|
+
dependencyMode,
|
|
817
|
+
workspaceMode,
|
|
818
|
+
packageManager,
|
|
819
|
+
dryRun: false,
|
|
820
|
+
};
|
|
821
|
+
}
|