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 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? (Y/n)
62
+ Use Tailwind CSS?
63
63
  Select a database:
64
- 1) None
65
- 2) MySQL
66
- 3) PostgreSQL
67
- 4) SQLite
68
- 5) MongoDB
64
+ None
65
+ MySQL
66
+ PostgreSQL
67
+ SQLite
68
+ MongoDB
69
69
  Select authentication:
70
- 1) None
71
- 2) JWT Cookie
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 a reusable `db` pool and caches that pool on `globalThis` during development so hot reloads do not create a new connection pool every time:
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 installs database drivers directly and does not force an ORM.
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
- -y, --yes Accept defaults (Tailwind enabled, no database, no auth)
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.9.tgz
303
+ npx create-bcp-app my-app --bcp file:../bcp-0.1.27.tgz
238
304
  ```
@@ -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
- -y, --yes Accept defaults (Tailwind enabled, no database, no auth)
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 --no-tailwind --database mongodb
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-bcp-app",
3
- "version": "0.1.26",
3
+ "version": "0.1.28",
4
4
  "description": "Create a new BCP Framework application.",
5
5
  "type": "module",
6
6
  "license": "MIT",
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
+ }