create-bw-app 0.26.2 → 0.27.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 +33 -0
- package/package.json +1 -1
- package/src/bw.mjs +4 -2
- package/src/constants.mjs +1 -1
- package/src/setup.mjs +96 -0
package/README.md
CHANGED
|
@@ -106,3 +106,36 @@ The scaffold records `SUPABASE_PROJECT_REGION` in `.env.local` and infrastructur
|
|
|
106
106
|
- `packages/create-bw-app/template/site/base`
|
|
107
107
|
- `packages/create-bw-app/template/modules`
|
|
108
108
|
- `packages/create-bw-app/template/supabase`
|
|
109
|
+
|
|
110
|
+
### Opt-in social media viewer
|
|
111
|
+
|
|
112
|
+
For a platform app with marketing enabled, first install a marketing release that
|
|
113
|
+
exports `social-media` and `withSocialMediaNavigation`, then run:
|
|
114
|
+
|
|
115
|
+
```sh
|
|
116
|
+
bw setup social-media --dry-run
|
|
117
|
+
bw setup social-media
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Use `--target-dir <path>` to select another app directory. Setup creates
|
|
121
|
+
`config/social-media-plan.json` with an empty, neutral plan and the thin page at
|
|
122
|
+
`app/(shell)/marketing/social-media/page.tsx`. All client copy belongs in that
|
|
123
|
+
app-owned JSON; components, styles, validation, and interactions stay in marketing.
|
|
124
|
+
No database or publishing service is created.
|
|
125
|
+
|
|
126
|
+
Setup appends navigation wiring to the standard app-owned
|
|
127
|
+
`config/shell.overrides.ts`, which must declare
|
|
128
|
+
`export const shellRegistrationOverrides: ShellRegistrationOverrides = { ... };`.
|
|
129
|
+
Existing marketing overrides are composed through the shared helper:
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
import { withSocialMediaNavigation } from "@brightweblabs/module-marketing/registration";
|
|
133
|
+
shellRegistrationOverrides.marketing = withSocialMediaNavigation(shellRegistrationOverrides.marketing);
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Custom shell export shapes require manual wiring before adopting the standard
|
|
137
|
+
shape. Setup checks prerequisites and paths before writing and rejects symlinks
|
|
138
|
+
on generated paths. Existing JSON and page files are never replaced, even when
|
|
139
|
+
empty or customized; reruns preserve them. These files are not managed templates
|
|
140
|
+
or refreshable starters, and `bw update --refresh-starters` preserves them and
|
|
141
|
+
shell overrides. New marketing apps do not receive this feature until opted in.
|
package/package.json
CHANGED
package/src/bw.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { setupBrightwebFeature } from "./setup.mjs";
|
|
1
2
|
import { addBrightwebModule } from "./add.mjs";
|
|
2
3
|
import { createFirstAdmin } from "./admin.mjs";
|
|
3
4
|
import { adoptBrightwebApp } from "./adopt.mjs";
|
|
@@ -8,7 +9,7 @@ import { scaffoldBrightwebApp } from "./scaffold-cmd.mjs";
|
|
|
8
9
|
import { updateBrightwebApp } from "./update.mjs";
|
|
9
10
|
import { upgradeBrightwebApp } from "./upgrade.mjs";
|
|
10
11
|
|
|
11
|
-
const HELP = `Usage: bw <command> [options]\n\nCommands:\n add <moduleKey> Install a module and its requirements\n admin create Bootstrap a passwordless administrator safely\n adopt Create an honest manifest for a legacy app\n diff <relpath> Compare a tracked scaffold file with its template\n scaffold <action> List or record per-file scaffold intent\n remove <moduleKey> Conservatively remove module package wiring\n upgrade [moduleKey] Upgrade packages, managed files, and migrations\n update Alias for the legacy create-bw-app update flow\n doctor Validate app health and manifest consistency\n\nRun bw <command> --help for command-specific options.`;
|
|
12
|
+
const HELP = `Usage: bw <command> [options]\n\nCommands:\n setup social-media Create app-owned social media content and wiring\n add <moduleKey> Install a module and its requirements\n admin create Bootstrap a passwordless administrator safely\n adopt Create an honest manifest for a legacy app\n diff <relpath> Compare a tracked scaffold file with its template\n scaffold <action> List or record per-file scaffold intent\n remove <moduleKey> Conservatively remove module package wiring\n upgrade [moduleKey] Upgrade packages, managed files, and migrations\n update Alias for the legacy create-bw-app update flow\n doctor Validate app health and manifest consistency\n\nRun bw <command> --help for command-specific options.`;
|
|
12
13
|
|
|
13
14
|
function parseOptions(argv) {
|
|
14
15
|
const options = {};
|
|
@@ -36,7 +37,8 @@ export async function runBwCli(argv = process.argv.slice(2), runtimeOptions = {}
|
|
|
36
37
|
if (!command || command === "--help" || command === "help") { process.stdout.write(`${HELP}\n`); return; }
|
|
37
38
|
const { options, positionals } = parseOptions(argv.slice(1));
|
|
38
39
|
try {
|
|
39
|
-
if (command === "
|
|
40
|
+
if (command === "setup") await setupBrightwebFeature(positionals[0], options, runtimeOptions);
|
|
41
|
+
else if (command === "add") await addBrightwebModule(positionals[0], options, runtimeOptions);
|
|
40
42
|
else if (command === "admin") await createFirstAdmin(positionals[0], options, runtimeOptions);
|
|
41
43
|
else if (command === "adopt") await adoptBrightwebApp(options, runtimeOptions);
|
|
42
44
|
else if (command === "diff") await diffBrightwebScaffold(positionals[0], options, runtimeOptions);
|
package/src/constants.mjs
CHANGED
|
@@ -191,7 +191,7 @@ export const APP_DEPENDENCY_DEFAULTS = {
|
|
|
191
191
|
"@brightweblabs/infra": "^0.7.0",
|
|
192
192
|
"@brightweblabs/module-admin": "^0.9.5",
|
|
193
193
|
"@brightweblabs/module-crm": "^0.18.2",
|
|
194
|
-
"@brightweblabs/module-marketing": "^0.
|
|
194
|
+
"@brightweblabs/module-marketing": "^0.5.0",
|
|
195
195
|
"@brightweblabs/module-orgs": "^0.7.1",
|
|
196
196
|
"@brightweblabs/module-projects": "^0.19.1",
|
|
197
197
|
"@brightweblabs/theme": "^0.8.3",
|
package/src/setup.mjs
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { readAppManifest, readConfiguredModuleFlags } from "./app-manifest.mjs";
|
|
5
|
+
|
|
6
|
+
const HELP = `Usage: bw setup social-media [options]
|
|
7
|
+
|
|
8
|
+
Create an app-owned empty social media plan, page, and navigation.
|
|
9
|
+
Existing plan and page files are preserved. Requires installed marketing support.
|
|
10
|
+
|
|
11
|
+
Options:
|
|
12
|
+
--target-dir <path> App directory (defaults to cwd)
|
|
13
|
+
--dry-run Show changes without writing
|
|
14
|
+
--help Show this help`;
|
|
15
|
+
const PLAN_PATH = "config/social-media-plan.json";
|
|
16
|
+
const PAGE_PATH = "app/(shell)/marketing/social-media/page.tsx";
|
|
17
|
+
const SHELL_PATH = "config/shell.overrides.ts";
|
|
18
|
+
const ASSIGNMENT = "shellRegistrationOverrides.marketing = withSocialMediaNavigation(shellRegistrationOverrides.marketing);";
|
|
19
|
+
const IMPORT = 'import { withSocialMediaNavigation } from "@brightweblabs/module-marketing/registration";';
|
|
20
|
+
const PLAN = { title: "Social media", period: "", sections: [{ id: "calendario", title: "Calendário" }], types: {}, events: [] };
|
|
21
|
+
const PAGE = `import { SocialMediaPage } from "@brightweblabs/module-marketing/social-media";
|
|
22
|
+
import plan from "../../../../config/social-media-plan.json";
|
|
23
|
+
|
|
24
|
+
export default function Page() {
|
|
25
|
+
return <SocialMediaPage plan={plan} />;
|
|
26
|
+
}
|
|
27
|
+
`;
|
|
28
|
+
|
|
29
|
+
// Generated paths must remain real directories/files inside the app, even when
|
|
30
|
+
// an intermediate directory or an existing target is a symlink.
|
|
31
|
+
async function inspectTarget(root, relativePath) {
|
|
32
|
+
let current = root;
|
|
33
|
+
const parts = relativePath.split("/");
|
|
34
|
+
for (const [index, part] of parts.entries()) {
|
|
35
|
+
current = path.join(current, part);
|
|
36
|
+
let stat;
|
|
37
|
+
try { stat = await fs.lstat(current); } catch (error) {
|
|
38
|
+
if (error.code === "ENOENT") return null;
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
if (stat.isSymbolicLink()) throw new Error(`Setup does not follow symlinks: ${relativePath}`);
|
|
42
|
+
if (index < parts.length - 1 && !stat.isDirectory()) throw new Error(`Expected a directory at ${current}`);
|
|
43
|
+
if (index === parts.length - 1 && !stat.isFile()) throw new Error(`Expected a file at ${relativePath}`);
|
|
44
|
+
}
|
|
45
|
+
return fs.readFile(current, "utf8");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function navigationSource(source) {
|
|
49
|
+
const code = source.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "");
|
|
50
|
+
if (!/^export const shellRegistrationOverrides\s*:\s*ShellRegistrationOverrides\s*=\s*\{/m.test(code)) {
|
|
51
|
+
throw new Error(`Cannot safely identify the standard exported const shellRegistrationOverrides: ShellRegistrationOverrides in ${SHELL_PATH}. Wire withSocialMediaNavigation manually before rerunning setup.`);
|
|
52
|
+
}
|
|
53
|
+
if (code.includes(ASSIGNMENT) && code.includes(IMPORT)) return source;
|
|
54
|
+
if (/\bwithSocialMediaNavigation\b/.test(code)) {
|
|
55
|
+
throw new Error(`Existing custom withSocialMediaNavigation wiring in ${SHELL_PATH}; use the documented import and assignment before rerunning setup.`);
|
|
56
|
+
}
|
|
57
|
+
return `${source}${source.endsWith("\n") ? "" : "\n"}\n${IMPORT}\n${ASSIGNMENT}\n`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function setupBrightwebFeature(feature, options = {}, runtimeOptions = {}) {
|
|
61
|
+
if (options.help) { process.stdout.write(`${HELP}\n`); return; }
|
|
62
|
+
if (feature !== "social-media") throw new Error(`Unknown setup feature: ${feature || "(missing)"}\n${HELP}`);
|
|
63
|
+
const targetDir = await fs.realpath(path.resolve(runtimeOptions.targetDir || options.targetDir || process.cwd()));
|
|
64
|
+
const manifest = await readAppManifest(targetDir);
|
|
65
|
+
const flags = await readConfiguredModuleFlags(targetDir);
|
|
66
|
+
if (manifest.app.template !== "platform" || !manifest.modules.marketing || flags.marketing !== true) {
|
|
67
|
+
throw new Error("Social media setup requires a platform app with marketing installed and enabled. Run bw add marketing first.");
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
const require = createRequire(path.join(targetDir, "package.json"));
|
|
71
|
+
require.resolve("@brightweblabs/module-marketing/social-media");
|
|
72
|
+
const registration = await fs.readFile(require.resolve("@brightweblabs/module-marketing/registration"), "utf8");
|
|
73
|
+
if (!/export\s+(?:function|const)\s+withSocialMediaNavigation\b/.test(registration)) throw new Error("Missing navigation helper");
|
|
74
|
+
} catch {
|
|
75
|
+
throw new Error("Install or upgrade @brightweblabs/module-marketing to a release with social-media and withSocialMediaNavigation support, then rerun setup.");
|
|
76
|
+
}
|
|
77
|
+
const plan = await inspectTarget(targetDir, PLAN_PATH);
|
|
78
|
+
const page = await inspectTarget(targetDir, PAGE_PATH);
|
|
79
|
+
const shell = await inspectTarget(targetDir, SHELL_PATH);
|
|
80
|
+
if (shell === null) throw new Error(`Missing ${SHELL_PATH}; restore the app-owned shell overrides file before setup.`);
|
|
81
|
+
const nextShell = navigationSource(shell);
|
|
82
|
+
const changes = [
|
|
83
|
+
...(plan === null ? [{ path: PLAN_PATH, content: `${JSON.stringify(PLAN, null, 2)}\n`, create: true }] : []),
|
|
84
|
+
...(page === null ? [{ path: PAGE_PATH, content: PAGE, create: true }] : []),
|
|
85
|
+
...(nextShell !== shell ? [{ path: SHELL_PATH, content: nextShell, create: false }] : []),
|
|
86
|
+
];
|
|
87
|
+
for (const change of changes) {
|
|
88
|
+
process.stdout.write(`${options.dryRun ? "Would " : ""}${change.create ? "Create" : "Update"} ${change.path}\n`);
|
|
89
|
+
}
|
|
90
|
+
if (!options.dryRun) for (const change of changes) {
|
|
91
|
+
await fs.mkdir(path.dirname(path.join(targetDir, change.path)), { recursive: true });
|
|
92
|
+
await fs.writeFile(path.join(targetDir, change.path), change.content, { flag: change.create ? "wx" : "w" });
|
|
93
|
+
}
|
|
94
|
+
process.stdout.write(changes.length ? "Social media setup ready. Content belongs to this app.\n" : "Social media is already configured; existing files preserved.\n");
|
|
95
|
+
return { changes: changes.map(({ path: relativePath }) => relativePath), dryRun: !!options.dryRun };
|
|
96
|
+
}
|