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.
@@ -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,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 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' });
194
-
195
- // Ensure supabase assets exist (config + migrations) after link in case the CLI created/overrode files
196
- await ensureSupabaseAssets(projectPath);
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 configTomlPath = resolve(projectPath, 'supabase', 'config.toml');
199
- let projectId = null;
203
+ const assetsState = await ensureSupabaseAssets(projectPath, { required: true });
204
+ let projectId = extractProjectRefFromOutput(linkResult.stdout || '');
200
205
 
201
- if (await fs.pathExists(configTomlPath)) {
202
- const configToml = await fs.readFile(configTomlPath, 'utf8');
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
- '# NextBlock core',
257
- 'NEXT_PUBLIC_URL=http://localhost:3000',
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
- const r2Env = [
362
- '# Cloudflare R2',
363
- `NEXT_PUBLIC_R2_BASE_URL=${r2Keys.publicBaseUrl}`,
364
- `R2_ACCOUNT_ID=${r2Keys.accountId}`,
365
- `R2_BUCKET_NAME=${r2Keys.bucketName}`,
366
- `R2_ACCESS_KEY_ID=${r2Keys.accessKey}`,
367
- `R2_SECRET_ACCESS_KEY=${r2Keys.secretKey}`,
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
- if (canWriteEnv) {
372
- await fs.appendFile(envPath, r2Env);
373
- clack.note('Cloudflare R2 configuration saved!');
374
- } else {
375
- clack.note('Add the following R2 values to your existing .env:\n' + r2Env);
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
- const smtpEnv = [
423
- '# Email SMTP Configuration',
424
- `SMTP_HOST=${smtpKeys.host}`,
425
- `SMTP_PORT=${smtpKeys.port}`,
426
- `SMTP_USER=${smtpKeys.user}`,
427
- `SMTP_PASS=${smtpKeys.pass}`,
428
- `SMTP_FROM_EMAIL=${smtpKeys.fromEmail}`,
429
- `SMTP_FROM_NAME=${smtpKeys.fromName}`,
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
- if (canWriteEnv) {
434
- await fs.appendFile(envPath, smtpEnv);
435
- clack.note('SMTP configuration saved!');
436
- } else {
437
- clack.note('Add the following SMTP values to your existing .env:\n' + smtpEnv);
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
- const sources = [packageSupabaseDir].filter(Boolean);
674
- if (sources.length === 0) {
675
- 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
+ }
676
736
  }
677
- for (const sourceSupabaseDir of sources) {
678
- const sourceConfig = resolve(sourceSupabaseDir, 'config.toml');
679
- const destConfig = resolve(destSupabaseDir, 'config.toml');
680
- if ((await fs.pathExists(sourceConfig)) && !(await fs.pathExists(destConfig))) {
681
- await fs.copy(sourceConfig, destConfig, { overwrite: false });
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
- const sourceMigrations = resolve(sourceSupabaseDir, 'migrations');
685
- const destMigrations = resolve(destSupabaseDir, 'migrations');
686
- if (await fs.pathExists(sourceMigrations)) {
687
- await fs.copy(sourceMigrations, destMigrations, { overwrite: true, errorOnExist: false });
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 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) {
1198
1345
  const aliasLines = [];
1199
1346
 
1200
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.18",
3
+ "version": "0.2.20",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {