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.
- package/README.md +175 -0
- package/bin/create.js +141 -23
- 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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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(
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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 =
|
|
277
|
-
|
|
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
|
-
|
|
288
|
-
|
|
381
|
+
const finalFrontendApp = await createHerokuAppWithFallback({
|
|
382
|
+
cwd: feDir,
|
|
383
|
+
requestedName: frontendApp,
|
|
384
|
+
region,
|
|
385
|
+
label: "FRONTEND",
|
|
386
|
+
});
|
|
289
387
|
|
|
290
|
-
await run("heroku", ["
|
|
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 =
|
|
297
|
-
|
|
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
|
|
503
|
+
const backendResult = await deployBackendHeroku({
|
|
390
504
|
projectDir,
|
|
391
505
|
backendApp: manifest.backendApp,
|
|
392
506
|
region: manifest.region,
|
|
393
507
|
});
|
|
394
508
|
|
|
395
|
-
const
|
|
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
|
-
|
|
405
|
-
|
|
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:",
|
|
410
|
-
console.log("
|
|
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.
|
|
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": [
|
|
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
|
+
}
|