@thinhnguyencth1204/nextcli 0.4.2 → 0.6.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/README.md +12 -8
- package/dist/cli.js +314 -216
- package/package.json +2 -1
- package/templates/features/{resend/src/lib/resend/send-email.ts → email/src/lib/email/providers/resend.ts} +14 -49
- package/templates/features/email/src/lib/email/providers/smtp.ts +116 -0
- package/templates/features/email/src/lib/email/send-email.ts +68 -0
- package/templates/features/email/src/lib/email/types.ts +34 -0
- package/templates/next-base/.env +12 -7
- package/templates/next-base/.env.development +12 -7
- package/templates/next-base/.env.example +12 -7
- package/templates/next-base/PROJECT_STRUCTURE.md +11 -12
- package/templates/next-base/SETUP.md +29 -17
- package/templates/next-base/package.json +1 -0
- package/templates/next-base/prisma/schema.prisma +3 -1
- package/templates/{features/supabase → next-base}/src/lib/supabase/client.ts +1 -4
- package/templates/{features/supabase → next-base}/src/lib/supabase/storage.ts +1 -4
- package/templates/features/resend/src/lib/resend/client.ts +0 -5
- package/templates/features/resend/src/lib/resend/config.ts +0 -8
- /package/templates/features/{resend → email}/src/emails/welcome-email.tsx +0 -0
- /package/templates/{features/supabase → next-base}/src/lib/supabase/storage-config.ts +0 -0
package/dist/cli.js
CHANGED
|
@@ -137,7 +137,7 @@ async function replaceTokensInDirectory(directoryPath, replacements) {
|
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
|
-
async function mergeEnvFile(envFilePath, entries) {
|
|
140
|
+
async function mergeEnvFile(envFilePath, entries, options = {}) {
|
|
141
141
|
const envExists = await pathExists(envFilePath);
|
|
142
142
|
const currentContent = envExists ? await readFile(envFilePath, "utf8") : "";
|
|
143
143
|
const lines = currentContent ? currentContent.split("\n") : [];
|
|
@@ -161,6 +161,18 @@ async function mergeEnvFile(envFilePath, entries) {
|
|
|
161
161
|
if (additions.length === 0) {
|
|
162
162
|
return;
|
|
163
163
|
}
|
|
164
|
+
if (options.header && currentContent.includes(options.header)) {
|
|
165
|
+
const nextContent2 = currentContent.replace(
|
|
166
|
+
options.header,
|
|
167
|
+
`${options.header}
|
|
168
|
+
${additions.join("\n")}`
|
|
169
|
+
);
|
|
170
|
+
await writeFile(envFilePath, nextContent2, "utf8");
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (options.header) {
|
|
174
|
+
additions.unshift(options.header);
|
|
175
|
+
}
|
|
164
176
|
const separator = currentContent.endsWith("\n") || currentContent.length === 0 ? "" : "\n";
|
|
165
177
|
const nextContent = `${currentContent}${separator}${additions.join("\n")}
|
|
166
178
|
`;
|
|
@@ -202,10 +214,9 @@ var rootDir = path2.resolve(currentDir, "../");
|
|
|
202
214
|
var templatePaths = {
|
|
203
215
|
base: path2.join(rootDir, "templates/next-base"),
|
|
204
216
|
chat: path2.join(rootDir, "templates/features/chat"),
|
|
205
|
-
supabase: path2.join(rootDir, "templates/features/supabase"),
|
|
206
217
|
supabaseRealtime: path2.join(rootDir, "templates/features/supabase-realtime"),
|
|
207
218
|
seo: path2.join(rootDir, "templates/features/seo"),
|
|
208
|
-
|
|
219
|
+
email: path2.join(rootDir, "templates/features/email")
|
|
209
220
|
};
|
|
210
221
|
|
|
211
222
|
// src/core/modules.ts
|
|
@@ -223,25 +234,6 @@ var optionalModules = [
|
|
|
223
234
|
| \`NEXT_PUBLIC_ENABLE_CHAT\` | Set \`true\` when chat module is enabled (auto on add) |
|
|
224
235
|
|
|
225
236
|
Requires \`supabase-realtime\` (auto-added). Run \`db:migrate\` after add \u2014 chat Prisma models are appended.`
|
|
226
|
-
},
|
|
227
|
-
{
|
|
228
|
-
id: "supabase",
|
|
229
|
-
label: "Supabase",
|
|
230
|
-
description: "Adds Supabase client and storage upload helpers",
|
|
231
|
-
templatePath: templatePaths.supabase,
|
|
232
|
-
env: {
|
|
233
|
-
NEXT_PUBLIC_SUPABASE_URL: "",
|
|
234
|
-
NEXT_PUBLIC_SUPABASE_ANON_KEY: "",
|
|
235
|
-
NEXT_PUBLIC_SUPABASE_STORAGE_BUCKET: "public"
|
|
236
|
-
},
|
|
237
|
-
dependencies: {
|
|
238
|
-
"@supabase/supabase-js": "^2.44.2"
|
|
239
|
-
},
|
|
240
|
-
setupSection: `| Variable | Where to get |
|
|
241
|
-
| -------- | ------------ |
|
|
242
|
-
| \`NEXT_PUBLIC_SUPABASE_URL\` | Supabase Dashboard \u2192 Project Settings \u2192 API \u2192 Project URL |
|
|
243
|
-
| \`NEXT_PUBLIC_SUPABASE_ANON_KEY\` | Same page \u2192 Project API keys \u2192 \`anon\` \`public\` |
|
|
244
|
-
| \`NEXT_PUBLIC_SUPABASE_STORAGE_BUCKET\` | Storage \u2192 create bucket \u2192 use bucket name (default scaffold: \`public\`) |`
|
|
245
237
|
},
|
|
246
238
|
{
|
|
247
239
|
id: "supabase-realtime",
|
|
@@ -255,7 +247,7 @@ Requires \`supabase-realtime\` (auto-added). Run \`db:migrate\` after add \u2014
|
|
|
255
247
|
dependencies: {
|
|
256
248
|
"@supabase/supabase-js": "^2.44.2"
|
|
257
249
|
},
|
|
258
|
-
setupSection: `Uses
|
|
250
|
+
setupSection: `Uses the base Supabase URL/anon key. Enable Realtime on tables in Supabase Dashboard \u2192 Database \u2192 Replication.`
|
|
259
251
|
},
|
|
260
252
|
{
|
|
261
253
|
id: "seo",
|
|
@@ -266,23 +258,17 @@ Requires \`supabase-realtime\` (auto-added). Run \`db:migrate\` after add \u2014
|
|
|
266
258
|
setupSection: `No extra env keys. Edit \`src/app/robots.ts\`, \`sitemap.ts\`, and JSON-LD helpers after add.`
|
|
267
259
|
},
|
|
268
260
|
{
|
|
269
|
-
id: "
|
|
270
|
-
label: "Resend
|
|
271
|
-
description: "Adds
|
|
272
|
-
templatePath: templatePaths.
|
|
273
|
-
env: {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
"react-email": "^4.0.0"
|
|
281
|
-
},
|
|
282
|
-
setupSection: `| Variable | Where to get |
|
|
283
|
-
| -------- | ------------ |
|
|
284
|
-
| \`RESEND_API_KEY\` | resend.com \u2192 API Keys \u2192 Create |
|
|
285
|
-
| \`RESEND_FROM_EMAIL\` | resend.com \u2192 Domains \u2192 verify domain \u2192 use \`Name <you@domain.com>\` |`
|
|
261
|
+
id: "email",
|
|
262
|
+
label: "Email module (SMTP or Resend)",
|
|
263
|
+
description: "Adds provider-agnostic sendEmail helper with SMTP/Resend adapters",
|
|
264
|
+
templatePath: templatePaths.email,
|
|
265
|
+
env: {},
|
|
266
|
+
setupSection: `When you add this module, NexTCLI asks which provider to configure:
|
|
267
|
+
|
|
268
|
+
- **Resend**: \`EMAIL_PROVIDER=resend\`, \`RESEND_API_KEY\`, \`RESEND_FROM_EMAIL\`
|
|
269
|
+
- **SMTP**: \`EMAIL_PROVIDER=smtp\`, \`SMTP_HOST\`, \`SMTP_PORT\`, \`SMTP_USER\`, \`SMTP_PASSWORD\`, \`SMTP_FROM\`
|
|
270
|
+
|
|
271
|
+
Only the selected provider keys are merged into your env files.`
|
|
286
272
|
}
|
|
287
273
|
];
|
|
288
274
|
function getModuleById(moduleId) {
|
|
@@ -456,6 +442,42 @@ async function ensureBetterAuthGenerate(cwd, options = {}) {
|
|
|
456
442
|
}
|
|
457
443
|
}
|
|
458
444
|
|
|
445
|
+
// src/core/email-module.ts
|
|
446
|
+
var EMAIL_PROVIDER_OPTIONS = ["resend", "smtp"];
|
|
447
|
+
function isEmailProvider(value) {
|
|
448
|
+
return EMAIL_PROVIDER_OPTIONS.includes(value);
|
|
449
|
+
}
|
|
450
|
+
function getEmailProviderEnv(provider) {
|
|
451
|
+
if (provider === "smtp") {
|
|
452
|
+
return {
|
|
453
|
+
EMAIL_PROVIDER: "smtp",
|
|
454
|
+
SMTP_HOST: "",
|
|
455
|
+
SMTP_PORT: "587",
|
|
456
|
+
SMTP_USER: "",
|
|
457
|
+
SMTP_PASSWORD: "",
|
|
458
|
+
SMTP_FROM: ""
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
return {
|
|
462
|
+
EMAIL_PROVIDER: "resend",
|
|
463
|
+
RESEND_API_KEY: "",
|
|
464
|
+
RESEND_FROM_EMAIL: ""
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
function getEmailProviderDependencies(provider) {
|
|
468
|
+
if (provider === "smtp") {
|
|
469
|
+
return {
|
|
470
|
+
nodemailer: "^6.10.1",
|
|
471
|
+
"@types/nodemailer": "^6.4.17"
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
return {
|
|
475
|
+
resend: "^6.9.2",
|
|
476
|
+
"@react-email/components": "^1.0.12",
|
|
477
|
+
"react-email": "^4.0.0"
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
459
481
|
// src/core/chat-schema.ts
|
|
460
482
|
import path3 from "path";
|
|
461
483
|
import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
@@ -560,7 +582,7 @@ import { readdir as readdir3, readFile as readFile4, writeFile as writeFile4 } f
|
|
|
560
582
|
import path4 from "path";
|
|
561
583
|
import { readdir as readdir2, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
|
|
562
584
|
var defaultManifest = {
|
|
563
|
-
cli: "0.
|
|
585
|
+
cli: "0.6.0",
|
|
564
586
|
defaultLocale: "vi",
|
|
565
587
|
locales: ["vi"],
|
|
566
588
|
namespaces: ["common", "auth", "example"],
|
|
@@ -625,7 +647,7 @@ async function reconcileManifest(projectDir) {
|
|
|
625
647
|
]
|
|
626
648
|
};
|
|
627
649
|
merged.defaultLocale = merged.locales.includes(merged.defaultLocale) ? merged.defaultLocale : merged.locales[0] ?? "vi";
|
|
628
|
-
merged.modules = [...new Set(merged.modules)];
|
|
650
|
+
merged.modules = [...new Set(merged.modules)].map((moduleId) => moduleId === "resend" ? "email" : moduleId).filter((moduleId) => moduleId !== "supabase");
|
|
629
651
|
merged.features = [...new Set(merged.features)];
|
|
630
652
|
await writeManifest(projectDir, merged);
|
|
631
653
|
return merged;
|
|
@@ -1307,24 +1329,6 @@ function normalizeModuleSelection(moduleIds) {
|
|
|
1307
1329
|
autoAddedModules: autoAdded
|
|
1308
1330
|
};
|
|
1309
1331
|
}
|
|
1310
|
-
async function upsertEnvValue(envFilePath, key, value) {
|
|
1311
|
-
if (!await pathExists(envFilePath)) {
|
|
1312
|
-
return;
|
|
1313
|
-
}
|
|
1314
|
-
const content = await readFile6(envFilePath, "utf8");
|
|
1315
|
-
const entry = `${key}=${value}`;
|
|
1316
|
-
const pattern = new RegExp(`^${key}=.*$`, "m");
|
|
1317
|
-
if (pattern.test(content)) {
|
|
1318
|
-
const next = content.replace(pattern, entry);
|
|
1319
|
-
if (next !== content) {
|
|
1320
|
-
await writeFile6(envFilePath, next, "utf8");
|
|
1321
|
-
}
|
|
1322
|
-
return;
|
|
1323
|
-
}
|
|
1324
|
-
const separator = content.endsWith("\n") || content.length === 0 ? "" : "\n";
|
|
1325
|
-
await writeFile6(envFilePath, `${content}${separator}${entry}
|
|
1326
|
-
`, "utf8");
|
|
1327
|
-
}
|
|
1328
1332
|
function registerAddCommand(program2) {
|
|
1329
1333
|
const add = program2.command("add").description("Add modules to an existing app");
|
|
1330
1334
|
add.command("feature").description("Scaffold a feature folder under src/features").argument("<feature-name>", "Feature name").action(async (featureName) => {
|
|
@@ -1443,167 +1447,239 @@ function registerAddCommand(program2) {
|
|
|
1443
1447
|
"No migration was executed. Run your migration command manually when ready."
|
|
1444
1448
|
);
|
|
1445
1449
|
});
|
|
1446
|
-
add.command("module").description("Add optional modules using interactive multi-select").option("--module <module...>", "Preselect module ids").option(
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
if (!validIds.has(moduleId)) {
|
|
1459
|
-
log.error(`Unknown module: ${moduleId}`);
|
|
1450
|
+
add.command("module").description("Add optional modules using interactive multi-select").option("--module <module...>", "Preselect module ids").option(
|
|
1451
|
+
"--email-provider <provider>",
|
|
1452
|
+
"Email provider for email module: resend or smtp"
|
|
1453
|
+
).option("--yes", "Skip prompts").action(
|
|
1454
|
+
async (options) => {
|
|
1455
|
+
const cwd = process.cwd();
|
|
1456
|
+
const hasSrc = await pathExists(path7.join(cwd, "src"));
|
|
1457
|
+
const hasPackageJson = await pathExists(path7.join(cwd, "package.json"));
|
|
1458
|
+
if (!hasSrc || !hasPackageJson) {
|
|
1459
|
+
log.error(
|
|
1460
|
+
"Run this command from your generated Next.js project root."
|
|
1461
|
+
);
|
|
1460
1462
|
process.exitCode = 1;
|
|
1461
1463
|
return;
|
|
1462
1464
|
}
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
"
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
const { selectedModules, autoAddedModules } = normalizeModuleSelection(rawModules);
|
|
1475
|
-
if (selectedModules.length === 0) {
|
|
1476
|
-
finishPrompt("No modules selected.");
|
|
1477
|
-
return;
|
|
1478
|
-
}
|
|
1479
|
-
const skippedConflictsByModule = [];
|
|
1480
|
-
let copiedFileCount = 0;
|
|
1481
|
-
for (const moduleId of selectedModules) {
|
|
1482
|
-
const module = getModuleById(moduleId);
|
|
1483
|
-
const copyReport = await copyDirectorySafely(module.templatePath, cwd);
|
|
1484
|
-
copiedFileCount += copyReport.copiedCount;
|
|
1485
|
-
if (copyReport.skippedConflicts.length > 0) {
|
|
1486
|
-
skippedConflictsByModule.push({
|
|
1487
|
-
moduleId,
|
|
1488
|
-
paths: copyReport.skippedConflicts
|
|
1489
|
-
});
|
|
1465
|
+
const validIds = new Set(
|
|
1466
|
+
optionalModules.map((module) => module.id)
|
|
1467
|
+
);
|
|
1468
|
+
const requestedValues = options.module ? options.module.flatMap((value) => value.split(",")).map((value) => value.trim()).filter(Boolean) : [];
|
|
1469
|
+
const requestedIds = [];
|
|
1470
|
+
let emailProviderFromModuleAlias;
|
|
1471
|
+
const optionEmailProvider = options.emailProvider ? options.emailProvider.trim().toLowerCase() : void 0;
|
|
1472
|
+
if (optionEmailProvider && !isEmailProvider(optionEmailProvider)) {
|
|
1473
|
+
log.error(`Unknown email provider: ${options.emailProvider}`);
|
|
1474
|
+
process.exitCode = 1;
|
|
1475
|
+
return;
|
|
1490
1476
|
}
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1477
|
+
for (const moduleId of requestedValues) {
|
|
1478
|
+
if (validIds.has(moduleId)) {
|
|
1479
|
+
requestedIds.push(moduleId);
|
|
1480
|
+
continue;
|
|
1481
|
+
}
|
|
1482
|
+
if (moduleId === "supabase") {
|
|
1483
|
+
log.info(
|
|
1484
|
+
"Supabase is now part of the base template; only supabase-realtime is optional."
|
|
1485
|
+
);
|
|
1486
|
+
continue;
|
|
1487
|
+
}
|
|
1488
|
+
if (moduleId === "resend") {
|
|
1489
|
+
log.info(
|
|
1490
|
+
"Module 'resend' is now 'email'; selecting email with provider resend."
|
|
1491
|
+
);
|
|
1492
|
+
requestedIds.push("email");
|
|
1493
|
+
emailProviderFromModuleAlias = "resend";
|
|
1494
|
+
continue;
|
|
1495
|
+
}
|
|
1496
|
+
if (!validIds.has(moduleId)) {
|
|
1497
|
+
log.error(`Unknown module: ${moduleId}`);
|
|
1498
|
+
process.exitCode = 1;
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
startPrompt("NexTCLI optional modules");
|
|
1503
|
+
const rawModules = requestedIds.length > 0 ? [...new Set(requestedIds)] : options.yes ? [] : await askMultiSelect(
|
|
1504
|
+
"Select modules to add:",
|
|
1505
|
+
optionalModules.map((module) => ({
|
|
1506
|
+
value: module.id,
|
|
1507
|
+
label: module.label,
|
|
1508
|
+
hint: module.description
|
|
1509
|
+
})),
|
|
1510
|
+
[]
|
|
1511
|
+
);
|
|
1512
|
+
const { selectedModules, autoAddedModules } = normalizeModuleSelection(rawModules);
|
|
1513
|
+
if (optionEmailProvider && !selectedModules.includes("email")) {
|
|
1514
|
+
log.warn(
|
|
1515
|
+
"Ignoring --email-provider because email module is not selected."
|
|
1516
|
+
);
|
|
1517
|
+
}
|
|
1518
|
+
let emailProvider;
|
|
1519
|
+
if (selectedModules.includes("email")) {
|
|
1520
|
+
if (emailProviderFromModuleAlias) {
|
|
1521
|
+
emailProvider = emailProviderFromModuleAlias;
|
|
1522
|
+
} else if (optionEmailProvider && isEmailProvider(optionEmailProvider)) {
|
|
1523
|
+
emailProvider = optionEmailProvider;
|
|
1524
|
+
} else if (options.yes) {
|
|
1525
|
+
emailProvider = "resend";
|
|
1526
|
+
log.info(
|
|
1527
|
+
"No --email-provider supplied in --yes mode; defaulting email provider to resend."
|
|
1528
|
+
);
|
|
1529
|
+
} else {
|
|
1530
|
+
emailProvider = await askSelect(
|
|
1531
|
+
"Select email provider:",
|
|
1532
|
+
[
|
|
1533
|
+
{
|
|
1534
|
+
value: "resend",
|
|
1535
|
+
label: "Resend",
|
|
1536
|
+
hint: "Transactional email API with React Email support"
|
|
1537
|
+
},
|
|
1538
|
+
{
|
|
1539
|
+
value: "smtp",
|
|
1540
|
+
label: "SMTP",
|
|
1541
|
+
hint: "Use your SMTP relay credentials"
|
|
1542
|
+
}
|
|
1543
|
+
],
|
|
1544
|
+
"resend"
|
|
1545
|
+
);
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
if (selectedModules.length === 0) {
|
|
1549
|
+
finishPrompt("No modules selected.");
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
const skippedConflictsByModule = [];
|
|
1553
|
+
let copiedFileCount = 0;
|
|
1554
|
+
for (const moduleId of selectedModules) {
|
|
1498
1555
|
const module = getModuleById(moduleId);
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
const envPath = path7.join(cwd, envFile);
|
|
1510
|
-
if (await pathExists(envPath)) {
|
|
1511
|
-
await mergeEnvFile(envPath, envEntries);
|
|
1556
|
+
const copyReport = await copyDirectorySafely(
|
|
1557
|
+
module.templatePath,
|
|
1558
|
+
cwd
|
|
1559
|
+
);
|
|
1560
|
+
copiedFileCount += copyReport.copiedCount;
|
|
1561
|
+
if (copyReport.skippedConflicts.length > 0) {
|
|
1562
|
+
skippedConflictsByModule.push({
|
|
1563
|
+
moduleId,
|
|
1564
|
+
paths: copyReport.skippedConflicts
|
|
1565
|
+
});
|
|
1512
1566
|
}
|
|
1513
1567
|
}
|
|
1514
|
-
|
|
1515
|
-
|
|
1568
|
+
let chatSchemaStatus;
|
|
1569
|
+
if (selectedModules.includes("chat")) {
|
|
1570
|
+
chatSchemaStatus = await ensureChatSchemaInProject(cwd);
|
|
1571
|
+
}
|
|
1516
1572
|
const envTargets = [".env", ".env.example", ".env.development"];
|
|
1517
|
-
for (const
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1573
|
+
for (const moduleId of selectedModules) {
|
|
1574
|
+
const module = getModuleById(moduleId);
|
|
1575
|
+
const moduleEnv = moduleId === "email" && emailProvider ? getEmailProviderEnv(emailProvider) : module.env;
|
|
1576
|
+
if (Object.keys(moduleEnv).length === 0) {
|
|
1577
|
+
continue;
|
|
1578
|
+
}
|
|
1579
|
+
for (const envFile of envTargets) {
|
|
1580
|
+
const envPath = path7.join(cwd, envFile);
|
|
1581
|
+
if (await pathExists(envPath)) {
|
|
1582
|
+
await mergeEnvFile(envPath, moduleEnv, {
|
|
1583
|
+
header: `# --- module: ${module.id} ---`
|
|
1584
|
+
});
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1523
1587
|
}
|
|
1524
|
-
|
|
1525
|
-
const dependencyEntries = selectedModules.reduce(
|
|
1526
|
-
(acc, moduleId) => {
|
|
1588
|
+
const dependencyEntries = selectedModules.reduce((acc, moduleId) => {
|
|
1527
1589
|
const module = getModuleById(moduleId);
|
|
1590
|
+
const emailDependencies = moduleId === "email" && emailProvider ? getEmailProviderDependencies(emailProvider) : {};
|
|
1528
1591
|
return {
|
|
1529
1592
|
...acc,
|
|
1593
|
+
...emailDependencies,
|
|
1530
1594
|
...module.dependencies ?? {}
|
|
1531
1595
|
};
|
|
1532
|
-
},
|
|
1533
|
-
{
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
dependencyEntries
|
|
1539
|
-
);
|
|
1540
|
-
}
|
|
1541
|
-
const state = await detectProjectState(cwd);
|
|
1542
|
-
const namespaceSet = new Set(state.namespaces);
|
|
1543
|
-
for (const moduleId of selectedModules) {
|
|
1544
|
-
const moduleTemplateMessages = path7.join(
|
|
1545
|
-
getModuleById(moduleId).templatePath,
|
|
1546
|
-
"messages/vi"
|
|
1547
|
-
);
|
|
1548
|
-
if (!await pathExists(moduleTemplateMessages)) {
|
|
1549
|
-
continue;
|
|
1596
|
+
}, {});
|
|
1597
|
+
if (Object.keys(dependencyEntries).length > 0) {
|
|
1598
|
+
await addDependencies(
|
|
1599
|
+
path7.join(cwd, "package.json"),
|
|
1600
|
+
dependencyEntries
|
|
1601
|
+
);
|
|
1550
1602
|
}
|
|
1551
|
-
const
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1603
|
+
const state = await detectProjectState(cwd);
|
|
1604
|
+
const namespaceSet = new Set(state.namespaces);
|
|
1605
|
+
for (const moduleId of selectedModules) {
|
|
1606
|
+
const moduleTemplateMessages = path7.join(
|
|
1607
|
+
getModuleById(moduleId).templatePath,
|
|
1608
|
+
"messages/vi"
|
|
1609
|
+
);
|
|
1610
|
+
if (!await pathExists(moduleTemplateMessages)) {
|
|
1556
1611
|
continue;
|
|
1557
1612
|
}
|
|
1558
|
-
const
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1613
|
+
const files = await readdir4(moduleTemplateMessages, {
|
|
1614
|
+
withFileTypes: true
|
|
1615
|
+
});
|
|
1616
|
+
for (const file of files) {
|
|
1617
|
+
if (!file.isFile() || !file.name.endsWith(".json")) {
|
|
1618
|
+
continue;
|
|
1619
|
+
}
|
|
1620
|
+
const namespace = file.name.replace(/\.json$/, "");
|
|
1621
|
+
namespaceSet.add(namespace);
|
|
1622
|
+
const templateData = JSON.parse(
|
|
1623
|
+
await readFile6(
|
|
1624
|
+
path7.join(moduleTemplateMessages, file.name),
|
|
1625
|
+
"utf8"
|
|
1626
|
+
)
|
|
1627
|
+
);
|
|
1628
|
+
await writeNamespaceMessages(cwd, namespace, templateData);
|
|
1629
|
+
await appendNamespace(cwd, namespace);
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
await writeManifest(cwd, {
|
|
1633
|
+
...state,
|
|
1634
|
+
namespaces: [...namespaceSet],
|
|
1635
|
+
modules: [
|
|
1636
|
+
...new Set(
|
|
1637
|
+
[...state.modules, ...selectedModules].map(
|
|
1638
|
+
(moduleId) => moduleId === "resend" ? "email" : moduleId
|
|
1639
|
+
)
|
|
1640
|
+
)
|
|
1641
|
+
]
|
|
1642
|
+
});
|
|
1643
|
+
const mergedModules = [
|
|
1644
|
+
...new Set(
|
|
1645
|
+
[...state.modules, ...selectedModules].map(
|
|
1646
|
+
(moduleId) => moduleId === "resend" ? "email" : moduleId
|
|
1564
1647
|
)
|
|
1648
|
+
)
|
|
1649
|
+
];
|
|
1650
|
+
await mergeModuleSetupSections(cwd, selectedModules, mergedModules);
|
|
1651
|
+
finishPrompt(`Added modules: ${selectedModules.join(", ")}`);
|
|
1652
|
+
log.detail("Copied files", String(copiedFileCount));
|
|
1653
|
+
if (autoAddedModules.length > 0) {
|
|
1654
|
+
log.detail("Auto-added", autoAddedModules.join(", "));
|
|
1655
|
+
}
|
|
1656
|
+
if (emailProvider) {
|
|
1657
|
+
log.detail("Email provider", emailProvider);
|
|
1658
|
+
}
|
|
1659
|
+
if (skippedConflictsByModule.length > 0) {
|
|
1660
|
+
const totalSkipped = skippedConflictsByModule.reduce(
|
|
1661
|
+
(total, item) => total + item.paths.length,
|
|
1662
|
+
0
|
|
1565
1663
|
);
|
|
1566
|
-
|
|
1567
|
-
|
|
1664
|
+
log.warn(
|
|
1665
|
+
`Skipped ${totalSkipped} existing file(s). Existing project files were kept unchanged.`
|
|
1666
|
+
);
|
|
1667
|
+
for (const conflict of skippedConflictsByModule) {
|
|
1668
|
+
const preview = conflict.paths.slice(0, 3).join(", ");
|
|
1669
|
+
const suffix = conflict.paths.length > 3 ? ", ..." : "";
|
|
1670
|
+
log.detail(`Skipped (${conflict.moduleId})`, `${preview}${suffix}`);
|
|
1671
|
+
}
|
|
1568
1672
|
}
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
modules: [.../* @__PURE__ */ new Set([...state.modules, ...selectedModules])]
|
|
1574
|
-
});
|
|
1575
|
-
const mergedModules = [
|
|
1576
|
-
.../* @__PURE__ */ new Set([...state.modules, ...selectedModules])
|
|
1577
|
-
];
|
|
1578
|
-
await mergeModuleSetupSections(cwd, selectedModules, mergedModules);
|
|
1579
|
-
finishPrompt(`Added modules: ${selectedModules.join(", ")}`);
|
|
1580
|
-
log.detail("Copied files", String(copiedFileCount));
|
|
1581
|
-
if (autoAddedModules.length > 0) {
|
|
1582
|
-
log.detail("Auto-added", autoAddedModules.join(", "));
|
|
1583
|
-
}
|
|
1584
|
-
if (skippedConflictsByModule.length > 0) {
|
|
1585
|
-
const totalSkipped = skippedConflictsByModule.reduce(
|
|
1586
|
-
(total, item) => total + item.paths.length,
|
|
1587
|
-
0
|
|
1588
|
-
);
|
|
1589
|
-
log.warn(
|
|
1590
|
-
`Skipped ${totalSkipped} existing file(s). Existing project files were kept unchanged.`
|
|
1591
|
-
);
|
|
1592
|
-
for (const conflict of skippedConflictsByModule) {
|
|
1593
|
-
const preview = conflict.paths.slice(0, 3).join(", ");
|
|
1594
|
-
const suffix = conflict.paths.length > 3 ? ", ..." : "";
|
|
1595
|
-
log.detail(`Skipped (${conflict.moduleId})`, `${preview}${suffix}`);
|
|
1673
|
+
if (chatSchemaStatus === "added") {
|
|
1674
|
+
log.info(
|
|
1675
|
+
"Optional chat schema block was appended to prisma/schema.prisma."
|
|
1676
|
+
);
|
|
1596
1677
|
}
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
log.info(
|
|
1600
|
-
"Optional chat schema block was appended to prisma/schema.prisma."
|
|
1678
|
+
log.step(
|
|
1679
|
+
"Next: run your package manager install to apply new dependencies."
|
|
1601
1680
|
);
|
|
1602
1681
|
}
|
|
1603
|
-
|
|
1604
|
-
"Next: run your package manager install to apply new dependencies."
|
|
1605
|
-
);
|
|
1606
|
-
});
|
|
1682
|
+
);
|
|
1607
1683
|
add.command("language").description("Add locales and clone message files from Vietnamese").option("--locale <locale...>", "Preselect locales: en,ja,ko").option("--yes", "Skip prompts").action(async (options) => {
|
|
1608
1684
|
const cwd = process.cwd();
|
|
1609
1685
|
const hasMessages = await pathExists(path7.join(cwd, "messages"));
|
|
@@ -1735,7 +1811,9 @@ function registerAddCommand(program2) {
|
|
|
1735
1811
|
for (const envFile of envTargets) {
|
|
1736
1812
|
const envPath = path7.join(cwd, envFile);
|
|
1737
1813
|
if (await pathExists(envPath)) {
|
|
1738
|
-
await mergeEnvFile(envPath, envEntries
|
|
1814
|
+
await mergeEnvFile(envPath, envEntries, {
|
|
1815
|
+
header: "# --- auth providers ---"
|
|
1816
|
+
});
|
|
1739
1817
|
}
|
|
1740
1818
|
}
|
|
1741
1819
|
finishPrompt(`Enabled providers: ${mergedProviders.join(", ")}`);
|
|
@@ -1844,6 +1922,25 @@ function registerCreateCommand(program2) {
|
|
|
1844
1922
|
[]
|
|
1845
1923
|
);
|
|
1846
1924
|
const { selectedModules, autoAddedModules } = normalizeModuleSelection2(rawModules);
|
|
1925
|
+
let emailProvider;
|
|
1926
|
+
if (selectedModules.includes("email")) {
|
|
1927
|
+
emailProvider = await askSelect(
|
|
1928
|
+
"Select email provider:",
|
|
1929
|
+
[
|
|
1930
|
+
{
|
|
1931
|
+
value: "resend",
|
|
1932
|
+
label: "Resend",
|
|
1933
|
+
hint: "Transactional email API with React Email support"
|
|
1934
|
+
},
|
|
1935
|
+
{
|
|
1936
|
+
value: "smtp",
|
|
1937
|
+
label: "SMTP",
|
|
1938
|
+
hint: "Use your SMTP relay credentials"
|
|
1939
|
+
}
|
|
1940
|
+
],
|
|
1941
|
+
"resend"
|
|
1942
|
+
);
|
|
1943
|
+
}
|
|
1847
1944
|
const shouldInstall = await askConfirm("Install dependencies now?", true);
|
|
1848
1945
|
await copyDirectory(templatePaths.base, targetPath);
|
|
1849
1946
|
for (const moduleId of selectedModules) {
|
|
@@ -1854,30 +1951,29 @@ function registerCreateCommand(program2) {
|
|
|
1854
1951
|
if (selectedModules.includes("chat")) {
|
|
1855
1952
|
chatSchemaStatus = await ensureChatSchemaInProject(targetPath);
|
|
1856
1953
|
}
|
|
1857
|
-
const
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
},
|
|
1865
|
-
{}
|
|
1866
|
-
);
|
|
1867
|
-
if (Object.keys(moduleEnvEntries).length > 0) {
|
|
1868
|
-
const envTargets = [".env", ".env.example", ".env.development"];
|
|
1954
|
+
const envTargets = [".env", ".env.example", ".env.development"];
|
|
1955
|
+
for (const moduleId of selectedModules) {
|
|
1956
|
+
const module = getModuleById(moduleId);
|
|
1957
|
+
const moduleEnv = moduleId === "email" && emailProvider ? getEmailProviderEnv(emailProvider) : module.env;
|
|
1958
|
+
if (Object.keys(moduleEnv).length === 0) {
|
|
1959
|
+
continue;
|
|
1960
|
+
}
|
|
1869
1961
|
for (const envFile of envTargets) {
|
|
1870
1962
|
const envPath = path8.join(targetPath, envFile);
|
|
1871
1963
|
if (await pathExists(envPath)) {
|
|
1872
|
-
await mergeEnvFile(envPath,
|
|
1964
|
+
await mergeEnvFile(envPath, moduleEnv, {
|
|
1965
|
+
header: `# --- module: ${module.id} ---`
|
|
1966
|
+
});
|
|
1873
1967
|
}
|
|
1874
1968
|
}
|
|
1875
1969
|
}
|
|
1876
1970
|
const dependencyEntries = selectedModules.reduce(
|
|
1877
1971
|
(acc, moduleId) => {
|
|
1878
1972
|
const module = getModuleById(moduleId);
|
|
1973
|
+
const emailDependencies = moduleId === "email" && emailProvider ? getEmailProviderDependencies(emailProvider) : {};
|
|
1879
1974
|
return {
|
|
1880
1975
|
...acc,
|
|
1976
|
+
...emailDependencies,
|
|
1881
1977
|
...module.dependencies ?? {}
|
|
1882
1978
|
};
|
|
1883
1979
|
},
|
|
@@ -1892,9 +1988,8 @@ function registerCreateCommand(program2) {
|
|
|
1892
1988
|
const betterAuthSecret = randomBytes(32).toString("base64url");
|
|
1893
1989
|
await replaceTokensInDirectory(targetPath, {
|
|
1894
1990
|
__PROJECT_NAME__: projectSlug,
|
|
1895
|
-
__ENABLE_CHAT__: selectedModules.includes("chat") ? "true" : "false",
|
|
1896
1991
|
__BETTER_AUTH_SECRET__: betterAuthSecret,
|
|
1897
|
-
__NEXTCLI_VERSION__: "0.
|
|
1992
|
+
__NEXTCLI_VERSION__: "0.6.0"
|
|
1898
1993
|
});
|
|
1899
1994
|
await mergeModuleSetupSections(
|
|
1900
1995
|
targetPath,
|
|
@@ -1905,7 +2000,7 @@ function registerCreateCommand(program2) {
|
|
|
1905
2000
|
if (manifest) {
|
|
1906
2001
|
await writeManifest(targetPath, {
|
|
1907
2002
|
...manifest,
|
|
1908
|
-
cli: "0.
|
|
2003
|
+
cli: "0.6.0",
|
|
1909
2004
|
modules: selectedModules
|
|
1910
2005
|
});
|
|
1911
2006
|
}
|
|
@@ -1925,6 +2020,9 @@ function registerCreateCommand(program2) {
|
|
|
1925
2020
|
if (autoAddedModules.length > 0) {
|
|
1926
2021
|
log.detail("Auto-added", autoAddedModules.join(", "));
|
|
1927
2022
|
}
|
|
2023
|
+
if (emailProvider) {
|
|
2024
|
+
log.detail("Email provider", emailProvider);
|
|
2025
|
+
}
|
|
1928
2026
|
if (chatSchemaStatus === "added") {
|
|
1929
2027
|
log.info(
|
|
1930
2028
|
"Optional chat schema block was appended to prisma/schema.prisma."
|
|
@@ -2147,7 +2245,7 @@ var NexTCLICommand = class _NexTCLICommand extends Command {
|
|
|
2147
2245
|
|
|
2148
2246
|
// src/cli.ts
|
|
2149
2247
|
var program = new NexTCLICommand();
|
|
2150
|
-
program.name("nextcli").description("Scaffold outsource-ready Next.js projects").version("0.
|
|
2248
|
+
program.name("nextcli").description("Scaffold outsource-ready Next.js projects").version("0.6.0");
|
|
2151
2249
|
registerCreateCommand(program);
|
|
2152
2250
|
registerAddCommand(program);
|
|
2153
2251
|
registerMigrateCommand(program);
|