create-bcp-app 0.1.26 → 0.1.28
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 +91 -25
- 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 +369 -0
package/README.md
CHANGED
|
@@ -59,16 +59,21 @@ Using the project-local CLI also keeps the CLI version aligned with the exact BC
|
|
|
59
59
|
When running interactively, the generator asks:
|
|
60
60
|
|
|
61
61
|
```text
|
|
62
|
-
Use Tailwind CSS?
|
|
62
|
+
Use Tailwind CSS?
|
|
63
63
|
Select a database:
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
64
|
+
None
|
|
65
|
+
MySQL
|
|
66
|
+
PostgreSQL
|
|
67
|
+
SQLite
|
|
68
|
+
MongoDB
|
|
69
69
|
Select authentication:
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
None
|
|
71
|
+
JWT Cookie
|
|
72
|
+
Select storage provider:
|
|
73
|
+
None
|
|
74
|
+
Local Server
|
|
75
|
+
Amazon S3
|
|
76
|
+
Cloudflare R2
|
|
72
77
|
```
|
|
73
78
|
|
|
74
79
|
## Generated application defaults
|
|
@@ -144,18 +149,7 @@ import "bcp/server-only";
|
|
|
144
149
|
|
|
145
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.
|
|
146
151
|
|
|
147
|
-
For MySQL, the generated helper exports
|
|
148
|
-
|
|
149
|
-
```ts
|
|
150
|
-
import {
|
|
151
|
-
db,
|
|
152
|
-
} from "@/lib/database";
|
|
153
|
-
|
|
154
|
-
const [rows] =
|
|
155
|
-
await db.query(
|
|
156
|
-
"SELECT 1"
|
|
157
|
-
);
|
|
158
|
-
```
|
|
152
|
+
For MySQL, the generated helper exports the BCP database primitives through `bcp/database`.
|
|
159
153
|
|
|
160
154
|
The generated MySQL `.env.example` uses separate connection settings:
|
|
161
155
|
|
|
@@ -167,7 +161,77 @@ DB_PASSWORD=
|
|
|
167
161
|
DB_NAME=bcp_app
|
|
168
162
|
```
|
|
169
163
|
|
|
170
|
-
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.
|
|
171
235
|
|
|
172
236
|
### JWT Cookie authentication
|
|
173
237
|
|
|
@@ -217,7 +281,8 @@ POST /api/auth/logout
|
|
|
217
281
|
--no-tailwind Disable Tailwind CSS without prompting
|
|
218
282
|
--database <database> none | mysql | postgresql | sqlite | mongodb
|
|
219
283
|
--auth <preset> none | jwt-cookie
|
|
220
|
-
|
|
284
|
+
--storage <provider> none | local | amazon-s3 | cloudflare-r2
|
|
285
|
+
-y, --yes Accept defaults (Tailwind enabled, no database, no auth, no storage)
|
|
221
286
|
--bcp <specifier> Override dependencies.bcp
|
|
222
287
|
-h, --help Show help
|
|
223
288
|
```
|
|
@@ -225,14 +290,15 @@ POST /api/auth/logout
|
|
|
225
290
|
Examples:
|
|
226
291
|
|
|
227
292
|
```bash
|
|
228
|
-
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
|
|
229
296
|
npx create-bcp-app my-app --no-tailwind --database mongodb
|
|
230
|
-
npx create-bcp-app my-app --auth jwt-cookie
|
|
231
297
|
npx create-bcp-app my-app --yes
|
|
232
298
|
```
|
|
233
299
|
|
|
234
300
|
The `--bcp` option is primarily useful for prerelease and local package testing, for example:
|
|
235
301
|
|
|
236
302
|
```bash
|
|
237
|
-
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
|
|
238
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,369 @@
|
|
|
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 LOCAL_STORAGE_README = `# Local storage\n\nBCP writes local storage objects into this directory at runtime.\n\nApplication code should import the configured adapter from \`lib/storage.ts\` instead of reading or writing files here directly. BCP may also create an internal \`.bcp-storage-meta\` directory for object metadata.\n\nRuntime objects are ignored by Git. Keep this README and \`.gitkeep\` file so a newly generated project has an explicit local-storage directory before the first upload.\n`;
|
|
12
|
+
|
|
13
|
+
const STORAGE_PRESETS = {
|
|
14
|
+
local: {
|
|
15
|
+
env: [
|
|
16
|
+
"STORAGE_LOCAL_DIRECTORY=./storage",
|
|
17
|
+
],
|
|
18
|
+
gitIgnore: [
|
|
19
|
+
"storage/*",
|
|
20
|
+
"!storage/.gitkeep",
|
|
21
|
+
"!storage/README.md",
|
|
22
|
+
],
|
|
23
|
+
files: {
|
|
24
|
+
"storage/.gitkeep": "",
|
|
25
|
+
"storage/README.md":
|
|
26
|
+
LOCAL_STORAGE_README,
|
|
27
|
+
},
|
|
28
|
+
source: `import "bcp/server-only";
|
|
29
|
+
|
|
30
|
+
import {
|
|
31
|
+
createLocalStorage,
|
|
32
|
+
} from "bcp/server";
|
|
33
|
+
|
|
34
|
+
export const storage =
|
|
35
|
+
createLocalStorage({
|
|
36
|
+
directory:
|
|
37
|
+
process.env.STORAGE_LOCAL_DIRECTORY ??
|
|
38
|
+
"./storage",
|
|
39
|
+
});
|
|
40
|
+
`,
|
|
41
|
+
},
|
|
42
|
+
"amazon-s3": {
|
|
43
|
+
env: [
|
|
44
|
+
"AWS_S3_BUCKET=",
|
|
45
|
+
"AWS_REGION=ap-southeast-1",
|
|
46
|
+
"AWS_ACCESS_KEY_ID=",
|
|
47
|
+
"AWS_SECRET_ACCESS_KEY=",
|
|
48
|
+
"AWS_SESSION_TOKEN=",
|
|
49
|
+
"AWS_S3_PREFIX=",
|
|
50
|
+
],
|
|
51
|
+
gitIgnore: [],
|
|
52
|
+
files: {},
|
|
53
|
+
source: `import "bcp/server-only";
|
|
54
|
+
|
|
55
|
+
import {
|
|
56
|
+
createS3Storage,
|
|
57
|
+
} from "bcp/server";
|
|
58
|
+
|
|
59
|
+
const bucket =
|
|
60
|
+
requireEnvironmentVariable(
|
|
61
|
+
"AWS_S3_BUCKET"
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
export const storage =
|
|
65
|
+
createS3Storage({
|
|
66
|
+
bucket,
|
|
67
|
+
region:
|
|
68
|
+
process.env.AWS_REGION ??
|
|
69
|
+
"ap-southeast-1",
|
|
70
|
+
prefix:
|
|
71
|
+
optionalEnvironmentVariable(
|
|
72
|
+
"AWS_S3_PREFIX"
|
|
73
|
+
),
|
|
74
|
+
accessKeyId:
|
|
75
|
+
optionalEnvironmentVariable(
|
|
76
|
+
"AWS_ACCESS_KEY_ID"
|
|
77
|
+
),
|
|
78
|
+
secretAccessKey:
|
|
79
|
+
optionalEnvironmentVariable(
|
|
80
|
+
"AWS_SECRET_ACCESS_KEY"
|
|
81
|
+
),
|
|
82
|
+
sessionToken:
|
|
83
|
+
optionalEnvironmentVariable(
|
|
84
|
+
"AWS_SESSION_TOKEN"
|
|
85
|
+
),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
function requireEnvironmentVariable(
|
|
89
|
+
name: string
|
|
90
|
+
): string {
|
|
91
|
+
const value =
|
|
92
|
+
process.env[name]
|
|
93
|
+
?.trim();
|
|
94
|
+
|
|
95
|
+
if (!value) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
\`BCP storage: \${name} is required.\`
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function optionalEnvironmentVariable(
|
|
105
|
+
name: string
|
|
106
|
+
): string | undefined {
|
|
107
|
+
const value =
|
|
108
|
+
process.env[name]
|
|
109
|
+
?.trim();
|
|
110
|
+
|
|
111
|
+
return value || undefined;
|
|
112
|
+
}
|
|
113
|
+
`,
|
|
114
|
+
},
|
|
115
|
+
"cloudflare-r2": {
|
|
116
|
+
env: [
|
|
117
|
+
"R2_ACCOUNT_ID=",
|
|
118
|
+
"R2_BUCKET=",
|
|
119
|
+
"R2_ACCESS_KEY_ID=",
|
|
120
|
+
"R2_SECRET_ACCESS_KEY=",
|
|
121
|
+
"R2_PREFIX=",
|
|
122
|
+
],
|
|
123
|
+
gitIgnore: [],
|
|
124
|
+
files: {},
|
|
125
|
+
source: `import "bcp/server-only";
|
|
126
|
+
|
|
127
|
+
import {
|
|
128
|
+
createS3Storage,
|
|
129
|
+
} from "bcp/server";
|
|
130
|
+
|
|
131
|
+
const accountId =
|
|
132
|
+
requireEnvironmentVariable(
|
|
133
|
+
"R2_ACCOUNT_ID"
|
|
134
|
+
);
|
|
135
|
+
const bucket =
|
|
136
|
+
requireEnvironmentVariable(
|
|
137
|
+
"R2_BUCKET"
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
export const storage =
|
|
141
|
+
createS3Storage({
|
|
142
|
+
bucket,
|
|
143
|
+
region:
|
|
144
|
+
"auto",
|
|
145
|
+
endpoint:
|
|
146
|
+
\`https://\${accountId}.r2.cloudflarestorage.com\`,
|
|
147
|
+
prefix:
|
|
148
|
+
optionalEnvironmentVariable(
|
|
149
|
+
"R2_PREFIX"
|
|
150
|
+
),
|
|
151
|
+
accessKeyId:
|
|
152
|
+
requireEnvironmentVariable(
|
|
153
|
+
"R2_ACCESS_KEY_ID"
|
|
154
|
+
),
|
|
155
|
+
secretAccessKey:
|
|
156
|
+
requireEnvironmentVariable(
|
|
157
|
+
"R2_SECRET_ACCESS_KEY"
|
|
158
|
+
),
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
function requireEnvironmentVariable(
|
|
162
|
+
name: string
|
|
163
|
+
): string {
|
|
164
|
+
const value =
|
|
165
|
+
process.env[name]
|
|
166
|
+
?.trim();
|
|
167
|
+
|
|
168
|
+
if (!value) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
\`BCP storage: \${name} is required.\`
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return value;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function optionalEnvironmentVariable(
|
|
178
|
+
name: string
|
|
179
|
+
): string | undefined {
|
|
180
|
+
const value =
|
|
181
|
+
process.env[name]
|
|
182
|
+
?.trim();
|
|
183
|
+
|
|
184
|
+
return value || undefined;
|
|
185
|
+
}
|
|
186
|
+
`,
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
export function normalizeStorageProvider(
|
|
191
|
+
value
|
|
192
|
+
) {
|
|
193
|
+
const normalized =
|
|
194
|
+
String(
|
|
195
|
+
value ?? "none"
|
|
196
|
+
)
|
|
197
|
+
.trim()
|
|
198
|
+
.toLowerCase();
|
|
199
|
+
|
|
200
|
+
if (
|
|
201
|
+
!STORAGE_CHOICES.includes(
|
|
202
|
+
normalized
|
|
203
|
+
)
|
|
204
|
+
) {
|
|
205
|
+
throw new Error(
|
|
206
|
+
`Unsupported storage provider: ${normalized}. Choose one of: ${STORAGE_CHOICES.join(", ")}.`
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return normalized;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function applyStorageOption({
|
|
214
|
+
targetDirectory,
|
|
215
|
+
storage = "none",
|
|
216
|
+
}) {
|
|
217
|
+
const selectedStorage =
|
|
218
|
+
normalizeStorageProvider(
|
|
219
|
+
storage
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
if (
|
|
223
|
+
selectedStorage === "none"
|
|
224
|
+
) {
|
|
225
|
+
return selectedStorage;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const preset =
|
|
229
|
+
STORAGE_PRESETS[
|
|
230
|
+
selectedStorage
|
|
231
|
+
];
|
|
232
|
+
|
|
233
|
+
writeFile(
|
|
234
|
+
targetDirectory,
|
|
235
|
+
"lib/storage.ts",
|
|
236
|
+
preset.source
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
for (
|
|
240
|
+
const [
|
|
241
|
+
relativePath,
|
|
242
|
+
content,
|
|
243
|
+
]
|
|
244
|
+
of Object.entries(
|
|
245
|
+
preset.files
|
|
246
|
+
)
|
|
247
|
+
) {
|
|
248
|
+
writeFile(
|
|
249
|
+
targetDirectory,
|
|
250
|
+
relativePath,
|
|
251
|
+
content
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
appendEnvExample(
|
|
256
|
+
targetDirectory,
|
|
257
|
+
preset.env,
|
|
258
|
+
`Storage (${formatStorageProvider(selectedStorage)})`
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
for (const entry of preset.gitIgnore) {
|
|
262
|
+
appendGitIgnore(
|
|
263
|
+
targetDirectory,
|
|
264
|
+
entry
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return selectedStorage;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function formatStorageProvider(
|
|
272
|
+
value
|
|
273
|
+
) {
|
|
274
|
+
const names = {
|
|
275
|
+
none: "None",
|
|
276
|
+
local: "Local Server",
|
|
277
|
+
"amazon-s3": "Amazon S3",
|
|
278
|
+
"cloudflare-r2": "Cloudflare R2",
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
return names[value] ?? value;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function appendEnvExample(
|
|
285
|
+
targetDirectory,
|
|
286
|
+
lines,
|
|
287
|
+
section
|
|
288
|
+
) {
|
|
289
|
+
const filePath =
|
|
290
|
+
path.join(
|
|
291
|
+
targetDirectory,
|
|
292
|
+
".env.example"
|
|
293
|
+
);
|
|
294
|
+
const current =
|
|
295
|
+
fs.existsSync(
|
|
296
|
+
filePath
|
|
297
|
+
)
|
|
298
|
+
? fs.readFileSync(
|
|
299
|
+
filePath,
|
|
300
|
+
"utf8"
|
|
301
|
+
).trimEnd()
|
|
302
|
+
: "";
|
|
303
|
+
|
|
304
|
+
fs.writeFileSync(
|
|
305
|
+
filePath,
|
|
306
|
+
`${current}${current ? "\n\n" : ""}# ${section}\n${lines.join("\n")}\n`,
|
|
307
|
+
"utf8"
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function appendGitIgnore(
|
|
312
|
+
targetDirectory,
|
|
313
|
+
entry
|
|
314
|
+
) {
|
|
315
|
+
const filePath =
|
|
316
|
+
path.join(
|
|
317
|
+
targetDirectory,
|
|
318
|
+
".gitignore"
|
|
319
|
+
);
|
|
320
|
+
const current =
|
|
321
|
+
fs.existsSync(
|
|
322
|
+
filePath
|
|
323
|
+
)
|
|
324
|
+
? fs.readFileSync(
|
|
325
|
+
filePath,
|
|
326
|
+
"utf8"
|
|
327
|
+
).trimEnd()
|
|
328
|
+
: "";
|
|
329
|
+
const lines =
|
|
330
|
+
new Set(
|
|
331
|
+
current
|
|
332
|
+
.split(/\r?\n/)
|
|
333
|
+
.filter(Boolean)
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
lines.add(entry);
|
|
337
|
+
|
|
338
|
+
fs.writeFileSync(
|
|
339
|
+
filePath,
|
|
340
|
+
`${Array.from(lines).join("\n")}\n`,
|
|
341
|
+
"utf8"
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function writeFile(
|
|
346
|
+
targetDirectory,
|
|
347
|
+
relativePath,
|
|
348
|
+
content
|
|
349
|
+
) {
|
|
350
|
+
const filePath =
|
|
351
|
+
path.join(
|
|
352
|
+
targetDirectory,
|
|
353
|
+
relativePath
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
fs.mkdirSync(
|
|
357
|
+
path.dirname(
|
|
358
|
+
filePath
|
|
359
|
+
),
|
|
360
|
+
{
|
|
361
|
+
recursive: true,
|
|
362
|
+
}
|
|
363
|
+
);
|
|
364
|
+
fs.writeFileSync(
|
|
365
|
+
filePath,
|
|
366
|
+
content,
|
|
367
|
+
"utf8"
|
|
368
|
+
);
|
|
369
|
+
}
|