create-nextblock 0.2.19 → 0.2.20
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-nextblock.js +228 -84
- package/package.json +1 -1
package/bin/create-nextblock.js
CHANGED
|
@@ -8,7 +8,6 @@ import { fileURLToPath } from 'node:url';
|
|
|
8
8
|
import { createRequire } from 'node:module';
|
|
9
9
|
import { execa } from 'execa';
|
|
10
10
|
import { program } from 'commander';
|
|
11
|
-
import inquirer from 'inquirer';
|
|
12
11
|
import chalk from 'chalk';
|
|
13
12
|
import fs from 'fs-extra';
|
|
14
13
|
import open from 'open';
|
|
@@ -73,23 +72,23 @@ async function handleCommand(projectDirectory, options) {
|
|
|
73
72
|
try {
|
|
74
73
|
let projectName = projectDirectory;
|
|
75
74
|
|
|
76
|
-
if (!projectName) {
|
|
77
|
-
if (yes) {
|
|
78
|
-
projectName = DEFAULT_PROJECT_NAME;
|
|
79
|
-
console.log(chalk.blue(`Using default project name because --yes was provided: ${projectName}`));
|
|
80
|
-
} else {
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
projectName =
|
|
91
|
-
}
|
|
92
|
-
}
|
|
75
|
+
if (!projectName) {
|
|
76
|
+
if (yes) {
|
|
77
|
+
projectName = DEFAULT_PROJECT_NAME;
|
|
78
|
+
console.log(chalk.blue(`Using default project name because --yes was provided: ${projectName}`));
|
|
79
|
+
} else {
|
|
80
|
+
const projectPrompt = await clack.text({
|
|
81
|
+
message: 'What is your project named?',
|
|
82
|
+
initialValue: DEFAULT_PROJECT_NAME,
|
|
83
|
+
validate: (value) => (!value ? 'Project name is required' : undefined),
|
|
84
|
+
});
|
|
85
|
+
if (clack.isCancel(projectPrompt)) {
|
|
86
|
+
handleWizardCancel('Setup cancelled.');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
projectName = projectPrompt.trim() || DEFAULT_PROJECT_NAME;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
93
92
|
|
|
94
93
|
const projectDir = resolve(process.cwd(), projectName);
|
|
95
94
|
await ensureEmptyDirectory(projectDir);
|
|
@@ -187,10 +186,10 @@ async function runSetupWizard(projectDir, projectName) {
|
|
|
187
186
|
|
|
188
187
|
clack.note('Connecting to Supabase...');
|
|
189
188
|
clack.note('I will now open your browser to log into Supabase.');
|
|
190
|
-
await
|
|
189
|
+
await runSupabaseCli(['login'], { cwd: projectPath });
|
|
191
190
|
|
|
192
191
|
clack.note('Now, please select your NextBlock project when prompted.');
|
|
193
|
-
await
|
|
192
|
+
const linkResult = await runSupabaseCli(['link'], { cwd: projectPath });
|
|
194
193
|
if (process.stdin.isTTY) {
|
|
195
194
|
try {
|
|
196
195
|
process.stdin.setRawMode(false);
|
|
@@ -201,21 +200,15 @@ async function runSetupWizard(projectDir, projectName) {
|
|
|
201
200
|
process.stdin.resume();
|
|
202
201
|
}
|
|
203
202
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
const configTomlPath = resolve(projectPath, 'supabase', 'config.toml');
|
|
208
|
-
let projectId = null;
|
|
203
|
+
const assetsState = await ensureSupabaseAssets(projectPath, { required: true });
|
|
204
|
+
let projectId = extractProjectRefFromOutput(linkResult.stdout || '');
|
|
209
205
|
|
|
210
|
-
if (
|
|
211
|
-
|
|
212
|
-
const projectMatch = configToml.match(/project_id\s*=\s*"([^"]+)"/);
|
|
213
|
-
if (projectMatch?.[1] && !projectMatch[1].includes('env(')) {
|
|
214
|
-
projectId = projectMatch[1];
|
|
215
|
-
}
|
|
206
|
+
if (!projectId && assetsState.projectId) {
|
|
207
|
+
projectId = assetsState.projectId;
|
|
216
208
|
}
|
|
217
209
|
|
|
218
210
|
if (!projectId) {
|
|
211
|
+
clack.note('I could not detect your Supabase project ref automatically.');
|
|
219
212
|
const manual = await clack.text({
|
|
220
213
|
message:
|
|
221
214
|
'Enter your Supabase project ref (from the Supabase dashboard URL or the link output, e.g., abcdefghijklmnopqrstu):',
|
|
@@ -227,6 +220,16 @@ async function runSetupWizard(projectDir, projectName) {
|
|
|
227
220
|
projectId = manual.trim();
|
|
228
221
|
}
|
|
229
222
|
|
|
223
|
+
const siteUrlPrompt = await clack.text({
|
|
224
|
+
message: 'What is the public URL of your site? (NEXT_PUBLIC_URL)',
|
|
225
|
+
initialValue: 'http://localhost:3000',
|
|
226
|
+
validate: (val) => (!val ? 'URL is required' : undefined),
|
|
227
|
+
});
|
|
228
|
+
if (clack.isCancel(siteUrlPrompt)) {
|
|
229
|
+
handleWizardCancel('Setup cancelled.');
|
|
230
|
+
}
|
|
231
|
+
const siteUrl = siteUrlPrompt.trim();
|
|
232
|
+
|
|
230
233
|
clack.note('Please go to your Supabase project dashboard to get the following secrets.');
|
|
231
234
|
const supabaseKeys = await clack.group(
|
|
232
235
|
{
|
|
@@ -261,11 +264,18 @@ async function runSetupWizard(projectDir, projectName) {
|
|
|
261
264
|
const postgresUrl = `postgresql://${dbUser}:${dbPassword}@${dbHost}:5432/${dbName}`;
|
|
262
265
|
|
|
263
266
|
const envPath = resolve(projectPath, '.env');
|
|
267
|
+
const appendEnvBlock = async (label, lines) => {
|
|
268
|
+
const normalized = lines.join('\n');
|
|
269
|
+
const blockContent = normalized.endsWith('\n') ? normalized : `${normalized}\n`;
|
|
270
|
+
if (canWriteEnv) {
|
|
271
|
+
await fs.appendFile(envPath, blockContent);
|
|
272
|
+
} else {
|
|
273
|
+
clack.note(`Add the following ${label} values to your existing .env:\n${blockContent}`);
|
|
274
|
+
}
|
|
275
|
+
};
|
|
264
276
|
const envLines = [
|
|
265
|
-
|
|
266
|
-
'
|
|
267
|
-
'',
|
|
268
|
-
'# Supabase',
|
|
277
|
+
`NEXT_PUBLIC_URL=${siteUrl}`,
|
|
278
|
+
'# Vercel / Supabase',
|
|
269
279
|
`SUPABASE_PROJECT_ID=${projectId}`,
|
|
270
280
|
`NEXT_PUBLIC_SUPABASE_URL=${supabaseUrl}`,
|
|
271
281
|
`NEXT_PUBLIC_SUPABASE_ANON_KEY=${supabaseKeys.anonKey}`,
|
|
@@ -332,6 +342,15 @@ async function runSetupWizard(projectDir, projectName) {
|
|
|
332
342
|
if (clack.isCancel(setupR2)) {
|
|
333
343
|
handleWizardCancel('Setup cancelled.');
|
|
334
344
|
}
|
|
345
|
+
|
|
346
|
+
let r2Values = {
|
|
347
|
+
publicBaseUrl: '',
|
|
348
|
+
accountId: '',
|
|
349
|
+
bucketName: '',
|
|
350
|
+
accessKey: '',
|
|
351
|
+
secretKey: '',
|
|
352
|
+
};
|
|
353
|
+
|
|
335
354
|
if (setupR2) {
|
|
336
355
|
clack.note('I will open your browser to the R2 dashboard.\nYou need to create a bucket and an R2 API Token.');
|
|
337
356
|
await open('https://dash.cloudflare.com/?to=/:account/r2', { wait: false });
|
|
@@ -367,22 +386,29 @@ async function runSetupWizard(projectDir, projectName) {
|
|
|
367
386
|
{ onCancel: () => handleWizardCancel('Setup cancelled.') },
|
|
368
387
|
);
|
|
369
388
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
].join('\n');
|
|
389
|
+
r2Values = {
|
|
390
|
+
publicBaseUrl: r2Keys.publicBaseUrl,
|
|
391
|
+
accountId: r2Keys.accountId,
|
|
392
|
+
bucketName: r2Keys.bucketName,
|
|
393
|
+
accessKey: r2Keys.accessKey,
|
|
394
|
+
secretKey: r2Keys.secretKey,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
379
397
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
}
|
|
398
|
+
await appendEnvBlock('Cloudflare R2', [
|
|
399
|
+
'',
|
|
400
|
+
'# Cloudflare',
|
|
401
|
+
`NEXT_PUBLIC_R2_BASE_URL=${r2Values.publicBaseUrl}`,
|
|
402
|
+
`R2_ACCOUNT_ID=${r2Values.accountId}`,
|
|
403
|
+
`R2_BUCKET_NAME=${r2Values.bucketName}`,
|
|
404
|
+
`R2_ACCESS_KEY_ID=${r2Values.accessKey}`,
|
|
405
|
+
`R2_SECRET_ACCESS_KEY=${r2Values.secretKey}`,
|
|
406
|
+
'',
|
|
407
|
+
]);
|
|
408
|
+
if (setupR2) {
|
|
409
|
+
clack.note('Cloudflare R2 configuration saved!');
|
|
410
|
+
} else if (canWriteEnv) {
|
|
411
|
+
clack.note('Cloudflare R2 placeholders added to .env. Configure them later when ready.');
|
|
386
412
|
}
|
|
387
413
|
|
|
388
414
|
const setupSMTP = await clack.confirm({
|
|
@@ -391,6 +417,16 @@ async function runSetupWizard(projectDir, projectName) {
|
|
|
391
417
|
if (clack.isCancel(setupSMTP)) {
|
|
392
418
|
handleWizardCancel('Setup cancelled.');
|
|
393
419
|
}
|
|
420
|
+
|
|
421
|
+
let smtpValues = {
|
|
422
|
+
host: '',
|
|
423
|
+
port: '',
|
|
424
|
+
user: '',
|
|
425
|
+
pass: '',
|
|
426
|
+
fromEmail: '',
|
|
427
|
+
fromName: '',
|
|
428
|
+
};
|
|
429
|
+
|
|
394
430
|
if (setupSMTP) {
|
|
395
431
|
const smtpKeys = await clack.group(
|
|
396
432
|
{
|
|
@@ -428,23 +464,31 @@ async function runSetupWizard(projectDir, projectName) {
|
|
|
428
464
|
{ onCancel: () => handleWizardCancel('Setup cancelled.') },
|
|
429
465
|
);
|
|
430
466
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
].join('\n');
|
|
467
|
+
smtpValues = {
|
|
468
|
+
host: smtpKeys.host,
|
|
469
|
+
port: smtpKeys.port,
|
|
470
|
+
user: smtpKeys.user,
|
|
471
|
+
pass: smtpKeys.pass,
|
|
472
|
+
fromEmail: smtpKeys.fromEmail,
|
|
473
|
+
fromName: smtpKeys.fromName,
|
|
474
|
+
};
|
|
475
|
+
}
|
|
441
476
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
}
|
|
477
|
+
await appendEnvBlock('SMTP', [
|
|
478
|
+
'',
|
|
479
|
+
'# Email SMTP Configuration',
|
|
480
|
+
`SMTP_HOST=${smtpValues.host}`,
|
|
481
|
+
`SMTP_PORT=${smtpValues.port}`,
|
|
482
|
+
`SMTP_USER=${smtpValues.user}`,
|
|
483
|
+
`SMTP_PASS=${smtpValues.pass}`,
|
|
484
|
+
`SMTP_FROM_EMAIL=${smtpValues.fromEmail}`,
|
|
485
|
+
`SMTP_FROM_NAME=${smtpValues.fromName}`,
|
|
486
|
+
'',
|
|
487
|
+
]);
|
|
488
|
+
if (setupSMTP) {
|
|
489
|
+
clack.note('SMTP configuration saved!');
|
|
490
|
+
} else if (canWriteEnv) {
|
|
491
|
+
clack.note('SMTP placeholders added to .env. Configure them later when ready.');
|
|
448
492
|
}
|
|
449
493
|
|
|
450
494
|
clack.outro(`🎉 Your NextBlock project ${projectName ? `"${projectName}" ` : ''}is ready!\n\nNext step:\n npm run dev`);
|
|
@@ -674,22 +718,57 @@ SMTP_FROM_NAME=
|
|
|
674
718
|
await fs.writeFile(destination, placeholder);
|
|
675
719
|
}
|
|
676
720
|
|
|
677
|
-
async function ensureSupabaseAssets(projectDir) {
|
|
721
|
+
async function ensureSupabaseAssets(projectDir, options = {}) {
|
|
722
|
+
const { required = false } = options;
|
|
678
723
|
const destSupabaseDir = resolve(projectDir, 'supabase');
|
|
679
724
|
await fs.ensureDir(destSupabaseDir);
|
|
680
725
|
|
|
681
726
|
const packageSupabaseDir = await resolvePackageSupabaseDir();
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
727
|
+
if (!packageSupabaseDir) {
|
|
728
|
+
const message =
|
|
729
|
+
'Unable to locate supabase assets in @nextblock-cms/db. Please reinstall dependencies and try again.';
|
|
730
|
+
if (required) {
|
|
731
|
+
throw new Error(message);
|
|
732
|
+
} else {
|
|
733
|
+
clack.note(message);
|
|
734
|
+
return { migrationsCopied: false, configCopied: false, projectId: null };
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
let migrationsCopied = false;
|
|
739
|
+
let configCopied = false;
|
|
740
|
+
let detectedProjectId = null;
|
|
741
|
+
|
|
742
|
+
const sourceConfigPath = resolve(packageSupabaseDir, 'config.toml');
|
|
743
|
+
const destinationConfigPath = resolve(destSupabaseDir, 'config.toml');
|
|
744
|
+
if (await fs.pathExists(sourceConfigPath)) {
|
|
745
|
+
await fs.copy(sourceConfigPath, destinationConfigPath, { overwrite: true, errorOnExist: false });
|
|
746
|
+
configCopied = true;
|
|
747
|
+
|
|
748
|
+
const sourceConfig = await fs.readFile(sourceConfigPath, 'utf8');
|
|
749
|
+
const configMatch = sourceConfig.match(/project_id\s*=\s*"([^"]+)"/);
|
|
750
|
+
if (configMatch?.[1] && !configMatch[1].includes('env(')) {
|
|
751
|
+
detectedProjectId = configMatch[1];
|
|
752
|
+
}
|
|
685
753
|
}
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
754
|
+
|
|
755
|
+
const sourceMigrations = resolve(packageSupabaseDir, 'migrations');
|
|
756
|
+
const destMigrations = resolve(destSupabaseDir, 'migrations');
|
|
757
|
+
if (await fs.pathExists(sourceMigrations)) {
|
|
758
|
+
await fs.copy(sourceMigrations, destMigrations, { overwrite: true, errorOnExist: false });
|
|
759
|
+
migrationsCopied = true;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
if (required) {
|
|
763
|
+
if (!configCopied) {
|
|
764
|
+
throw new Error('Missing supabase/config.toml in the installed @nextblock-cms/db package.');
|
|
765
|
+
}
|
|
766
|
+
if (!migrationsCopied) {
|
|
767
|
+
throw new Error('Missing supabase/migrations in the installed @nextblock-cms/db package.');
|
|
691
768
|
}
|
|
692
769
|
}
|
|
770
|
+
|
|
771
|
+
return { migrationsCopied, configCopied, projectId: detectedProjectId };
|
|
693
772
|
}
|
|
694
773
|
|
|
695
774
|
async function resolvePackageSupabaseDir() {
|
|
@@ -1175,11 +1254,11 @@ async function initializeGit(projectDir) {
|
|
|
1175
1254
|
}
|
|
1176
1255
|
}
|
|
1177
1256
|
|
|
1178
|
-
function runCommand(command, args, options = {}) {
|
|
1179
|
-
return new Promise((resolve, reject) => {
|
|
1180
|
-
const child = spawn(command, args, {
|
|
1181
|
-
stdio: 'inherit',
|
|
1182
|
-
shell: IS_WINDOWS,
|
|
1257
|
+
function runCommand(command, args, options = {}) {
|
|
1258
|
+
return new Promise((resolve, reject) => {
|
|
1259
|
+
const child = spawn(command, args, {
|
|
1260
|
+
stdio: 'inherit',
|
|
1261
|
+
shell: IS_WINDOWS,
|
|
1183
1262
|
...options,
|
|
1184
1263
|
});
|
|
1185
1264
|
|
|
@@ -1194,10 +1273,75 @@ function runCommand(command, args, options = {}) {
|
|
|
1194
1273
|
reject(new Error(`${command} exited with code ${code}`));
|
|
1195
1274
|
}
|
|
1196
1275
|
});
|
|
1197
|
-
});
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
|
-
function
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
async function runSupabaseCli(args, options = {}) {
|
|
1280
|
+
const { cwd } = options;
|
|
1281
|
+
return new Promise((resolve, reject) => {
|
|
1282
|
+
const child = spawn('npx', ['supabase', ...args], {
|
|
1283
|
+
cwd,
|
|
1284
|
+
shell: IS_WINDOWS,
|
|
1285
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
1286
|
+
});
|
|
1287
|
+
|
|
1288
|
+
let stdout = '';
|
|
1289
|
+
let stderr = '';
|
|
1290
|
+
|
|
1291
|
+
child.stdout?.on('data', (chunk) => {
|
|
1292
|
+
const text = chunk.toString();
|
|
1293
|
+
stdout += text;
|
|
1294
|
+
process.stdout.write(text);
|
|
1295
|
+
});
|
|
1296
|
+
|
|
1297
|
+
child.stderr?.on('data', (chunk) => {
|
|
1298
|
+
const text = chunk.toString();
|
|
1299
|
+
stderr += text;
|
|
1300
|
+
process.stderr.write(text);
|
|
1301
|
+
});
|
|
1302
|
+
|
|
1303
|
+
child.on('error', (error) => {
|
|
1304
|
+
reject(error);
|
|
1305
|
+
});
|
|
1306
|
+
|
|
1307
|
+
child.on('close', (code) => {
|
|
1308
|
+
if (code === 0) {
|
|
1309
|
+
resolve({ stdout, stderr });
|
|
1310
|
+
} else {
|
|
1311
|
+
reject(new Error(`supabase ${args.join(' ')} exited with code ${code}`));
|
|
1312
|
+
}
|
|
1313
|
+
});
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
function extractProjectRefFromOutput(output) {
|
|
1318
|
+
const sanitized = stripAnsiCodes(output);
|
|
1319
|
+
const regexes = [
|
|
1320
|
+
/project(?:\s+ref(?:erence)?)?\s*[:=]\s*([a-z0-9]{20,})/i,
|
|
1321
|
+
/project_ref\s*[:=]\s*([a-z0-9]{20,})/i,
|
|
1322
|
+
/\(ref:\s*([a-z0-9]{20,})\s*\)/i,
|
|
1323
|
+
];
|
|
1324
|
+
|
|
1325
|
+
for (const regex of regexes) {
|
|
1326
|
+
const match = sanitized.match(regex);
|
|
1327
|
+
if (match?.[1]) {
|
|
1328
|
+
return match[1];
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
const fallback = sanitized.match(/\b[a-z0-9]{20,}\b/gi);
|
|
1333
|
+
if (fallback?.length === 1) {
|
|
1334
|
+
return fallback[0];
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
return null;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
function stripAnsiCodes(input = '') {
|
|
1341
|
+
return input.replace(/\x1b\[[0-9;]*m/g, '').replace(/\u001b\[[0-9;]*[A-Za-z]/g, '');
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
function buildNextConfigContent(editorUtilNames) {
|
|
1201
1345
|
const aliasLines = [];
|
|
1202
1346
|
|
|
1203
1347
|
for (const moduleName of UI_PROXY_MODULES) {
|