abw-react-starter 1.0.0 โ†’ 1.0.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.
Files changed (3) hide show
  1. package/README.md +175 -0
  2. package/bin/create.js +141 -23
  3. package/package.json +8 -3
package/README.md ADDED
@@ -0,0 +1,175 @@
1
+ # ๐Ÿš€ abw-react-starter
2
+
3
+ CLI to quickly bootstrap a fullstack project with:
4
+
5
+ * โš™๏ธ **Strapi (Backend)**
6
+ * โšก **Next.js (Frontend)**
7
+ * โ˜๏ธ **Heroku deployment (optional)**
8
+ * ๐Ÿชฃ **AWS S3 upload support (optional)**
9
+
10
+ ---
11
+
12
+ ## ๐Ÿ“ฆ Installation
13
+
14
+ ### Option 1: Use without installing (recommended)
15
+
16
+ ```bash
17
+ npx abw-react-starter create my-app
18
+ ```
19
+
20
+ ### Option 2: Install globally
21
+
22
+ ```bash
23
+ npm install -g abw-react-starter
24
+ ```
25
+
26
+ Then:
27
+
28
+ ```bash
29
+ abw-react-starter create my-app
30
+ ```
31
+
32
+ ---
33
+
34
+ ## ๐Ÿš€ Usage
35
+
36
+ ### Create a new project
37
+
38
+ ```bash
39
+ abw-react-starter create my-app
40
+ ```
41
+
42
+ or simply:
43
+
44
+ ```bash
45
+ abw-react-starter my-app
46
+ ```
47
+
48
+ ---
49
+
50
+ ## โš™๏ธ What it does
51
+
52
+ * Creates a full project structure:
53
+
54
+ * `/backend` โ†’ Strapi (TypeScript)
55
+ * `/frontend` โ†’ Next.js
56
+ * Initializes Git repositories
57
+ * Configures environment variables
58
+ * Optionally sets up:
59
+
60
+ * AWS S3 uploads
61
+ * Heroku deployment (backend + frontend)
62
+
63
+ ---
64
+
65
+ ## โ˜๏ธ Deployment
66
+
67
+ During setup, you will be asked:
68
+
69
+ * Whether to deploy to Heroku
70
+ * App names for backend and frontend
71
+ * Login (if not authenticated)
72
+
73
+ ### Or deploy later:
74
+
75
+ ```bash
76
+ abw-react-starter publish my-app
77
+ ```
78
+
79
+ ---
80
+
81
+ ## ๐Ÿชฃ AWS S3 Support
82
+
83
+ If enabled, the CLI will:
84
+
85
+ * Install S3 upload provider
86
+ * Configure:
87
+
88
+ * `plugins.ts`
89
+ * `middlewares.ts`
90
+ * Ask for:
91
+
92
+ * Bucket name
93
+ * Region
94
+ * Access keys
95
+ * Automatically configure Heroku env variables
96
+
97
+ ---
98
+
99
+ ## ๐Ÿ” Requirements
100
+
101
+ Make sure you have installed:
102
+
103
+ * Node.js (>= 18)
104
+ * npm
105
+ * Git
106
+ * Heroku CLI
107
+
108
+ ---
109
+
110
+ ## ๐Ÿงช Local Development
111
+
112
+ ### Backend
113
+
114
+ ```bash
115
+ cd backend
116
+ npm run develop
117
+ ```
118
+
119
+ ### Frontend
120
+
121
+ ```bash
122
+ cd frontend
123
+ npm run dev
124
+ ```
125
+
126
+ ---
127
+
128
+ ## ๐Ÿ“ Project Structure
129
+
130
+ ```
131
+ my-app/
132
+ โ”œโ”€โ”€ backend/ # Strapi API
133
+ โ”œโ”€โ”€ frontend/ # Next.js app
134
+ โ””โ”€โ”€ .abw-starter.json
135
+ ```
136
+
137
+ ---
138
+
139
+ ## ๐Ÿ”„ Updating
140
+
141
+ If installed globally:
142
+
143
+ ```bash
144
+ npm update -g abw-react-starter
145
+ ```
146
+
147
+ ---
148
+
149
+ ## ๐Ÿ› Troubleshooting
150
+
151
+ ### Heroku app name already taken
152
+
153
+ The CLI will automatically:
154
+
155
+ 1. Try your chosen name
156
+ 2. Try a random suffix
157
+ 3. Fallback to Heroku-generated name
158
+
159
+ ---
160
+
161
+ ### Not logged into Heroku
162
+
163
+ The CLI will prompt you to login automatically.
164
+
165
+ ---
166
+
167
+ ## ๐Ÿ“„ License
168
+
169
+ MIT
170
+
171
+ ---
172
+
173
+ ## ๐Ÿ‘จโ€๐Ÿ’ป Author
174
+
175
+ Built with โค๏ธ by ABWorks
package/bin/create.js CHANGED
@@ -18,6 +18,10 @@ function randHex(bytes = 16) {
18
18
  return crypto.randomBytes(bytes).toString("hex");
19
19
  }
20
20
 
21
+ function randSuffix(size = 6) {
22
+ return crypto.randomBytes(size).toString("hex").slice(0, size);
23
+ }
24
+
21
25
  async function run(cmd, args, opts = {}) {
22
26
  const p = execa(cmd, args, {
23
27
  stdio: "inherit",
@@ -43,6 +47,63 @@ async function assertCmdExists(cmd, args = ["--version"]) {
43
47
  }
44
48
  }
45
49
 
50
+ function looksLikeHerokuNameTakenError(err) {
51
+ const msg = String(err?.stderr || err?.shortMessage || err?.message || "").toLowerCase();
52
+ return msg.includes("name") && (msg.includes("taken") || msg.includes("already exists"));
53
+ }
54
+
55
+ async function getHerokuAppInfo(appName, cwd) {
56
+ const raw = await runCapture("heroku", ["apps:info", "-a", appName, "--json"], { cwd });
57
+ return JSON.parse(raw || "{}");
58
+ }
59
+
60
+ async function createHerokuAppWithFallback({ cwd, requestedName, region, label }) {
61
+ const attempts = [];
62
+
63
+ if (requestedName) {
64
+ attempts.push(requestedName);
65
+ attempts.push(`${requestedName}-${randSuffix(4)}`);
66
+ attempts.push(`${requestedName}-${randSuffix(6)}`);
67
+ }
68
+
69
+ for (const candidate of attempts) {
70
+ try {
71
+ console.log(`\n=== ${label}: tentar criar app Heroku "${candidate}" ===\n`);
72
+ await run("heroku", ["create", candidate, "--region", region], { cwd });
73
+ return candidate;
74
+ } catch (err) {
75
+ if (looksLikeHerokuNameTakenError(err)) {
76
+ console.log(`โš ๏ธ Nome jรก ocupado: ${candidate}`);
77
+ continue;
78
+ }
79
+ throw err;
80
+ }
81
+ }
82
+
83
+ console.log(`\n=== ${label}: a criar app Heroku com nome automรกtico ===\n`);
84
+ await run("heroku", ["create", "--region", region], { cwd });
85
+
86
+ const whoRaw = await runCapture("heroku", ["apps", "--json"], { cwd });
87
+ const apps = JSON.parse(whoRaw || "[]");
88
+
89
+ if (!Array.isArray(apps) || apps.length === 0) {
90
+ throw new Error(`Nรฃo consegui descobrir o nome final da app Heroku (${label}).`);
91
+ }
92
+
93
+ apps.sort((a, b) => {
94
+ const da = new Date(a?.created_at || 0).getTime();
95
+ const db = new Date(b?.created_at || 0).getTime();
96
+ return db - da;
97
+ });
98
+
99
+ const latest = apps[0]?.name;
100
+ if (!latest) {
101
+ throw new Error(`Nรฃo consegui descobrir o nome final da app Heroku (${label}).`);
102
+ }
103
+
104
+ return latest;
105
+ }
106
+
46
107
  /* =========================
47
108
  Heroku Login (melhorado)
48
109
  ========================= */
@@ -176,6 +237,11 @@ async function configureBackendS3(projectDir, aws) {
176
237
  },
177
238
  },
178
239
  },
240
+ actionOptions: {
241
+ upload: {},
242
+ uploadStream: {},
243
+ delete: {},
244
+ },
179
245
  },
180
246
  },
181
247
  });
@@ -255,26 +321,54 @@ async function createFrontendLocal(projectDir, nodeVersion) {
255
321
  async function deployBackendHeroku({ projectDir, backendApp, region }) {
256
322
  const beDir = path.join(projectDir, "backend");
257
323
 
258
- await run("heroku", ["create", backendApp, "--region", region], { cwd: beDir });
259
- await run("heroku", ["git:remote", "-a", backendApp], { cwd: beDir });
260
- await run("heroku", ["addons:create", "heroku-postgresql", "-a", backendApp], { cwd: beDir });
324
+ const finalBackendApp = await createHerokuAppWithFallback({
325
+ cwd: beDir,
326
+ requestedName: backendApp,
327
+ region,
328
+ label: "BACKEND",
329
+ });
330
+
331
+ await run("heroku", ["git:remote", "-a", finalBackendApp], { cwd: beDir });
332
+ await run("heroku", ["addons:create", "heroku-postgresql", "-a", finalBackendApp], { cwd: beDir });
261
333
 
262
334
  const envPath = path.join(beDir, ".env");
263
335
 
264
336
  if (await fs.pathExists(envPath)) {
265
337
  const env = await fs.readFile(envPath, "utf8");
266
- const get = (k) => env.match(new RegExp(`${k}=(.*)`))?.[1]?.trim();
267
-
268
- await run("heroku", ["config:set", `AWS_BUCKET=${get("AWS_BUCKET")}`, "-a", backendApp], { cwd: beDir });
269
- await run("heroku", ["config:set", `AWS_REGION=${get("AWS_REGION")}`, "-a", backendApp], { cwd: beDir });
270
- await run("heroku", ["config:set", `AWS_ACCESS_KEY_ID=${get("AWS_ACCESS_KEY_ID")}`, "-a", backendApp], { cwd: beDir });
271
- await run("heroku", ["config:set", `AWS_ACCESS_SECRET=${get("AWS_ACCESS_SECRET")}`, "-a", backendApp], { cwd: beDir });
338
+ const get = (k) => env.match(new RegExp(`^${k}=(.*)$`, "m"))?.[1]?.trim();
339
+
340
+ const awsBucket = get("AWS_BUCKET");
341
+ const awsRegion = get("AWS_REGION");
342
+ const awsAccessKeyId = get("AWS_ACCESS_KEY_ID");
343
+ const awsAccessSecret = get("AWS_ACCESS_SECRET");
344
+
345
+ if (awsBucket) {
346
+ await run("heroku", ["config:set", `AWS_BUCKET=${awsBucket}`, "-a", finalBackendApp], { cwd: beDir });
347
+ }
348
+ if (awsRegion) {
349
+ await run("heroku", ["config:set", `AWS_REGION=${awsRegion}`, "-a", finalBackendApp], { cwd: beDir });
350
+ }
351
+ if (awsAccessKeyId) {
352
+ await run("heroku", ["config:set", `AWS_ACCESS_KEY_ID=${awsAccessKeyId}`, "-a", finalBackendApp], { cwd: beDir });
353
+ }
354
+ if (awsAccessSecret) {
355
+ await run("heroku", ["config:set", `AWS_ACCESS_SECRET=${awsAccessSecret}`, "-a", finalBackendApp], { cwd: beDir });
356
+ }
272
357
  }
273
358
 
274
359
  await run("git", ["push", "heroku", "main"], { cwd: beDir });
275
360
 
276
- const info = JSON.parse(await runCapture("heroku", ["apps:info", "-a", backendApp, "--json"]));
277
- return ensureNoTrailingSlash(info?.app?.web_url);
361
+ const info = await getHerokuAppInfo(finalBackendApp, beDir);
362
+ const backendWebUrl = ensureNoTrailingSlash(info?.app?.web_url);
363
+
364
+ if (!backendWebUrl) {
365
+ throw new Error("Nรฃo consegui obter web_url do backend no Heroku.");
366
+ }
367
+
368
+ return {
369
+ backendApp: finalBackendApp,
370
+ backendWebUrl,
371
+ };
278
372
  }
279
373
 
280
374
  /* =========================
@@ -284,17 +378,32 @@ async function deployBackendHeroku({ projectDir, backendApp, region }) {
284
378
  async function deployFrontendHeroku({ projectDir, frontendApp, region, backendWebUrl }) {
285
379
  const feDir = path.join(projectDir, "frontend");
286
380
 
287
- await run("heroku", ["create", frontendApp, "--region", region], { cwd: feDir });
288
- await run("heroku", ["git:remote", "-a", frontendApp], { cwd: feDir });
381
+ const finalFrontendApp = await createHerokuAppWithFallback({
382
+ cwd: feDir,
383
+ requestedName: frontendApp,
384
+ region,
385
+ label: "FRONTEND",
386
+ });
289
387
 
290
- await run("heroku", ["config:set", `NEXT_PUBLIC_STRAPI_URL=${backendWebUrl}`, "-a", frontendApp], {
388
+ await run("heroku", ["git:remote", "-a", finalFrontendApp], { cwd: feDir });
389
+
390
+ await run("heroku", ["config:set", `NEXT_PUBLIC_STRAPI_URL=${backendWebUrl}`, "-a", finalFrontendApp], {
291
391
  cwd: feDir,
292
392
  });
293
393
 
294
394
  await run("git", ["push", "heroku", "main"], { cwd: feDir });
295
395
 
296
- const info = JSON.parse(await runCapture("heroku", ["apps:info", "-a", frontendApp, "--json"]));
297
- return ensureNoTrailingSlash(info?.app?.web_url);
396
+ const info = await getHerokuAppInfo(finalFrontendApp, feDir);
397
+ const frontendWebUrl = ensureNoTrailingSlash(info?.app?.web_url);
398
+
399
+ if (!frontendWebUrl) {
400
+ throw new Error("Nรฃo consegui obter web_url do frontend no Heroku.");
401
+ }
402
+
403
+ return {
404
+ frontendApp: finalFrontendApp,
405
+ frontendWebUrl,
406
+ };
298
407
  }
299
408
 
300
409
  /* =========================
@@ -385,29 +494,38 @@ async function doPublish(projectDirArg) {
385
494
  if (!canPublish) return;
386
495
 
387
496
  const manifest = await readManifest(projectDir);
497
+ if (!manifest) {
498
+ throw new Error(
499
+ `Nรฃo encontrei ${MANIFEST_NAME} em ${projectDir}. Cria o projeto com "abw-react-starter create <dir>" primeiro.`
500
+ );
501
+ }
388
502
 
389
- const backendWebUrl = await deployBackendHeroku({
503
+ const backendResult = await deployBackendHeroku({
390
504
  projectDir,
391
505
  backendApp: manifest.backendApp,
392
506
  region: manifest.region,
393
507
  });
394
508
 
395
- const frontendWebUrl = await deployFrontendHeroku({
509
+ const frontendResult = await deployFrontendHeroku({
396
510
  projectDir,
397
511
  frontendApp: manifest.frontendApp,
398
512
  region: manifest.region,
399
- backendWebUrl,
513
+ backendWebUrl: backendResult.backendWebUrl,
400
514
  });
401
515
 
402
516
  await writeManifest(projectDir, {
403
517
  ...manifest,
404
- backendUrl: backendWebUrl,
405
- frontendUrl: frontendWebUrl,
518
+ backendApp: backendResult.backendApp,
519
+ frontendApp: frontendResult.frontendApp,
520
+ backendUrl: backendResult.backendWebUrl,
521
+ frontendUrl: frontendResult.frontendWebUrl,
406
522
  });
407
523
 
408
524
  console.log("\nโœ… Tudo pronto!");
409
- console.log("Backend:", backendWebUrl);
410
- console.log("Frontend:", frontendWebUrl);
525
+ console.log("Backend app:", backendResult.backendApp);
526
+ console.log("Backend:", backendResult.backendWebUrl);
527
+ console.log("Frontend app:", frontendResult.frontendApp);
528
+ console.log("Frontend:", frontendResult.frontendWebUrl);
411
529
  }
412
530
 
413
531
  /* =========================
package/package.json CHANGED
@@ -1,13 +1,18 @@
1
1
  {
2
2
  "name": "abw-react-starter",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "CLI para criar projetos Strapi + Next + Heroku",
5
5
  "bin": {
6
6
  "abw-react-starter": "./bin/create.js"
7
7
  },
8
8
  "type": "module",
9
9
  "scripts": {},
10
- "keywords": ["cli", "strapi", "nextjs", "heroku"],
10
+ "keywords": [
11
+ "cli",
12
+ "strapi",
13
+ "nextjs",
14
+ "heroku"
15
+ ],
11
16
  "author": "ABWorks",
12
17
  "license": "MIT",
13
18
  "dependencies": {
@@ -16,4 +21,4 @@
16
21
  "fs-extra": "^11.0.0",
17
22
  "execa": "^8.0.0"
18
23
  }
19
- }
24
+ }