create-nextblock 0.2.19 → 0.2.21

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,11 @@ 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
 
191
+ const assetsState = await ensureSupabaseAssets(projectPath, { required: true, resetProjectRef: true });
192
192
  clack.note('Now, please select your NextBlock project when prompted.');
193
- await execa('npx', ['supabase', 'link'], { cwd: projectPath, stdio: 'inherit' });
193
+ await runSupabaseCli(['link'], { cwd: projectPath });
194
194
  if (process.stdin.isTTY) {
195
195
  try {
196
196
  process.stdin.setRawMode(false);
@@ -201,21 +201,14 @@ async function runSetupWizard(projectDir, projectName) {
201
201
  process.stdin.resume();
202
202
  }
203
203
 
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;
204
+ let projectId = await readSupabaseProjectRef(projectPath);
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,29 +718,80 @@ 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, resetProjectRef = 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
+ }
685
736
  }
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 });
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
+ }
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
+ }
761
+
762
+ if (resetProjectRef) {
763
+ const tempDir = resolve(destSupabaseDir, '.temp');
764
+ const projectRefPath = resolve(tempDir, 'project-ref');
765
+ if (await fs.pathExists(projectRefPath)) {
766
+ await fs.writeFile(projectRefPath, '');
691
767
  }
692
768
  }
769
+
770
+ if (required) {
771
+ if (!configCopied) {
772
+ throw new Error('Missing supabase/config.toml in the installed @nextblock-cms/db package.');
773
+ }
774
+ if (!migrationsCopied) {
775
+ throw new Error('Missing supabase/migrations in the installed @nextblock-cms/db package.');
776
+ }
777
+ }
778
+
779
+ return { migrationsCopied, configCopied, projectId: detectedProjectId };
693
780
  }
694
781
 
695
782
  async function resolvePackageSupabaseDir() {
696
783
  try {
697
784
  const pkgPath = require.resolve('@nextblock-cms/db/package.json');
698
785
  const pkgDir = dirname(pkgPath);
699
- const candidates = [resolve(pkgDir, 'supabase'), resolve(pkgDir, 'lib', 'supabase')];
786
+ const candidateSegments = [
787
+ 'supabase',
788
+ 'src/supabase',
789
+ 'dist/supabase',
790
+ 'dist/libs/db/supabase',
791
+ 'dist/lib/supabase',
792
+ 'lib/supabase',
793
+ ];
794
+ const candidates = candidateSegments.map((segment) => resolve(pkgDir, segment));
700
795
  for (const candidate of candidates) {
701
796
  if (await fs.pathExists(candidate)) {
702
797
  return candidate;
@@ -708,6 +803,27 @@ async function resolvePackageSupabaseDir() {
708
803
  return null;
709
804
  }
710
805
 
806
+ async function readSupabaseProjectRef(projectDir) {
807
+ const configTomlPath = resolve(projectDir, 'supabase', 'config.toml');
808
+ if (await fs.pathExists(configTomlPath)) {
809
+ const config = await fs.readFile(configTomlPath, 'utf8');
810
+ const configMatch = config.match(/project_id\s*=\s*"([^"]+)"/);
811
+ if (configMatch?.[1] && !configMatch[1].includes('env(')) {
812
+ return configMatch[1];
813
+ }
814
+ }
815
+
816
+ const projectRefPath = resolve(projectDir, 'supabase', '.temp', 'project-ref');
817
+ if (await fs.pathExists(projectRefPath)) {
818
+ const value = (await fs.readFile(projectRefPath, 'utf8')).trim();
819
+ if (/^[a-z0-9]{20,}$/i.test(value)) {
820
+ return value;
821
+ }
822
+ }
823
+
824
+ return null;
825
+ }
826
+
711
827
  async function ensureClientComponents(projectDir) {
712
828
  const relativePaths = [
713
829
  'components/env-var-warning.tsx',
@@ -1175,11 +1291,11 @@ async function initializeGit(projectDir) {
1175
1291
  }
1176
1292
  }
1177
1293
 
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,
1294
+ function runCommand(command, args, options = {}) {
1295
+ return new Promise((resolve, reject) => {
1296
+ const child = spawn(command, args, {
1297
+ stdio: 'inherit',
1298
+ shell: IS_WINDOWS,
1183
1299
  ...options,
1184
1300
  });
1185
1301
 
@@ -1194,10 +1310,33 @@ function runCommand(command, args, options = {}) {
1194
1310
  reject(new Error(`${command} exited with code ${code}`));
1195
1311
  }
1196
1312
  });
1197
- });
1198
- }
1199
-
1200
- function buildNextConfigContent(editorUtilNames) {
1313
+ });
1314
+ }
1315
+
1316
+ async function runSupabaseCli(args, options = {}) {
1317
+ const { cwd } = options;
1318
+ return new Promise((resolve, reject) => {
1319
+ const child = spawn('npx', ['supabase', ...args], {
1320
+ cwd,
1321
+ shell: IS_WINDOWS,
1322
+ stdio: 'inherit',
1323
+ });
1324
+
1325
+ child.on('error', (error) => {
1326
+ reject(error);
1327
+ });
1328
+
1329
+ child.on('close', (code) => {
1330
+ if (code === 0) {
1331
+ resolve();
1332
+ } else {
1333
+ reject(new Error(`supabase ${args.join(' ')} exited with code ${code}`));
1334
+ }
1335
+ });
1336
+ });
1337
+ }
1338
+
1339
+ function buildNextConfigContent(editorUtilNames) {
1201
1340
  const aliasLines = [];
1202
1341
 
1203
1342
  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.21",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {