create-nextblock 0.2.24 → 0.2.26
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 +102 -88
- package/package.json +1 -1
- package/scripts/sync-template.js +51 -16
- package/templates/nextblock-template/.env.example +0 -24
package/bin/create-nextblock.js
CHANGED
|
@@ -177,18 +177,22 @@ async function runSetupWizard(projectDir, projectName) {
|
|
|
177
177
|
const projectPath = resolve(projectDir);
|
|
178
178
|
process.chdir(projectPath);
|
|
179
179
|
|
|
180
|
-
clack.intro('🚀 Welcome to the NextBlock setup wizard!');
|
|
181
|
-
|
|
180
|
+
clack.intro('🚀 Welcome to the NextBlock setup wizard!');
|
|
181
|
+
|
|
182
182
|
const supabaseDir = resolve(projectPath, 'supabase');
|
|
183
183
|
await fs.ensureDir(supabaseDir);
|
|
184
184
|
await resetSupabaseProjectRef(projectPath);
|
|
185
185
|
|
|
186
|
+
clack.note(
|
|
187
|
+
'Supabase Prerequisites:\n\nBefore proceeding, ensure you have a Supabase project ready.\n\nSupabase Cloud: Create a project at https://supabase.com/dashboard\n\nVercel Storage: If created via Vercel > Storage > Create Database, check your .env.local snippet on Vercel for the required keys.',
|
|
188
|
+
);
|
|
189
|
+
|
|
186
190
|
clack.note('Connecting to Supabase...');
|
|
187
191
|
clack.note('I will now open your browser to log into Supabase.');
|
|
188
192
|
await runSupabaseCli(['login'], { cwd: projectPath });
|
|
189
|
-
|
|
190
|
-
clack.note('Now, please select your NextBlock project when prompted.');
|
|
191
|
-
await runSupabaseCli(['link'], { cwd: projectPath });
|
|
193
|
+
|
|
194
|
+
clack.note('Now, please select your NextBlock project when prompted.');
|
|
195
|
+
await runSupabaseCli(['link'], { cwd: projectPath });
|
|
192
196
|
if (process.stdin.isTTY) {
|
|
193
197
|
try {
|
|
194
198
|
process.stdin.setRawMode(false);
|
|
@@ -199,10 +203,10 @@ async function runSetupWizard(projectDir, projectName) {
|
|
|
199
203
|
process.stdin.resume();
|
|
200
204
|
}
|
|
201
205
|
|
|
202
|
-
let projectId = await readSupabaseProjectRef(projectPath);
|
|
203
|
-
|
|
204
|
-
if (!projectId) {
|
|
205
|
-
clack.note('I could not detect your Supabase project ref automatically.');
|
|
206
|
+
let projectId = await readSupabaseProjectRef(projectPath);
|
|
207
|
+
|
|
208
|
+
if (!projectId) {
|
|
209
|
+
clack.note('I could not detect your Supabase project ref automatically.');
|
|
206
210
|
const manual = await clack.text({
|
|
207
211
|
message:
|
|
208
212
|
'Enter your Supabase project ref (from the Supabase dashboard URL or the link output, e.g., abcdefghijklmnopqrstu):',
|
|
@@ -212,8 +216,8 @@ async function runSetupWizard(projectDir, projectName) {
|
|
|
212
216
|
handleWizardCancel('Setup cancelled.');
|
|
213
217
|
}
|
|
214
218
|
projectId = manual.trim();
|
|
215
|
-
}
|
|
216
|
-
await ensureSupabaseAssets(projectPath, { required: true });
|
|
219
|
+
}
|
|
220
|
+
await ensureSupabaseAssets(projectPath, { required: true });
|
|
217
221
|
|
|
218
222
|
const siteUrlPrompt = await clack.text({
|
|
219
223
|
message: 'What is the public URL of your site? (NEXT_PUBLIC_URL)',
|
|
@@ -226,24 +230,27 @@ async function runSetupWizard(projectDir, projectName) {
|
|
|
226
230
|
const siteUrl = siteUrlPrompt.trim();
|
|
227
231
|
|
|
228
232
|
clack.note('Please go to your Supabase project dashboard to get the following secrets.');
|
|
229
|
-
const supabaseKeys = await clack.group(
|
|
230
|
-
{
|
|
231
|
-
dbPassword: () =>
|
|
232
|
-
clack.password({
|
|
233
|
-
message:
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
233
|
+
const supabaseKeys = await clack.group(
|
|
234
|
+
{
|
|
235
|
+
dbPassword: () =>
|
|
236
|
+
clack.password({
|
|
237
|
+
message:
|
|
238
|
+
'What is your Database Password? (Supabase: Database > Database Settings > Reset database password | Vercel: POSTGRES_PASSWORD)',
|
|
239
|
+
validate: (val) => (!val ? 'Password is required' : undefined),
|
|
240
|
+
}),
|
|
241
|
+
anonKey: () =>
|
|
242
|
+
clack.password({
|
|
243
|
+
message:
|
|
244
|
+
'What is your Project API Key (anon key)? (Supabase: Project Settings > API Keys > Project Legacy API Keys | Vercel: SUPABASE_ANON_KEY)',
|
|
245
|
+
validate: (val) => (!val ? 'Anon Key is required' : undefined),
|
|
246
|
+
}),
|
|
247
|
+
serviceKey: () =>
|
|
248
|
+
clack.password({
|
|
249
|
+
message:
|
|
250
|
+
'What is your Service Role Key (service_role key)? (Supabase: Project Settings > API Keys > Project Legacy API Keys | Vercel: SUPABASE_SERVICE_ROLE_KEY)',
|
|
251
|
+
validate: (val) => (!val ? 'Service Role Key is required' : undefined),
|
|
252
|
+
}),
|
|
253
|
+
},
|
|
247
254
|
{ onCancel: () => handleWizardCancel('Setup cancelled.') },
|
|
248
255
|
);
|
|
249
256
|
|
|
@@ -304,7 +311,7 @@ async function runSetupWizard(projectDir, projectName) {
|
|
|
304
311
|
|
|
305
312
|
clack.note('Setting up your database...');
|
|
306
313
|
const dbPushSpinner = clack.spinner();
|
|
307
|
-
dbPushSpinner.start('Pushing database schema... (~10 minutes, please keep this terminal open)');
|
|
314
|
+
dbPushSpinner.start('Pushing database schema... (~10-15 minutes, please keep this terminal open)');
|
|
308
315
|
try {
|
|
309
316
|
process.env.POSTGRES_URL = postgresUrl;
|
|
310
317
|
const migrationsDir = resolve(projectPath, 'supabase', 'migrations');
|
|
@@ -331,12 +338,15 @@ async function runSetupWizard(projectDir, projectName) {
|
|
|
331
338
|
}
|
|
332
339
|
}
|
|
333
340
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
}
|
|
341
|
+
clack.note(
|
|
342
|
+
'Optional Cloudflare R2 Setup:\nHave your Account ID, API token (Access + Secret), bucket name, and public bucket URL handy if you want media storage ready now.',
|
|
343
|
+
);
|
|
344
|
+
const setupR2 = await clack.confirm({
|
|
345
|
+
message: 'Do you want to set up Cloudflare R2 for media storage now? (Optional > populate .env keys)',
|
|
346
|
+
});
|
|
347
|
+
if (clack.isCancel(setupR2)) {
|
|
348
|
+
handleWizardCancel('Setup cancelled.');
|
|
349
|
+
}
|
|
340
350
|
|
|
341
351
|
let r2Values = {
|
|
342
352
|
publicBaseUrl: '',
|
|
@@ -350,33 +360,34 @@ async function runSetupWizard(projectDir, projectName) {
|
|
|
350
360
|
clack.note('I will open your browser to the R2 dashboard.\nYou need to create a bucket and an R2 API Token.');
|
|
351
361
|
await open('https://dash.cloudflare.com/?to=/:account/r2', { wait: false });
|
|
352
362
|
|
|
353
|
-
const r2Keys = await clack.group(
|
|
354
|
-
{
|
|
355
|
-
accountId: () =>
|
|
356
|
-
clack.text({
|
|
357
|
-
message: 'R2: Paste your Cloudflare Account ID:',
|
|
358
|
-
validate: (val) => (!val ? 'Account ID is required' : undefined),
|
|
359
|
-
}),
|
|
360
|
-
bucketName: () =>
|
|
363
|
+
const r2Keys = await clack.group(
|
|
364
|
+
{
|
|
365
|
+
accountId: () =>
|
|
366
|
+
clack.text({
|
|
367
|
+
message: 'R2: Paste your Cloudflare Account ID (Overview > Account Details - Bottom right):',
|
|
368
|
+
validate: (val) => (!val ? 'Account ID is required' : undefined),
|
|
369
|
+
}),
|
|
370
|
+
bucketName: () =>
|
|
361
371
|
clack.text({
|
|
362
372
|
message: 'R2: Paste your Bucket Name:',
|
|
363
373
|
validate: (val) => (!val ? 'Bucket name is required' : undefined),
|
|
364
374
|
}),
|
|
365
|
-
accessKey: () =>
|
|
366
|
-
clack.password({
|
|
367
|
-
message: 'R2: Paste your Access Key ID:',
|
|
368
|
-
validate: (val) => (!val ? 'Access Key ID is required' : undefined),
|
|
369
|
-
}),
|
|
375
|
+
accessKey: () =>
|
|
376
|
+
clack.password({
|
|
377
|
+
message: 'R2: Paste your Access Key ID (create API tokens):',
|
|
378
|
+
validate: (val) => (!val ? 'Access Key ID is required' : undefined),
|
|
379
|
+
}),
|
|
370
380
|
secretKey: () =>
|
|
371
381
|
clack.password({
|
|
372
382
|
message: 'R2: Paste your Secret Access Key:',
|
|
373
383
|
validate: (val) => (!val ? 'Secret Access Key is required' : undefined),
|
|
374
384
|
}),
|
|
375
|
-
publicBaseUrl: () =>
|
|
376
|
-
clack.text({
|
|
377
|
-
message:
|
|
378
|
-
|
|
379
|
-
|
|
385
|
+
publicBaseUrl: () =>
|
|
386
|
+
clack.text({
|
|
387
|
+
message:
|
|
388
|
+
'R2: Public Base URL (Bucket > Settings > Public Development URL-Enable: e.g., https://pub-xxx.r2.dev)',
|
|
389
|
+
validate: (val) => (!val ? 'Public base URL is required' : undefined),
|
|
390
|
+
}),
|
|
380
391
|
},
|
|
381
392
|
{ onCancel: () => handleWizardCancel('Setup cancelled.') },
|
|
382
393
|
);
|
|
@@ -406,7 +417,10 @@ async function runSetupWizard(projectDir, projectName) {
|
|
|
406
417
|
clack.note('Cloudflare R2 placeholders added to .env. Configure them later when ready.');
|
|
407
418
|
}
|
|
408
419
|
|
|
409
|
-
|
|
420
|
+
clack.note(
|
|
421
|
+
'Optional SMTP Setup:\nProvide the host, port, credentials, and from details for your email provider (e.g., Resend, Postmark) to send transactional emails immediately.',
|
|
422
|
+
);
|
|
423
|
+
const setupSMTP = await clack.confirm({
|
|
410
424
|
message: 'Do you want to set up an SMTP server for emails now? (Optional)',
|
|
411
425
|
});
|
|
412
426
|
if (clack.isCancel(setupSMTP)) {
|
|
@@ -713,8 +727,8 @@ SMTP_FROM_NAME=
|
|
|
713
727
|
await fs.writeFile(destination, placeholder);
|
|
714
728
|
}
|
|
715
729
|
|
|
716
|
-
async function ensureSupabaseAssets(projectDir, options = {}) {
|
|
717
|
-
const { required = false } = options;
|
|
730
|
+
async function ensureSupabaseAssets(projectDir, options = {}) {
|
|
731
|
+
const { required = false } = options;
|
|
718
732
|
const destSupabaseDir = resolve(projectDir, 'supabase');
|
|
719
733
|
await fs.ensureDir(destSupabaseDir);
|
|
720
734
|
|
|
@@ -731,14 +745,14 @@ async function ensureSupabaseAssets(projectDir, options = {}) {
|
|
|
731
745
|
}
|
|
732
746
|
}
|
|
733
747
|
|
|
734
|
-
let migrationsCopied = false;
|
|
735
|
-
let configCopied = false;
|
|
748
|
+
let migrationsCopied = false;
|
|
749
|
+
let configCopied = false;
|
|
736
750
|
|
|
737
751
|
const sourceConfigPath = resolve(packageSupabaseDir, 'config.toml');
|
|
738
752
|
const destinationConfigPath = resolve(destSupabaseDir, 'config.toml');
|
|
739
753
|
if (await fs.pathExists(sourceConfigPath)) {
|
|
740
|
-
await fs.copy(sourceConfigPath, destinationConfigPath, { overwrite: true, errorOnExist: false });
|
|
741
|
-
configCopied = true;
|
|
754
|
+
await fs.copy(sourceConfigPath, destinationConfigPath, { overwrite: true, errorOnExist: false });
|
|
755
|
+
configCopied = true;
|
|
742
756
|
}
|
|
743
757
|
|
|
744
758
|
const sourceMigrations = resolve(packageSupabaseDir, 'migrations');
|
|
@@ -748,11 +762,11 @@ async function ensureSupabaseAssets(projectDir, options = {}) {
|
|
|
748
762
|
migrationsCopied = true;
|
|
749
763
|
}
|
|
750
764
|
|
|
751
|
-
if (required) {
|
|
752
|
-
if (!configCopied) {
|
|
753
|
-
throw new Error(
|
|
754
|
-
`Missing supabase/config.toml in the installed @nextblock-cms/db package (checked ${packageSupabaseDir}).`,
|
|
755
|
-
);
|
|
765
|
+
if (required) {
|
|
766
|
+
if (!configCopied) {
|
|
767
|
+
throw new Error(
|
|
768
|
+
`Missing supabase/config.toml in the installed @nextblock-cms/db package (checked ${packageSupabaseDir}).`,
|
|
769
|
+
);
|
|
756
770
|
}
|
|
757
771
|
if (!migrationsCopied) {
|
|
758
772
|
throw new Error(
|
|
@@ -761,8 +775,8 @@ async function ensureSupabaseAssets(projectDir, options = {}) {
|
|
|
761
775
|
}
|
|
762
776
|
}
|
|
763
777
|
|
|
764
|
-
return { migrationsCopied, configCopied };
|
|
765
|
-
}
|
|
778
|
+
return { migrationsCopied, configCopied };
|
|
779
|
+
}
|
|
766
780
|
|
|
767
781
|
async function resolvePackageSupabaseDir(projectDir) {
|
|
768
782
|
const triedPaths = [];
|
|
@@ -821,26 +835,26 @@ async function resolvePackageSupabaseDir(projectDir) {
|
|
|
821
835
|
return { dir: null, triedPaths };
|
|
822
836
|
}
|
|
823
837
|
|
|
824
|
-
async function readSupabaseProjectRef(projectDir) {
|
|
825
|
-
const projectRefPath = resolve(projectDir, 'supabase', '.temp', 'project-ref');
|
|
826
|
-
if (await fs.pathExists(projectRefPath)) {
|
|
827
|
-
const value = (await fs.readFile(projectRefPath, 'utf8')).trim();
|
|
828
|
-
if (/^[a-z0-9]{20,}$/i.test(value)) {
|
|
829
|
-
return value;
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
return null;
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
async function resetSupabaseProjectRef(projectDir) {
|
|
837
|
-
const tempDir = resolve(projectDir, 'supabase', '.temp');
|
|
838
|
-
await fs.ensureDir(tempDir);
|
|
839
|
-
const projectRefPath = resolve(tempDir, 'project-ref');
|
|
840
|
-
if (await fs.pathExists(projectRefPath)) {
|
|
841
|
-
await fs.remove(projectRefPath);
|
|
842
|
-
}
|
|
843
|
-
}
|
|
838
|
+
async function readSupabaseProjectRef(projectDir) {
|
|
839
|
+
const projectRefPath = resolve(projectDir, 'supabase', '.temp', 'project-ref');
|
|
840
|
+
if (await fs.pathExists(projectRefPath)) {
|
|
841
|
+
const value = (await fs.readFile(projectRefPath, 'utf8')).trim();
|
|
842
|
+
if (/^[a-z0-9]{20,}$/i.test(value)) {
|
|
843
|
+
return value;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
return null;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
async function resetSupabaseProjectRef(projectDir) {
|
|
851
|
+
const tempDir = resolve(projectDir, 'supabase', '.temp');
|
|
852
|
+
await fs.ensureDir(tempDir);
|
|
853
|
+
const projectRefPath = resolve(tempDir, 'project-ref');
|
|
854
|
+
if (await fs.pathExists(projectRefPath)) {
|
|
855
|
+
await fs.remove(projectRefPath);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
844
858
|
|
|
845
859
|
async function ensureClientComponents(projectDir) {
|
|
846
860
|
const relativePaths = [
|
package/package.json
CHANGED
package/scripts/sync-template.js
CHANGED
|
@@ -36,16 +36,16 @@ const UI_PROXY_MODULES = [
|
|
|
36
36
|
'ui',
|
|
37
37
|
];
|
|
38
38
|
|
|
39
|
-
const IGNORED_SEGMENTS = new Set([
|
|
40
|
-
'node_modules',
|
|
41
|
-
'.git',
|
|
42
|
-
'.next',
|
|
43
|
-
'dist',
|
|
44
|
-
'tmp',
|
|
45
|
-
'coverage',
|
|
46
|
-
'backup',
|
|
47
|
-
'backups',
|
|
48
|
-
]);
|
|
39
|
+
const IGNORED_SEGMENTS = new Set([
|
|
40
|
+
'node_modules',
|
|
41
|
+
'.git',
|
|
42
|
+
'.next',
|
|
43
|
+
'dist',
|
|
44
|
+
'tmp',
|
|
45
|
+
'coverage',
|
|
46
|
+
'backup',
|
|
47
|
+
'backups',
|
|
48
|
+
]);
|
|
49
49
|
|
|
50
50
|
async function ensureTemplateSync() {
|
|
51
51
|
const sourceExists = await fs.pathExists(SOURCE_DIR);
|
|
@@ -79,9 +79,10 @@ async function ensureTemplateSync() {
|
|
|
79
79
|
},
|
|
80
80
|
});
|
|
81
81
|
|
|
82
|
-
await ensureEnvExample();
|
|
83
|
-
await
|
|
84
|
-
await
|
|
82
|
+
await ensureEnvExample();
|
|
83
|
+
await ensureTemplateGitignore();
|
|
84
|
+
await ensureGlobalStyles();
|
|
85
|
+
await ensureClientTranslations();
|
|
85
86
|
await sanitizeBlockEditorImports();
|
|
86
87
|
await sanitizeUiImports();
|
|
87
88
|
await ensureUiProxies();
|
|
@@ -91,7 +92,7 @@ async function ensureTemplateSync() {
|
|
|
91
92
|
console.log(chalk.green('Template sync complete.'));
|
|
92
93
|
}
|
|
93
94
|
|
|
94
|
-
async function ensureEnvExample() {
|
|
95
|
+
async function ensureEnvExample() {
|
|
95
96
|
const envTargets = [
|
|
96
97
|
resolve(REPO_ROOT, '.env.example'),
|
|
97
98
|
resolve(REPO_ROOT, '.env.exemple'),
|
|
@@ -117,8 +118,42 @@ NEXT_PUBLIC_URL=http://localhost:3000
|
|
|
117
118
|
`;
|
|
118
119
|
|
|
119
120
|
await fs.writeFile(destination, placeholder);
|
|
120
|
-
}
|
|
121
|
-
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function ensureTemplateGitignore() {
|
|
124
|
+
const destination = resolve(TARGET_DIR, '.gitignore');
|
|
125
|
+
const content = `.DS_Store
|
|
126
|
+
node_modules
|
|
127
|
+
dist
|
|
128
|
+
.next
|
|
129
|
+
out
|
|
130
|
+
build
|
|
131
|
+
coverage
|
|
132
|
+
*.log
|
|
133
|
+
logs
|
|
134
|
+
npm-debug.log*
|
|
135
|
+
yarn-debug.log*
|
|
136
|
+
yarn-error.log*
|
|
137
|
+
pnpm-debug.log*
|
|
138
|
+
|
|
139
|
+
.env
|
|
140
|
+
.env.*
|
|
141
|
+
.env.local
|
|
142
|
+
.env.development.local
|
|
143
|
+
.env.production.local
|
|
144
|
+
.env.test.local
|
|
145
|
+
|
|
146
|
+
.vscode
|
|
147
|
+
.idea
|
|
148
|
+
.swp
|
|
149
|
+
*.sw?
|
|
150
|
+
|
|
151
|
+
supabase/.temp
|
|
152
|
+
supabase/.branches
|
|
153
|
+
`;
|
|
154
|
+
await fs.outputFile(destination, content);
|
|
155
|
+
}
|
|
156
|
+
|
|
122
157
|
async function ensureGlobalStyles() {
|
|
123
158
|
const destination = resolve(TARGET_DIR, 'app/globals.css');
|
|
124
159
|
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
NEXT_PUBLIC_URL=
|
|
2
|
-
# Vercel / Supabase
|
|
3
|
-
SUPABASE_PROJECT_ID=
|
|
4
|
-
POSTGRES_URL=
|
|
5
|
-
NEXT_PUBLIC_SUPABASE_URL=
|
|
6
|
-
NEXT_PUBLIC_SUPABASE_ANON_KEY=
|
|
7
|
-
SUPABASE_SERVICE_ROLE_KEY=
|
|
8
|
-
|
|
9
|
-
# Cloudflare
|
|
10
|
-
NEXT_PUBLIC_R2_BASE_URL=
|
|
11
|
-
R2_ACCESS_KEY_ID=
|
|
12
|
-
R2_SECRET_ACCESS_KEY=
|
|
13
|
-
R2_BUCKET_NAME=
|
|
14
|
-
R2_ACCOUNT_ID=
|
|
15
|
-
|
|
16
|
-
REVALIDATE_SECRET_TOKEN=
|
|
17
|
-
|
|
18
|
-
# Email SMTP Configuration
|
|
19
|
-
SMTP_HOST=
|
|
20
|
-
SMTP_PORT=
|
|
21
|
-
SMTP_USER=
|
|
22
|
-
SMTP_PASS=
|
|
23
|
-
SMTP_FROM_EMAIL=
|
|
24
|
-
SMTP_FROM_NAME=
|