abw-react-starter 1.0.0
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/bin/create.js +452 -0
- package/package.json +19 -0
package/bin/create.js
ADDED
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import inquirer from "inquirer";
|
|
5
|
+
import fs from "fs-extra";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import { execa } from "execa";
|
|
8
|
+
import crypto from "crypto";
|
|
9
|
+
|
|
10
|
+
const program = new Command();
|
|
11
|
+
const MANIFEST_NAME = ".abw-starter.json";
|
|
12
|
+
|
|
13
|
+
/* =========================
|
|
14
|
+
Utils
|
|
15
|
+
========================= */
|
|
16
|
+
|
|
17
|
+
function randHex(bytes = 16) {
|
|
18
|
+
return crypto.randomBytes(bytes).toString("hex");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function run(cmd, args, opts = {}) {
|
|
22
|
+
const p = execa(cmd, args, {
|
|
23
|
+
stdio: "inherit",
|
|
24
|
+
...opts,
|
|
25
|
+
});
|
|
26
|
+
await p;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function runCapture(cmd, args, opts = {}) {
|
|
30
|
+
const p = await execa(cmd, args, { ...opts });
|
|
31
|
+
return p.stdout?.trim();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function ensureNoTrailingSlash(url) {
|
|
35
|
+
return (url || "").replace(/\/+$/, "");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function assertCmdExists(cmd, args = ["--version"]) {
|
|
39
|
+
try {
|
|
40
|
+
await execa(cmd, args, { stdio: "ignore" });
|
|
41
|
+
} catch {
|
|
42
|
+
throw new Error(`Falta o comando "${cmd}" no PATH.`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* =========================
|
|
47
|
+
Heroku Login (melhorado)
|
|
48
|
+
========================= */
|
|
49
|
+
|
|
50
|
+
async function isHerokuLoggedIn() {
|
|
51
|
+
try {
|
|
52
|
+
await execa("heroku", ["auth:whoami"], { stdio: "ignore" });
|
|
53
|
+
return true;
|
|
54
|
+
} catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function ensureHerokuLoginOrSkip() {
|
|
60
|
+
if (await isHerokuLoggedIn()) return true;
|
|
61
|
+
|
|
62
|
+
const { doLogin } = await inquirer.prompt([
|
|
63
|
+
{
|
|
64
|
+
type: "confirm",
|
|
65
|
+
name: "doLogin",
|
|
66
|
+
message: "Não estás autenticado no Heroku. Queres fazer login?",
|
|
67
|
+
default: true,
|
|
68
|
+
},
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
if (!doLogin) return false;
|
|
72
|
+
|
|
73
|
+
console.log("\n🔐 A abrir login do Heroku...\n");
|
|
74
|
+
await run("heroku", ["login"]);
|
|
75
|
+
|
|
76
|
+
if (!(await isHerokuLoggedIn())) {
|
|
77
|
+
throw new Error("Falha no login do Heroku.");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* =========================
|
|
84
|
+
Manifest
|
|
85
|
+
========================= */
|
|
86
|
+
|
|
87
|
+
async function writeManifest(projectDir, data) {
|
|
88
|
+
await fs.writeJson(path.join(projectDir, MANIFEST_NAME), data, {
|
|
89
|
+
spaces: 2,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function readManifest(projectDir) {
|
|
94
|
+
const p = path.join(projectDir, MANIFEST_NAME);
|
|
95
|
+
if (!(await fs.pathExists(p))) return null;
|
|
96
|
+
return fs.readJson(p);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function resolveProjectDir(p) {
|
|
100
|
+
return path.resolve(process.cwd(), p || ".");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function ensureEmptyDir(projectDir) {
|
|
104
|
+
if (await fs.pathExists(projectDir)) {
|
|
105
|
+
throw new Error(`A pasta já existe: ${projectDir}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* =========================
|
|
110
|
+
Git
|
|
111
|
+
========================= */
|
|
112
|
+
|
|
113
|
+
async function initGitRepo(cwd, message) {
|
|
114
|
+
await run("git", ["init"], { cwd });
|
|
115
|
+
await run("git", ["add", "."], { cwd });
|
|
116
|
+
await run("git", ["commit", "-m", message], { cwd });
|
|
117
|
+
await run("git", ["branch", "-M", "main"], { cwd });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* =========================
|
|
121
|
+
🆕 S3 Setup
|
|
122
|
+
========================= */
|
|
123
|
+
|
|
124
|
+
async function configureBackendS3(projectDir, aws) {
|
|
125
|
+
const beDir = path.join(projectDir, "backend");
|
|
126
|
+
|
|
127
|
+
console.log("\n=== BACKEND: instalar AWS S3 provider ===\n");
|
|
128
|
+
await run("npm", ["i", "@strapi/provider-upload-aws-s3"], { cwd: beDir });
|
|
129
|
+
|
|
130
|
+
const bucketUrl = `https://${aws.bucket}.s3.${aws.region}.amazonaws.com`;
|
|
131
|
+
|
|
132
|
+
await fs.writeFile(
|
|
133
|
+
path.join(beDir, "config", "middlewares.ts"),
|
|
134
|
+
`export default [
|
|
135
|
+
'strapi::logger',
|
|
136
|
+
'strapi::errors',
|
|
137
|
+
{
|
|
138
|
+
name: 'strapi::security',
|
|
139
|
+
config: {
|
|
140
|
+
contentSecurityPolicy: {
|
|
141
|
+
useDefaults: true,
|
|
142
|
+
directives: {
|
|
143
|
+
'img-src': ["'self'", 'data:', 'blob:', '${bucketUrl}'],
|
|
144
|
+
'media-src': ["'self'", 'data:', 'blob:', '${bucketUrl}'],
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
'strapi::cors',
|
|
150
|
+
'strapi::poweredBy',
|
|
151
|
+
'strapi::query',
|
|
152
|
+
'strapi::body',
|
|
153
|
+
'strapi::session',
|
|
154
|
+
'strapi::favicon',
|
|
155
|
+
'strapi::public',
|
|
156
|
+
];
|
|
157
|
+
`
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
await fs.writeFile(
|
|
161
|
+
path.join(beDir, "config", "plugins.ts"),
|
|
162
|
+
`export default ({ env }) => ({
|
|
163
|
+
upload: {
|
|
164
|
+
config: {
|
|
165
|
+
provider: 'aws-s3',
|
|
166
|
+
providerOptions: {
|
|
167
|
+
s3Options: {
|
|
168
|
+
credentials: {
|
|
169
|
+
accessKeyId: env('AWS_ACCESS_KEY_ID'),
|
|
170
|
+
secretAccessKey: env('AWS_ACCESS_SECRET'),
|
|
171
|
+
},
|
|
172
|
+
region: env('AWS_REGION'),
|
|
173
|
+
params: {
|
|
174
|
+
Bucket: env('AWS_BUCKET'),
|
|
175
|
+
ACL: 'public-read',
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
`
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
await fs.writeFile(
|
|
186
|
+
path.join(beDir, ".env"),
|
|
187
|
+
`AWS_ACCESS_KEY_ID=${aws.key}
|
|
188
|
+
AWS_ACCESS_SECRET=${aws.secret}
|
|
189
|
+
AWS_REGION=${aws.region}
|
|
190
|
+
AWS_BUCKET=${aws.bucket}
|
|
191
|
+
`
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/* =========================
|
|
196
|
+
Backend
|
|
197
|
+
========================= */
|
|
198
|
+
|
|
199
|
+
async function createBackendLocal(projectDir) {
|
|
200
|
+
console.log("\n=== BACKEND: criar Strapi ===\n");
|
|
201
|
+
await run("npx", [
|
|
202
|
+
"create-strapi-app@latest",
|
|
203
|
+
"backend",
|
|
204
|
+
"--non-interactive",
|
|
205
|
+
"--skip-cloud",
|
|
206
|
+
"--no-run",
|
|
207
|
+
"--dbclient",
|
|
208
|
+
"sqlite",
|
|
209
|
+
"--dbfile",
|
|
210
|
+
".tmp/data.db",
|
|
211
|
+
"--no-example",
|
|
212
|
+
"--typescript",
|
|
213
|
+
"--use-npm",
|
|
214
|
+
"--install",
|
|
215
|
+
"--no-git-init",
|
|
216
|
+
], { cwd: projectDir });
|
|
217
|
+
|
|
218
|
+
console.log("\n=== BACKEND: instalar pg ===\n");
|
|
219
|
+
await run("npm", ["i", "pg"], {
|
|
220
|
+
cwd: path.join(projectDir, "backend"),
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
await initGitRepo(path.join(projectDir, "backend"), "Init Strapi backend");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/* =========================
|
|
227
|
+
Frontend
|
|
228
|
+
========================= */
|
|
229
|
+
|
|
230
|
+
async function createFrontendLocal(projectDir, nodeVersion) {
|
|
231
|
+
console.log("\n=== FRONTEND: criar Next.js ===\n");
|
|
232
|
+
await run("npx", ["create-next-app@latest", "frontend"], {
|
|
233
|
+
cwd: projectDir,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
const feDir = path.join(projectDir, "frontend");
|
|
237
|
+
const pkgPath = path.join(feDir, "package.json");
|
|
238
|
+
const pkg = await fs.readJson(pkgPath);
|
|
239
|
+
|
|
240
|
+
pkg.engines = pkg.engines || {};
|
|
241
|
+
pkg.engines.node = nodeVersion;
|
|
242
|
+
|
|
243
|
+
pkg.scripts = pkg.scripts || {};
|
|
244
|
+
pkg.scripts.start = "next start -p $PORT";
|
|
245
|
+
|
|
246
|
+
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
247
|
+
|
|
248
|
+
await initGitRepo(feDir, "Init Next frontend");
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/* =========================
|
|
252
|
+
Deploy Backend
|
|
253
|
+
========================= */
|
|
254
|
+
|
|
255
|
+
async function deployBackendHeroku({ projectDir, backendApp, region }) {
|
|
256
|
+
const beDir = path.join(projectDir, "backend");
|
|
257
|
+
|
|
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 });
|
|
261
|
+
|
|
262
|
+
const envPath = path.join(beDir, ".env");
|
|
263
|
+
|
|
264
|
+
if (await fs.pathExists(envPath)) {
|
|
265
|
+
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 });
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
await run("git", ["push", "heroku", "main"], { cwd: beDir });
|
|
275
|
+
|
|
276
|
+
const info = JSON.parse(await runCapture("heroku", ["apps:info", "-a", backendApp, "--json"]));
|
|
277
|
+
return ensureNoTrailingSlash(info?.app?.web_url);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/* =========================
|
|
281
|
+
Deploy Frontend
|
|
282
|
+
========================= */
|
|
283
|
+
|
|
284
|
+
async function deployFrontendHeroku({ projectDir, frontendApp, region, backendWebUrl }) {
|
|
285
|
+
const feDir = path.join(projectDir, "frontend");
|
|
286
|
+
|
|
287
|
+
await run("heroku", ["create", frontendApp, "--region", region], { cwd: feDir });
|
|
288
|
+
await run("heroku", ["git:remote", "-a", frontendApp], { cwd: feDir });
|
|
289
|
+
|
|
290
|
+
await run("heroku", ["config:set", `NEXT_PUBLIC_STRAPI_URL=${backendWebUrl}`, "-a", frontendApp], {
|
|
291
|
+
cwd: feDir,
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
await run("git", ["push", "heroku", "main"], { cwd: feDir });
|
|
295
|
+
|
|
296
|
+
const info = JSON.parse(await runCapture("heroku", ["apps:info", "-a", frontendApp, "--json"]));
|
|
297
|
+
return ensureNoTrailingSlash(info?.app?.web_url);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/* =========================
|
|
301
|
+
Create
|
|
302
|
+
========================= */
|
|
303
|
+
|
|
304
|
+
async function doCreate(projectDirArg, opts) {
|
|
305
|
+
await assertCmdExists("git");
|
|
306
|
+
await assertCmdExists("node");
|
|
307
|
+
await assertCmdExists("npm");
|
|
308
|
+
|
|
309
|
+
const projectDir = resolveProjectDir(projectDirArg);
|
|
310
|
+
await ensureEmptyDir(projectDir);
|
|
311
|
+
|
|
312
|
+
const projectBase = path.basename(projectDir);
|
|
313
|
+
const region = (opts.region || "eu").toLowerCase();
|
|
314
|
+
const nodeVersion = opts.node || "20.x";
|
|
315
|
+
|
|
316
|
+
// S3
|
|
317
|
+
const { useS3 } = await inquirer.prompt([
|
|
318
|
+
{ type: "confirm", name: "useS3", message: "Usar S3?", default: true },
|
|
319
|
+
]);
|
|
320
|
+
|
|
321
|
+
let aws = null;
|
|
322
|
+
|
|
323
|
+
if (useS3) {
|
|
324
|
+
aws = await inquirer.prompt([
|
|
325
|
+
{ name: "bucket", message: "Bucket:" },
|
|
326
|
+
{ name: "region", default: "eu-north-1" },
|
|
327
|
+
{ name: "key", message: "Access Key:" },
|
|
328
|
+
{ type: "password", name: "secret", message: "Secret:" },
|
|
329
|
+
]);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const { deployNow } = await inquirer.prompt([
|
|
333
|
+
{
|
|
334
|
+
type: "confirm",
|
|
335
|
+
name: "deployNow",
|
|
336
|
+
message: "Queres publicar no Heroku agora?",
|
|
337
|
+
default: true,
|
|
338
|
+
},
|
|
339
|
+
]);
|
|
340
|
+
|
|
341
|
+
let backendApp = opts.backendApp || `${projectBase}-api`;
|
|
342
|
+
let frontendApp = opts.frontendApp || `${projectBase}-web`;
|
|
343
|
+
|
|
344
|
+
await fs.ensureDir(projectDir);
|
|
345
|
+
await fs.ensureDir(path.join(projectDir, "backend"));
|
|
346
|
+
await fs.ensureDir(path.join(projectDir, "frontend"));
|
|
347
|
+
|
|
348
|
+
await createBackendLocal(projectDir);
|
|
349
|
+
|
|
350
|
+
if (useS3) await configureBackendS3(projectDir, aws);
|
|
351
|
+
|
|
352
|
+
await createFrontendLocal(projectDir, nodeVersion);
|
|
353
|
+
|
|
354
|
+
await writeManifest(projectDir, {
|
|
355
|
+
version: 1,
|
|
356
|
+
region,
|
|
357
|
+
nodeVersion,
|
|
358
|
+
backendApp,
|
|
359
|
+
frontendApp,
|
|
360
|
+
useS3,
|
|
361
|
+
awsBucket: aws?.bucket,
|
|
362
|
+
awsRegion: aws?.region,
|
|
363
|
+
createdAt: new Date().toISOString(),
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
if (!deployNow) return;
|
|
367
|
+
|
|
368
|
+
const canPublish = await ensureHerokuLoginOrSkip();
|
|
369
|
+
if (!canPublish) return;
|
|
370
|
+
|
|
371
|
+
await doPublish(projectDir);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/* =========================
|
|
375
|
+
Publish
|
|
376
|
+
========================= */
|
|
377
|
+
|
|
378
|
+
async function doPublish(projectDirArg) {
|
|
379
|
+
const projectDir = resolveProjectDir(projectDirArg);
|
|
380
|
+
|
|
381
|
+
await assertCmdExists("git");
|
|
382
|
+
await assertCmdExists("heroku");
|
|
383
|
+
|
|
384
|
+
const canPublish = await ensureHerokuLoginOrSkip();
|
|
385
|
+
if (!canPublish) return;
|
|
386
|
+
|
|
387
|
+
const manifest = await readManifest(projectDir);
|
|
388
|
+
|
|
389
|
+
const backendWebUrl = await deployBackendHeroku({
|
|
390
|
+
projectDir,
|
|
391
|
+
backendApp: manifest.backendApp,
|
|
392
|
+
region: manifest.region,
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
const frontendWebUrl = await deployFrontendHeroku({
|
|
396
|
+
projectDir,
|
|
397
|
+
frontendApp: manifest.frontendApp,
|
|
398
|
+
region: manifest.region,
|
|
399
|
+
backendWebUrl,
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
await writeManifest(projectDir, {
|
|
403
|
+
...manifest,
|
|
404
|
+
backendUrl: backendWebUrl,
|
|
405
|
+
frontendUrl: frontendWebUrl,
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
console.log("\n✅ Tudo pronto!");
|
|
409
|
+
console.log("Backend:", backendWebUrl);
|
|
410
|
+
console.log("Frontend:", frontendWebUrl);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/* =========================
|
|
414
|
+
CLI (INALTERADO)
|
|
415
|
+
========================= */
|
|
416
|
+
|
|
417
|
+
program.name("abw-react-starter");
|
|
418
|
+
|
|
419
|
+
program
|
|
420
|
+
.command("create")
|
|
421
|
+
.argument("<projectDir>")
|
|
422
|
+
.option("--backend-app <name>")
|
|
423
|
+
.option("--frontend-app <name>")
|
|
424
|
+
.option("--node <version>", "20.x")
|
|
425
|
+
.option("--region <region>", "eu")
|
|
426
|
+
.action(async (projectDirArg, opts) => {
|
|
427
|
+
await doCreate(projectDirArg, opts);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
program
|
|
431
|
+
.argument("[projectDir]")
|
|
432
|
+
.option("--backend-app <name>")
|
|
433
|
+
.option("--frontend-app <name>")
|
|
434
|
+
.option("--node <version>", "20.x")
|
|
435
|
+
.option("--region <region>", "eu");
|
|
436
|
+
|
|
437
|
+
program
|
|
438
|
+
.command("publish")
|
|
439
|
+
.argument("[projectDir]")
|
|
440
|
+
.action(async (projectDirArg) => {
|
|
441
|
+
await doPublish(projectDirArg || ".");
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
program.action(async (projectDirArg, opts) => {
|
|
445
|
+
if (!projectDirArg) return program.help();
|
|
446
|
+
await doCreate(projectDirArg, opts);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
450
|
+
console.error("\n❌ Erro:", err?.message || err);
|
|
451
|
+
process.exit(1);
|
|
452
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "abw-react-starter",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI para criar projetos Strapi + Next + Heroku",
|
|
5
|
+
"bin": {
|
|
6
|
+
"abw-react-starter": "./bin/create.js"
|
|
7
|
+
},
|
|
8
|
+
"type": "module",
|
|
9
|
+
"scripts": {},
|
|
10
|
+
"keywords": ["cli", "strapi", "nextjs", "heroku"],
|
|
11
|
+
"author": "ABWorks",
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"commander": "^12.0.0",
|
|
15
|
+
"inquirer": "^9.0.0",
|
|
16
|
+
"fs-extra": "^11.0.0",
|
|
17
|
+
"execa": "^8.0.0"
|
|
18
|
+
}
|
|
19
|
+
}
|