abw-react-starter 1.0.1 → 1.0.3
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 +360 -59
- package/package.json +1 -1
package/bin/create.js
CHANGED
|
@@ -35,6 +35,32 @@ async function runCapture(cmd, args, opts = {}) {
|
|
|
35
35
|
return p.stdout?.trim();
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
async function runCombined(cmd, args, opts = {}) {
|
|
39
|
+
try {
|
|
40
|
+
const result = await execa(cmd, args, {
|
|
41
|
+
all: true,
|
|
42
|
+
...opts,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
ok: true,
|
|
47
|
+
code: 0,
|
|
48
|
+
output: result.all?.trim() || "",
|
|
49
|
+
stdout: result.stdout?.trim() || "",
|
|
50
|
+
stderr: result.stderr?.trim() || "",
|
|
51
|
+
};
|
|
52
|
+
} catch (err) {
|
|
53
|
+
return {
|
|
54
|
+
ok: false,
|
|
55
|
+
code: err?.exitCode ?? 1,
|
|
56
|
+
output: String(err?.all || err?.stderr || err?.stdout || err?.message || "").trim(),
|
|
57
|
+
stdout: String(err?.stdout || "").trim(),
|
|
58
|
+
stderr: String(err?.stderr || "").trim(),
|
|
59
|
+
error: err,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
38
64
|
function ensureNoTrailingSlash(url) {
|
|
39
65
|
return (url || "").replace(/\/+$/, "");
|
|
40
66
|
}
|
|
@@ -47,9 +73,15 @@ async function assertCmdExists(cmd, args = ["--version"]) {
|
|
|
47
73
|
}
|
|
48
74
|
}
|
|
49
75
|
|
|
50
|
-
function looksLikeHerokuNameTakenError(
|
|
51
|
-
const msg = String(
|
|
52
|
-
return
|
|
76
|
+
function looksLikeHerokuNameTakenError(output) {
|
|
77
|
+
const msg = String(output || "").toLowerCase();
|
|
78
|
+
return (
|
|
79
|
+
msg.includes("name") &&
|
|
80
|
+
(msg.includes("taken") ||
|
|
81
|
+
msg.includes("already taken") ||
|
|
82
|
+
msg.includes("already exists") ||
|
|
83
|
+
msg.includes("invalid_params"))
|
|
84
|
+
);
|
|
53
85
|
}
|
|
54
86
|
|
|
55
87
|
async function getHerokuAppInfo(appName, cwd) {
|
|
@@ -57,6 +89,14 @@ async function getHerokuAppInfo(appName, cwd) {
|
|
|
57
89
|
return JSON.parse(raw || "{}");
|
|
58
90
|
}
|
|
59
91
|
|
|
92
|
+
async function tryCreateHerokuApp(appName, region, cwd) {
|
|
93
|
+
const args = ["create"];
|
|
94
|
+
if (appName) args.push(appName);
|
|
95
|
+
args.push("--region", region);
|
|
96
|
+
|
|
97
|
+
return runCombined("heroku", args, { cwd });
|
|
98
|
+
}
|
|
99
|
+
|
|
60
100
|
async function createHerokuAppWithFallback({ cwd, requestedName, region, label }) {
|
|
61
101
|
const attempts = [];
|
|
62
102
|
|
|
@@ -67,24 +107,40 @@ async function createHerokuAppWithFallback({ cwd, requestedName, region, label }
|
|
|
67
107
|
}
|
|
68
108
|
|
|
69
109
|
for (const candidate of attempts) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
110
|
+
console.log(`\n=== ${label}: tentar criar app Heroku "${candidate}" ===\n`);
|
|
111
|
+
|
|
112
|
+
const res = await tryCreateHerokuApp(candidate, region, cwd);
|
|
113
|
+
|
|
114
|
+
if (res.output) {
|
|
115
|
+
console.log(res.output);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (res.ok) {
|
|
73
119
|
return candidate;
|
|
74
|
-
} catch (err) {
|
|
75
|
-
if (looksLikeHerokuNameTakenError(err)) {
|
|
76
|
-
console.log(`⚠️ Nome já ocupado: ${candidate}`);
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
throw err;
|
|
80
120
|
}
|
|
121
|
+
|
|
122
|
+
if (looksLikeHerokuNameTakenError(res.output)) {
|
|
123
|
+
console.log(`⚠️ Nome já ocupado: ${candidate}`);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
throw res.error || new Error(`Falha ao criar app Heroku (${label}).`);
|
|
81
128
|
}
|
|
82
129
|
|
|
83
130
|
console.log(`\n=== ${label}: a criar app Heroku com nome automático ===\n`);
|
|
84
|
-
await run("heroku", ["create", "--region", region], { cwd });
|
|
85
131
|
|
|
86
|
-
const
|
|
87
|
-
|
|
132
|
+
const autoRes = await tryCreateHerokuApp(null, region, cwd);
|
|
133
|
+
|
|
134
|
+
if (autoRes.output) {
|
|
135
|
+
console.log(autoRes.output);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!autoRes.ok) {
|
|
139
|
+
throw autoRes.error || new Error(`Falha ao criar app Heroku com nome automático (${label}).`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const appsRaw = await runCapture("heroku", ["apps", "--json"], { cwd });
|
|
143
|
+
const apps = JSON.parse(appsRaw || "[]");
|
|
88
144
|
|
|
89
145
|
if (!Array.isArray(apps) || apps.length === 0) {
|
|
90
146
|
throw new Error(`Não consegui descobrir o nome final da app Heroku (${label}).`);
|
|
@@ -104,8 +160,47 @@ async function createHerokuAppWithFallback({ cwd, requestedName, region, label }
|
|
|
104
160
|
return latest;
|
|
105
161
|
}
|
|
106
162
|
|
|
163
|
+
function upsertEnvValue(content, key, value) {
|
|
164
|
+
const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
165
|
+
const regex = new RegExp(`^${escapedKey}=.*$`, "m");
|
|
166
|
+
const line = `${key}=${value}`;
|
|
167
|
+
|
|
168
|
+
if (regex.test(content)) {
|
|
169
|
+
return content.replace(regex, line);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const trimmed = content.replace(/\s*$/, "");
|
|
173
|
+
return trimmed ? `${trimmed}\n${line}\n` : `${line}\n`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function mergeEnvFile(filePath, values) {
|
|
177
|
+
let content = "";
|
|
178
|
+
|
|
179
|
+
if (await fs.pathExists(filePath)) {
|
|
180
|
+
content = await fs.readFile(filePath, "utf8");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
for (const [key, value] of Object.entries(values)) {
|
|
184
|
+
content = upsertEnvValue(content, key, value ?? "");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
await fs.writeFile(filePath, content);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function commitAllIfNeeded(cwd, message) {
|
|
191
|
+
await run("git", ["add", "."], { cwd });
|
|
192
|
+
|
|
193
|
+
const status = await runCapture("git", ["status", "--porcelain"], { cwd });
|
|
194
|
+
if (!status) {
|
|
195
|
+
console.log("ℹ️ Nada novo para commitar");
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
await run("git", ["commit", "-m", message], { cwd });
|
|
200
|
+
}
|
|
201
|
+
|
|
107
202
|
/* =========================
|
|
108
|
-
Heroku Login
|
|
203
|
+
Heroku Login
|
|
109
204
|
========================= */
|
|
110
205
|
|
|
111
206
|
async function isHerokuLoggedIn() {
|
|
@@ -146,7 +241,8 @@ async function ensureHerokuLoginOrSkip() {
|
|
|
146
241
|
========================= */
|
|
147
242
|
|
|
148
243
|
async function writeManifest(projectDir, data) {
|
|
149
|
-
await
|
|
244
|
+
const current = (await readManifest(projectDir)) || {};
|
|
245
|
+
await fs.writeJson(path.join(projectDir, MANIFEST_NAME), { ...current, ...data }, {
|
|
150
246
|
spaces: 2,
|
|
151
247
|
});
|
|
152
248
|
}
|
|
@@ -179,7 +275,7 @@ async function initGitRepo(cwd, message) {
|
|
|
179
275
|
}
|
|
180
276
|
|
|
181
277
|
/* =========================
|
|
182
|
-
|
|
278
|
+
S3 Setup
|
|
183
279
|
========================= */
|
|
184
280
|
|
|
185
281
|
async function configureBackendS3(projectDir, aws) {
|
|
@@ -190,6 +286,7 @@ async function configureBackendS3(projectDir, aws) {
|
|
|
190
286
|
|
|
191
287
|
const bucketUrl = `https://${aws.bucket}.s3.${aws.region}.amazonaws.com`;
|
|
192
288
|
|
|
289
|
+
console.log("\n=== BACKEND: configurar middlewares.ts ===\n");
|
|
193
290
|
await fs.writeFile(
|
|
194
291
|
path.join(beDir, "config", "middlewares.ts"),
|
|
195
292
|
`export default [
|
|
@@ -218,6 +315,7 @@ async function configureBackendS3(projectDir, aws) {
|
|
|
218
315
|
`
|
|
219
316
|
);
|
|
220
317
|
|
|
318
|
+
console.log("\n=== BACKEND: configurar plugins.ts ===\n");
|
|
221
319
|
await fs.writeFile(
|
|
222
320
|
path.join(beDir, "config", "plugins.ts"),
|
|
223
321
|
`export default ({ env }) => ({
|
|
@@ -248,14 +346,37 @@ async function configureBackendS3(projectDir, aws) {
|
|
|
248
346
|
`
|
|
249
347
|
);
|
|
250
348
|
|
|
349
|
+
console.log("\n=== BACKEND: configurar server.ts ===\n");
|
|
251
350
|
await fs.writeFile(
|
|
252
|
-
path.join(beDir, ".
|
|
253
|
-
`
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
351
|
+
path.join(beDir, "config", "server.ts"),
|
|
352
|
+
`import type { Core } from '@strapi/strapi';
|
|
353
|
+
|
|
354
|
+
const config = ({ env }: Core.Config.Shared.ConfigParams): Core.Config.Server => {
|
|
355
|
+
return {
|
|
356
|
+
host: env('HOST', '0.0.0.0'),
|
|
357
|
+
port: env.int('PORT', 1337),
|
|
358
|
+
app: {
|
|
359
|
+
keys: env.array('APP_KEYS'),
|
|
360
|
+
},
|
|
361
|
+
transfer: {
|
|
362
|
+
remote: {
|
|
363
|
+
enabled: true,
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
};
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
export default config;
|
|
257
370
|
`
|
|
258
371
|
);
|
|
372
|
+
|
|
373
|
+
console.log("\n=== BACKEND: atualizar .env ===\n");
|
|
374
|
+
await mergeEnvFile(path.join(beDir, ".env"), {
|
|
375
|
+
AWS_ACCESS_KEY_ID: aws.key,
|
|
376
|
+
AWS_ACCESS_SECRET: aws.secret,
|
|
377
|
+
AWS_REGION: aws.region,
|
|
378
|
+
AWS_BUCKET: aws.bucket,
|
|
379
|
+
});
|
|
259
380
|
}
|
|
260
381
|
|
|
261
382
|
/* =========================
|
|
@@ -286,6 +407,7 @@ async function createBackendLocal(projectDir) {
|
|
|
286
407
|
cwd: path.join(projectDir, "backend"),
|
|
287
408
|
});
|
|
288
409
|
|
|
410
|
+
console.log("\n=== BACKEND: git init/commit ===\n");
|
|
289
411
|
await initGitRepo(path.join(projectDir, "backend"), "Init Strapi backend");
|
|
290
412
|
}
|
|
291
413
|
|
|
@@ -300,6 +422,31 @@ async function createFrontendLocal(projectDir, nodeVersion) {
|
|
|
300
422
|
});
|
|
301
423
|
|
|
302
424
|
const feDir = path.join(projectDir, "frontend");
|
|
425
|
+
|
|
426
|
+
const nextConfigTs = path.join(feDir, "next.config.ts");
|
|
427
|
+
const nextConfigJs = path.join(feDir, "next.config.js");
|
|
428
|
+
|
|
429
|
+
if (await fs.pathExists(nextConfigTs)) {
|
|
430
|
+
await fs.remove(nextConfigTs);
|
|
431
|
+
await fs.writeFile(
|
|
432
|
+
nextConfigJs,
|
|
433
|
+
`/** @type {import('next').NextConfig} */
|
|
434
|
+
const nextConfig = {};
|
|
435
|
+
|
|
436
|
+
module.exports = nextConfig;
|
|
437
|
+
`
|
|
438
|
+
);
|
|
439
|
+
} else if (!(await fs.pathExists(nextConfigJs))) {
|
|
440
|
+
await fs.writeFile(
|
|
441
|
+
nextConfigJs,
|
|
442
|
+
`/** @type {import('next').NextConfig} */
|
|
443
|
+
const nextConfig = {};
|
|
444
|
+
|
|
445
|
+
module.exports = nextConfig;
|
|
446
|
+
`
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
|
|
303
450
|
const pkgPath = path.join(feDir, "package.json");
|
|
304
451
|
const pkg = await fs.readJson(pkgPath);
|
|
305
452
|
|
|
@@ -307,10 +454,17 @@ async function createFrontendLocal(projectDir, nodeVersion) {
|
|
|
307
454
|
pkg.engines.node = nodeVersion;
|
|
308
455
|
|
|
309
456
|
pkg.scripts = pkg.scripts || {};
|
|
457
|
+
pkg.scripts.build = pkg.scripts.build || "next build";
|
|
310
458
|
pkg.scripts.start = "next start -p $PORT";
|
|
311
459
|
|
|
312
460
|
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
313
461
|
|
|
462
|
+
const envLocalPath = path.join(feDir, ".env.local");
|
|
463
|
+
if (!(await fs.pathExists(envLocalPath))) {
|
|
464
|
+
await fs.writeFile(envLocalPath, "NEXT_PUBLIC_STRAPI_URL=http://localhost:1337\n");
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
console.log("\n=== FRONTEND: git init/commit ===\n");
|
|
314
468
|
await initGitRepo(feDir, "Init Next frontend");
|
|
315
469
|
}
|
|
316
470
|
|
|
@@ -331,6 +485,24 @@ async function deployBackendHeroku({ projectDir, backendApp, region }) {
|
|
|
331
485
|
await run("heroku", ["git:remote", "-a", finalBackendApp], { cwd: beDir });
|
|
332
486
|
await run("heroku", ["addons:create", "heroku-postgresql", "-a", finalBackendApp], { cwd: beDir });
|
|
333
487
|
|
|
488
|
+
console.log("\n=== BACKEND: configurar env vars Strapi ===\n");
|
|
489
|
+
const appKeys = [randHex(16), randHex(16), randHex(16), randHex(16)].join(",");
|
|
490
|
+
const apiTokenSalt = randHex(16);
|
|
491
|
+
const adminJwtSecret = randHex(24);
|
|
492
|
+
const jwtSecret = randHex(24);
|
|
493
|
+
|
|
494
|
+
await run("heroku", ["config:set", `NODE_ENV=production`, "-a", finalBackendApp], { cwd: beDir });
|
|
495
|
+
await run("heroku", ["config:set", `APP_KEYS=${appKeys}`, "-a", finalBackendApp], { cwd: beDir });
|
|
496
|
+
await run("heroku", ["config:set", `API_TOKEN_SALT=${apiTokenSalt}`, "-a", finalBackendApp], { cwd: beDir });
|
|
497
|
+
await run("heroku", ["config:set", `ADMIN_JWT_SECRET=${adminJwtSecret}`, "-a", finalBackendApp], { cwd: beDir });
|
|
498
|
+
await run("heroku", ["config:set", `JWT_SECRET=${jwtSecret}`, "-a", finalBackendApp], { cwd: beDir });
|
|
499
|
+
|
|
500
|
+
await run("heroku", ["config:set", `DATABASE_CLIENT=postgres`, "-a", finalBackendApp], { cwd: beDir });
|
|
501
|
+
await run("heroku", ["config:set", `DATABASE_SSL=true`, "-a", finalBackendApp], { cwd: beDir });
|
|
502
|
+
await run("heroku", ["config:set", `DATABASE_SSL_REJECT_UNAUTHORIZED=false`, "-a", finalBackendApp], {
|
|
503
|
+
cwd: beDir,
|
|
504
|
+
});
|
|
505
|
+
|
|
334
506
|
const envPath = path.join(beDir, ".env");
|
|
335
507
|
|
|
336
508
|
if (await fs.pathExists(envPath)) {
|
|
@@ -349,13 +521,21 @@ async function deployBackendHeroku({ projectDir, backendApp, region }) {
|
|
|
349
521
|
await run("heroku", ["config:set", `AWS_REGION=${awsRegion}`, "-a", finalBackendApp], { cwd: beDir });
|
|
350
522
|
}
|
|
351
523
|
if (awsAccessKeyId) {
|
|
352
|
-
await run("heroku", ["config:set", `AWS_ACCESS_KEY_ID=${awsAccessKeyId}`, "-a", finalBackendApp], {
|
|
524
|
+
await run("heroku", ["config:set", `AWS_ACCESS_KEY_ID=${awsAccessKeyId}`, "-a", finalBackendApp], {
|
|
525
|
+
cwd: beDir,
|
|
526
|
+
});
|
|
353
527
|
}
|
|
354
528
|
if (awsAccessSecret) {
|
|
355
|
-
await run("heroku", ["config:set", `AWS_ACCESS_SECRET=${awsAccessSecret}`, "-a", finalBackendApp], {
|
|
529
|
+
await run("heroku", ["config:set", `AWS_ACCESS_SECRET=${awsAccessSecret}`, "-a", finalBackendApp], {
|
|
530
|
+
cwd: beDir,
|
|
531
|
+
});
|
|
356
532
|
}
|
|
357
533
|
}
|
|
358
534
|
|
|
535
|
+
console.log("\n=== BACKEND: garantir commit antes do deploy ===\n");
|
|
536
|
+
await commitAllIfNeeded(beDir, "Pre-deploy commit");
|
|
537
|
+
|
|
538
|
+
console.log("\n=== BACKEND: deploy ===\n");
|
|
359
539
|
await run("git", ["push", "heroku", "main"], { cwd: beDir });
|
|
360
540
|
|
|
361
541
|
const info = await getHerokuAppInfo(finalBackendApp, beDir);
|
|
@@ -387,10 +567,15 @@ async function deployFrontendHeroku({ projectDir, frontendApp, region, backendWe
|
|
|
387
567
|
|
|
388
568
|
await run("heroku", ["git:remote", "-a", finalFrontendApp], { cwd: feDir });
|
|
389
569
|
|
|
570
|
+
console.log("\n=== FRONTEND: configurar env var ===\n");
|
|
390
571
|
await run("heroku", ["config:set", `NEXT_PUBLIC_STRAPI_URL=${backendWebUrl}`, "-a", finalFrontendApp], {
|
|
391
572
|
cwd: feDir,
|
|
392
573
|
});
|
|
393
574
|
|
|
575
|
+
console.log("\n=== FRONTEND: garantir commit antes do deploy ===\n");
|
|
576
|
+
await commitAllIfNeeded(feDir, "Pre-deploy commit");
|
|
577
|
+
|
|
578
|
+
console.log("\n=== FRONTEND: deploy ===\n");
|
|
394
579
|
await run("git", ["push", "heroku", "main"], { cwd: feDir });
|
|
395
580
|
|
|
396
581
|
const info = await getHerokuAppInfo(finalFrontendApp, feDir);
|
|
@@ -422,7 +607,6 @@ async function doCreate(projectDirArg, opts) {
|
|
|
422
607
|
const region = (opts.region || "eu").toLowerCase();
|
|
423
608
|
const nodeVersion = opts.node || "20.x";
|
|
424
609
|
|
|
425
|
-
// S3
|
|
426
610
|
const { useS3 } = await inquirer.prompt([
|
|
427
611
|
{ type: "confirm", name: "useS3", message: "Usar S3?", default: true },
|
|
428
612
|
]);
|
|
@@ -432,9 +616,9 @@ async function doCreate(projectDirArg, opts) {
|
|
|
432
616
|
if (useS3) {
|
|
433
617
|
aws = await inquirer.prompt([
|
|
434
618
|
{ name: "bucket", message: "Bucket:" },
|
|
435
|
-
{ name: "region", default: "eu-north-1" },
|
|
619
|
+
{ name: "region", message: "Region:", default: "eu-north-1" },
|
|
436
620
|
{ name: "key", message: "Access Key:" },
|
|
437
|
-
{ type: "password", name: "secret", message: "Secret:" },
|
|
621
|
+
{ type: "password", name: "secret", message: "Secret:", mask: "*" },
|
|
438
622
|
]);
|
|
439
623
|
}
|
|
440
624
|
|
|
@@ -456,7 +640,12 @@ async function doCreate(projectDirArg, opts) {
|
|
|
456
640
|
|
|
457
641
|
await createBackendLocal(projectDir);
|
|
458
642
|
|
|
459
|
-
if (useS3
|
|
643
|
+
if (useS3 && aws) {
|
|
644
|
+
await configureBackendS3(projectDir, aws);
|
|
645
|
+
|
|
646
|
+
console.log("\n=== BACKEND: commit configs S3 ===\n");
|
|
647
|
+
await commitAllIfNeeded(path.join(projectDir, "backend"), "Configure S3 upload");
|
|
648
|
+
}
|
|
460
649
|
|
|
461
650
|
await createFrontendLocal(projectDir, nodeVersion);
|
|
462
651
|
|
|
@@ -467,31 +656,93 @@ async function doCreate(projectDirArg, opts) {
|
|
|
467
656
|
backendApp,
|
|
468
657
|
frontendApp,
|
|
469
658
|
useS3,
|
|
470
|
-
awsBucket: aws?.bucket,
|
|
471
|
-
awsRegion: aws?.region,
|
|
659
|
+
awsBucket: aws?.bucket || null,
|
|
660
|
+
awsRegion: aws?.region || null,
|
|
472
661
|
createdAt: new Date().toISOString(),
|
|
662
|
+
deployedAt: null,
|
|
663
|
+
backendUrl: null,
|
|
664
|
+
frontendUrl: null,
|
|
473
665
|
});
|
|
474
666
|
|
|
475
|
-
if (!deployNow)
|
|
667
|
+
if (!deployNow) {
|
|
668
|
+
console.log("\n✅ Projeto criado localmente (sem Heroku).");
|
|
669
|
+
console.log(`📁 Pasta: ${projectDir}`);
|
|
670
|
+
console.log("\nPara correr local:");
|
|
671
|
+
console.log("- Backend: cd backend && npm run develop");
|
|
672
|
+
console.log("- Frontend: cd frontend && npm run dev");
|
|
673
|
+
console.log("\nQuando quiseres publicar:");
|
|
674
|
+
console.log(`- abw-react-starter publish "${projectDir}"`);
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const publishAnswers = await inquirer.prompt([
|
|
679
|
+
{
|
|
680
|
+
type: "input",
|
|
681
|
+
name: "backendApp",
|
|
682
|
+
message: "Nome da app Heroku do backend (Strapi):",
|
|
683
|
+
default: backendApp,
|
|
684
|
+
validate: (v) => (v?.length >= 3 ? true : "Nome muito curto"),
|
|
685
|
+
},
|
|
686
|
+
{
|
|
687
|
+
type: "input",
|
|
688
|
+
name: "frontendApp",
|
|
689
|
+
message: "Nome da app Heroku do frontend (Next):",
|
|
690
|
+
default: frontendApp,
|
|
691
|
+
validate: (v) => (v?.length >= 3 ? true : "Nome muito curto"),
|
|
692
|
+
},
|
|
693
|
+
{
|
|
694
|
+
type: "confirm",
|
|
695
|
+
name: "confirm",
|
|
696
|
+
message: "Confirmas criar 2 apps Heroku e fazer deploy?",
|
|
697
|
+
default: true,
|
|
698
|
+
},
|
|
699
|
+
]);
|
|
700
|
+
|
|
701
|
+
if (!publishAnswers.confirm) {
|
|
702
|
+
console.log("\n✅ Projeto criado localmente (não publicaste no Heroku).");
|
|
703
|
+
console.log(`Quando quiseres publicar: abw-react-starter publish "${projectDir}"`);
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
backendApp = publishAnswers.backendApp;
|
|
708
|
+
frontendApp = publishAnswers.frontendApp;
|
|
709
|
+
|
|
710
|
+
await writeManifest(projectDir, {
|
|
711
|
+
backendApp,
|
|
712
|
+
frontendApp,
|
|
713
|
+
});
|
|
476
714
|
|
|
477
715
|
const canPublish = await ensureHerokuLoginOrSkip();
|
|
478
|
-
if (!canPublish) return;
|
|
479
716
|
|
|
480
|
-
|
|
717
|
+
if (!canPublish) {
|
|
718
|
+
console.log("\n✅ Projeto criado localmente (sem publicar no Heroku).");
|
|
719
|
+
console.log(`📁 Pasta: ${projectDir}`);
|
|
720
|
+
console.log("\nPara publicar mais tarde:");
|
|
721
|
+
console.log(`- abw-react-starter publish "${projectDir}"`);
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
await doPublish(projectDir, { skipHerokuLoginCheck: true });
|
|
481
726
|
}
|
|
482
727
|
|
|
483
728
|
/* =========================
|
|
484
729
|
Publish
|
|
485
730
|
========================= */
|
|
486
731
|
|
|
487
|
-
async function doPublish(projectDirArg) {
|
|
732
|
+
async function doPublish(projectDirArg, options = {}) {
|
|
733
|
+
const { skipHerokuLoginCheck = false } = options;
|
|
488
734
|
const projectDir = resolveProjectDir(projectDirArg);
|
|
489
735
|
|
|
490
736
|
await assertCmdExists("git");
|
|
491
737
|
await assertCmdExists("heroku");
|
|
492
738
|
|
|
493
|
-
|
|
494
|
-
|
|
739
|
+
if (!skipHerokuLoginCheck) {
|
|
740
|
+
const canPublish = await ensureHerokuLoginOrSkip();
|
|
741
|
+
if (!canPublish) {
|
|
742
|
+
console.log("\nℹ️ Publicação cancelada. O projeto continua localmente sem deploy no Heroku.");
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
495
746
|
|
|
496
747
|
const manifest = await readManifest(projectDir);
|
|
497
748
|
if (!manifest) {
|
|
@@ -500,21 +751,59 @@ async function doPublish(projectDirArg) {
|
|
|
500
751
|
);
|
|
501
752
|
}
|
|
502
753
|
|
|
754
|
+
const region = (manifest.region || "eu").toLowerCase();
|
|
755
|
+
const backendDir = path.join(projectDir, "backend");
|
|
756
|
+
const frontendDir = path.join(projectDir, "frontend");
|
|
757
|
+
|
|
758
|
+
if (!(await fs.pathExists(backendDir))) throw new Error("Não encontrei a pasta backend.");
|
|
759
|
+
if (!(await fs.pathExists(frontendDir))) throw new Error("Não encontrei a pasta frontend.");
|
|
760
|
+
|
|
761
|
+
let backendApp = manifest.backendApp;
|
|
762
|
+
let frontendApp = manifest.frontendApp;
|
|
763
|
+
|
|
764
|
+
if (!backendApp || !frontendApp) {
|
|
765
|
+
const projectBase = path.basename(projectDir);
|
|
766
|
+
const a = await inquirer.prompt([
|
|
767
|
+
{
|
|
768
|
+
type: "input",
|
|
769
|
+
name: "backendApp",
|
|
770
|
+
message: "Nome da app Heroku do backend (Strapi):",
|
|
771
|
+
default: backendApp || `${projectBase}-api`,
|
|
772
|
+
validate: (v) => (v?.length >= 3 ? true : "Nome muito curto"),
|
|
773
|
+
},
|
|
774
|
+
{
|
|
775
|
+
type: "input",
|
|
776
|
+
name: "frontendApp",
|
|
777
|
+
message: "Nome da app Heroku do frontend (Next):",
|
|
778
|
+
default: frontendApp || `${projectBase}-web`,
|
|
779
|
+
validate: (v) => (v?.length >= 3 ? true : "Nome muito curto"),
|
|
780
|
+
},
|
|
781
|
+
]);
|
|
782
|
+
|
|
783
|
+
backendApp = a.backendApp;
|
|
784
|
+
frontendApp = a.frontendApp;
|
|
785
|
+
|
|
786
|
+
await writeManifest(projectDir, {
|
|
787
|
+
backendApp,
|
|
788
|
+
frontendApp,
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
|
|
503
792
|
const backendResult = await deployBackendHeroku({
|
|
504
793
|
projectDir,
|
|
505
|
-
backendApp
|
|
506
|
-
region
|
|
794
|
+
backendApp,
|
|
795
|
+
region,
|
|
507
796
|
});
|
|
508
797
|
|
|
509
798
|
const frontendResult = await deployFrontendHeroku({
|
|
510
799
|
projectDir,
|
|
511
|
-
frontendApp
|
|
512
|
-
region
|
|
800
|
+
frontendApp,
|
|
801
|
+
region,
|
|
513
802
|
backendWebUrl: backendResult.backendWebUrl,
|
|
514
803
|
});
|
|
515
804
|
|
|
516
805
|
await writeManifest(projectDir, {
|
|
517
|
-
|
|
806
|
+
deployedAt: new Date().toISOString(),
|
|
518
807
|
backendApp: backendResult.backendApp,
|
|
519
808
|
frontendApp: frontendResult.frontendApp,
|
|
520
809
|
backendUrl: backendResult.backendWebUrl,
|
|
@@ -522,45 +811,57 @@ async function doPublish(projectDirArg) {
|
|
|
522
811
|
});
|
|
523
812
|
|
|
524
813
|
console.log("\n✅ Tudo pronto!");
|
|
525
|
-
console.log(
|
|
526
|
-
console.log(
|
|
527
|
-
console.log(
|
|
528
|
-
console.log(
|
|
814
|
+
console.log(`Backend app: ${backendResult.backendApp}`);
|
|
815
|
+
console.log(`Backend (Strapi): ${backendResult.backendWebUrl}`);
|
|
816
|
+
console.log(`Admin: ${backendResult.backendWebUrl}/admin`);
|
|
817
|
+
console.log(`Frontend app: ${frontendResult.frontendApp}`);
|
|
818
|
+
console.log(`Frontend (Next): ${frontendResult.frontendWebUrl}`);
|
|
819
|
+
console.log("\nNotas:");
|
|
820
|
+
console.log("- Content types normalmente crias localmente e depois fazes push para o backend Heroku.");
|
|
821
|
+
console.log("- Em produção, dá permissões em Public role para expor endpoints.");
|
|
529
822
|
}
|
|
530
823
|
|
|
531
824
|
/* =========================
|
|
532
|
-
CLI
|
|
825
|
+
CLI
|
|
533
826
|
========================= */
|
|
534
827
|
|
|
535
828
|
program.name("abw-react-starter");
|
|
536
829
|
|
|
830
|
+
// Comando create (default)
|
|
537
831
|
program
|
|
538
832
|
.command("create")
|
|
539
|
-
.argument("<projectDir>")
|
|
540
|
-
.option("--backend-app <name>")
|
|
541
|
-
.option("--frontend-app <name>")
|
|
542
|
-
.option("--node <version>", "20.x")
|
|
543
|
-
.option("--region <region>", "eu")
|
|
833
|
+
.argument("<projectDir>", "Diretório do projeto (será criado)")
|
|
834
|
+
.option("--backend-app <name>", "Nome da app Heroku do backend (Strapi)")
|
|
835
|
+
.option("--frontend-app <name>", "Nome da app Heroku do frontend (Next)")
|
|
836
|
+
.option("--node <version>", "Versão Node para Heroku (ex: 20.x)", "20.x")
|
|
837
|
+
.option("--region <region>", "Heroku region (eu | us)", "eu")
|
|
544
838
|
.action(async (projectDirArg, opts) => {
|
|
545
839
|
await doCreate(projectDirArg, opts);
|
|
546
840
|
});
|
|
547
841
|
|
|
842
|
+
// Para compatibilidade: se chamarem sem "create", trata como create
|
|
548
843
|
program
|
|
549
|
-
.argument("[projectDir]")
|
|
550
|
-
.option("--backend-app <name>")
|
|
551
|
-
.option("--frontend-app <name>")
|
|
552
|
-
.option("--node <version>", "20.x")
|
|
553
|
-
.option("--region <region>", "eu");
|
|
844
|
+
.argument("[projectDir]", "Diretório do projeto (será criado)")
|
|
845
|
+
.option("--backend-app <name>", "Nome da app Heroku do backend (Strapi)")
|
|
846
|
+
.option("--frontend-app <name>", "Nome da app Heroku do frontend (Next)")
|
|
847
|
+
.option("--node <version>", "Versão Node para Heroku (ex: 20.x)", "20.x")
|
|
848
|
+
.option("--region <region>", "Heroku region (eu | us)", "eu");
|
|
554
849
|
|
|
850
|
+
// Comando publish
|
|
555
851
|
program
|
|
556
852
|
.command("publish")
|
|
557
|
-
.argument("[projectDir]")
|
|
853
|
+
.argument("[projectDir]", "Diretório do projeto (default: .)")
|
|
854
|
+
.description("Publicar um projeto existente no Heroku")
|
|
558
855
|
.action(async (projectDirArg) => {
|
|
559
856
|
await doPublish(projectDirArg || ".");
|
|
560
857
|
});
|
|
561
858
|
|
|
859
|
+
// Se executarem sem subcomando, faz create
|
|
562
860
|
program.action(async (projectDirArg, opts) => {
|
|
563
|
-
if (!projectDirArg)
|
|
861
|
+
if (!projectDirArg) {
|
|
862
|
+
program.help();
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
564
865
|
await doCreate(projectDirArg, opts);
|
|
565
866
|
});
|
|
566
867
|
|