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.
@@ -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 answers = await inquirer.prompt([
82
- {
83
- type: 'input',
84
- name: 'projectName',
85
- message: 'What is your project named?',
86
- default: DEFAULT_PROJECT_NAME,
87
- },
88
- ]);
89
-
90
- projectName = answers.projectName?.trim() || DEFAULT_PROJECT_NAME;
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 execa('npx', ['supabase', 'login'], { stdio: 'inherit', cwd: projectPath });
189
+ await runSupabaseCli(['login'], { cwd: projectPath });
191
190
 
192
191
  clack.note('Now, please select your NextBlock project when prompted.');
193
- await execa('npx', ['supabase', 'link'], { cwd: projectPath, stdio: 'inherit' });
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
- // Ensure supabase assets exist (config + migrations) after link in case the CLI created/overrode files
205
- await ensureSupabaseAssets(projectPath);
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 (await fs.pathExists(configTomlPath)) {
211
- const configToml = await fs.readFile(configTomlPath, 'utf8');
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
- '# NextBlock core',
266
- 'NEXT_PUBLIC_URL=http://localhost:3000',
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
- const r2Env = [
371
- '# Cloudflare R2',
372
- `NEXT_PUBLIC_R2_BASE_URL=${r2Keys.publicBaseUrl}`,
373
- `R2_ACCOUNT_ID=${r2Keys.accountId}`,
374
- `R2_BUCKET_NAME=${r2Keys.bucketName}`,
375
- `R2_ACCESS_KEY_ID=${r2Keys.accessKey}`,
376
- `R2_SECRET_ACCESS_KEY=${r2Keys.secretKey}`,
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
- if (canWriteEnv) {
381
- await fs.appendFile(envPath, r2Env);
382
- clack.note('Cloudflare R2 configuration saved!');
383
- } else {
384
- clack.note('Add the following R2 values to your existing .env:\n' + r2Env);
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
- const smtpEnv = [
432
- '# Email SMTP Configuration',
433
- `SMTP_HOST=${smtpKeys.host}`,
434
- `SMTP_PORT=${smtpKeys.port}`,
435
- `SMTP_USER=${smtpKeys.user}`,
436
- `SMTP_PASS=${smtpKeys.pass}`,
437
- `SMTP_FROM_EMAIL=${smtpKeys.fromEmail}`,
438
- `SMTP_FROM_NAME=${smtpKeys.fromName}`,
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
- if (canWriteEnv) {
443
- await fs.appendFile(envPath, smtpEnv);
444
- clack.note('SMTP configuration saved!');
445
- } else {
446
- clack.note('Add the following SMTP values to your existing .env:\n' + smtpEnv);
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
- const sources = [packageSupabaseDir].filter(Boolean);
683
- if (sources.length === 0) {
684
- clack.note('Warning: No supabase assets found in installed packages.');
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
- for (const sourceSupabaseDir of sources) {
687
- const sourceMigrations = resolve(sourceSupabaseDir, 'migrations');
688
- const destMigrations = resolve(destSupabaseDir, 'migrations');
689
- if (await fs.pathExists(sourceMigrations)) {
690
- await fs.copy(sourceMigrations, destMigrations, { overwrite: true, errorOnExist: false });
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 buildNextConfigContent(editorUtilNames) {
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-nextblock",
3
- "version": "0.2.19",
3
+ "version": "0.2.20",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {