create-bcp-app 0.1.25 → 0.1.27
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 +140 -26
- package/bin/create-bcp-app.mjs +63 -3
- package/package.json +1 -1
- package/src/index.d.mts +8 -0
- package/src/index.mjs +15 -0
- package/src/storage-options.mjs +341 -0
- package/template/README.md +62 -1
package/README.md
CHANGED
|
@@ -8,19 +8,72 @@ cd my-app
|
|
|
8
8
|
npm run dev
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
## Project-local BCP CLI
|
|
12
|
+
|
|
13
|
+
`create-bcp-app` installs BCP Framework as a **project-local dependency**. It does not install the framework CLI globally.
|
|
14
|
+
|
|
15
|
+
Generated npm scripts can use `bcp` directly because npm automatically prepends the project's `node_modules/.bin` directory to `PATH` while scripts are running:
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"scripts": {
|
|
20
|
+
"dev": "bcp dev",
|
|
21
|
+
"build": "bcp build",
|
|
22
|
+
"start": "bcp start",
|
|
23
|
+
"routes": "bcp routes",
|
|
24
|
+
"update": "bcp update"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
For direct PowerShell usage, invoke the local CLI through `npm exec`:
|
|
30
|
+
|
|
31
|
+
```powershell
|
|
32
|
+
npm exec -- bcp-framework --version
|
|
33
|
+
npm exec -- bcp-framework doctor
|
|
34
|
+
npm exec -- bcp-framework inspect
|
|
35
|
+
npm exec -- bcp-framework routes
|
|
36
|
+
npm exec -- bcp-framework dev
|
|
37
|
+
npm exec -- bcp-framework build
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Typing `bcp-framework` directly in a normal PowerShell session may return `CommandNotFoundException` because PowerShell does not automatically add `node_modules/.bin` to its normal command search path.
|
|
41
|
+
|
|
42
|
+
The Windows command shim can also be executed explicitly:
|
|
43
|
+
|
|
44
|
+
```powershell
|
|
45
|
+
.\node_modules\.bin\bcp-framework.cmd --version
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
BCP Framework publishes the `bcp-framework` alias because Microsoft SQL Server can install another Windows executable named `bcp.exe`.
|
|
49
|
+
|
|
50
|
+
Recommended convention:
|
|
51
|
+
|
|
52
|
+
```text
|
|
53
|
+
Inside npm scripts -> bcp ...
|
|
54
|
+
Direct PowerShell usage -> npm exec -- bcp-framework ...
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Using the project-local CLI also keeps the CLI version aligned with the exact BCP Framework dependency installed by the project.
|
|
58
|
+
|
|
11
59
|
When running interactively, the generator asks:
|
|
12
60
|
|
|
13
61
|
```text
|
|
14
|
-
Use Tailwind CSS?
|
|
62
|
+
Use Tailwind CSS?
|
|
15
63
|
Select a database:
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
64
|
+
None
|
|
65
|
+
MySQL
|
|
66
|
+
PostgreSQL
|
|
67
|
+
SQLite
|
|
68
|
+
MongoDB
|
|
21
69
|
Select authentication:
|
|
22
|
-
|
|
23
|
-
|
|
70
|
+
None
|
|
71
|
+
JWT Cookie
|
|
72
|
+
Select storage provider:
|
|
73
|
+
None
|
|
74
|
+
Local Server
|
|
75
|
+
Amazon S3
|
|
76
|
+
Cloudflare R2
|
|
24
77
|
```
|
|
25
78
|
|
|
26
79
|
## Generated application defaults
|
|
@@ -53,7 +106,7 @@ After a newer stable BCP release is published, update the project with:
|
|
|
53
106
|
npm run update
|
|
54
107
|
```
|
|
55
108
|
|
|
56
|
-
This resolves the npm `latest` release through BCP's updater, updates the exact framework dependency and refreshes the active package-manager lockfile. Use `bcp update --check` to inspect an available update without changing files.
|
|
109
|
+
This resolves the npm `latest` release through BCP's updater, updates the exact framework dependency and refreshes the active package-manager lockfile. Use `bcp update --check` inside an npm script or `npm exec -- bcp-framework update --check` from PowerShell to inspect an available update without changing files.
|
|
57
110
|
|
|
58
111
|
## Generated optional setup
|
|
59
112
|
|
|
@@ -96,18 +149,7 @@ import "bcp/server-only";
|
|
|
96
149
|
|
|
97
150
|
This prevents database code from being used in a page/client module graph. Query the database from an API route or a route-level server data loader and consume only serializable results from hydrated pages.
|
|
98
151
|
|
|
99
|
-
For MySQL, the generated helper exports
|
|
100
|
-
|
|
101
|
-
```ts
|
|
102
|
-
import {
|
|
103
|
-
db,
|
|
104
|
-
} from "@/lib/database";
|
|
105
|
-
|
|
106
|
-
const [rows] =
|
|
107
|
-
await db.query(
|
|
108
|
-
"SELECT 1"
|
|
109
|
-
);
|
|
110
|
-
```
|
|
152
|
+
For MySQL, the generated helper exports the BCP database primitives through `bcp/database`.
|
|
111
153
|
|
|
112
154
|
The generated MySQL `.env.example` uses separate connection settings:
|
|
113
155
|
|
|
@@ -119,7 +161,77 @@ DB_PASSWORD=
|
|
|
119
161
|
DB_NAME=bcp_app
|
|
120
162
|
```
|
|
121
163
|
|
|
122
|
-
The generator intentionally
|
|
164
|
+
The generator intentionally does not force an ORM.
|
|
165
|
+
|
|
166
|
+
### Storage provider
|
|
167
|
+
|
|
168
|
+
Selecting a storage provider creates:
|
|
169
|
+
|
|
170
|
+
```text
|
|
171
|
+
lib/storage.ts
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
and adds the provider-specific settings to `.env.example`.
|
|
175
|
+
|
|
176
|
+
#### Local Server
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
npx create-bcp-app my-app --storage local
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Generated environment setting:
|
|
183
|
+
|
|
184
|
+
```dotenv
|
|
185
|
+
STORAGE_LOCAL_DIRECTORY=./storage
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
The generated helper uses `createLocalStorage()` and adds `storage/` to `.gitignore` so uploaded files are not accidentally committed.
|
|
189
|
+
|
|
190
|
+
#### Amazon S3
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
npx create-bcp-app my-app --storage amazon-s3
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Generated environment settings:
|
|
197
|
+
|
|
198
|
+
```dotenv
|
|
199
|
+
AWS_S3_BUCKET=
|
|
200
|
+
AWS_REGION=ap-southeast-1
|
|
201
|
+
AWS_ACCESS_KEY_ID=
|
|
202
|
+
AWS_SECRET_ACCESS_KEY=
|
|
203
|
+
AWS_SESSION_TOKEN=
|
|
204
|
+
AWS_S3_PREFIX=
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
The generated helper uses `createS3Storage()`. Explicit credentials can be left unset when the deployment uses the AWS SDK server-side credential provider chain, such as an IAM role.
|
|
208
|
+
|
|
209
|
+
#### Cloudflare R2
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
npx create-bcp-app my-app --storage cloudflare-r2
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Generated environment settings:
|
|
216
|
+
|
|
217
|
+
```dotenv
|
|
218
|
+
R2_ACCOUNT_ID=
|
|
219
|
+
R2_BUCKET=
|
|
220
|
+
R2_ACCESS_KEY_ID=
|
|
221
|
+
R2_SECRET_ACCESS_KEY=
|
|
222
|
+
R2_PREFIX=
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
The generated helper uses the S3-compatible BCP adapter with:
|
|
226
|
+
|
|
227
|
+
```text
|
|
228
|
+
region: auto
|
|
229
|
+
endpoint: https://<account-id>.r2.cloudflarestorage.com
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Storage credentials are server-only. Do not expose them through `BCP_PUBLIC_*` variables.
|
|
233
|
+
|
|
234
|
+
The selected preset only scaffolds the initial provider configuration. Application code can later switch to another BCP storage adapter without changing the generic storage APIs used by routes and services.
|
|
123
235
|
|
|
124
236
|
### JWT Cookie authentication
|
|
125
237
|
|
|
@@ -169,7 +281,8 @@ POST /api/auth/logout
|
|
|
169
281
|
--no-tailwind Disable Tailwind CSS without prompting
|
|
170
282
|
--database <database> none | mysql | postgresql | sqlite | mongodb
|
|
171
283
|
--auth <preset> none | jwt-cookie
|
|
172
|
-
|
|
284
|
+
--storage <provider> none | local | amazon-s3 | cloudflare-r2
|
|
285
|
+
-y, --yes Accept defaults (Tailwind enabled, no database, no auth, no storage)
|
|
173
286
|
--bcp <specifier> Override dependencies.bcp
|
|
174
287
|
-h, --help Show help
|
|
175
288
|
```
|
|
@@ -177,14 +290,15 @@ POST /api/auth/logout
|
|
|
177
290
|
Examples:
|
|
178
291
|
|
|
179
292
|
```bash
|
|
180
|
-
npx create-bcp-app my-app --tailwind --database mysql --auth jwt-cookie
|
|
293
|
+
npx create-bcp-app my-app --tailwind --database mysql --auth jwt-cookie --storage local
|
|
294
|
+
npx create-bcp-app my-app --storage amazon-s3
|
|
295
|
+
npx create-bcp-app my-app --storage cloudflare-r2
|
|
181
296
|
npx create-bcp-app my-app --no-tailwind --database mongodb
|
|
182
|
-
npx create-bcp-app my-app --auth jwt-cookie
|
|
183
297
|
npx create-bcp-app my-app --yes
|
|
184
298
|
```
|
|
185
299
|
|
|
186
300
|
The `--bcp` option is primarily useful for prerelease and local package testing, for example:
|
|
187
301
|
|
|
188
302
|
```bash
|
|
189
|
-
npx create-bcp-app my-app --bcp file:../bcp-0.1.
|
|
303
|
+
npx create-bcp-app my-app --bcp file:../bcp-0.1.27.tgz
|
|
190
304
|
```
|
package/bin/create-bcp-app.mjs
CHANGED
|
@@ -18,6 +18,11 @@ import {
|
|
|
18
18
|
normalizeAuth,
|
|
19
19
|
normalizeDatabase,
|
|
20
20
|
} from "../src/project-options.mjs";
|
|
21
|
+
import {
|
|
22
|
+
formatStorageProvider,
|
|
23
|
+
normalizeStorageProvider,
|
|
24
|
+
STORAGE_CHOICES,
|
|
25
|
+
} from "../src/storage-options.mjs";
|
|
21
26
|
|
|
22
27
|
const ANSI = {
|
|
23
28
|
reset: "\u001B[0m",
|
|
@@ -71,6 +76,7 @@ async function main() {
|
|
|
71
76
|
tailwind: resolved.tailwind,
|
|
72
77
|
database: resolved.database,
|
|
73
78
|
auth: resolved.auth,
|
|
79
|
+
storage: resolved.storage,
|
|
74
80
|
packageManager: "npm",
|
|
75
81
|
});
|
|
76
82
|
|
|
@@ -87,6 +93,7 @@ function parseArguments(args) {
|
|
|
87
93
|
let tailwind = undefined;
|
|
88
94
|
let database = undefined;
|
|
89
95
|
let auth = undefined;
|
|
96
|
+
let storage = undefined;
|
|
90
97
|
let acceptDefaults = false;
|
|
91
98
|
|
|
92
99
|
for (
|
|
@@ -147,6 +154,20 @@ function parseArguments(args) {
|
|
|
147
154
|
continue;
|
|
148
155
|
}
|
|
149
156
|
|
|
157
|
+
if (value === "--storage") {
|
|
158
|
+
const nextValue = args[index + 1];
|
|
159
|
+
|
|
160
|
+
if (!nextValue) {
|
|
161
|
+
throw new Error(
|
|
162
|
+
"--storage requires a value."
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
storage = normalizeStorageProvider(nextValue);
|
|
167
|
+
index++;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
150
171
|
if (value === "--bcp") {
|
|
151
172
|
const nextValue = args[index + 1];
|
|
152
173
|
|
|
@@ -183,6 +204,7 @@ function parseArguments(args) {
|
|
|
183
204
|
tailwind,
|
|
184
205
|
database,
|
|
185
206
|
auth,
|
|
207
|
+
storage,
|
|
186
208
|
acceptDefaults,
|
|
187
209
|
};
|
|
188
210
|
}
|
|
@@ -191,11 +213,13 @@ async function resolveOptions({
|
|
|
191
213
|
tailwind: selectedTailwind,
|
|
192
214
|
database: selectedDatabase,
|
|
193
215
|
auth: selectedAuth,
|
|
216
|
+
storage: selectedStorage,
|
|
194
217
|
acceptDefaults,
|
|
195
218
|
}) {
|
|
196
219
|
let tailwind = selectedTailwind;
|
|
197
220
|
let database = selectedDatabase;
|
|
198
221
|
let auth = selectedAuth;
|
|
222
|
+
let storage = selectedStorage;
|
|
199
223
|
|
|
200
224
|
if (acceptDefaults) {
|
|
201
225
|
return {
|
|
@@ -206,6 +230,9 @@ async function resolveOptions({
|
|
|
206
230
|
auth: normalizeAuth(
|
|
207
231
|
auth ?? "none"
|
|
208
232
|
),
|
|
233
|
+
storage: normalizeStorageProvider(
|
|
234
|
+
storage ?? "none"
|
|
235
|
+
),
|
|
209
236
|
};
|
|
210
237
|
}
|
|
211
238
|
|
|
@@ -223,6 +250,9 @@ async function resolveOptions({
|
|
|
223
250
|
auth: normalizeAuth(
|
|
224
251
|
auth ?? "none"
|
|
225
252
|
),
|
|
253
|
+
storage: normalizeStorageProvider(
|
|
254
|
+
storage ?? "none"
|
|
255
|
+
),
|
|
226
256
|
};
|
|
227
257
|
}
|
|
228
258
|
|
|
@@ -242,10 +272,15 @@ async function resolveOptions({
|
|
|
242
272
|
auth = await askAuth();
|
|
243
273
|
}
|
|
244
274
|
|
|
275
|
+
if (storage === undefined) {
|
|
276
|
+
storage = await askStorage();
|
|
277
|
+
}
|
|
278
|
+
|
|
245
279
|
return {
|
|
246
280
|
tailwind: Boolean(tailwind),
|
|
247
281
|
database: normalizeDatabase(database),
|
|
248
282
|
auth: normalizeAuth(auth),
|
|
283
|
+
storage: normalizeStorageProvider(storage),
|
|
249
284
|
};
|
|
250
285
|
}
|
|
251
286
|
|
|
@@ -302,6 +337,21 @@ function askAuth() {
|
|
|
302
337
|
);
|
|
303
338
|
}
|
|
304
339
|
|
|
340
|
+
function askStorage() {
|
|
341
|
+
return askTerminalSelect(
|
|
342
|
+
"Select storage provider:",
|
|
343
|
+
STORAGE_CHOICES.map(
|
|
344
|
+
(value) => ({
|
|
345
|
+
value,
|
|
346
|
+
label:
|
|
347
|
+
formatStorageProvider(
|
|
348
|
+
value
|
|
349
|
+
),
|
|
350
|
+
})
|
|
351
|
+
)
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
|
|
305
355
|
function askTerminalSelect(
|
|
306
356
|
label,
|
|
307
357
|
choices
|
|
@@ -486,6 +536,9 @@ function printSuccess(
|
|
|
486
536
|
console.log(
|
|
487
537
|
`Authentication: ${formatAuthName(result.auth)}`
|
|
488
538
|
);
|
|
539
|
+
console.log(
|
|
540
|
+
`Storage: ${formatStorageProvider(result.storage)}`
|
|
541
|
+
);
|
|
489
542
|
console.log("");
|
|
490
543
|
console.log("Next steps:");
|
|
491
544
|
console.log(
|
|
@@ -496,6 +549,10 @@ function printSuccess(
|
|
|
496
549
|
console.log(" npm install");
|
|
497
550
|
}
|
|
498
551
|
|
|
552
|
+
if (result.storage !== "none") {
|
|
553
|
+
console.log(" Copy .env.example to .env and configure storage credentials");
|
|
554
|
+
}
|
|
555
|
+
|
|
499
556
|
console.log(" npm run dev");
|
|
500
557
|
console.log("");
|
|
501
558
|
}
|
|
@@ -533,6 +590,7 @@ Interactive setup:
|
|
|
533
590
|
- Choose Tailwind CSS: Yes or No
|
|
534
591
|
- Choose a database: None, MySQL, PostgreSQL, SQLite or MongoDB
|
|
535
592
|
- Choose authentication: None or JWT Cookie
|
|
593
|
+
- Choose storage: None, Local Server, Amazon S3 or Cloudflare R2
|
|
536
594
|
|
|
537
595
|
Options:
|
|
538
596
|
--no-install Create files without running npm install
|
|
@@ -540,14 +598,16 @@ Options:
|
|
|
540
598
|
--no-tailwind Disable Tailwind CSS without prompting
|
|
541
599
|
--database <database> none | mysql | postgresql | sqlite | mongodb
|
|
542
600
|
--auth <preset> none | jwt-cookie
|
|
543
|
-
|
|
601
|
+
--storage <provider> none | local | amazon-s3 | cloudflare-r2
|
|
602
|
+
-y, --yes Accept defaults (Tailwind enabled, no database, no auth, no storage)
|
|
544
603
|
--bcp <specifier> Override the package specifier stored under dependencies.bcp
|
|
545
604
|
-h, --help Show this help message
|
|
546
605
|
|
|
547
606
|
Examples:
|
|
548
607
|
npx create-bcp-app my-app
|
|
549
|
-
npx create-bcp-app my-app --tailwind --database mysql --auth jwt-cookie
|
|
550
|
-
npx create-bcp-app my-app --
|
|
608
|
+
npx create-bcp-app my-app --tailwind --database mysql --auth jwt-cookie --storage local
|
|
609
|
+
npx create-bcp-app my-app --storage amazon-s3
|
|
610
|
+
npx create-bcp-app my-app --storage cloudflare-r2
|
|
551
611
|
npx create-bcp-app my-app --yes
|
|
552
612
|
npx create-bcp-app my-app --no-install
|
|
553
613
|
`);
|
package/package.json
CHANGED
package/src/index.d.mts
CHANGED
|
@@ -9,6 +9,12 @@ export type BcpAuthPreset =
|
|
|
9
9
|
| "none"
|
|
10
10
|
| "jwt-cookie";
|
|
11
11
|
|
|
12
|
+
export type BcpStorageProvider =
|
|
13
|
+
| "none"
|
|
14
|
+
| "local"
|
|
15
|
+
| "amazon-s3"
|
|
16
|
+
| "cloudflare-r2";
|
|
17
|
+
|
|
12
18
|
export interface CreateBcpAppOptions {
|
|
13
19
|
projectDirectory: string;
|
|
14
20
|
install?: boolean;
|
|
@@ -16,6 +22,7 @@ export interface CreateBcpAppOptions {
|
|
|
16
22
|
tailwind?: boolean;
|
|
17
23
|
database?: BcpDatabase;
|
|
18
24
|
auth?: BcpAuthPreset;
|
|
25
|
+
storage?: BcpStorageProvider;
|
|
19
26
|
packageManager?: "npm";
|
|
20
27
|
}
|
|
21
28
|
|
|
@@ -26,6 +33,7 @@ export interface CreateBcpAppResult {
|
|
|
26
33
|
tailwind: boolean;
|
|
27
34
|
database: BcpDatabase;
|
|
28
35
|
auth: BcpAuthPreset;
|
|
36
|
+
storage: BcpStorageProvider;
|
|
29
37
|
}
|
|
30
38
|
|
|
31
39
|
export interface ResolveNpmInvocationOptions {
|
package/src/index.mjs
CHANGED
|
@@ -12,6 +12,10 @@ import {
|
|
|
12
12
|
normalizeAuth,
|
|
13
13
|
normalizeDatabase,
|
|
14
14
|
} from "./project-options.mjs";
|
|
15
|
+
import {
|
|
16
|
+
applyStorageOption,
|
|
17
|
+
normalizeStorageProvider,
|
|
18
|
+
} from "./storage-options.mjs";
|
|
15
19
|
|
|
16
20
|
const packageRoot =
|
|
17
21
|
path.resolve(
|
|
@@ -47,6 +51,10 @@ export async function createBcpApp(
|
|
|
47
51
|
normalizeAuth(
|
|
48
52
|
options.auth
|
|
49
53
|
);
|
|
54
|
+
const storage =
|
|
55
|
+
normalizeStorageProvider(
|
|
56
|
+
options.storage
|
|
57
|
+
);
|
|
50
58
|
|
|
51
59
|
ensureTargetDirectory(
|
|
52
60
|
targetDirectory
|
|
@@ -128,6 +136,11 @@ export async function createBcpApp(
|
|
|
128
136
|
database,
|
|
129
137
|
auth,
|
|
130
138
|
});
|
|
139
|
+
const selectedStorage =
|
|
140
|
+
applyStorageOption({
|
|
141
|
+
targetDirectory,
|
|
142
|
+
storage,
|
|
143
|
+
});
|
|
131
144
|
|
|
132
145
|
configureFrameworkDatabaseEntry(
|
|
133
146
|
targetDirectory,
|
|
@@ -171,6 +184,8 @@ export async function createBcpApp(
|
|
|
171
184
|
selectedOptions.database,
|
|
172
185
|
auth:
|
|
173
186
|
selectedOptions.auth,
|
|
187
|
+
storage:
|
|
188
|
+
selectedStorage,
|
|
174
189
|
};
|
|
175
190
|
}
|
|
176
191
|
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export const STORAGE_CHOICES = [
|
|
5
|
+
"none",
|
|
6
|
+
"local",
|
|
7
|
+
"amazon-s3",
|
|
8
|
+
"cloudflare-r2",
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
const STORAGE_PRESETS = {
|
|
12
|
+
local: {
|
|
13
|
+
env: [
|
|
14
|
+
"STORAGE_LOCAL_DIRECTORY=./storage",
|
|
15
|
+
],
|
|
16
|
+
gitIgnore: [
|
|
17
|
+
"storage/",
|
|
18
|
+
],
|
|
19
|
+
source: `import "bcp/server-only";
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
createLocalStorage,
|
|
23
|
+
} from "bcp/server";
|
|
24
|
+
|
|
25
|
+
export const storage =
|
|
26
|
+
createLocalStorage({
|
|
27
|
+
directory:
|
|
28
|
+
process.env.STORAGE_LOCAL_DIRECTORY ??
|
|
29
|
+
"./storage",
|
|
30
|
+
});
|
|
31
|
+
`,
|
|
32
|
+
},
|
|
33
|
+
"amazon-s3": {
|
|
34
|
+
env: [
|
|
35
|
+
"AWS_S3_BUCKET=",
|
|
36
|
+
"AWS_REGION=ap-southeast-1",
|
|
37
|
+
"AWS_ACCESS_KEY_ID=",
|
|
38
|
+
"AWS_SECRET_ACCESS_KEY=",
|
|
39
|
+
"AWS_SESSION_TOKEN=",
|
|
40
|
+
"AWS_S3_PREFIX=",
|
|
41
|
+
],
|
|
42
|
+
gitIgnore: [],
|
|
43
|
+
source: `import "bcp/server-only";
|
|
44
|
+
|
|
45
|
+
import {
|
|
46
|
+
createS3Storage,
|
|
47
|
+
} from "bcp/server";
|
|
48
|
+
|
|
49
|
+
const bucket =
|
|
50
|
+
requireEnvironmentVariable(
|
|
51
|
+
"AWS_S3_BUCKET"
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
export const storage =
|
|
55
|
+
createS3Storage({
|
|
56
|
+
bucket,
|
|
57
|
+
region:
|
|
58
|
+
process.env.AWS_REGION ??
|
|
59
|
+
"ap-southeast-1",
|
|
60
|
+
prefix:
|
|
61
|
+
optionalEnvironmentVariable(
|
|
62
|
+
"AWS_S3_PREFIX"
|
|
63
|
+
),
|
|
64
|
+
accessKeyId:
|
|
65
|
+
optionalEnvironmentVariable(
|
|
66
|
+
"AWS_ACCESS_KEY_ID"
|
|
67
|
+
),
|
|
68
|
+
secretAccessKey:
|
|
69
|
+
optionalEnvironmentVariable(
|
|
70
|
+
"AWS_SECRET_ACCESS_KEY"
|
|
71
|
+
),
|
|
72
|
+
sessionToken:
|
|
73
|
+
optionalEnvironmentVariable(
|
|
74
|
+
"AWS_SESSION_TOKEN"
|
|
75
|
+
),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
function requireEnvironmentVariable(
|
|
79
|
+
name: string
|
|
80
|
+
): string {
|
|
81
|
+
const value =
|
|
82
|
+
process.env[name]
|
|
83
|
+
?.trim();
|
|
84
|
+
|
|
85
|
+
if (!value) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
\`BCP storage: \${name} is required.\`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return value;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function optionalEnvironmentVariable(
|
|
95
|
+
name: string
|
|
96
|
+
): string | undefined {
|
|
97
|
+
const value =
|
|
98
|
+
process.env[name]
|
|
99
|
+
?.trim();
|
|
100
|
+
|
|
101
|
+
return value || undefined;
|
|
102
|
+
}
|
|
103
|
+
`,
|
|
104
|
+
},
|
|
105
|
+
"cloudflare-r2": {
|
|
106
|
+
env: [
|
|
107
|
+
"R2_ACCOUNT_ID=",
|
|
108
|
+
"R2_BUCKET=",
|
|
109
|
+
"R2_ACCESS_KEY_ID=",
|
|
110
|
+
"R2_SECRET_ACCESS_KEY=",
|
|
111
|
+
"R2_PREFIX=",
|
|
112
|
+
],
|
|
113
|
+
gitIgnore: [],
|
|
114
|
+
source: `import "bcp/server-only";
|
|
115
|
+
|
|
116
|
+
import {
|
|
117
|
+
createS3Storage,
|
|
118
|
+
} from "bcp/server";
|
|
119
|
+
|
|
120
|
+
const accountId =
|
|
121
|
+
requireEnvironmentVariable(
|
|
122
|
+
"R2_ACCOUNT_ID"
|
|
123
|
+
);
|
|
124
|
+
const bucket =
|
|
125
|
+
requireEnvironmentVariable(
|
|
126
|
+
"R2_BUCKET"
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
export const storage =
|
|
130
|
+
createS3Storage({
|
|
131
|
+
bucket,
|
|
132
|
+
region:
|
|
133
|
+
"auto",
|
|
134
|
+
endpoint:
|
|
135
|
+
\`https://\${accountId}.r2.cloudflarestorage.com\`,
|
|
136
|
+
prefix:
|
|
137
|
+
optionalEnvironmentVariable(
|
|
138
|
+
"R2_PREFIX"
|
|
139
|
+
),
|
|
140
|
+
accessKeyId:
|
|
141
|
+
requireEnvironmentVariable(
|
|
142
|
+
"R2_ACCESS_KEY_ID"
|
|
143
|
+
),
|
|
144
|
+
secretAccessKey:
|
|
145
|
+
requireEnvironmentVariable(
|
|
146
|
+
"R2_SECRET_ACCESS_KEY"
|
|
147
|
+
),
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
function requireEnvironmentVariable(
|
|
151
|
+
name: string
|
|
152
|
+
): string {
|
|
153
|
+
const value =
|
|
154
|
+
process.env[name]
|
|
155
|
+
?.trim();
|
|
156
|
+
|
|
157
|
+
if (!value) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
\`BCP storage: \${name} is required.\`
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return value;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function optionalEnvironmentVariable(
|
|
167
|
+
name: string
|
|
168
|
+
): string | undefined {
|
|
169
|
+
const value =
|
|
170
|
+
process.env[name]
|
|
171
|
+
?.trim();
|
|
172
|
+
|
|
173
|
+
return value || undefined;
|
|
174
|
+
}
|
|
175
|
+
`,
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
export function normalizeStorageProvider(
|
|
180
|
+
value
|
|
181
|
+
) {
|
|
182
|
+
const normalized =
|
|
183
|
+
String(
|
|
184
|
+
value ?? "none"
|
|
185
|
+
)
|
|
186
|
+
.trim()
|
|
187
|
+
.toLowerCase();
|
|
188
|
+
|
|
189
|
+
if (
|
|
190
|
+
!STORAGE_CHOICES.includes(
|
|
191
|
+
normalized
|
|
192
|
+
)
|
|
193
|
+
) {
|
|
194
|
+
throw new Error(
|
|
195
|
+
`Unsupported storage provider: ${normalized}. Choose one of: ${STORAGE_CHOICES.join(", ")}.`
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return normalized;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function applyStorageOption({
|
|
203
|
+
targetDirectory,
|
|
204
|
+
storage = "none",
|
|
205
|
+
}) {
|
|
206
|
+
const selectedStorage =
|
|
207
|
+
normalizeStorageProvider(
|
|
208
|
+
storage
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
if (
|
|
212
|
+
selectedStorage === "none"
|
|
213
|
+
) {
|
|
214
|
+
return selectedStorage;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const preset =
|
|
218
|
+
STORAGE_PRESETS[
|
|
219
|
+
selectedStorage
|
|
220
|
+
];
|
|
221
|
+
|
|
222
|
+
writeFile(
|
|
223
|
+
targetDirectory,
|
|
224
|
+
"lib/storage.ts",
|
|
225
|
+
preset.source
|
|
226
|
+
);
|
|
227
|
+
appendEnvExample(
|
|
228
|
+
targetDirectory,
|
|
229
|
+
preset.env,
|
|
230
|
+
`Storage (${formatStorageProvider(selectedStorage)})`
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
for (const entry of preset.gitIgnore) {
|
|
234
|
+
appendGitIgnore(
|
|
235
|
+
targetDirectory,
|
|
236
|
+
entry
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return selectedStorage;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function formatStorageProvider(
|
|
244
|
+
value
|
|
245
|
+
) {
|
|
246
|
+
const names = {
|
|
247
|
+
none: "None",
|
|
248
|
+
local: "Local Server",
|
|
249
|
+
"amazon-s3": "Amazon S3",
|
|
250
|
+
"cloudflare-r2": "Cloudflare R2",
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
return names[value] ?? value;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function appendEnvExample(
|
|
257
|
+
targetDirectory,
|
|
258
|
+
lines,
|
|
259
|
+
section
|
|
260
|
+
) {
|
|
261
|
+
const filePath =
|
|
262
|
+
path.join(
|
|
263
|
+
targetDirectory,
|
|
264
|
+
".env.example"
|
|
265
|
+
);
|
|
266
|
+
const current =
|
|
267
|
+
fs.existsSync(
|
|
268
|
+
filePath
|
|
269
|
+
)
|
|
270
|
+
? fs.readFileSync(
|
|
271
|
+
filePath,
|
|
272
|
+
"utf8"
|
|
273
|
+
).trimEnd()
|
|
274
|
+
: "";
|
|
275
|
+
|
|
276
|
+
fs.writeFileSync(
|
|
277
|
+
filePath,
|
|
278
|
+
`${current}${current ? "\n\n" : ""}# ${section}\n${lines.join("\n")}\n`,
|
|
279
|
+
"utf8"
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function appendGitIgnore(
|
|
284
|
+
targetDirectory,
|
|
285
|
+
entry
|
|
286
|
+
) {
|
|
287
|
+
const filePath =
|
|
288
|
+
path.join(
|
|
289
|
+
targetDirectory,
|
|
290
|
+
".gitignore"
|
|
291
|
+
);
|
|
292
|
+
const current =
|
|
293
|
+
fs.existsSync(
|
|
294
|
+
filePath
|
|
295
|
+
)
|
|
296
|
+
? fs.readFileSync(
|
|
297
|
+
filePath,
|
|
298
|
+
"utf8"
|
|
299
|
+
).trimEnd()
|
|
300
|
+
: "";
|
|
301
|
+
const lines =
|
|
302
|
+
new Set(
|
|
303
|
+
current
|
|
304
|
+
.split(/\r?\n/)
|
|
305
|
+
.filter(Boolean)
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
lines.add(entry);
|
|
309
|
+
|
|
310
|
+
fs.writeFileSync(
|
|
311
|
+
filePath,
|
|
312
|
+
`${Array.from(lines).join("\n")}\n`,
|
|
313
|
+
"utf8"
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function writeFile(
|
|
318
|
+
targetDirectory,
|
|
319
|
+
relativePath,
|
|
320
|
+
content
|
|
321
|
+
) {
|
|
322
|
+
const filePath =
|
|
323
|
+
path.join(
|
|
324
|
+
targetDirectory,
|
|
325
|
+
relativePath
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
fs.mkdirSync(
|
|
329
|
+
path.dirname(
|
|
330
|
+
filePath
|
|
331
|
+
),
|
|
332
|
+
{
|
|
333
|
+
recursive: true,
|
|
334
|
+
}
|
|
335
|
+
);
|
|
336
|
+
fs.writeFileSync(
|
|
337
|
+
filePath,
|
|
338
|
+
content,
|
|
339
|
+
"utf8"
|
|
340
|
+
);
|
|
341
|
+
}
|
package/template/README.md
CHANGED
|
@@ -10,7 +10,9 @@ npm run dev
|
|
|
10
10
|
|
|
11
11
|
Open `http://localhost:3000`.
|
|
12
12
|
|
|
13
|
-
##
|
|
13
|
+
## Project commands
|
|
14
|
+
|
|
15
|
+
Generated projects use the project-local BCP Framework CLI through npm scripts:
|
|
14
16
|
|
|
15
17
|
```bash
|
|
16
18
|
npm run dev
|
|
@@ -18,6 +20,65 @@ npm run routes
|
|
|
18
20
|
npm run typecheck
|
|
19
21
|
npm run build
|
|
20
22
|
npm start
|
|
23
|
+
npm run update
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The generated scripts call commands such as `bcp dev`, `bcp build` and `bcp start`. This works because npm automatically adds the project's `node_modules/.bin` directory to `PATH` while an npm script is running.
|
|
27
|
+
|
|
28
|
+
## Direct CLI usage
|
|
29
|
+
|
|
30
|
+
BCP Framework is installed as a **project-local dependency**. It is not installed globally by `create-bcp-app`.
|
|
31
|
+
|
|
32
|
+
Because of that, typing this directly in a normal PowerShell session may not work:
|
|
33
|
+
|
|
34
|
+
```powershell
|
|
35
|
+
bcp-framework doctor
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
PowerShell does not automatically add `node_modules/.bin` to its normal command search path.
|
|
39
|
+
|
|
40
|
+
Use `npm exec` when you want to invoke the project-local CLI directly:
|
|
41
|
+
|
|
42
|
+
```powershell
|
|
43
|
+
npm exec -- bcp-framework --version
|
|
44
|
+
npm exec -- bcp-framework doctor
|
|
45
|
+
npm exec -- bcp-framework inspect
|
|
46
|
+
npm exec -- bcp-framework routes
|
|
47
|
+
npm exec -- bcp-framework dev
|
|
48
|
+
npm exec -- bcp-framework build
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
You can also execute the Windows command shim explicitly:
|
|
52
|
+
|
|
53
|
+
```powershell
|
|
54
|
+
.\node_modules\.bin\bcp-framework.cmd --version
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Using the project-local CLI is recommended because it guarantees that the CLI version matches the BCP Framework version installed by this application.
|
|
58
|
+
|
|
59
|
+
### Why `bcp-framework` instead of `bcp` in PowerShell?
|
|
60
|
+
|
|
61
|
+
Microsoft SQL Server can install another Windows executable named `bcp.exe`. To avoid that command-name collision, BCP Framework publishes the additional `bcp-framework` alias.
|
|
62
|
+
|
|
63
|
+
Use this convention:
|
|
64
|
+
|
|
65
|
+
```text
|
|
66
|
+
Inside npm scripts -> bcp ...
|
|
67
|
+
Direct PowerShell usage -> npm exec -- bcp-framework ...
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Production
|
|
71
|
+
|
|
72
|
+
Create the standalone production build:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
npm run build
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Start it with:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
npm start
|
|
21
82
|
```
|
|
22
83
|
|
|
23
84
|
The production build is written to `.bcp-framework/build` and runs as a standalone Node.js server.
|