create-nextblock 0.2.18 → 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 +235 -88
- 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,26 +186,29 @@ 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
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
192
|
+
const linkResult = await runSupabaseCli(['link'], { cwd: projectPath });
|
|
193
|
+
if (process.stdin.isTTY) {
|
|
194
|
+
try {
|
|
195
|
+
process.stdin.setRawMode(false);
|
|
196
|
+
} catch (_) {
|
|
197
|
+
// ignore
|
|
198
|
+
}
|
|
199
|
+
process.stdin.setEncoding('utf8');
|
|
200
|
+
process.stdin.resume();
|
|
201
|
+
}
|
|
197
202
|
|
|
198
|
-
const
|
|
199
|
-
let projectId =
|
|
203
|
+
const assetsState = await ensureSupabaseAssets(projectPath, { required: true });
|
|
204
|
+
let projectId = extractProjectRefFromOutput(linkResult.stdout || '');
|
|
200
205
|
|
|
201
|
-
if (
|
|
202
|
-
|
|
203
|
-
const projectMatch = configToml.match(/project_id\s*=\s*"([^"]+)"/);
|
|
204
|
-
if (projectMatch?.[1] && !projectMatch[1].includes('env(')) {
|
|
205
|
-
projectId = projectMatch[1];
|
|
206
|
-
}
|
|
206
|
+
if (!projectId && assetsState.projectId) {
|
|
207
|
+
projectId = assetsState.projectId;
|
|
207
208
|
}
|
|
208
209
|
|
|
209
210
|
if (!projectId) {
|
|
211
|
+
clack.note('I could not detect your Supabase project ref automatically.');
|
|
210
212
|
const manual = await clack.text({
|
|
211
213
|
message:
|
|
212
214
|
'Enter your Supabase project ref (from the Supabase dashboard URL or the link output, e.g., abcdefghijklmnopqrstu):',
|
|
@@ -218,6 +220,16 @@ async function runSetupWizard(projectDir, projectName) {
|
|
|
218
220
|
projectId = manual.trim();
|
|
219
221
|
}
|
|
220
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
|
+
|
|
221
233
|
clack.note('Please go to your Supabase project dashboard to get the following secrets.');
|
|
222
234
|
const supabaseKeys = await clack.group(
|
|
223
235
|
{
|
|
@@ -252,11 +264,18 @@ async function runSetupWizard(projectDir, projectName) {
|
|
|
252
264
|
const postgresUrl = `postgresql://${dbUser}:${dbPassword}@${dbHost}:5432/${dbName}`;
|
|
253
265
|
|
|
254
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
|
+
};
|
|
255
276
|
const envLines = [
|
|
256
|
-
|
|
257
|
-
'
|
|
258
|
-
'',
|
|
259
|
-
'# Supabase',
|
|
277
|
+
`NEXT_PUBLIC_URL=${siteUrl}`,
|
|
278
|
+
'# Vercel / Supabase',
|
|
260
279
|
`SUPABASE_PROJECT_ID=${projectId}`,
|
|
261
280
|
`NEXT_PUBLIC_SUPABASE_URL=${supabaseUrl}`,
|
|
262
281
|
`NEXT_PUBLIC_SUPABASE_ANON_KEY=${supabaseKeys.anonKey}`,
|
|
@@ -323,6 +342,15 @@ async function runSetupWizard(projectDir, projectName) {
|
|
|
323
342
|
if (clack.isCancel(setupR2)) {
|
|
324
343
|
handleWizardCancel('Setup cancelled.');
|
|
325
344
|
}
|
|
345
|
+
|
|
346
|
+
let r2Values = {
|
|
347
|
+
publicBaseUrl: '',
|
|
348
|
+
accountId: '',
|
|
349
|
+
bucketName: '',
|
|
350
|
+
accessKey: '',
|
|
351
|
+
secretKey: '',
|
|
352
|
+
};
|
|
353
|
+
|
|
326
354
|
if (setupR2) {
|
|
327
355
|
clack.note('I will open your browser to the R2 dashboard.\nYou need to create a bucket and an R2 API Token.');
|
|
328
356
|
await open('https://dash.cloudflare.com/?to=/:account/r2', { wait: false });
|
|
@@ -358,22 +386,29 @@ async function runSetupWizard(projectDir, projectName) {
|
|
|
358
386
|
{ onCancel: () => handleWizardCancel('Setup cancelled.') },
|
|
359
387
|
);
|
|
360
388
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
].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
|
+
}
|
|
370
397
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
}
|
|
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.');
|
|
377
412
|
}
|
|
378
413
|
|
|
379
414
|
const setupSMTP = await clack.confirm({
|
|
@@ -382,6 +417,16 @@ async function runSetupWizard(projectDir, projectName) {
|
|
|
382
417
|
if (clack.isCancel(setupSMTP)) {
|
|
383
418
|
handleWizardCancel('Setup cancelled.');
|
|
384
419
|
}
|
|
420
|
+
|
|
421
|
+
let smtpValues = {
|
|
422
|
+
host: '',
|
|
423
|
+
port: '',
|
|
424
|
+
user: '',
|
|
425
|
+
pass: '',
|
|
426
|
+
fromEmail: '',
|
|
427
|
+
fromName: '',
|
|
428
|
+
};
|
|
429
|
+
|
|
385
430
|
if (setupSMTP) {
|
|
386
431
|
const smtpKeys = await clack.group(
|
|
387
432
|
{
|
|
@@ -419,23 +464,31 @@ async function runSetupWizard(projectDir, projectName) {
|
|
|
419
464
|
{ onCancel: () => handleWizardCancel('Setup cancelled.') },
|
|
420
465
|
);
|
|
421
466
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
].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
|
+
}
|
|
432
476
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
}
|
|
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.');
|
|
439
492
|
}
|
|
440
493
|
|
|
441
494
|
clack.outro(`🎉 Your NextBlock project ${projectName ? `"${projectName}" ` : ''}is ready!\n\nNext step:\n npm run dev`);
|
|
@@ -665,28 +718,57 @@ SMTP_FROM_NAME=
|
|
|
665
718
|
await fs.writeFile(destination, placeholder);
|
|
666
719
|
}
|
|
667
720
|
|
|
668
|
-
async function ensureSupabaseAssets(projectDir) {
|
|
721
|
+
async function ensureSupabaseAssets(projectDir, options = {}) {
|
|
722
|
+
const { required = false } = options;
|
|
669
723
|
const destSupabaseDir = resolve(projectDir, 'supabase');
|
|
670
724
|
await fs.ensureDir(destSupabaseDir);
|
|
671
725
|
|
|
672
726
|
const packageSupabaseDir = await resolvePackageSupabaseDir();
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
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
|
+
}
|
|
676
736
|
}
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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];
|
|
682
752
|
}
|
|
753
|
+
}
|
|
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
|
+
}
|
|
683
761
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
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.');
|
|
688
768
|
}
|
|
689
769
|
}
|
|
770
|
+
|
|
771
|
+
return { migrationsCopied, configCopied, projectId: detectedProjectId };
|
|
690
772
|
}
|
|
691
773
|
|
|
692
774
|
async function resolvePackageSupabaseDir() {
|
|
@@ -1172,11 +1254,11 @@ async function initializeGit(projectDir) {
|
|
|
1172
1254
|
}
|
|
1173
1255
|
}
|
|
1174
1256
|
|
|
1175
|
-
function runCommand(command, args, options = {}) {
|
|
1176
|
-
return new Promise((resolve, reject) => {
|
|
1177
|
-
const child = spawn(command, args, {
|
|
1178
|
-
stdio: 'inherit',
|
|
1179
|
-
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,
|
|
1180
1262
|
...options,
|
|
1181
1263
|
});
|
|
1182
1264
|
|
|
@@ -1191,10 +1273,75 @@ function runCommand(command, args, options = {}) {
|
|
|
1191
1273
|
reject(new Error(`${command} exited with code ${code}`));
|
|
1192
1274
|
}
|
|
1193
1275
|
});
|
|
1194
|
-
});
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
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) {
|
|
1198
1345
|
const aliasLines = [];
|
|
1199
1346
|
|
|
1200
1347
|
for (const moduleName of UI_PROXY_MODULES) {
|