create-bw-app 0.13.2 → 0.14.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 +2 -2
- package/package.json +1 -1
- package/src/app-manifest.mjs +4 -2
- package/src/constants.mjs +95 -18
- package/src/doctor.mjs +38 -0
- package/src/generator.mjs +75 -19
- package/template/base/app/(auth)/auth/confirmed/page.tsx +1 -0
- package/template/base/app/(auth)/auth/post-login/page.tsx +1 -0
- package/template/base/app/(auth)/auth-provider.tsx +25 -0
- package/template/base/app/(auth)/forgot-password/page.tsx +1 -0
- package/template/base/app/(auth)/invite/[invitationId]/invite-route-mount.tsx +10 -0
- package/template/base/app/(auth)/invite/[invitationId]/page.tsx +1 -0
- package/template/base/app/(auth)/layout.tsx +5 -0
- package/template/base/app/(auth)/login/page.tsx +1 -0
- package/template/base/app/(auth)/reset-password/page.tsx +1 -0
- package/template/base/app/(auth)/signup/page.tsx +1 -0
- package/template/base/app/(shell)/account/page.tsx +5 -0
- package/template/base/app/(shell)/dashboard/dashboard-live-mount.tsx +21 -0
- package/template/base/app/(shell)/dashboard/page.tsx +1 -0
- package/template/base/app/(shell)/layout.tsx +24 -0
- package/template/base/app/(shell)/shell-layout-client.tsx +134 -0
- package/template/base/app/api/account/route.ts +2 -0
- package/template/base/app/auth/callback/route.ts +1 -0
- package/template/base/app/auth/cleanup/route.ts +1 -0
- package/template/base/app/layout.tsx +8 -2
- package/template/base/config/env.ts +98 -0
- package/template/module-manifests/marketing/brightweb.module.json +12 -0
- package/template/modules/marketing/app/(shell)/marketing/page.tsx +1 -0
- package/template/modules/marketing/app/api/marketing/_handlers.ts +144 -0
- package/template/modules/marketing/app/api/marketing/analytics/campaigns/[id]/route.ts +2 -0
- package/template/modules/marketing/app/api/marketing/analytics/overview/route.ts +2 -0
- package/template/modules/marketing/app/api/marketing/analytics/segments/[id]/route.ts +2 -0
- package/template/modules/marketing/app/api/marketing/campaigns/[id]/cancel/route.ts +2 -0
- package/template/modules/marketing/app/api/marketing/campaigns/[id]/recipients/route.ts +2 -0
- package/template/modules/marketing/app/api/marketing/campaigns/[id]/retry/route.ts +2 -0
- package/template/modules/marketing/app/api/marketing/campaigns/[id]/route.ts +2 -0
- package/template/modules/marketing/app/api/marketing/campaigns/[id]/schedule/route.ts +2 -0
- package/template/modules/marketing/app/api/marketing/campaigns/[id]/send/route.ts +2 -0
- package/template/modules/marketing/app/api/marketing/campaigns/[id]/test/route.ts +2 -0
- package/template/modules/marketing/app/api/marketing/campaigns/route.ts +2 -0
- package/template/modules/marketing/app/api/marketing/segments/[id]/preview/route.ts +2 -0
- package/template/modules/marketing/app/api/marketing/segments/[id]/route.ts +2 -0
- package/template/modules/marketing/app/api/marketing/segments/preview/route.ts +2 -0
- package/template/modules/marketing/app/api/marketing/segments/route.ts +2 -0
- package/template/modules/marketing/app/api/marketing/topics/route.ts +2 -0
- package/template/modules/marketing/app/api/marketing/unsubscribe/[token]/route.ts +2 -0
- package/template/modules/marketing/app/api/marketing/webhooks/resend/route.ts +2 -0
- package/template/modules/marketing/app/api/marketing/worker/route.ts +2 -0
- package/template/modules/marketing/app/api/marketing/workflows/[id]/activate/route.ts +2 -0
- package/template/modules/marketing/app/api/marketing/workflows/[id]/nodes/[nodeId]/route.ts +2 -0
- package/template/modules/marketing/app/api/marketing/workflows/[id]/nodes/route.ts +2 -0
- package/template/modules/marketing/app/api/marketing/workflows/[id]/pause/route.ts +2 -0
- package/template/modules/marketing/app/api/marketing/workflows/[id]/route.ts +2 -0
- package/template/modules/marketing/app/api/marketing/workflows/[id]/runs/route.ts +2 -0
- package/template/modules/marketing/app/api/marketing/workflows/route.ts +2 -0
- package/template/modules/projects/app/(shell)/projects/[id]/board/page.tsx +1 -0
- package/template/modules/projects/app/(shell)/projects/[id]/page.tsx +1 -0
- package/template/modules/projects/app/(shell)/projects/[id]/tasks/page.tsx +1 -0
- package/template/modules/projects/app/(shell)/projects/layout.tsx +5 -0
- package/template/modules/projects/app/(shell)/projects/page.tsx +1 -0
- package/template/modules/projects/app/(shell)/projects/projects-live-mounts.tsx +27 -0
- package/template/modules/projects/app/(shell)/projects/projects-server-mounts.tsx +54 -0
- package/template/modules/projects/app/api/projects/[id]/activity/route.ts +2 -0
- package/template/modules/projects/app/api/projects/[id]/links/[itemId]/route.ts +2 -0
- package/template/modules/projects/app/api/projects/[id]/links/route.ts +2 -0
- package/template/modules/projects/app/api/projects/[id]/members/route.ts +2 -0
- package/template/modules/projects/app/api/projects/[id]/milestones/[itemId]/route.ts +2 -0
- package/template/modules/projects/app/api/projects/[id]/milestones/route.ts +2 -0
- package/template/modules/projects/app/api/projects/[id]/route.ts +2 -0
- package/template/modules/projects/app/api/projects/[id]/tasks/[itemId]/route.ts +2 -0
- package/template/modules/projects/app/api/projects/[id]/tasks/route.ts +2 -0
- package/template/modules/projects/app/api/projects/_handlers.ts +86 -0
- package/template/modules/projects/app/api/projects/organizations/route.ts +2 -0
- package/template/modules/projects/app/api/projects/route.ts +2 -0
- package/template/modules/projects/app/api/projects/stats/route.ts +2 -0
- package/template/supabase/README.md +3 -0
- package/template/supabase/module-registry.json +6 -0
- package/template/supabase/modules/core/migrations/20260726180000_enable_rate_limit_counters_rls.sql +2 -0
- package/template/supabase/modules/marketing/README.md +12 -0
- package/template/supabase/modules/marketing/migrations/20260725120000_marketing_v1.sql +118 -0
- package/template/supabase/modules/marketing/migrations/20260725130000_marketing_campaigns.sql +133 -0
- package/template/supabase/modules/marketing/migrations/20260726120000_marketing_segments.sql +41 -0
- package/template/supabase/modules/marketing/migrations/20260726150000_marketing_workflows.sql +174 -0
- package/template/supabase/modules/marketing/migrations/20260726170000_marketing_engine_safety.sql +68 -0
- /package/template/modules/admin/app/{admin → (shell)/admin}/users/page.tsx +0 -0
- /package/template/modules/crm/app/{crm → (shell)/crm}/layout.tsx +0 -0
- /package/template/modules/crm/app/{crm → (shell)/crm}/page.tsx +0 -0
- /package/template/modules/crm/app/{crm → (shell)/crm}/report/page.tsx +0 -0
package/README.md
CHANGED
|
@@ -66,11 +66,11 @@ Current updater behavior:
|
|
|
66
66
|
|
|
67
67
|
- prompts for app type: `platform` or `site`
|
|
68
68
|
- prompts for project name
|
|
69
|
-
- prompts for optional platform modules: `admin`, `crm`, and `projects`
|
|
69
|
+
- prompts for optional platform modules: `admin`, `crm`, `marketing`, and `projects`
|
|
70
70
|
- prompts to install dependencies immediately
|
|
71
71
|
- copies a clean Next.js App Router starter template
|
|
72
72
|
- platform apps include BrightWeb auth, shell wiring, and optional direct package mounts
|
|
73
|
-
- generated route files
|
|
73
|
+
- generated route files are direct `@brightweblabs/*` re-exports or one-line exports from an app-owned dependency adapter such as Marketing's delivery handler composition
|
|
74
74
|
- platform apps in published mode also write `supabase/config.toml`, a CLI-ready flat `supabase/migrations/` folder, `supabase/module-registry.json`, `supabase/clients/<slug>/stack.json`, and the resolved shared SQL migrations under `supabase/modules/<module>/migrations`
|
|
75
75
|
- site apps include a thin Next.js and Tailwind CSS shell with no default feature page or local component library
|
|
76
76
|
- writes `package.json`, `next.config.ts`, `.gitignore`, and `README.md` for both templates
|
package/package.json
CHANGED
package/src/app-manifest.mjs
CHANGED
|
@@ -27,6 +27,7 @@ export const MANAGED_APP_FILES = [
|
|
|
27
27
|
export const MODULE_PACKAGES = {
|
|
28
28
|
admin: "@brightweblabs/module-admin",
|
|
29
29
|
crm: "@brightweblabs/module-crm",
|
|
30
|
+
marketing: "@brightweblabs/module-marketing",
|
|
30
31
|
orgs: "@brightweblabs/module-orgs",
|
|
31
32
|
projects: "@brightweblabs/module-projects",
|
|
32
33
|
};
|
|
@@ -36,6 +37,7 @@ const FALLBACK_REQUIRES = {
|
|
|
36
37
|
admin: { core: ">=0.3" },
|
|
37
38
|
orgs: { core: ">=0.3", admin: ">=0.3" },
|
|
38
39
|
crm: { core: ">=0.3", admin: ">=0.3", orgs: ">=0.1" },
|
|
40
|
+
marketing: { core: ">=0.3", admin: ">=0.3", orgs: ">=0.1", crm: ">=0.6" },
|
|
39
41
|
projects: { core: ">=0.3", admin: ">=0.3", orgs: ">=0.1" },
|
|
40
42
|
};
|
|
41
43
|
|
|
@@ -159,7 +161,7 @@ export async function createInitialAppManifest({ targetDir, slug, template, sele
|
|
|
159
161
|
const modules = {};
|
|
160
162
|
if (template === "platform") {
|
|
161
163
|
for (const moduleKey of selectedModules) modules[moduleKey] = { version: moduleVersion(moduleKey, versionMap), installedAt: now, exposed: true };
|
|
162
|
-
if ((selectedModules.includes("crm") || selectedModules.includes("projects")) && !modules.orgs) {
|
|
164
|
+
if ((selectedModules.includes("crm") || selectedModules.includes("marketing") || selectedModules.includes("projects")) && !modules.orgs) {
|
|
163
165
|
modules.orgs = { version: moduleVersion("orgs", versionMap), installedAt: now, exposed: true };
|
|
164
166
|
}
|
|
165
167
|
}
|
|
@@ -250,7 +252,7 @@ export async function readConfiguredModuleFlags(targetDir) {
|
|
|
250
252
|
if (!(await pathExists(filePath))) return {};
|
|
251
253
|
const content = await fs.readFile(filePath, "utf8");
|
|
252
254
|
const flags = {};
|
|
253
|
-
for (const key of ["core-auth", "orgs", "crm", "projects", "admin"]) {
|
|
255
|
+
for (const key of ["core-auth", "orgs", "crm", "marketing", "projects", "admin"]) {
|
|
254
256
|
const match = content.match(new RegExp(`key:\\s*"${key}"[\\s\\S]*?enabled:\\s*(true|false)`));
|
|
255
257
|
if (match) flags[key === "core-auth" ? "core" : key] = match[1] === "true";
|
|
256
258
|
}
|
package/src/constants.mjs
CHANGED
|
@@ -21,6 +21,12 @@ export const SELECTABLE_MODULES = [
|
|
|
21
21
|
packageName: "@brightweblabs/module-crm",
|
|
22
22
|
templateFolder: "crm",
|
|
23
23
|
},
|
|
24
|
+
{
|
|
25
|
+
key: "marketing",
|
|
26
|
+
label: "Marketing",
|
|
27
|
+
packageName: "@brightweblabs/module-marketing",
|
|
28
|
+
templateFolder: "marketing",
|
|
29
|
+
},
|
|
24
30
|
{
|
|
25
31
|
key: "projects",
|
|
26
32
|
label: "Projects",
|
|
@@ -55,12 +61,12 @@ export const MODULE_STARTER_FILES = {
|
|
|
55
61
|
admin: [
|
|
56
62
|
"app/api/admin/users/route.ts",
|
|
57
63
|
"app/api/admin/users/roles/route.ts",
|
|
58
|
-
"app/admin/users/page.tsx",
|
|
64
|
+
"app/(shell)/admin/users/page.tsx",
|
|
59
65
|
],
|
|
60
66
|
crm: [
|
|
61
|
-
"app/crm/layout.tsx",
|
|
62
|
-
"app/crm/page.tsx",
|
|
63
|
-
"app/crm/report/page.tsx",
|
|
67
|
+
"app/(shell)/crm/layout.tsx",
|
|
68
|
+
"app/(shell)/crm/page.tsx",
|
|
69
|
+
"app/(shell)/crm/report/page.tsx",
|
|
64
70
|
"app/api/crm/contacts/route.ts",
|
|
65
71
|
"app/api/crm/organizations/route.ts",
|
|
66
72
|
"app/api/crm/owners/route.ts",
|
|
@@ -68,36 +74,107 @@ export const MODULE_STARTER_FILES = {
|
|
|
68
74
|
"app/api/crm/stats/route.ts",
|
|
69
75
|
"app/api/crm/timeline/route.ts",
|
|
70
76
|
],
|
|
77
|
+
marketing: [
|
|
78
|
+
"app/(shell)/marketing/page.tsx",
|
|
79
|
+
"app/api/marketing/_handlers.ts",
|
|
80
|
+
"app/api/marketing/analytics/campaigns/[id]/route.ts",
|
|
81
|
+
"app/api/marketing/analytics/overview/route.ts",
|
|
82
|
+
"app/api/marketing/analytics/segments/[id]/route.ts",
|
|
83
|
+
"app/api/marketing/campaigns/[id]/cancel/route.ts",
|
|
84
|
+
"app/api/marketing/campaigns/[id]/recipients/route.ts",
|
|
85
|
+
"app/api/marketing/campaigns/[id]/retry/route.ts",
|
|
86
|
+
"app/api/marketing/campaigns/[id]/route.ts",
|
|
87
|
+
"app/api/marketing/campaigns/[id]/schedule/route.ts",
|
|
88
|
+
"app/api/marketing/campaigns/[id]/send/route.ts",
|
|
89
|
+
"app/api/marketing/campaigns/[id]/test/route.ts",
|
|
90
|
+
"app/api/marketing/campaigns/route.ts",
|
|
91
|
+
"app/api/marketing/segments/[id]/preview/route.ts",
|
|
92
|
+
"app/api/marketing/segments/[id]/route.ts",
|
|
93
|
+
"app/api/marketing/segments/preview/route.ts",
|
|
94
|
+
"app/api/marketing/segments/route.ts",
|
|
95
|
+
"app/api/marketing/topics/route.ts",
|
|
96
|
+
"app/api/marketing/unsubscribe/[token]/route.ts",
|
|
97
|
+
"app/api/marketing/webhooks/resend/route.ts",
|
|
98
|
+
"app/api/marketing/worker/route.ts",
|
|
99
|
+
"app/api/marketing/workflows/[id]/activate/route.ts",
|
|
100
|
+
"app/api/marketing/workflows/[id]/nodes/[nodeId]/route.ts",
|
|
101
|
+
"app/api/marketing/workflows/[id]/nodes/route.ts",
|
|
102
|
+
"app/api/marketing/workflows/[id]/pause/route.ts",
|
|
103
|
+
"app/api/marketing/workflows/[id]/route.ts",
|
|
104
|
+
"app/api/marketing/workflows/[id]/runs/route.ts",
|
|
105
|
+
"app/api/marketing/workflows/route.ts",
|
|
106
|
+
],
|
|
71
107
|
projects: [
|
|
108
|
+
"app/(shell)/projects/layout.tsx",
|
|
109
|
+
"app/(shell)/projects/page.tsx",
|
|
110
|
+
"app/(shell)/projects/projects-live-mounts.tsx",
|
|
111
|
+
"app/(shell)/projects/projects-server-mounts.tsx",
|
|
112
|
+
"app/(shell)/projects/[id]/page.tsx",
|
|
113
|
+
"app/(shell)/projects/[id]/board/page.tsx",
|
|
114
|
+
"app/(shell)/projects/[id]/tasks/page.tsx",
|
|
115
|
+
"app/api/projects/_handlers.ts",
|
|
116
|
+
"app/api/projects/route.ts",
|
|
117
|
+
"app/api/projects/stats/route.ts",
|
|
118
|
+
"app/api/projects/organizations/route.ts",
|
|
119
|
+
"app/api/projects/[id]/route.ts",
|
|
120
|
+
"app/api/projects/[id]/activity/route.ts",
|
|
121
|
+
"app/api/projects/[id]/members/route.ts",
|
|
122
|
+
"app/api/projects/[id]/links/route.ts",
|
|
123
|
+
"app/api/projects/[id]/links/[itemId]/route.ts",
|
|
124
|
+
"app/api/projects/[id]/milestones/route.ts",
|
|
125
|
+
"app/api/projects/[id]/milestones/[itemId]/route.ts",
|
|
126
|
+
"app/api/projects/[id]/tasks/route.ts",
|
|
127
|
+
"app/api/projects/[id]/tasks/[itemId]/route.ts",
|
|
72
128
|
],
|
|
73
129
|
};
|
|
74
130
|
|
|
75
131
|
export const PLATFORM_STARTER_FILES = [
|
|
132
|
+
"app/(auth)/auth-provider.tsx",
|
|
133
|
+
"app/(auth)/layout.tsx",
|
|
134
|
+
"app/(auth)/login/page.tsx",
|
|
135
|
+
"app/(auth)/signup/page.tsx",
|
|
136
|
+
"app/(auth)/forgot-password/page.tsx",
|
|
137
|
+
"app/(auth)/reset-password/page.tsx",
|
|
138
|
+
"app/(auth)/auth/post-login/page.tsx",
|
|
139
|
+
"app/(auth)/auth/confirmed/page.tsx",
|
|
140
|
+
"app/(auth)/invite/[invitationId]/page.tsx",
|
|
141
|
+
"app/(auth)/invite/[invitationId]/invite-route-mount.tsx",
|
|
142
|
+
"app/(shell)/layout.tsx",
|
|
143
|
+
"app/(shell)/shell-layout-client.tsx",
|
|
144
|
+
"app/(shell)/account/page.tsx",
|
|
145
|
+
"app/(shell)/dashboard/dashboard-live-mount.tsx",
|
|
146
|
+
"app/(shell)/dashboard/page.tsx",
|
|
147
|
+
"app/api/account/route.ts",
|
|
148
|
+
"app/auth/callback/route.ts",
|
|
149
|
+
"app/auth/cleanup/route.ts",
|
|
150
|
+
"app/layout.tsx",
|
|
151
|
+
"config/env.ts",
|
|
76
152
|
"config/shell.overrides.ts",
|
|
77
153
|
];
|
|
78
154
|
|
|
79
155
|
export const APP_DEPENDENCY_DEFAULTS = {
|
|
80
|
-
"@brightweblabs/app-shell": "^0.6.
|
|
81
|
-
"@brightweblabs/core-auth": "^0.
|
|
82
|
-
"@brightweblabs/infra": "^0.3.
|
|
83
|
-
"@brightweblabs/module-admin": "^0.5.
|
|
84
|
-
"@brightweblabs/module-crm": "^0.8.
|
|
85
|
-
"@brightweblabs/module-
|
|
86
|
-
"@brightweblabs/module-
|
|
156
|
+
"@brightweblabs/app-shell": "^0.6.1",
|
|
157
|
+
"@brightweblabs/core-auth": "^0.5.1",
|
|
158
|
+
"@brightweblabs/infra": "^0.3.3",
|
|
159
|
+
"@brightweblabs/module-admin": "^0.5.2",
|
|
160
|
+
"@brightweblabs/module-crm": "^0.8.2",
|
|
161
|
+
"@brightweblabs/module-marketing": "^0.2.1",
|
|
162
|
+
"@brightweblabs/module-orgs": "^0.3.2",
|
|
163
|
+
"@brightweblabs/module-projects": "^0.6.1",
|
|
87
164
|
"@brightweblabs/theme": "^0.4.0",
|
|
88
165
|
"@brightweblabs/ui": "^1.1.0",
|
|
89
166
|
"geist": "1.7.2",
|
|
90
167
|
"lucide-react": "^1.8.0",
|
|
91
|
-
"next": "16.
|
|
92
|
-
"react": "19.
|
|
93
|
-
"react-dom": "19.
|
|
168
|
+
"next": "^16.0.0",
|
|
169
|
+
"react": "^19.0.0",
|
|
170
|
+
"react-dom": "^19.0.0",
|
|
94
171
|
};
|
|
95
172
|
|
|
96
173
|
export const SITE_DEPENDENCY_DEFAULTS = {
|
|
97
174
|
"geist": "1.7.2",
|
|
98
|
-
"next": "16.
|
|
99
|
-
"react": "19.
|
|
100
|
-
"react-dom": "19.
|
|
175
|
+
"next": "^16.0.0",
|
|
176
|
+
"react": "^19.0.0",
|
|
177
|
+
"react-dom": "^19.0.0",
|
|
101
178
|
};
|
|
102
179
|
|
|
103
180
|
export const APP_DEV_DEPENDENCY_DEFAULTS = {
|
|
@@ -131,7 +208,7 @@ Usage:
|
|
|
131
208
|
Scaffold options:
|
|
132
209
|
--template <platform|site> Scaffold a platform app or a standalone site
|
|
133
210
|
--name, --slug <name> Project name and default directory name
|
|
134
|
-
--modules <list> Comma-separated modules: crm,projects,admin
|
|
211
|
+
--modules <list> Comma-separated modules: crm,marketing,projects,admin
|
|
135
212
|
--output-dir <path> Parent folder for the generated app
|
|
136
213
|
--target-dir <path> Exact output directory, bypassing slug folder creation
|
|
137
214
|
--workspace-root <path> BrightWeb workspace root for local mode
|
package/src/doctor.mjs
CHANGED
|
@@ -7,6 +7,34 @@ import { pathExists, readJsonIfPresent } from "./generator.mjs";
|
|
|
7
7
|
import { scaffoldDrift } from "./scaffold.mjs";
|
|
8
8
|
|
|
9
9
|
const HELP = `Usage: bw doctor [options]\n\nOptions:\n --target-dir <path> App directory (defaults to cwd)\n --workspace-root <path> BrightWeb workspace root\n --strict Treat warnings as failures\n --report Stamp lastDoctor in the app manifest\n --help Show this help`;
|
|
10
|
+
const RUNTIME_PACKAGE_NAMES = ["react", "react-dom", "next"];
|
|
11
|
+
|
|
12
|
+
export async function findInstalledRuntimeVersions(targetDir) {
|
|
13
|
+
const storeDir = path.join(targetDir, "node_modules", ".pnpm");
|
|
14
|
+
if (!(await pathExists(storeDir))) {
|
|
15
|
+
return Object.fromEntries(RUNTIME_PACKAGE_NAMES.map((packageName) => [packageName, []]));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const entries = await fs.readdir(storeDir, { withFileTypes: true });
|
|
19
|
+
const resolved = {};
|
|
20
|
+
for (const packageName of RUNTIME_PACKAGE_NAMES) {
|
|
21
|
+
const versions = new Set();
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
if (!entry.isDirectory() || !entry.name.startsWith(`${packageName}@`)) continue;
|
|
24
|
+
const manifest = await readJsonIfPresent(
|
|
25
|
+
path.join(storeDir, entry.name, "node_modules", packageName, "package.json"),
|
|
26
|
+
);
|
|
27
|
+
const fallbackVersion = entry.name
|
|
28
|
+
.slice(packageName.length + 1)
|
|
29
|
+
.split("_", 1)[0]
|
|
30
|
+
.split("(", 1)[0];
|
|
31
|
+
const version = manifest?.version || fallbackVersion;
|
|
32
|
+
if (version) versions.add(version);
|
|
33
|
+
}
|
|
34
|
+
resolved[packageName] = Array.from(versions).sort();
|
|
35
|
+
}
|
|
36
|
+
return resolved;
|
|
37
|
+
}
|
|
10
38
|
|
|
11
39
|
export async function doctorBrightwebApp(argvOptions = {}, runtimeOptions = {}) {
|
|
12
40
|
if (argvOptions.help) { output.write(`${HELP}\n`); return { help: true, ok: true }; }
|
|
@@ -35,6 +63,16 @@ export async function doctorBrightwebApp(argvOptions = {}, runtimeOptions = {})
|
|
|
35
63
|
}
|
|
36
64
|
add(packageProblems.length ? "FAIL" : "PASS", "packages", packageProblems.join("; ") || "Installed module packages agree with the manifest.");
|
|
37
65
|
|
|
66
|
+
const runtimeVersions = await findInstalledRuntimeVersions(targetDir);
|
|
67
|
+
const duplicateRuntimes = Object.entries(runtimeVersions)
|
|
68
|
+
.filter(([, versions]) => versions.length > 1)
|
|
69
|
+
.map(([packageName, versions]) => `${packageName}: ${versions.join(", ")}`);
|
|
70
|
+
add(
|
|
71
|
+
duplicateRuntimes.length ? "FAIL" : "PASS",
|
|
72
|
+
"runtime-singletons",
|
|
73
|
+
duplicateRuntimes.join("; ") || "React, React DOM, and Next resolve to at most one version each.",
|
|
74
|
+
);
|
|
75
|
+
|
|
38
76
|
const flags = await readConfiguredModuleFlags(targetDir);
|
|
39
77
|
const exposureProblems = Object.entries(appManifest.modules || {}).filter(([key, entry]) => typeof flags[key] === "boolean" && flags[key] !== entry.exposed).map(([key, entry]) => `${key}: manifest exposed=${entry.exposed}, config enabled=${String(flags[key])}`);
|
|
40
78
|
add(exposureProblems.length ? "FAIL" : "PASS", "exposure", exposureProblems.join("; ") || "Module exposure flags agree.");
|
package/src/generator.mjs
CHANGED
|
@@ -28,6 +28,7 @@ const DEFAULT_DB_MODULE_REGISTRY = {
|
|
|
28
28
|
admin: { label: "Admin", dependsOn: ["core"] },
|
|
29
29
|
orgs: { label: "Organizations", dependsOn: ["core", "admin"] },
|
|
30
30
|
crm: { label: "CRM", dependsOn: ["core", "admin", "orgs"] },
|
|
31
|
+
marketing: { label: "Marketing", dependsOn: ["core", "admin", "orgs", "crm"] },
|
|
31
32
|
projects: { label: "Projects", dependsOn: ["core", "admin", "orgs"] },
|
|
32
33
|
},
|
|
33
34
|
};
|
|
@@ -251,6 +252,7 @@ export async function getVersionMap(workspaceRoot) {
|
|
|
251
252
|
"@brightweblabs/infra",
|
|
252
253
|
"@brightweblabs/module-admin",
|
|
253
254
|
"@brightweblabs/module-crm",
|
|
255
|
+
"@brightweblabs/module-marketing",
|
|
254
256
|
"@brightweblabs/module-orgs",
|
|
255
257
|
"@brightweblabs/module-projects",
|
|
256
258
|
"@brightweblabs/theme",
|
|
@@ -264,6 +266,10 @@ export async function getVersionMap(workspaceRoot) {
|
|
|
264
266
|
}
|
|
265
267
|
}
|
|
266
268
|
|
|
269
|
+
versionMap.next = APP_DEPENDENCY_DEFAULTS.next;
|
|
270
|
+
versionMap.react = APP_DEPENDENCY_DEFAULTS.react;
|
|
271
|
+
versionMap["react-dom"] = APP_DEPENDENCY_DEFAULTS["react-dom"];
|
|
272
|
+
|
|
267
273
|
return versionMap;
|
|
268
274
|
}
|
|
269
275
|
|
|
@@ -304,10 +310,10 @@ function createPlatformBrandConfigFile({ slug, brandValues }) {
|
|
|
304
310
|
|
|
305
311
|
export function createPlatformModulesConfigFile(selectedModules) {
|
|
306
312
|
const selected = new Set(selectedModules);
|
|
307
|
-
const orgsEnabled = selected.has("crm") || selected.has("projects");
|
|
313
|
+
const orgsEnabled = selected.has("crm") || selected.has("marketing") || selected.has("projects");
|
|
308
314
|
|
|
309
315
|
return [
|
|
310
|
-
'export type StarterModuleKey = "core-auth" | "orgs" | "crm" | "projects" | "admin";',
|
|
316
|
+
'export type StarterModuleKey = "core-auth" | "orgs" | "crm" | "marketing" | "projects" | "admin";',
|
|
311
317
|
"",
|
|
312
318
|
"export type StarterModuleConfig = {",
|
|
313
319
|
" key: StarterModuleKey;",
|
|
@@ -331,7 +337,7 @@ export function createPlatformModulesConfigFile(selectedModules) {
|
|
|
331
337
|
" {",
|
|
332
338
|
' key: "orgs",',
|
|
333
339
|
' label: "Organizations",',
|
|
334
|
-
' description: "Shared organizations, membership, and invitation foundation for CRM and Projects.",',
|
|
340
|
+
' description: "Shared organizations, membership, and invitation foundation for CRM, Marketing, and Projects.",',
|
|
335
341
|
` enabled: ${String(orgsEnabled)},`,
|
|
336
342
|
' packageName: "@brightweblabs/module-orgs",',
|
|
337
343
|
' placement: "hidden",',
|
|
@@ -346,11 +352,21 @@ export function createPlatformModulesConfigFile(selectedModules) {
|
|
|
346
352
|
' placement: "primary",',
|
|
347
353
|
" },",
|
|
348
354
|
" {",
|
|
355
|
+
' key: "marketing",',
|
|
356
|
+
' label: "Marketing",',
|
|
357
|
+
' description: "Consent-first campaigns, segments, analytics, and automated email workflows.",',
|
|
358
|
+
` enabled: ${String(selected.has("marketing"))},`,
|
|
359
|
+
' packageName: "@brightweblabs/module-marketing",',
|
|
360
|
+
' routeHref: "/marketing",',
|
|
361
|
+
' placement: "primary",',
|
|
362
|
+
" },",
|
|
363
|
+
" {",
|
|
349
364
|
' key: "projects",',
|
|
350
365
|
' label: "Projects",',
|
|
351
366
|
' description: "Project portfolio, detail routes, and work-management server logic.",',
|
|
352
367
|
` enabled: ${String(selected.has("projects"))},`,
|
|
353
368
|
' packageName: "@brightweblabs/module-projects",',
|
|
369
|
+
' routeHref: "/projects",',
|
|
354
370
|
' placement: "primary",',
|
|
355
371
|
" },",
|
|
356
372
|
" {",
|
|
@@ -374,15 +390,20 @@ export function createPlatformModulesConfigFile(selectedModules) {
|
|
|
374
390
|
function createEnvFileContent() {
|
|
375
391
|
return [
|
|
376
392
|
"NEXT_PUBLIC_APP_URL=http://localhost:3000",
|
|
393
|
+
"PUBLIC_APP_URL=http://localhost:3000",
|
|
377
394
|
"NEXT_PUBLIC_SUPABASE_URL=",
|
|
378
395
|
"NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY=",
|
|
379
396
|
"SUPABASE_SECRET_DEFAULT_KEY=",
|
|
380
397
|
"RESEND_API_KEY=",
|
|
398
|
+
"RESEND_WEBHOOK_SECRET=",
|
|
381
399
|
"RESEND_FROM_TRANSACTIONAL=",
|
|
382
400
|
"RESEND_FROM_MARKETING=",
|
|
383
|
-
"
|
|
384
|
-
"
|
|
401
|
+
"RESEND_FROM_EMAIL=",
|
|
402
|
+
"RESEND_FROM_NAME=",
|
|
403
|
+
"MARKETING_FROM_EMAIL=",
|
|
404
|
+
"MARKETING_FROM_NAME=BrightWeb",
|
|
385
405
|
"MARKETING_WORKER_SECRET=",
|
|
406
|
+
"CONTACT_TO_EMAIL=",
|
|
386
407
|
"MARKETING_TEST_EMAIL=",
|
|
387
408
|
"",
|
|
388
409
|
].join("\n");
|
|
@@ -436,11 +457,21 @@ function createGitignore() {
|
|
|
436
457
|
}
|
|
437
458
|
|
|
438
459
|
function getPlatformStarterRoutes(selectedModules) {
|
|
439
|
-
return
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
460
|
+
return [
|
|
461
|
+
"/login",
|
|
462
|
+
"/signup",
|
|
463
|
+
"/forgot-password",
|
|
464
|
+
"/reset-password",
|
|
465
|
+
"/account",
|
|
466
|
+
"/dashboard",
|
|
467
|
+
...selectedModules.flatMap((moduleKey) => {
|
|
468
|
+
if (moduleKey === "crm") return ["/crm"];
|
|
469
|
+
if (moduleKey === "marketing") return ["/marketing"];
|
|
470
|
+
if (moduleKey === "projects") return ["/projects"];
|
|
471
|
+
if (moduleKey === "admin") return ["/admin/users"];
|
|
472
|
+
return [];
|
|
473
|
+
}),
|
|
474
|
+
];
|
|
444
475
|
}
|
|
445
476
|
|
|
446
477
|
function getSiteStarterRoutes() {
|
|
@@ -675,7 +706,7 @@ export function createAppContextFile({
|
|
|
675
706
|
],
|
|
676
707
|
packageOwned: [
|
|
677
708
|
...CORE_PACKAGES,
|
|
678
|
-
...(selectedModules.includes("crm") || selectedModules.includes("projects") ? [ORGS_PACKAGE_NAME] : []),
|
|
709
|
+
...(selectedModules.includes("crm") || selectedModules.includes("marketing") || selectedModules.includes("projects") ? [ORGS_PACKAGE_NAME] : []),
|
|
679
710
|
...SELECTABLE_MODULES
|
|
680
711
|
.filter((moduleDefinition) => selectedModules.includes(moduleDefinition.key))
|
|
681
712
|
.map((moduleDefinition) => moduleDefinition.packageName),
|
|
@@ -748,7 +779,7 @@ export function createPackageJson({
|
|
|
748
779
|
dependencies[moduleDefinition.packageName] = internalDependencyVersion(moduleDefinition.packageName);
|
|
749
780
|
}
|
|
750
781
|
}
|
|
751
|
-
if (selectedModules.includes("crm") || selectedModules.includes("projects")) {
|
|
782
|
+
if (selectedModules.includes("crm") || selectedModules.includes("marketing") || selectedModules.includes("projects")) {
|
|
752
783
|
dependencies[ORGS_PACKAGE_NAME] = internalDependencyVersion(ORGS_PACKAGE_NAME);
|
|
753
784
|
}
|
|
754
785
|
|
|
@@ -787,7 +818,7 @@ export function createNextConfig({ template, selectedModules }) {
|
|
|
787
818
|
}
|
|
788
819
|
|
|
789
820
|
const transpilePackages = [...CORE_PACKAGES];
|
|
790
|
-
if (selectedModules.includes("crm") || selectedModules.includes("projects")) {
|
|
821
|
+
if (selectedModules.includes("crm") || selectedModules.includes("marketing") || selectedModules.includes("projects")) {
|
|
791
822
|
transpilePackages.push(ORGS_PACKAGE_NAME);
|
|
792
823
|
}
|
|
793
824
|
|
|
@@ -836,7 +867,7 @@ export function createShellConfig(selectedModules) {
|
|
|
836
867
|
const importLines = [];
|
|
837
868
|
const registrationLines = [];
|
|
838
869
|
|
|
839
|
-
if (selectedModules.includes("crm") || selectedModules.includes("projects")) {
|
|
870
|
+
if (selectedModules.includes("crm") || selectedModules.includes("marketing") || selectedModules.includes("projects")) {
|
|
840
871
|
importLines.push('import { orgsModuleRegistration } from "@brightweblabs/module-orgs/registration";');
|
|
841
872
|
registrationLines.push(' if (enabled.has("orgs")) registrations.push(orgsModuleRegistration);');
|
|
842
873
|
}
|
|
@@ -851,9 +882,19 @@ export function createShellConfig(selectedModules) {
|
|
|
851
882
|
registrationLines.push(' if (enabled.has("crm")) registrations.push(crmModuleRegistration);');
|
|
852
883
|
}
|
|
853
884
|
|
|
885
|
+
if (selectedModules.includes("marketing")) {
|
|
886
|
+
importLines.push('import { marketingModuleRegistration } from "@brightweblabs/module-marketing/registration";');
|
|
887
|
+
registrationLines.push(' if (enabled.has("marketing")) registrations.push(marketingModuleRegistration);');
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
if (selectedModules.includes("projects")) {
|
|
891
|
+
importLines.push('import { projectsPreviewModuleRegistration } from "@brightweblabs/module-projects/registration";');
|
|
892
|
+
registrationLines.push(' if (enabled.has("projects")) registrations.push(projectsPreviewModuleRegistration);');
|
|
893
|
+
}
|
|
894
|
+
|
|
854
895
|
return [
|
|
855
896
|
"// MANAGED BY BRIGHTWEB — regenerated by create-bw-app update; put customizations in config/shell.overrides.ts",
|
|
856
|
-
'import { Wrench } from "lucide-react";',
|
|
897
|
+
'import { LayoutDashboard, Wrench } from "lucide-react";',
|
|
857
898
|
"import {",
|
|
858
899
|
" applyShellRegistrationOverrides,",
|
|
859
900
|
" buildClientAppShellRegistration,",
|
|
@@ -867,9 +908,16 @@ export function createShellConfig(selectedModules) {
|
|
|
867
908
|
'import { getEnabledStarterModules } from "./modules";',
|
|
868
909
|
'import { shellRegistrationOverrides } from "./shell.overrides";',
|
|
869
910
|
"",
|
|
911
|
+
"const dashboardModuleRegistration: ShellModuleRegistration<ShellContextualAction> = {",
|
|
912
|
+
' key: "dashboard",',
|
|
913
|
+
' placement: "primary",',
|
|
914
|
+
' navItems: [{ href: "/dashboard", label: "Dashboard", icon: LayoutDashboard }],',
|
|
915
|
+
' toolbarRoutes: [{ surface: "dashboard", match: { exact: ["/dashboard"] } }],',
|
|
916
|
+
"};",
|
|
917
|
+
"",
|
|
870
918
|
"function getStarterModuleRegistrations() {",
|
|
871
919
|
' const enabled = new Set(getEnabledStarterModules().map((moduleConfig) => moduleConfig.key));',
|
|
872
|
-
" const registrations: ShellModuleRegistration<ShellContextualAction>[] = [];",
|
|
920
|
+
" const registrations: ShellModuleRegistration<ShellContextualAction>[] = [dashboardModuleRegistration];",
|
|
873
921
|
"",
|
|
874
922
|
...registrationLines,
|
|
875
923
|
"",
|
|
@@ -923,6 +971,8 @@ export function createShellConfig(selectedModules) {
|
|
|
923
971
|
" shellConfig: builtRegistration.shellConfig,",
|
|
924
972
|
" shellPreview,",
|
|
925
973
|
" toolbarRoutes: builtRegistration.toolbarRoutes,",
|
|
974
|
+
" toolbarActions: builtRegistration.toolbarActions,",
|
|
975
|
+
" dashboardContributions: builtRegistration.dashboardContributions,",
|
|
926
976
|
" };",
|
|
927
977
|
"}",
|
|
928
978
|
"",
|
|
@@ -1430,13 +1480,19 @@ export async function createBrightwebClientApp(argvOptions, runtimeOptions = {})
|
|
|
1430
1480
|
workspaceMode,
|
|
1431
1481
|
registry: dbModuleRegistry,
|
|
1432
1482
|
});
|
|
1483
|
+
const requestedModules = new Set(answers.selectedModules);
|
|
1484
|
+
const selectedModules = dbInstallPlan.resolvedOrder.filter(
|
|
1485
|
+
(moduleKey) =>
|
|
1486
|
+
SELECTABLE_MODULES.some((moduleDefinition) => moduleDefinition.key === moduleKey)
|
|
1487
|
+
&& (moduleKey !== "admin" || requestedModules.has("admin")),
|
|
1488
|
+
);
|
|
1433
1489
|
const install = answers.install && !argvOptions.dryRun;
|
|
1434
1490
|
|
|
1435
1491
|
if (argvOptions.dryRun) {
|
|
1436
1492
|
output.write(`${renderPlanSummary({
|
|
1437
1493
|
targetDir,
|
|
1438
1494
|
dependencyMode,
|
|
1439
|
-
selectedModules
|
|
1495
|
+
selectedModules,
|
|
1440
1496
|
packageManager,
|
|
1441
1497
|
workspaceMode,
|
|
1442
1498
|
install: answers.install,
|
|
@@ -1456,7 +1512,7 @@ export async function createBrightwebClientApp(argvOptions, runtimeOptions = {})
|
|
|
1456
1512
|
output.write(`${renderPlanSummary({
|
|
1457
1513
|
targetDir,
|
|
1458
1514
|
dependencyMode,
|
|
1459
|
-
selectedModules
|
|
1515
|
+
selectedModules,
|
|
1460
1516
|
packageManager,
|
|
1461
1517
|
workspaceMode,
|
|
1462
1518
|
install: answers.install,
|
|
@@ -1476,7 +1532,7 @@ export async function createBrightwebClientApp(argvOptions, runtimeOptions = {})
|
|
|
1476
1532
|
} else {
|
|
1477
1533
|
await scaffoldPlatformProject({
|
|
1478
1534
|
targetDir,
|
|
1479
|
-
selectedModules
|
|
1535
|
+
selectedModules,
|
|
1480
1536
|
versionMap,
|
|
1481
1537
|
dependencyMode,
|
|
1482
1538
|
packageManager,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { AuthConfirmedPage as default } from "@brightweblabs/core-auth/ui";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { PostLoginPage as default } from "@brightweblabs/core-auth/ui";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { AuthUiProvider, createAuthUiClient } from "@brightweblabs/core-auth/ui";
|
|
4
|
+
import { starterBrandConfig } from "../../config/brand";
|
|
5
|
+
|
|
6
|
+
const authClient = createAuthUiClient();
|
|
7
|
+
|
|
8
|
+
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|
9
|
+
return (
|
|
10
|
+
<AuthUiProvider
|
|
11
|
+
client={authClient}
|
|
12
|
+
signupMode="invite-only"
|
|
13
|
+
layoutVariant="split"
|
|
14
|
+
brand={{
|
|
15
|
+
companyName: starterBrandConfig.companyName,
|
|
16
|
+
helpHref: `mailto:${starterBrandConfig.supportEmail}`,
|
|
17
|
+
splitHeadline: "Operações claras. Decisões com contexto.",
|
|
18
|
+
splitDescription: "Um espaço reservado para equipas e clientes acompanharem projetos, relações e trabalho em curso.",
|
|
19
|
+
logo: <img src="/brand/logo-dark.svg" alt={starterBrandConfig.companyName} width={176} height={44} />,
|
|
20
|
+
}}
|
|
21
|
+
>
|
|
22
|
+
{children}
|
|
23
|
+
</AuthUiProvider>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ForgotPasswordPage as default } from "@brightweblabs/core-auth/ui";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { InvitePage } from "@brightweblabs/core-auth/ui";
|
|
2
|
+
|
|
3
|
+
export async function InviteRouteMount({
|
|
4
|
+
params,
|
|
5
|
+
}: {
|
|
6
|
+
params: Promise<{ invitationId: string }>;
|
|
7
|
+
}) {
|
|
8
|
+
const { invitationId } = await params;
|
|
9
|
+
return <InvitePage invitationId={invitationId} />;
|
|
10
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { InviteRouteMount as default } from "./invite-route-mount";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { LoginPage as default } from "@brightweblabs/core-auth/ui";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ResetPasswordPage as default } from "@brightweblabs/core-auth/ui";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { SignupPage as default } from "@brightweblabs/core-auth/ui";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { AppDashboard, type DashboardDataClient } from "@brightweblabs/app-shell";
|
|
4
|
+
import { getStarterShellConfig } from "../../../config/shell";
|
|
5
|
+
|
|
6
|
+
async function getJson(path: string) {
|
|
7
|
+
const response = await fetch(path);
|
|
8
|
+
return response.json();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const dashboardClient: DashboardDataClient = {
|
|
12
|
+
getOverview: () => Promise.reject(new Error("No aggregate dashboard endpoint configured.")),
|
|
13
|
+
getProjects: () => getJson("/api/projects/stats"),
|
|
14
|
+
getCrm: () => getJson("/api/crm/stats"),
|
|
15
|
+
getTasks: () => Promise.reject(new Error("No task dashboard endpoint configured.")),
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function DashboardLiveMount() {
|
|
19
|
+
const { dashboardContributions } = getStarterShellConfig();
|
|
20
|
+
return <AppDashboard client={dashboardClient} contributions={dashboardContributions} />;
|
|
21
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DashboardLiveMount as default } from "./dashboard-live-mount";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { requireServerPageAccess } from "@brightweblabs/core-auth/server";
|
|
3
|
+
import { ShellLayoutClient, type ShellViewer } from "./shell-layout-client";
|
|
4
|
+
|
|
5
|
+
export const dynamic = "force-dynamic";
|
|
6
|
+
|
|
7
|
+
export default async function ShellLayout({ children }: Readonly<{ children: ReactNode }>) {
|
|
8
|
+
const { supabase, user, role } = await requireServerPageAccess();
|
|
9
|
+
const { data: profile } = await supabase
|
|
10
|
+
.from("profiles")
|
|
11
|
+
.select("first_name, last_name")
|
|
12
|
+
.eq("user_id", user.id)
|
|
13
|
+
.maybeSingle<{ first_name: string | null; last_name: string | null }>();
|
|
14
|
+
|
|
15
|
+
const viewer: ShellViewer = {
|
|
16
|
+
email: user.email ?? null,
|
|
17
|
+
firstName: profile?.first_name ?? null,
|
|
18
|
+
lastName: profile?.last_name ?? null,
|
|
19
|
+
isAdmin: role === "admin",
|
|
20
|
+
isStaff: role === "staff" || role === "admin",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
return <ShellLayoutClient viewer={viewer}>{children}</ShellLayoutClient>;
|
|
24
|
+
}
|