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.
- package/bin/create-nextblock.js +224 -85
- 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,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
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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,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
|
-
|
|
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
|
+
}
|
|
685
736
|
}
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
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
|
|
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
|
|
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) {
|