abw-react-starter 1.0.1 → 1.0.2
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 +336 -58
- package/package.json +1 -1
package/bin/create.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { Command } from "commander";
|
|
4
4
|
import inquirer from "inquirer";
|
|
@@ -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,6 +160,33 @@ 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
|
+
|
|
107
190
|
/* =========================
|
|
108
191
|
Heroku Login (melhorado)
|
|
109
192
|
========================= */
|
|
@@ -146,7 +229,8 @@ async function ensureHerokuLoginOrSkip() {
|
|
|
146
229
|
========================= */
|
|
147
230
|
|
|
148
231
|
async function writeManifest(projectDir, data) {
|
|
149
|
-
await
|
|
232
|
+
const current = (await readManifest(projectDir)) || {};
|
|
233
|
+
await fs.writeJson(path.join(projectDir, MANIFEST_NAME), { ...current, ...data }, {
|
|
150
234
|
spaces: 2,
|
|
151
235
|
});
|
|
152
236
|
}
|
|
@@ -190,6 +274,7 @@ async function configureBackendS3(projectDir, aws) {
|
|
|
190
274
|
|
|
191
275
|
const bucketUrl = `https://${aws.bucket}.s3.${aws.region}.amazonaws.com`;
|
|
192
276
|
|
|
277
|
+
console.log("\n=== BACKEND: configurar middlewares.ts ===\n");
|
|
193
278
|
await fs.writeFile(
|
|
194
279
|
path.join(beDir, "config", "middlewares.ts"),
|
|
195
280
|
`export default [
|
|
@@ -218,6 +303,7 @@ async function configureBackendS3(projectDir, aws) {
|
|
|
218
303
|
`
|
|
219
304
|
);
|
|
220
305
|
|
|
306
|
+
console.log("\n=== BACKEND: configurar plugins.ts ===\n");
|
|
221
307
|
await fs.writeFile(
|
|
222
308
|
path.join(beDir, "config", "plugins.ts"),
|
|
223
309
|
`export default ({ env }) => ({
|
|
@@ -248,14 +334,37 @@ async function configureBackendS3(projectDir, aws) {
|
|
|
248
334
|
`
|
|
249
335
|
);
|
|
250
336
|
|
|
337
|
+
console.log("\n=== BACKEND: configurar server.ts ===\n");
|
|
251
338
|
await fs.writeFile(
|
|
252
|
-
path.join(beDir, ".
|
|
253
|
-
`
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
339
|
+
path.join(beDir, "config", "server.ts"),
|
|
340
|
+
`import type { Core } from '@strapi/strapi';
|
|
341
|
+
|
|
342
|
+
const config = ({ env }: Core.Config.Shared.ConfigParams): Core.Config.Server => {
|
|
343
|
+
return {
|
|
344
|
+
host: env('HOST', '0.0.0.0'),
|
|
345
|
+
port: env.int('PORT', 1337),
|
|
346
|
+
app: {
|
|
347
|
+
keys: env.array('APP_KEYS'),
|
|
348
|
+
},
|
|
349
|
+
transfer: {
|
|
350
|
+
remote: {
|
|
351
|
+
enabled: true,
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
};
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
export default config;
|
|
257
358
|
`
|
|
258
359
|
);
|
|
360
|
+
|
|
361
|
+
console.log("\n=== BACKEND: atualizar .env ===\n");
|
|
362
|
+
await mergeEnvFile(path.join(beDir, ".env"), {
|
|
363
|
+
AWS_ACCESS_KEY_ID: aws.key,
|
|
364
|
+
AWS_ACCESS_SECRET: aws.secret,
|
|
365
|
+
AWS_REGION: aws.region,
|
|
366
|
+
AWS_BUCKET: aws.bucket,
|
|
367
|
+
});
|
|
259
368
|
}
|
|
260
369
|
|
|
261
370
|
/* =========================
|
|
@@ -300,6 +409,31 @@ async function createFrontendLocal(projectDir, nodeVersion) {
|
|
|
300
409
|
});
|
|
301
410
|
|
|
302
411
|
const feDir = path.join(projectDir, "frontend");
|
|
412
|
+
|
|
413
|
+
const nextConfigTs = path.join(feDir, "next.config.ts");
|
|
414
|
+
const nextConfigJs = path.join(feDir, "next.config.js");
|
|
415
|
+
|
|
416
|
+
if (await fs.pathExists(nextConfigTs)) {
|
|
417
|
+
await fs.remove(nextConfigTs);
|
|
418
|
+
await fs.writeFile(
|
|
419
|
+
nextConfigJs,
|
|
420
|
+
`/** @type {import('next').NextConfig} */
|
|
421
|
+
const nextConfig = {};
|
|
422
|
+
|
|
423
|
+
module.exports = nextConfig;
|
|
424
|
+
`
|
|
425
|
+
);
|
|
426
|
+
} else if (!(await fs.pathExists(nextConfigJs))) {
|
|
427
|
+
await fs.writeFile(
|
|
428
|
+
nextConfigJs,
|
|
429
|
+
`/** @type {import('next').NextConfig} */
|
|
430
|
+
const nextConfig = {};
|
|
431
|
+
|
|
432
|
+
module.exports = nextConfig;
|
|
433
|
+
`
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
|
|
303
437
|
const pkgPath = path.join(feDir, "package.json");
|
|
304
438
|
const pkg = await fs.readJson(pkgPath);
|
|
305
439
|
|
|
@@ -307,10 +441,16 @@ async function createFrontendLocal(projectDir, nodeVersion) {
|
|
|
307
441
|
pkg.engines.node = nodeVersion;
|
|
308
442
|
|
|
309
443
|
pkg.scripts = pkg.scripts || {};
|
|
444
|
+
pkg.scripts.build = pkg.scripts.build || "next build";
|
|
310
445
|
pkg.scripts.start = "next start -p $PORT";
|
|
311
446
|
|
|
312
447
|
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
313
448
|
|
|
449
|
+
const envLocalPath = path.join(feDir, ".env.local");
|
|
450
|
+
if (!(await fs.pathExists(envLocalPath))) {
|
|
451
|
+
await fs.writeFile(envLocalPath, "NEXT_PUBLIC_STRAPI_URL=http://localhost:1337\n");
|
|
452
|
+
}
|
|
453
|
+
|
|
314
454
|
await initGitRepo(feDir, "Init Next frontend");
|
|
315
455
|
}
|
|
316
456
|
|
|
@@ -331,6 +471,24 @@ async function deployBackendHeroku({ projectDir, backendApp, region }) {
|
|
|
331
471
|
await run("heroku", ["git:remote", "-a", finalBackendApp], { cwd: beDir });
|
|
332
472
|
await run("heroku", ["addons:create", "heroku-postgresql", "-a", finalBackendApp], { cwd: beDir });
|
|
333
473
|
|
|
474
|
+
console.log("\n=== BACKEND: configurar env vars Strapi ===\n");
|
|
475
|
+
const appKeys = [randHex(16), randHex(16), randHex(16), randHex(16)].join(",");
|
|
476
|
+
const apiTokenSalt = randHex(16);
|
|
477
|
+
const adminJwtSecret = randHex(24);
|
|
478
|
+
const jwtSecret = randHex(24);
|
|
479
|
+
|
|
480
|
+
await run("heroku", ["config:set", `NODE_ENV=production`, "-a", finalBackendApp], { cwd: beDir });
|
|
481
|
+
await run("heroku", ["config:set", `APP_KEYS=${appKeys}`, "-a", finalBackendApp], { cwd: beDir });
|
|
482
|
+
await run("heroku", ["config:set", `API_TOKEN_SALT=${apiTokenSalt}`, "-a", finalBackendApp], { cwd: beDir });
|
|
483
|
+
await run("heroku", ["config:set", `ADMIN_JWT_SECRET=${adminJwtSecret}`, "-a", finalBackendApp], { cwd: beDir });
|
|
484
|
+
await run("heroku", ["config:set", `JWT_SECRET=${jwtSecret}`, "-a", finalBackendApp], { cwd: beDir });
|
|
485
|
+
|
|
486
|
+
await run("heroku", ["config:set", `DATABASE_CLIENT=postgres`, "-a", finalBackendApp], { cwd: beDir });
|
|
487
|
+
await run("heroku", ["config:set", `DATABASE_SSL=true`, "-a", finalBackendApp], { cwd: beDir });
|
|
488
|
+
await run("heroku", ["config:set", `DATABASE_SSL_REJECT_UNAUTHORIZED=false`, "-a", finalBackendApp], {
|
|
489
|
+
cwd: beDir,
|
|
490
|
+
});
|
|
491
|
+
|
|
334
492
|
const envPath = path.join(beDir, ".env");
|
|
335
493
|
|
|
336
494
|
if (await fs.pathExists(envPath)) {
|
|
@@ -349,13 +507,18 @@ async function deployBackendHeroku({ projectDir, backendApp, region }) {
|
|
|
349
507
|
await run("heroku", ["config:set", `AWS_REGION=${awsRegion}`, "-a", finalBackendApp], { cwd: beDir });
|
|
350
508
|
}
|
|
351
509
|
if (awsAccessKeyId) {
|
|
352
|
-
await run("heroku", ["config:set", `AWS_ACCESS_KEY_ID=${awsAccessKeyId}`, "-a", finalBackendApp], {
|
|
510
|
+
await run("heroku", ["config:set", `AWS_ACCESS_KEY_ID=${awsAccessKeyId}`, "-a", finalBackendApp], {
|
|
511
|
+
cwd: beDir,
|
|
512
|
+
});
|
|
353
513
|
}
|
|
354
514
|
if (awsAccessSecret) {
|
|
355
|
-
await run("heroku", ["config:set", `AWS_ACCESS_SECRET=${awsAccessSecret}`, "-a", finalBackendApp], {
|
|
515
|
+
await run("heroku", ["config:set", `AWS_ACCESS_SECRET=${awsAccessSecret}`, "-a", finalBackendApp], {
|
|
516
|
+
cwd: beDir,
|
|
517
|
+
});
|
|
356
518
|
}
|
|
357
519
|
}
|
|
358
520
|
|
|
521
|
+
console.log("\n=== BACKEND: deploy ===\n");
|
|
359
522
|
await run("git", ["push", "heroku", "main"], { cwd: beDir });
|
|
360
523
|
|
|
361
524
|
const info = await getHerokuAppInfo(finalBackendApp, beDir);
|
|
@@ -387,10 +550,12 @@ async function deployFrontendHeroku({ projectDir, frontendApp, region, backendWe
|
|
|
387
550
|
|
|
388
551
|
await run("heroku", ["git:remote", "-a", finalFrontendApp], { cwd: feDir });
|
|
389
552
|
|
|
553
|
+
console.log("\n=== FRONTEND: configurar env var ===\n");
|
|
390
554
|
await run("heroku", ["config:set", `NEXT_PUBLIC_STRAPI_URL=${backendWebUrl}`, "-a", finalFrontendApp], {
|
|
391
555
|
cwd: feDir,
|
|
392
556
|
});
|
|
393
557
|
|
|
558
|
+
console.log("\n=== FRONTEND: deploy ===\n");
|
|
394
559
|
await run("git", ["push", "heroku", "main"], { cwd: feDir });
|
|
395
560
|
|
|
396
561
|
const info = await getHerokuAppInfo(finalFrontendApp, feDir);
|
|
@@ -422,7 +587,6 @@ async function doCreate(projectDirArg, opts) {
|
|
|
422
587
|
const region = (opts.region || "eu").toLowerCase();
|
|
423
588
|
const nodeVersion = opts.node || "20.x";
|
|
424
589
|
|
|
425
|
-
// S3
|
|
426
590
|
const { useS3 } = await inquirer.prompt([
|
|
427
591
|
{ type: "confirm", name: "useS3", message: "Usar S3?", default: true },
|
|
428
592
|
]);
|
|
@@ -432,9 +596,9 @@ async function doCreate(projectDirArg, opts) {
|
|
|
432
596
|
if (useS3) {
|
|
433
597
|
aws = await inquirer.prompt([
|
|
434
598
|
{ name: "bucket", message: "Bucket:" },
|
|
435
|
-
{ name: "region", default: "eu-north-1" },
|
|
599
|
+
{ name: "region", message: "Region:", default: "eu-north-1" },
|
|
436
600
|
{ name: "key", message: "Access Key:" },
|
|
437
|
-
{ type: "password", name: "secret", message: "Secret:" },
|
|
601
|
+
{ type: "password", name: "secret", message: "Secret:", mask: "*" },
|
|
438
602
|
]);
|
|
439
603
|
}
|
|
440
604
|
|
|
@@ -456,7 +620,9 @@ async function doCreate(projectDirArg, opts) {
|
|
|
456
620
|
|
|
457
621
|
await createBackendLocal(projectDir);
|
|
458
622
|
|
|
459
|
-
if (useS3
|
|
623
|
+
if (useS3 && aws) {
|
|
624
|
+
await configureBackendS3(projectDir, aws);
|
|
625
|
+
}
|
|
460
626
|
|
|
461
627
|
await createFrontendLocal(projectDir, nodeVersion);
|
|
462
628
|
|
|
@@ -467,31 +633,93 @@ async function doCreate(projectDirArg, opts) {
|
|
|
467
633
|
backendApp,
|
|
468
634
|
frontendApp,
|
|
469
635
|
useS3,
|
|
470
|
-
awsBucket: aws?.bucket,
|
|
471
|
-
awsRegion: aws?.region,
|
|
636
|
+
awsBucket: aws?.bucket || null,
|
|
637
|
+
awsRegion: aws?.region || null,
|
|
472
638
|
createdAt: new Date().toISOString(),
|
|
639
|
+
deployedAt: null,
|
|
640
|
+
backendUrl: null,
|
|
641
|
+
frontendUrl: null,
|
|
473
642
|
});
|
|
474
643
|
|
|
475
|
-
if (!deployNow)
|
|
644
|
+
if (!deployNow) {
|
|
645
|
+
console.log("\n✅ Projeto criado localmente (sem Heroku).");
|
|
646
|
+
console.log(`📁 Pasta: ${projectDir}`);
|
|
647
|
+
console.log("\nPara correr local:");
|
|
648
|
+
console.log("- Backend: cd backend && npm run develop");
|
|
649
|
+
console.log("- Frontend: cd frontend && npm run dev");
|
|
650
|
+
console.log("\nQuando quiseres publicar:");
|
|
651
|
+
console.log(`- abw-react-starter publish "${projectDir}"`);
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const publishAnswers = await inquirer.prompt([
|
|
656
|
+
{
|
|
657
|
+
type: "input",
|
|
658
|
+
name: "backendApp",
|
|
659
|
+
message: "Nome da app Heroku do backend (Strapi):",
|
|
660
|
+
default: backendApp,
|
|
661
|
+
validate: (v) => (v?.length >= 3 ? true : "Nome muito curto"),
|
|
662
|
+
},
|
|
663
|
+
{
|
|
664
|
+
type: "input",
|
|
665
|
+
name: "frontendApp",
|
|
666
|
+
message: "Nome da app Heroku do frontend (Next):",
|
|
667
|
+
default: frontendApp,
|
|
668
|
+
validate: (v) => (v?.length >= 3 ? true : "Nome muito curto"),
|
|
669
|
+
},
|
|
670
|
+
{
|
|
671
|
+
type: "confirm",
|
|
672
|
+
name: "confirm",
|
|
673
|
+
message: "Confirmas criar 2 apps Heroku e fazer deploy?",
|
|
674
|
+
default: true,
|
|
675
|
+
},
|
|
676
|
+
]);
|
|
677
|
+
|
|
678
|
+
if (!publishAnswers.confirm) {
|
|
679
|
+
console.log("\n✅ Projeto criado localmente (não publicaste no Heroku).");
|
|
680
|
+
console.log(`Quando quiseres publicar: abw-react-starter publish "${projectDir}"`);
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
backendApp = publishAnswers.backendApp;
|
|
685
|
+
frontendApp = publishAnswers.frontendApp;
|
|
686
|
+
|
|
687
|
+
await writeManifest(projectDir, {
|
|
688
|
+
backendApp,
|
|
689
|
+
frontendApp,
|
|
690
|
+
});
|
|
476
691
|
|
|
477
692
|
const canPublish = await ensureHerokuLoginOrSkip();
|
|
478
|
-
if (!canPublish) return;
|
|
479
693
|
|
|
480
|
-
|
|
694
|
+
if (!canPublish) {
|
|
695
|
+
console.log("\n✅ Projeto criado localmente (sem publicar no Heroku).");
|
|
696
|
+
console.log(`📁 Pasta: ${projectDir}`);
|
|
697
|
+
console.log("\nPara publicar mais tarde:");
|
|
698
|
+
console.log(`- abw-react-starter publish "${projectDir}"`);
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
await doPublish(projectDir, { skipHerokuLoginCheck: true });
|
|
481
703
|
}
|
|
482
704
|
|
|
483
705
|
/* =========================
|
|
484
706
|
Publish
|
|
485
707
|
========================= */
|
|
486
708
|
|
|
487
|
-
async function doPublish(projectDirArg) {
|
|
709
|
+
async function doPublish(projectDirArg, options = {}) {
|
|
710
|
+
const { skipHerokuLoginCheck = false } = options;
|
|
488
711
|
const projectDir = resolveProjectDir(projectDirArg);
|
|
489
712
|
|
|
490
713
|
await assertCmdExists("git");
|
|
491
714
|
await assertCmdExists("heroku");
|
|
492
715
|
|
|
493
|
-
|
|
494
|
-
|
|
716
|
+
if (!skipHerokuLoginCheck) {
|
|
717
|
+
const canPublish = await ensureHerokuLoginOrSkip();
|
|
718
|
+
if (!canPublish) {
|
|
719
|
+
console.log("\nℹ️ Publicação cancelada. O projeto continua localmente sem deploy no Heroku.");
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
495
723
|
|
|
496
724
|
const manifest = await readManifest(projectDir);
|
|
497
725
|
if (!manifest) {
|
|
@@ -500,21 +728,59 @@ async function doPublish(projectDirArg) {
|
|
|
500
728
|
);
|
|
501
729
|
}
|
|
502
730
|
|
|
731
|
+
const region = (manifest.region || "eu").toLowerCase();
|
|
732
|
+
const backendDir = path.join(projectDir, "backend");
|
|
733
|
+
const frontendDir = path.join(projectDir, "frontend");
|
|
734
|
+
|
|
735
|
+
if (!(await fs.pathExists(backendDir))) throw new Error("Não encontrei a pasta backend.");
|
|
736
|
+
if (!(await fs.pathExists(frontendDir))) throw new Error("Não encontrei a pasta frontend.");
|
|
737
|
+
|
|
738
|
+
let backendApp = manifest.backendApp;
|
|
739
|
+
let frontendApp = manifest.frontendApp;
|
|
740
|
+
|
|
741
|
+
if (!backendApp || !frontendApp) {
|
|
742
|
+
const projectBase = path.basename(projectDir);
|
|
743
|
+
const a = await inquirer.prompt([
|
|
744
|
+
{
|
|
745
|
+
type: "input",
|
|
746
|
+
name: "backendApp",
|
|
747
|
+
message: "Nome da app Heroku do backend (Strapi):",
|
|
748
|
+
default: backendApp || `${projectBase}-api`,
|
|
749
|
+
validate: (v) => (v?.length >= 3 ? true : "Nome muito curto"),
|
|
750
|
+
},
|
|
751
|
+
{
|
|
752
|
+
type: "input",
|
|
753
|
+
name: "frontendApp",
|
|
754
|
+
message: "Nome da app Heroku do frontend (Next):",
|
|
755
|
+
default: frontendApp || `${projectBase}-web`,
|
|
756
|
+
validate: (v) => (v?.length >= 3 ? true : "Nome muito curto"),
|
|
757
|
+
},
|
|
758
|
+
]);
|
|
759
|
+
|
|
760
|
+
backendApp = a.backendApp;
|
|
761
|
+
frontendApp = a.frontendApp;
|
|
762
|
+
|
|
763
|
+
await writeManifest(projectDir, {
|
|
764
|
+
backendApp,
|
|
765
|
+
frontendApp,
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
|
|
503
769
|
const backendResult = await deployBackendHeroku({
|
|
504
770
|
projectDir,
|
|
505
|
-
backendApp
|
|
506
|
-
region
|
|
771
|
+
backendApp,
|
|
772
|
+
region,
|
|
507
773
|
});
|
|
508
774
|
|
|
509
775
|
const frontendResult = await deployFrontendHeroku({
|
|
510
776
|
projectDir,
|
|
511
|
-
frontendApp
|
|
512
|
-
region
|
|
777
|
+
frontendApp,
|
|
778
|
+
region,
|
|
513
779
|
backendWebUrl: backendResult.backendWebUrl,
|
|
514
780
|
});
|
|
515
781
|
|
|
516
782
|
await writeManifest(projectDir, {
|
|
517
|
-
|
|
783
|
+
deployedAt: new Date().toISOString(),
|
|
518
784
|
backendApp: backendResult.backendApp,
|
|
519
785
|
frontendApp: frontendResult.frontendApp,
|
|
520
786
|
backendUrl: backendResult.backendWebUrl,
|
|
@@ -522,45 +788,57 @@ async function doPublish(projectDirArg) {
|
|
|
522
788
|
});
|
|
523
789
|
|
|
524
790
|
console.log("\n✅ Tudo pronto!");
|
|
525
|
-
console.log(
|
|
526
|
-
console.log(
|
|
527
|
-
console.log(
|
|
528
|
-
console.log(
|
|
791
|
+
console.log(`Backend app: ${backendResult.backendApp}`);
|
|
792
|
+
console.log(`Backend (Strapi): ${backendResult.backendWebUrl}`);
|
|
793
|
+
console.log(`Admin: ${backendResult.backendWebUrl}/admin`);
|
|
794
|
+
console.log(`Frontend app: ${frontendResult.frontendApp}`);
|
|
795
|
+
console.log(`Frontend (Next): ${frontendResult.frontendWebUrl}`);
|
|
796
|
+
console.log("\nNotas:");
|
|
797
|
+
console.log("- Content types normalmente crias localmente e depois fazes push para o backend Heroku.");
|
|
798
|
+
console.log("- Em produção, dá permissões em Public role para expor endpoints.");
|
|
529
799
|
}
|
|
530
800
|
|
|
531
801
|
/* =========================
|
|
532
|
-
CLI
|
|
802
|
+
CLI
|
|
533
803
|
========================= */
|
|
534
804
|
|
|
535
805
|
program.name("abw-react-starter");
|
|
536
806
|
|
|
807
|
+
// Comando create (default)
|
|
537
808
|
program
|
|
538
809
|
.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")
|
|
810
|
+
.argument("<projectDir>", "Diretório do projeto (será criado)")
|
|
811
|
+
.option("--backend-app <name>", "Nome da app Heroku do backend (Strapi)")
|
|
812
|
+
.option("--frontend-app <name>", "Nome da app Heroku do frontend (Next)")
|
|
813
|
+
.option("--node <version>", "Versão Node para Heroku (ex: 20.x)", "20.x")
|
|
814
|
+
.option("--region <region>", "Heroku region (eu | us)", "eu")
|
|
544
815
|
.action(async (projectDirArg, opts) => {
|
|
545
816
|
await doCreate(projectDirArg, opts);
|
|
546
817
|
});
|
|
547
818
|
|
|
819
|
+
// Para compatibilidade: se chamarem sem "create", trata como create
|
|
548
820
|
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");
|
|
821
|
+
.argument("[projectDir]", "Diretório do projeto (será criado)")
|
|
822
|
+
.option("--backend-app <name>", "Nome da app Heroku do backend (Strapi)")
|
|
823
|
+
.option("--frontend-app <name>", "Nome da app Heroku do frontend (Next)")
|
|
824
|
+
.option("--node <version>", "Versão Node para Heroku (ex: 20.x)", "20.x")
|
|
825
|
+
.option("--region <region>", "Heroku region (eu | us)", "eu");
|
|
554
826
|
|
|
827
|
+
// Comando publish
|
|
555
828
|
program
|
|
556
829
|
.command("publish")
|
|
557
|
-
.argument("[projectDir]")
|
|
830
|
+
.argument("[projectDir]", "Diretório do projeto (default: .)")
|
|
831
|
+
.description("Publicar um projeto existente no Heroku")
|
|
558
832
|
.action(async (projectDirArg) => {
|
|
559
833
|
await doPublish(projectDirArg || ".");
|
|
560
834
|
});
|
|
561
835
|
|
|
836
|
+
// Se executarem sem subcomando, faz create
|
|
562
837
|
program.action(async (projectDirArg, opts) => {
|
|
563
|
-
if (!projectDirArg)
|
|
838
|
+
if (!projectDirArg) {
|
|
839
|
+
program.help();
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
564
842
|
await doCreate(projectDirArg, opts);
|
|
565
843
|
});
|
|
566
844
|
|