create-bw-app 0.9.0 → 0.9.1
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 +22 -0
- package/package.json +1 -1
- package/src/cli.mjs +8 -1
- package/src/constants.mjs +33 -1
- package/src/generator.mjs +17 -16
- package/src/update.mjs +481 -0
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Scaffold a new BrightWeb app from either the `platform` or `site` starter.
|
|
4
4
|
|
|
5
|
+
The CLI can also update an existing generated platform app in place.
|
|
6
|
+
|
|
5
7
|
## Workspace usage
|
|
6
8
|
|
|
7
9
|
From the BrightWeb platform repo root:
|
|
@@ -21,9 +23,29 @@ Once this package is published to npm:
|
|
|
21
23
|
```bash
|
|
22
24
|
pnpm dlx create-bw-app
|
|
23
25
|
pnpm dlx create-bw-app --template site
|
|
26
|
+
pnpm dlx create-bw-app update
|
|
24
27
|
npm create bw-app@latest
|
|
25
28
|
```
|
|
26
29
|
|
|
30
|
+
## Update existing apps
|
|
31
|
+
|
|
32
|
+
Run the updater from an existing generated app directory, or point it at one with `--target-dir`:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pnpm dlx create-bw-app update
|
|
36
|
+
pnpm dlx create-bw-app update --dry-run
|
|
37
|
+
pnpm dlx create-bw-app update --refresh-starters
|
|
38
|
+
pnpm dlx create-bw-app update --target-dir ./apps/client-portal
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Current updater behavior:
|
|
42
|
+
|
|
43
|
+
- updates installed `@brightweblabs/*` packages only
|
|
44
|
+
- re-syncs managed BrightWeb config files such as `next.config.ts`, `config/modules.ts`, and `config/shell.ts`
|
|
45
|
+
- reports missing or drifted starter files and only rewrites them with `--refresh-starters`
|
|
46
|
+
- prints the follow-up install command unless `--install` is passed
|
|
47
|
+
- preserves unrelated third-party dependencies and app-owned product pages
|
|
48
|
+
|
|
27
49
|
## Template behavior
|
|
28
50
|
|
|
29
51
|
- prompts for app type: `platform` or `site`
|
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { HELP_TEXT } from "./constants.mjs";
|
|
2
2
|
import { createBrightwebClientApp } from "./generator.mjs";
|
|
3
|
+
import { updateBrightwebApp } from "./update.mjs";
|
|
3
4
|
|
|
4
5
|
function toCamelCase(flagName) {
|
|
5
6
|
return flagName.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
@@ -56,7 +57,8 @@ function parseArgv(argv) {
|
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
export async function runCreateBwAppCli(argv = process.argv.slice(2), runtimeOptions = {}) {
|
|
59
|
-
const
|
|
60
|
+
const isUpdateCommand = argv[0] === "update";
|
|
61
|
+
const argvOptions = parseArgv(isUpdateCommand ? argv.slice(1) : argv);
|
|
60
62
|
|
|
61
63
|
if (argvOptions.help) {
|
|
62
64
|
process.stdout.write(`${HELP_TEXT}\n`);
|
|
@@ -64,6 +66,11 @@ export async function runCreateBwAppCli(argv = process.argv.slice(2), runtimeOpt
|
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
try {
|
|
69
|
+
if (isUpdateCommand) {
|
|
70
|
+
await updateBrightwebApp(argvOptions, runtimeOptions);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
67
74
|
await createBrightwebClientApp(argvOptions, runtimeOptions);
|
|
68
75
|
} catch (error) {
|
|
69
76
|
const message = error instanceof Error ? error.message : "Unknown error";
|
package/src/constants.mjs
CHANGED
|
@@ -42,6 +42,29 @@ export const CORE_PACKAGES = [
|
|
|
42
42
|
"@brightweblabs/ui",
|
|
43
43
|
];
|
|
44
44
|
|
|
45
|
+
export const BRIGHTWEB_PACKAGE_NAMES = [
|
|
46
|
+
...CORE_PACKAGES,
|
|
47
|
+
...SELECTABLE_MODULES.map((moduleDefinition) => moduleDefinition.packageName),
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
export const MODULE_STARTER_FILES = {
|
|
51
|
+
admin: [
|
|
52
|
+
"app/api/admin/users/route.ts",
|
|
53
|
+
"app/api/admin/users/roles/route.ts",
|
|
54
|
+
"app/playground/admin/page.tsx",
|
|
55
|
+
],
|
|
56
|
+
crm: [
|
|
57
|
+
"app/api/crm/contacts/route.ts",
|
|
58
|
+
"app/api/crm/organizations/route.ts",
|
|
59
|
+
"app/api/crm/owners/route.ts",
|
|
60
|
+
"app/api/crm/stats/route.ts",
|
|
61
|
+
"app/playground/crm/page.tsx",
|
|
62
|
+
],
|
|
63
|
+
projects: [
|
|
64
|
+
"app/playground/projects/page.tsx",
|
|
65
|
+
],
|
|
66
|
+
};
|
|
67
|
+
|
|
45
68
|
export const APP_DEPENDENCY_DEFAULTS = {
|
|
46
69
|
"@brightweblabs/app-shell": "^0.1.1",
|
|
47
70
|
"@brightweblabs/core-auth": "^0.1.1",
|
|
@@ -93,8 +116,9 @@ export const DEFAULTS = {
|
|
|
93
116
|
export const HELP_TEXT = `
|
|
94
117
|
Usage:
|
|
95
118
|
create-bw-app [options]
|
|
119
|
+
create-bw-app update [options]
|
|
96
120
|
|
|
97
|
-
|
|
121
|
+
Scaffold options:
|
|
98
122
|
--template <platform|site> Scaffold a platform app or a standalone site
|
|
99
123
|
--name, --slug <name> Project name and default directory name
|
|
100
124
|
--modules <list> Comma-separated modules: crm,projects,admin
|
|
@@ -107,4 +131,12 @@ Options:
|
|
|
107
131
|
--yes Accept defaults for any missing optional prompt
|
|
108
132
|
--dry-run Print planned actions without writing files
|
|
109
133
|
--help Show this help message
|
|
134
|
+
|
|
135
|
+
Update options:
|
|
136
|
+
--target-dir <path> Existing app directory to update (defaults to cwd)
|
|
137
|
+
--workspace-root <path> BrightWeb workspace root for workspace:* apps
|
|
138
|
+
--package-manager <name> Override package manager: pnpm, npm, yarn, or bun
|
|
139
|
+
--install Run install after writing package changes
|
|
140
|
+
--refresh-starters Rewrite starter route files from the latest template
|
|
141
|
+
--dry-run Print the update plan without writing files
|
|
110
142
|
`.trim();
|
package/src/generator.mjs
CHANGED
|
@@ -16,8 +16,8 @@ import {
|
|
|
16
16
|
TEMPLATE_OPTIONS,
|
|
17
17
|
} from "./constants.mjs";
|
|
18
18
|
|
|
19
|
-
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
20
|
-
const TEMPLATE_ROOT = path.join(PACKAGE_ROOT, "template");
|
|
19
|
+
export const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
20
|
+
export const TEMPLATE_ROOT = path.join(PACKAGE_ROOT, "template");
|
|
21
21
|
const TEMPLATE_KEY_SET = new Set(TEMPLATE_OPTIONS.map((templateOption) => templateOption.key));
|
|
22
22
|
const DEFAULT_DB_MODULE_REGISTRY = {
|
|
23
23
|
modules: {
|
|
@@ -77,7 +77,7 @@ function parseTemplateInput(rawValue) {
|
|
|
77
77
|
throw new Error(`Unknown template: ${rawValue}`);
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
function detectPackageManager(explicitManager) {
|
|
80
|
+
export function detectPackageManager(explicitManager) {
|
|
81
81
|
if (explicitManager) return explicitManager;
|
|
82
82
|
|
|
83
83
|
const userAgent = process.env.npm_config_user_agent || "";
|
|
@@ -88,13 +88,13 @@ function detectPackageManager(explicitManager) {
|
|
|
88
88
|
return "pnpm";
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
function sortObjectKeys(inputObject) {
|
|
91
|
+
export function sortObjectKeys(inputObject) {
|
|
92
92
|
return Object.fromEntries(
|
|
93
93
|
Object.entries(inputObject).sort(([leftKey], [rightKey]) => leftKey.localeCompare(rightKey)),
|
|
94
94
|
);
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
async function pathExists(targetPath) {
|
|
97
|
+
export async function pathExists(targetPath) {
|
|
98
98
|
try {
|
|
99
99
|
await fs.access(targetPath);
|
|
100
100
|
return true;
|
|
@@ -103,7 +103,7 @@ async function pathExists(targetPath) {
|
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
async function readJsonIfPresent(filePath) {
|
|
106
|
+
export async function readJsonIfPresent(filePath) {
|
|
107
107
|
if (!(await pathExists(filePath))) {
|
|
108
108
|
return null;
|
|
109
109
|
}
|
|
@@ -111,7 +111,7 @@ async function readJsonIfPresent(filePath) {
|
|
|
111
111
|
return JSON.parse(await fs.readFile(filePath, "utf8"));
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
async function getDbModuleRegistry(workspaceRoot) {
|
|
114
|
+
export async function getDbModuleRegistry(workspaceRoot) {
|
|
115
115
|
if (!workspaceRoot) {
|
|
116
116
|
return DEFAULT_DB_MODULE_REGISTRY;
|
|
117
117
|
}
|
|
@@ -171,7 +171,7 @@ function getModuleLabel(moduleKey) {
|
|
|
171
171
|
return titleizeSlug(moduleKey);
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
function createDbInstallPlan({ selectedModules, workspaceMode, registry }) {
|
|
174
|
+
export function createDbInstallPlan({ selectedModules, workspaceMode, registry }) {
|
|
175
175
|
if (!workspaceMode) {
|
|
176
176
|
return {
|
|
177
177
|
selectedLabels: getSelectedModuleLabels(selectedModules),
|
|
@@ -218,7 +218,7 @@ function createDbInstallPlan({ selectedModules, workspaceMode, registry }) {
|
|
|
218
218
|
};
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
-
async function getVersionMap(workspaceRoot) {
|
|
221
|
+
export async function getVersionMap(workspaceRoot) {
|
|
222
222
|
const versionMap = {
|
|
223
223
|
...APP_DEPENDENCY_DEFAULTS,
|
|
224
224
|
...APP_DEV_DEPENDENCY_DEFAULTS,
|
|
@@ -294,7 +294,7 @@ function createPlatformBrandConfigFile({ slug, brandValues }) {
|
|
|
294
294
|
].join("\n");
|
|
295
295
|
}
|
|
296
296
|
|
|
297
|
-
function createPlatformModulesConfigFile(selectedModules) {
|
|
297
|
+
export function createPlatformModulesConfigFile(selectedModules) {
|
|
298
298
|
const selected = new Set(selectedModules);
|
|
299
299
|
|
|
300
300
|
return [
|
|
@@ -515,7 +515,7 @@ function createSiteReadme({ slug, workspaceMode, packageManager }) {
|
|
|
515
515
|
].join("\n");
|
|
516
516
|
}
|
|
517
517
|
|
|
518
|
-
function createPackageJson({
|
|
518
|
+
export function createPackageJson({
|
|
519
519
|
slug,
|
|
520
520
|
dependencyMode,
|
|
521
521
|
selectedModules,
|
|
@@ -595,7 +595,7 @@ function createPackageJson({
|
|
|
595
595
|
};
|
|
596
596
|
}
|
|
597
597
|
|
|
598
|
-
function createNextConfig({ template, selectedModules }) {
|
|
598
|
+
export function createNextConfig({ template, selectedModules }) {
|
|
599
599
|
if (template === "site") {
|
|
600
600
|
return [
|
|
601
601
|
'import type { NextConfig } from "next";',
|
|
@@ -629,7 +629,7 @@ function createNextConfig({ template, selectedModules }) {
|
|
|
629
629
|
].join("\n");
|
|
630
630
|
}
|
|
631
631
|
|
|
632
|
-
function createShellConfig(selectedModules) {
|
|
632
|
+
export function createShellConfig(selectedModules) {
|
|
633
633
|
const importLines = [];
|
|
634
634
|
const registrationLines = [];
|
|
635
635
|
|
|
@@ -745,7 +745,7 @@ async function copyDirectory(sourceDir, targetDir) {
|
|
|
745
745
|
await fs.cp(sourceDir, targetDir, { recursive: true });
|
|
746
746
|
}
|
|
747
747
|
|
|
748
|
-
async function ensureDirectory(targetDir) {
|
|
748
|
+
export async function ensureDirectory(targetDir) {
|
|
749
749
|
await fs.mkdir(targetDir, { recursive: true });
|
|
750
750
|
}
|
|
751
751
|
|
|
@@ -793,7 +793,7 @@ async function writeWorkspaceClientStack(workspaceRoot, slug, selectedModules) {
|
|
|
793
793
|
);
|
|
794
794
|
}
|
|
795
795
|
|
|
796
|
-
async function runInstall(command, cwd) {
|
|
796
|
+
export async function runInstall(command, cwd) {
|
|
797
797
|
return new Promise((resolve, reject) => {
|
|
798
798
|
const child = spawn(command, ["install"], {
|
|
799
799
|
cwd,
|
|
@@ -1140,7 +1140,8 @@ export async function createBrightwebClientApp(argvOptions, runtimeOptions = {})
|
|
|
1140
1140
|
|
|
1141
1141
|
if (install) {
|
|
1142
1142
|
const installCwd = workspaceMode ? workspaceRoot : targetDir;
|
|
1143
|
-
|
|
1143
|
+
const installRunner = runtimeOptions.installRunner || runInstall;
|
|
1144
|
+
await installRunner(packageManager, installCwd);
|
|
1144
1145
|
}
|
|
1145
1146
|
|
|
1146
1147
|
printCompletionMessage({
|
package/src/update.mjs
ADDED
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { stdout as output } from "node:process";
|
|
4
|
+
import {
|
|
5
|
+
BRIGHTWEB_PACKAGE_NAMES,
|
|
6
|
+
CLI_DISPLAY_NAME,
|
|
7
|
+
MODULE_STARTER_FILES,
|
|
8
|
+
SELECTABLE_MODULES,
|
|
9
|
+
} from "./constants.mjs";
|
|
10
|
+
import {
|
|
11
|
+
TEMPLATE_ROOT,
|
|
12
|
+
createDbInstallPlan,
|
|
13
|
+
createNextConfig,
|
|
14
|
+
createPackageJson,
|
|
15
|
+
createPlatformModulesConfigFile,
|
|
16
|
+
createShellConfig,
|
|
17
|
+
detectPackageManager,
|
|
18
|
+
getDbModuleRegistry,
|
|
19
|
+
getVersionMap,
|
|
20
|
+
pathExists,
|
|
21
|
+
readJsonIfPresent,
|
|
22
|
+
runInstall,
|
|
23
|
+
} from "./generator.mjs";
|
|
24
|
+
|
|
25
|
+
const MANAGED_PLATFORM_FILES = [
|
|
26
|
+
"next.config.ts",
|
|
27
|
+
path.join("config", "modules.ts"),
|
|
28
|
+
path.join("config", "shell.ts"),
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
function resolveUpdateTargetDirectory(runtimeOptions, argvOptions) {
|
|
32
|
+
if (runtimeOptions.targetDir || argvOptions.targetDir) {
|
|
33
|
+
return path.resolve(runtimeOptions.targetDir || argvOptions.targetDir);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return process.cwd();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function findWorkspaceRoot(startDir) {
|
|
40
|
+
let currentDir = path.resolve(startDir);
|
|
41
|
+
|
|
42
|
+
while (true) {
|
|
43
|
+
const registryPath = path.join(currentDir, "supabase", "module-registry.json");
|
|
44
|
+
const cliPackagePath = path.join(currentDir, "packages", "create-bw-app", "package.json");
|
|
45
|
+
|
|
46
|
+
if ((await pathExists(registryPath)) && (await pathExists(cliPackagePath))) {
|
|
47
|
+
return currentDir;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const parentDir = path.dirname(currentDir);
|
|
51
|
+
if (parentDir === currentDir) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
currentDir = parentDir;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function collectInstalledBrightwebPackages(manifest) {
|
|
60
|
+
const installed = new Map();
|
|
61
|
+
|
|
62
|
+
for (const section of ["dependencies", "devDependencies"]) {
|
|
63
|
+
const sectionManifest = manifest[section] || {};
|
|
64
|
+
for (const [packageName, version] of Object.entries(sectionManifest)) {
|
|
65
|
+
if (!packageName.startsWith("@brightweblabs/")) continue;
|
|
66
|
+
installed.set(packageName, { section, version });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return installed;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function parseConfiguredModules(content) {
|
|
74
|
+
const enabledModules = [];
|
|
75
|
+
|
|
76
|
+
for (const moduleDefinition of SELECTABLE_MODULES) {
|
|
77
|
+
const matcher = new RegExp(`key:\\s*"${moduleDefinition.key}"[\\s\\S]*?enabled:\\s*(true|false)`);
|
|
78
|
+
const match = content.match(matcher);
|
|
79
|
+
if (match?.[1] === "true") {
|
|
80
|
+
enabledModules.push(moduleDefinition.key);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return enabledModules;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function detectTemplate(targetDir, installedBrightwebPackages) {
|
|
88
|
+
if (await pathExists(path.join(targetDir, "config", "modules.ts"))) {
|
|
89
|
+
return "platform";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return installedBrightwebPackages.size > 0 ? "platform" : "site";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function detectDependencyMode(installedBrightwebPackages) {
|
|
96
|
+
for (const { version } of installedBrightwebPackages.values()) {
|
|
97
|
+
if (typeof version === "string" && version.startsWith("workspace:")) {
|
|
98
|
+
return "workspace";
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return "published";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function detectInstalledModules(installedBrightwebPackages) {
|
|
106
|
+
return SELECTABLE_MODULES
|
|
107
|
+
.filter((moduleDefinition) => installedBrightwebPackages.has(moduleDefinition.packageName))
|
|
108
|
+
.map((moduleDefinition) => moduleDefinition.key);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function getCanonicalBrightwebVersions({ manifest, template, dependencyMode, installedModules, versionMap }) {
|
|
112
|
+
const canonicalManifest = createPackageJson({
|
|
113
|
+
slug: manifest.name || path.basename(process.cwd()),
|
|
114
|
+
dependencyMode,
|
|
115
|
+
selectedModules: installedModules,
|
|
116
|
+
versionMap,
|
|
117
|
+
template,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const versions = {};
|
|
121
|
+
|
|
122
|
+
for (const section of ["dependencies", "devDependencies"]) {
|
|
123
|
+
for (const [packageName, version] of Object.entries(canonicalManifest[section] || {})) {
|
|
124
|
+
if (BRIGHTWEB_PACKAGE_NAMES.includes(packageName)) {
|
|
125
|
+
versions[packageName] = version;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return versions;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function mergeManagedPackageUpdates({ manifest, targetVersions, installedBrightwebPackages }) {
|
|
134
|
+
let changed = false;
|
|
135
|
+
const nextManifest = {
|
|
136
|
+
...manifest,
|
|
137
|
+
dependencies: manifest.dependencies ? { ...manifest.dependencies } : undefined,
|
|
138
|
+
devDependencies: manifest.devDependencies ? { ...manifest.devDependencies } : undefined,
|
|
139
|
+
};
|
|
140
|
+
const packageUpdates = [];
|
|
141
|
+
|
|
142
|
+
for (const [packageName, details] of installedBrightwebPackages.entries()) {
|
|
143
|
+
if (!BRIGHTWEB_PACKAGE_NAMES.includes(packageName)) continue;
|
|
144
|
+
const targetVersion = targetVersions[packageName];
|
|
145
|
+
if (!targetVersion) continue;
|
|
146
|
+
|
|
147
|
+
const currentSection = nextManifest[details.section] || {};
|
|
148
|
+
if (currentSection[packageName] === targetVersion) continue;
|
|
149
|
+
|
|
150
|
+
currentSection[packageName] = targetVersion;
|
|
151
|
+
nextManifest[details.section] = currentSection;
|
|
152
|
+
packageUpdates.push({
|
|
153
|
+
packageName,
|
|
154
|
+
from: details.version,
|
|
155
|
+
to: targetVersion,
|
|
156
|
+
section: details.section,
|
|
157
|
+
});
|
|
158
|
+
changed = true;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
changed,
|
|
163
|
+
packageUpdates,
|
|
164
|
+
content: `${JSON.stringify(nextManifest, null, 2)}\n`,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function getStarterFileStatus(targetDir, installedModules) {
|
|
169
|
+
const starterFiles = [];
|
|
170
|
+
|
|
171
|
+
for (const moduleKey of installedModules) {
|
|
172
|
+
const templateFolder = SELECTABLE_MODULES.find((moduleDefinition) => moduleDefinition.key === moduleKey)?.templateFolder;
|
|
173
|
+
if (!templateFolder) continue;
|
|
174
|
+
|
|
175
|
+
for (const relativePath of MODULE_STARTER_FILES[moduleKey] || []) {
|
|
176
|
+
const sourcePath = path.join(TEMPLATE_ROOT, "modules", templateFolder, relativePath);
|
|
177
|
+
const targetPath = path.join(targetDir, relativePath);
|
|
178
|
+
const exists = await pathExists(targetPath);
|
|
179
|
+
|
|
180
|
+
if (!exists) {
|
|
181
|
+
starterFiles.push({
|
|
182
|
+
moduleKey,
|
|
183
|
+
relativePath,
|
|
184
|
+
sourcePath,
|
|
185
|
+
targetPath,
|
|
186
|
+
status: "missing",
|
|
187
|
+
});
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const [sourceContent, targetContent] = await Promise.all([
|
|
192
|
+
fs.readFile(sourcePath, "utf8"),
|
|
193
|
+
fs.readFile(targetPath, "utf8"),
|
|
194
|
+
]);
|
|
195
|
+
|
|
196
|
+
starterFiles.push({
|
|
197
|
+
moduleKey,
|
|
198
|
+
relativePath,
|
|
199
|
+
sourcePath,
|
|
200
|
+
targetPath,
|
|
201
|
+
status: sourceContent === targetContent ? "current" : "drifted",
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return starterFiles;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function detectModulesConfigMismatch(targetDir, installedModules) {
|
|
210
|
+
const modulesConfigPath = path.join(targetDir, "config", "modules.ts");
|
|
211
|
+
if (!(await pathExists(modulesConfigPath))) {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const content = await fs.readFile(modulesConfigPath, "utf8");
|
|
216
|
+
const configuredModules = parseConfiguredModules(content);
|
|
217
|
+
const installed = new Set(installedModules);
|
|
218
|
+
const configured = new Set(configuredModules);
|
|
219
|
+
|
|
220
|
+
const mismatch = installedModules.length !== configuredModules.length
|
|
221
|
+
|| installedModules.some((moduleKey) => !configured.has(moduleKey))
|
|
222
|
+
|| configuredModules.some((moduleKey) => !installed.has(moduleKey));
|
|
223
|
+
|
|
224
|
+
if (!mismatch) {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
installedModules,
|
|
230
|
+
configuredModules,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function renderInstallCommand({ packageManager, dependencyMode, targetDir, workspaceRoot }) {
|
|
235
|
+
if (dependencyMode === "workspace" && workspaceRoot) {
|
|
236
|
+
return `cd ${workspaceRoot} && ${packageManager} install`;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return `cd ${targetDir} && ${packageManager} install`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function renderPlanSummary(plan, options = {}) {
|
|
243
|
+
const lines = [
|
|
244
|
+
`${CLI_DISPLAY_NAME} update`,
|
|
245
|
+
"",
|
|
246
|
+
`Detected app type: ${plan.template}`,
|
|
247
|
+
`Dependency mode: ${plan.dependencyMode}`,
|
|
248
|
+
`Installed BrightWeb packages: ${plan.installedBrightwebPackages.length > 0 ? plan.installedBrightwebPackages.join(", ") : "none"}`,
|
|
249
|
+
`Installed modules: ${plan.installedModules.length > 0 ? plan.installedModules.join(", ") : "none"}`,
|
|
250
|
+
];
|
|
251
|
+
|
|
252
|
+
if (plan.modulesConfigMismatch) {
|
|
253
|
+
lines.push(
|
|
254
|
+
`Config mismatch: installed modules (${plan.modulesConfigMismatch.installedModules.join(", ") || "none"}) differ from config/modules.ts (${plan.modulesConfigMismatch.configuredModules.join(", ") || "none"}). Using installed packages as source of truth.`,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
lines.push("");
|
|
259
|
+
|
|
260
|
+
if (plan.packageUpdates.length > 0) {
|
|
261
|
+
lines.push("Packages to update:");
|
|
262
|
+
for (const update of plan.packageUpdates) {
|
|
263
|
+
lines.push(`- ${update.packageName}: ${update.from} -> ${update.to}`);
|
|
264
|
+
}
|
|
265
|
+
} else {
|
|
266
|
+
lines.push("Packages to update: none");
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
lines.push("");
|
|
270
|
+
|
|
271
|
+
if (plan.configFilesToWrite.length > 0) {
|
|
272
|
+
lines.push("Config files to rewrite:");
|
|
273
|
+
for (const relativePath of plan.configFilesToWrite) {
|
|
274
|
+
lines.push(`- ${relativePath}`);
|
|
275
|
+
}
|
|
276
|
+
} else {
|
|
277
|
+
lines.push("Config files to rewrite: none");
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
lines.push("");
|
|
281
|
+
|
|
282
|
+
if (plan.starterFilesMissing.length > 0 || plan.starterFilesDrifted.length > 0) {
|
|
283
|
+
lines.push("Starter file status:");
|
|
284
|
+
for (const relativePath of plan.starterFilesMissing) {
|
|
285
|
+
lines.push(`- missing: ${relativePath}`);
|
|
286
|
+
}
|
|
287
|
+
for (const relativePath of plan.starterFilesDrifted) {
|
|
288
|
+
lines.push(`- drifted: ${relativePath}`);
|
|
289
|
+
}
|
|
290
|
+
} else {
|
|
291
|
+
lines.push("Starter file status: all current");
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
lines.push("");
|
|
295
|
+
|
|
296
|
+
if (plan.dbInstallPlan.resolvedOrder.length > 0) {
|
|
297
|
+
lines.push(`Resolved database stack: ${plan.dbInstallPlan.resolvedOrder.join(" -> ")}`);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (plan.dbInstallPlan.notes.length > 0) {
|
|
301
|
+
lines.push("Database notes:");
|
|
302
|
+
for (const note of plan.dbInstallPlan.notes) {
|
|
303
|
+
lines.push(`- ${note}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (options.installCommand) {
|
|
308
|
+
lines.push("", `Next install command: ${options.installCommand}`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return `${lines.join("\n")}\n`;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export async function buildBrightwebAppUpdatePlan(argvOptions = {}, runtimeOptions = {}) {
|
|
315
|
+
const targetDir = resolveUpdateTargetDirectory(runtimeOptions, argvOptions);
|
|
316
|
+
const packageJsonPath = path.join(targetDir, "package.json");
|
|
317
|
+
const manifest = await readJsonIfPresent(packageJsonPath);
|
|
318
|
+
|
|
319
|
+
if (!manifest) {
|
|
320
|
+
throw new Error(`Target directory does not contain package.json: ${targetDir}`);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const installedBrightwebPackagesMap = collectInstalledBrightwebPackages(manifest);
|
|
324
|
+
const template = await detectTemplate(targetDir, installedBrightwebPackagesMap);
|
|
325
|
+
const dependencyMode = detectDependencyMode(installedBrightwebPackagesMap);
|
|
326
|
+
const workspaceRoot = runtimeOptions.workspaceRoot
|
|
327
|
+
? path.resolve(runtimeOptions.workspaceRoot)
|
|
328
|
+
: argvOptions.workspaceRoot
|
|
329
|
+
? path.resolve(argvOptions.workspaceRoot)
|
|
330
|
+
: dependencyMode === "workspace"
|
|
331
|
+
? await findWorkspaceRoot(targetDir)
|
|
332
|
+
: null;
|
|
333
|
+
const packageManager = detectPackageManager(argvOptions.packageManager || runtimeOptions.packageManager);
|
|
334
|
+
const installedModules = detectInstalledModules(installedBrightwebPackagesMap);
|
|
335
|
+
const versionMap = await getVersionMap(workspaceRoot);
|
|
336
|
+
const dbRegistry = await getDbModuleRegistry(workspaceRoot);
|
|
337
|
+
const dbInstallPlan = template === "platform"
|
|
338
|
+
? createDbInstallPlan({
|
|
339
|
+
selectedModules: installedModules,
|
|
340
|
+
workspaceMode: true,
|
|
341
|
+
registry: dbRegistry,
|
|
342
|
+
})
|
|
343
|
+
: {
|
|
344
|
+
selectedLabels: [],
|
|
345
|
+
resolvedOrder: [],
|
|
346
|
+
notes: [],
|
|
347
|
+
};
|
|
348
|
+
const canonicalVersions = getCanonicalBrightwebVersions({
|
|
349
|
+
manifest,
|
|
350
|
+
template,
|
|
351
|
+
dependencyMode,
|
|
352
|
+
installedModules,
|
|
353
|
+
versionMap,
|
|
354
|
+
});
|
|
355
|
+
const packageJsonUpdate = mergeManagedPackageUpdates({
|
|
356
|
+
manifest,
|
|
357
|
+
targetVersions: canonicalVersions,
|
|
358
|
+
installedBrightwebPackages: installedBrightwebPackagesMap,
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
const fileWrites = [];
|
|
362
|
+
if (packageJsonUpdate.changed) {
|
|
363
|
+
fileWrites.push({
|
|
364
|
+
relativePath: "package.json",
|
|
365
|
+
targetPath: packageJsonPath,
|
|
366
|
+
content: packageJsonUpdate.content,
|
|
367
|
+
type: "config",
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (template === "platform") {
|
|
372
|
+
const canonicalConfigFiles = {
|
|
373
|
+
"next.config.ts": createNextConfig({ template: "platform", selectedModules: installedModules }),
|
|
374
|
+
[path.join("config", "modules.ts")]: createPlatformModulesConfigFile(installedModules),
|
|
375
|
+
[path.join("config", "shell.ts")]: createShellConfig(installedModules),
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
for (const relativePath of MANAGED_PLATFORM_FILES) {
|
|
379
|
+
const targetPath = path.join(targetDir, relativePath);
|
|
380
|
+
const currentContent = (await pathExists(targetPath)) ? await fs.readFile(targetPath, "utf8") : null;
|
|
381
|
+
const nextContent = canonicalConfigFiles[relativePath];
|
|
382
|
+
|
|
383
|
+
if (currentContent !== nextContent) {
|
|
384
|
+
fileWrites.push({
|
|
385
|
+
relativePath,
|
|
386
|
+
targetPath,
|
|
387
|
+
content: nextContent,
|
|
388
|
+
type: "config",
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const starterFiles = template === "platform"
|
|
395
|
+
? await getStarterFileStatus(targetDir, installedModules)
|
|
396
|
+
: [];
|
|
397
|
+
const starterFilesMissing = starterFiles.filter((entry) => entry.status === "missing");
|
|
398
|
+
const starterFilesDrifted = starterFiles.filter((entry) => entry.status === "drifted");
|
|
399
|
+
|
|
400
|
+
if (argvOptions.refreshStarters) {
|
|
401
|
+
for (const entry of starterFiles.filter((candidate) => candidate.status !== "current")) {
|
|
402
|
+
fileWrites.push({
|
|
403
|
+
relativePath: entry.relativePath,
|
|
404
|
+
targetPath: entry.targetPath,
|
|
405
|
+
content: await fs.readFile(entry.sourcePath, "utf8"),
|
|
406
|
+
type: "starter",
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const modulesConfigMismatch = template === "platform"
|
|
412
|
+
? await detectModulesConfigMismatch(targetDir, installedModules)
|
|
413
|
+
: null;
|
|
414
|
+
|
|
415
|
+
return {
|
|
416
|
+
targetDir,
|
|
417
|
+
workspaceRoot,
|
|
418
|
+
template,
|
|
419
|
+
dependencyMode,
|
|
420
|
+
packageManager,
|
|
421
|
+
manifest,
|
|
422
|
+
installedModules,
|
|
423
|
+
installedBrightwebPackages: Array.from(installedBrightwebPackagesMap.keys()).sort(),
|
|
424
|
+
packageUpdates: packageJsonUpdate.packageUpdates,
|
|
425
|
+
configFilesToWrite: fileWrites.filter((entry) => entry.type === "config").map((entry) => entry.relativePath),
|
|
426
|
+
starterFilesMissing: starterFilesMissing.map((entry) => entry.relativePath),
|
|
427
|
+
starterFilesDrifted: starterFilesDrifted.map((entry) => entry.relativePath),
|
|
428
|
+
starterFilesToRefresh: fileWrites.filter((entry) => entry.type === "starter").map((entry) => entry.relativePath),
|
|
429
|
+
dbInstallPlan,
|
|
430
|
+
modulesConfigMismatch,
|
|
431
|
+
fileWrites,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export async function updateBrightwebApp(argvOptions = {}, runtimeOptions = {}) {
|
|
436
|
+
const plan = await buildBrightwebAppUpdatePlan(argvOptions, runtimeOptions);
|
|
437
|
+
const installCommand = renderInstallCommand({
|
|
438
|
+
packageManager: plan.packageManager,
|
|
439
|
+
dependencyMode: plan.dependencyMode,
|
|
440
|
+
targetDir: plan.targetDir,
|
|
441
|
+
workspaceRoot: plan.workspaceRoot,
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
output.write(renderPlanSummary(plan, { installCommand }));
|
|
445
|
+
|
|
446
|
+
if (argvOptions.dryRun) {
|
|
447
|
+
return {
|
|
448
|
+
dryRun: true,
|
|
449
|
+
plan,
|
|
450
|
+
installCommand,
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
for (const fileWrite of plan.fileWrites) {
|
|
455
|
+
await fs.mkdir(path.dirname(fileWrite.targetPath), { recursive: true });
|
|
456
|
+
await fs.writeFile(fileWrite.targetPath, fileWrite.content, "utf8");
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const packageJsonChanged = plan.fileWrites.some((entry) => entry.relativePath === "package.json");
|
|
460
|
+
if (argvOptions.install && packageJsonChanged) {
|
|
461
|
+
const installRunner = runtimeOptions.installRunner || runInstall;
|
|
462
|
+
const installCwd = plan.dependencyMode === "workspace" && plan.workspaceRoot ? plan.workspaceRoot : plan.targetDir;
|
|
463
|
+
await installRunner(plan.packageManager, installCwd);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (!argvOptions.install && packageJsonChanged) {
|
|
467
|
+
output.write(`Run \`${installCommand}\` to install updated package versions.\n`);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (plan.fileWrites.length === 0) {
|
|
471
|
+
output.write("No managed changes were required.\n");
|
|
472
|
+
} else {
|
|
473
|
+
output.write(`Applied ${plan.fileWrites.length} managed change${plan.fileWrites.length === 1 ? "" : "s"}.\n`);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return {
|
|
477
|
+
dryRun: false,
|
|
478
|
+
plan,
|
|
479
|
+
installCommand,
|
|
480
|
+
};
|
|
481
|
+
}
|