create-nextblock 0.1.0 → 0.2.1

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.
Files changed (42) hide show
  1. package/bin/create-nextblock.js +1193 -920
  2. package/package.json +6 -2
  3. package/scripts/sync-template.js +279 -276
  4. package/templates/nextblock-template/.env.example +1 -14
  5. package/templates/nextblock-template/README.md +1 -1
  6. package/templates/nextblock-template/app/[slug]/PageClientContent.tsx +67 -40
  7. package/templates/nextblock-template/app/[slug]/page.tsx +45 -10
  8. package/templates/nextblock-template/app/[slug]/page.utils.ts +92 -45
  9. package/templates/nextblock-template/app/api/revalidate/route.ts +15 -15
  10. package/templates/nextblock-template/app/{blog → article}/[slug]/PostClientContent.tsx +45 -43
  11. package/templates/nextblock-template/app/{blog → article}/[slug]/page.tsx +108 -98
  12. package/templates/nextblock-template/app/{blog → article}/[slug]/page.utils.ts +10 -3
  13. package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +25 -19
  14. package/templates/nextblock-template/app/cms/blocks/actions.ts +1 -1
  15. package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +1 -1
  16. package/templates/nextblock-template/app/cms/posts/actions.ts +47 -44
  17. package/templates/nextblock-template/app/cms/posts/page.tsx +2 -2
  18. package/templates/nextblock-template/app/cms/settings/languages/actions.ts +16 -15
  19. package/templates/nextblock-template/app/layout.tsx +9 -9
  20. package/templates/nextblock-template/app/lib/sitemap-utils.ts +52 -52
  21. package/templates/nextblock-template/app/sitemap.xml/route.ts +2 -2
  22. package/templates/nextblock-template/components/ResponsiveNav.tsx +22 -16
  23. package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -7
  24. package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +25 -26
  25. package/templates/nextblock-template/package.json +1 -1
  26. package/templates/nextblock-template/proxy.ts +4 -4
  27. package/templates/nextblock-template/public/images/NBcover.webp +0 -0
  28. package/templates/nextblock-template/public/images/developer.webp +0 -0
  29. package/templates/nextblock-template/public/images/nextblock-logo-small.webp +0 -0
  30. package/templates/nextblock-template/public/images/nx-graph.webp +0 -0
  31. package/templates/nextblock-template/public/images/programmer-upscaled.webp +0 -0
  32. package/templates/nextblock-template/scripts/backup.js +142 -47
  33. package/templates/nextblock-template/scripts/restore-working.js +102 -0
  34. package/templates/nextblock-template/scripts/restore.js +434 -0
  35. package/templates/nextblock-template/app/blog/page.tsx +0 -77
  36. package/templates/nextblock-template/backup/backup_2025-06-19.sql +0 -8057
  37. package/templates/nextblock-template/backup/backup_2025-06-20.sql +0 -8159
  38. package/templates/nextblock-template/backup/backup_2025-07-08.sql +0 -8411
  39. package/templates/nextblock-template/backup/backup_2025-07-09.sql +0 -8442
  40. package/templates/nextblock-template/backup/backup_2025-07-10.sql +0 -8442
  41. package/templates/nextblock-template/backup/backup_2025-10-01.sql +0 -8803
  42. package/templates/nextblock-template/backup/backup_2025-10-02.sql +0 -9749
@@ -1,166 +1,176 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import * as clack from '@clack/prompts';
3
4
  import { spawn } from 'node:child_process';
5
+ import crypto from 'node:crypto';
4
6
  import { dirname, resolve, relative, sep, basename } from 'node:path';
5
7
  import { fileURLToPath } from 'node:url';
8
+ import { execa } from 'execa';
6
9
  import { program } from 'commander';
7
10
  import inquirer from 'inquirer';
8
11
  import chalk from 'chalk';
9
12
  import fs from 'fs-extra';
10
-
11
- const DEFAULT_PROJECT_NAME = 'nextblock-cms';
12
- const __filename = fileURLToPath(import.meta.url);
13
- const __dirname = dirname(__filename);
14
- const TEMPLATE_DIR = resolve(__dirname, '../templates/nextblock-template');
15
- const REPO_ROOT = resolve(__dirname, '../../..');
16
- const EDITOR_UTILS_SOURCE_DIR = resolve(REPO_ROOT, 'libs/editor/src/lib/utils');
17
- const IS_WINDOWS = process.platform === 'win32';
18
-
19
- const UI_PROXY_MODULES = [
20
- 'avatar',
21
- 'badge',
22
- 'button',
23
- 'card',
24
- 'checkbox',
25
- 'ColorPicker',
26
- 'ConfirmationDialog',
27
- 'CustomSelectWithInput',
28
- 'dialog',
29
- 'dropdown-menu',
30
- 'input',
31
- 'label',
32
- 'popover',
33
- 'progress',
34
- 'select',
35
- 'separator',
36
- 'Skeleton',
37
- 'table',
38
- 'textarea',
39
- 'tooltip',
40
- 'ui',
41
- ];
42
-
43
- const PACKAGE_VERSION_SOURCES = {
44
- '@nextblock-cms/ui': resolve(REPO_ROOT, 'libs/ui/package.json'),
45
- '@nextblock-cms/utils': resolve(REPO_ROOT, 'libs/utils/package.json'),
46
- '@nextblock-cms/db': resolve(REPO_ROOT, 'libs/db/package.json'),
47
- '@nextblock-cms/editor': resolve(REPO_ROOT, 'libs/editor/package.json'),
48
- '@nextblock-cms/sdk': resolve(REPO_ROOT, 'libs/sdk/package.json'),
49
- };
50
-
51
- program
52
- .name('create-nextblock')
53
- .description('Bootstrap a NextBlock CMS project')
54
- .argument('[project-directory]', 'The name of the project directory to create')
55
- .option('--skip-install', 'Skip installing dependencies')
56
- .option('-y, --yes', 'Skip all interactive prompts and use defaults')
57
- .action(handleCommand);
58
-
59
- await program.parseAsync(process.argv).catch((error) => {
60
- console.error(chalk.red(error instanceof Error ? error.message : String(error)));
61
- process.exit(1);
62
- });
63
-
64
- async function handleCommand(projectDirectory, options) {
65
- const { skipInstall, yes } = options;
66
-
67
- try {
68
- let projectName = projectDirectory;
69
-
70
- if (!projectName) {
71
- if (yes) {
72
- projectName = DEFAULT_PROJECT_NAME;
73
- console.log(chalk.blue(`Using default project name because --yes was provided: ${projectName}`));
74
- } else {
75
- const answers = await inquirer.prompt([
76
- {
77
- type: 'input',
78
- name: 'projectName',
79
- message: 'What is your project named?',
80
- default: DEFAULT_PROJECT_NAME,
81
- },
82
- ]);
83
-
84
- projectName = answers.projectName?.trim() || DEFAULT_PROJECT_NAME;
85
- }
86
- }
87
-
88
- const projectDir = resolve(process.cwd(), projectName);
89
- await ensureEmptyDirectory(projectDir);
90
-
91
- console.log(chalk.green(`Project name: ${projectName}`));
92
- console.log(
93
- chalk.blue(
94
- `Options: skipInstall=${skipInstall ? 'true' : 'false'}, yes=${yes ? 'true' : 'false'}`,
95
- ),
96
- );
97
-
98
- console.log(chalk.blue('Copying project files...'));
99
- await copyTemplateTo(projectDir);
100
- console.log(chalk.green('Template copied successfully.'));
101
-
102
- await removeBackups(projectDir);
103
-
104
- await ensureClientComponents(projectDir);
105
- console.log(chalk.green('Client component directives applied.'));
106
-
107
- await ensureClientProviders(projectDir);
108
- console.log(chalk.green('Client provider wrappers configured.'));
109
-
110
- await sanitizeBlockEditorImports(projectDir);
111
- console.log(chalk.green('Block editor imports sanitized.'));
112
-
113
- await sanitizeUiImports(projectDir);
114
- console.log(chalk.green('UI component imports normalized.'));
115
-
116
- await ensureUiProxies(projectDir);
117
- console.log(chalk.green('UI proxy modules generated.'));
118
-
119
- const editorUtilNames = await ensureEditorUtils(projectDir);
120
- if (editorUtilNames.length > 0) {
121
- console.log(chalk.green('Editor utility shims generated.'));
122
- }
123
-
13
+ import open from 'open';
14
+
15
+ const DEFAULT_PROJECT_NAME = 'nextblock-cms';
16
+ const __filename = fileURLToPath(import.meta.url);
17
+ const __dirname = dirname(__filename);
18
+ const TEMPLATE_DIR = resolve(__dirname, '../templates/nextblock-template');
19
+ const REPO_ROOT = resolve(__dirname, '../../..');
20
+ const EDITOR_UTILS_SOURCE_DIR = resolve(REPO_ROOT, 'libs/editor/src/lib/utils');
21
+ const IS_WINDOWS = process.platform === 'win32';
22
+
23
+ const UI_PROXY_MODULES = [
24
+ 'avatar',
25
+ 'badge',
26
+ 'button',
27
+ 'card',
28
+ 'checkbox',
29
+ 'ColorPicker',
30
+ 'ConfirmationDialog',
31
+ 'CustomSelectWithInput',
32
+ 'dialog',
33
+ 'dropdown-menu',
34
+ 'input',
35
+ 'label',
36
+ 'popover',
37
+ 'progress',
38
+ 'select',
39
+ 'separator',
40
+ 'Skeleton',
41
+ 'table',
42
+ 'textarea',
43
+ 'tooltip',
44
+ 'ui',
45
+ ];
46
+
47
+ const PACKAGE_VERSION_SOURCES = {
48
+ '@nextblock-cms/ui': resolve(REPO_ROOT, 'libs/ui/package.json'),
49
+ '@nextblock-cms/utils': resolve(REPO_ROOT, 'libs/utils/package.json'),
50
+ '@nextblock-cms/db': resolve(REPO_ROOT, 'libs/db/package.json'),
51
+ '@nextblock-cms/editor': resolve(REPO_ROOT, 'libs/editor/package.json'),
52
+ '@nextblock-cms/sdk': resolve(REPO_ROOT, 'libs/sdk/package.json'),
53
+ };
54
+
55
+ program
56
+ .name('create-nextblock')
57
+ .description('Bootstrap a NextBlock CMS project')
58
+ .argument('[project-directory]', 'The name of the project directory to create')
59
+ .option('--skip-install', 'Skip installing dependencies')
60
+ .option('-y, --yes', 'Skip all interactive prompts and use defaults')
61
+ .action(handleCommand);
62
+
63
+ await program.parseAsync(process.argv).catch((error) => {
64
+ console.error(chalk.red(error instanceof Error ? error.message : String(error)));
65
+ process.exit(1);
66
+ });
67
+
68
+ async function handleCommand(projectDirectory, options) {
69
+ const { skipInstall, yes } = options;
70
+
71
+ try {
72
+ let projectName = projectDirectory;
73
+
74
+ if (!projectName) {
75
+ if (yes) {
76
+ projectName = DEFAULT_PROJECT_NAME;
77
+ console.log(chalk.blue(`Using default project name because --yes was provided: ${projectName}`));
78
+ } else {
79
+ const answers = await inquirer.prompt([
80
+ {
81
+ type: 'input',
82
+ name: 'projectName',
83
+ message: 'What is your project named?',
84
+ default: DEFAULT_PROJECT_NAME,
85
+ },
86
+ ]);
87
+
88
+ projectName = answers.projectName?.trim() || DEFAULT_PROJECT_NAME;
89
+ }
90
+ }
91
+
92
+ const projectDir = resolve(process.cwd(), projectName);
93
+ await ensureEmptyDirectory(projectDir);
94
+
95
+ console.log(chalk.green(`Project name: ${projectName}`));
96
+ console.log(
97
+ chalk.blue(
98
+ `Options: skipInstall=${skipInstall ? 'true' : 'false'}, yes=${yes ? 'true' : 'false'}`,
99
+ ),
100
+ );
101
+
102
+ console.log(chalk.blue('Copying project files...'));
103
+ await copyTemplateTo(projectDir);
104
+ console.log(chalk.green('Template copied successfully.'));
105
+
106
+ await removeBackups(projectDir);
107
+
108
+ await ensureClientComponents(projectDir);
109
+ console.log(chalk.green('Client component directives applied.'));
110
+
111
+ await ensureClientProviders(projectDir);
112
+ console.log(chalk.green('Client provider wrappers configured.'));
113
+
114
+ await sanitizeBlockEditorImports(projectDir);
115
+ console.log(chalk.green('Block editor imports sanitized.'));
116
+
117
+ await sanitizeUiImports(projectDir);
118
+ console.log(chalk.green('UI component imports normalized.'));
119
+
120
+ await ensureUiProxies(projectDir);
121
+ console.log(chalk.green('UI proxy modules generated.'));
122
+
123
+ const editorUtilNames = await ensureEditorUtils(projectDir);
124
+ if (editorUtilNames.length > 0) {
125
+ console.log(chalk.green('Editor utility shims generated.'));
126
+ }
127
+
124
128
  await ensureGitignore(projectDir);
125
129
  console.log(chalk.green('.gitignore ready.'));
126
130
 
127
131
  await ensureEnvExample(projectDir);
128
132
  console.log(chalk.green('.env.example ready.'));
129
133
 
130
- await sanitizeLayout(projectDir);
131
- console.log(chalk.green('Global styles configured.'));
132
-
133
- await sanitizeTailwindConfig(projectDir);
134
- console.log(chalk.green('tailwind.config.js sanitized.'));
135
-
136
- await normalizeTsconfig(projectDir);
137
- console.log(chalk.green('tsconfig.json normalized.'));
138
-
139
- await sanitizeNextConfig(projectDir, editorUtilNames);
140
- console.log(chalk.green('next.config.js sanitized.'));
141
-
142
- await transformPackageJson(projectDir);
143
- console.log(chalk.green('Dependencies updated for public packages.'));
144
-
145
- if (!skipInstall) {
146
- await installDependencies(projectDir);
134
+ if (!yes) {
135
+ await runSetupWizard(projectDir, projectName);
147
136
  } else {
148
- console.log(chalk.yellow('Skipping dependency installation.'));
137
+ console.log(chalk.yellow('Skipping interactive setup wizard because --yes was provided.'));
149
138
  }
150
139
 
151
- await initializeGit(projectDir);
152
- console.log(chalk.green('Initialized a new Git repository.'));
153
-
154
- console.log(
155
- chalk.green(
156
- `\nSuccess! Your NextBlock CMS project "${projectName}" is ready.\n\n` +
157
- 'Next steps:\n' +
158
- `1. \`cd ${projectName}\`\n` +
159
- '2. Copy your existing `.env` file or rename `.env.example` to `.env` and fill in your credentials.\n' +
160
- '3. `npm run dev` to start the development server.\n\n' +
161
- 'Happy building!',
162
- ),
163
- );
140
+ await sanitizeLayout(projectDir);
141
+ console.log(chalk.green('Global styles configured.'));
142
+
143
+ await sanitizeTailwindConfig(projectDir);
144
+ console.log(chalk.green('tailwind.config.js sanitized.'));
145
+
146
+ await normalizeTsconfig(projectDir);
147
+ console.log(chalk.green('tsconfig.json normalized.'));
148
+
149
+ await sanitizeNextConfig(projectDir, editorUtilNames);
150
+ console.log(chalk.green('next.config.js sanitized.'));
151
+
152
+ await transformPackageJson(projectDir);
153
+ console.log(chalk.green('Dependencies updated for public packages.'));
154
+
155
+ if (!skipInstall) {
156
+ await installDependencies(projectDir);
157
+ } else {
158
+ console.log(chalk.yellow('Skipping dependency installation.'));
159
+ }
160
+
161
+ await initializeGit(projectDir);
162
+ console.log(chalk.green('Initialized a new Git repository.'));
163
+
164
+ console.log(
165
+ chalk.green(
166
+ `\nSuccess! Your NextBlock CMS project "${projectName}" is ready.\n\n` +
167
+ 'Next steps:\n' +
168
+ `1. \`cd ${projectName}\`\n` +
169
+ '2. Copy your existing `.env` file or rename `.env.example` to `.env` and fill in your credentials.\n' +
170
+ '3. `npm run dev` to start the development server.\n\n' +
171
+ 'Happy building!',
172
+ ),
173
+ );
164
174
  } catch (error) {
165
175
  console.error(
166
176
  chalk.red(error instanceof Error ? error.message : 'An unexpected error occurred'),
@@ -169,825 +179,1088 @@ async function handleCommand(projectDirectory, options) {
169
179
  }
170
180
  }
171
181
 
172
- async function ensureEmptyDirectory(projectDir) {
173
- const exists = await fs.pathExists(projectDir);
174
- if (!exists) {
175
- return;
176
- }
177
-
178
- const contents = await fs.readdir(projectDir);
179
- if (contents.length > 0) {
180
- throw new Error(`Directory "${projectDir}" already exists and is not empty.`);
181
- }
182
- }
182
+ async function runSetupWizard(projectDir, projectName) {
183
+ const projectPath = resolve(projectDir);
184
+ process.chdir(projectPath);
183
185
 
184
- async function copyTemplateTo(projectDir) {
185
- const templateExists = await fs.pathExists(TEMPLATE_DIR);
186
- if (!templateExists) {
187
- throw new Error(
188
- `Template directory not found at ${TEMPLATE_DIR}. Run "npm run sync:create-nextblock" to populate it.`,
189
- );
190
- }
186
+ clack.intro('🚀 Welcome to the NextBlock setup wizard!');
191
187
 
192
- await fs.ensureDir(projectDir);
188
+ clack.note('Connecting to Supabase...');
189
+ clack.note('I will now open your browser to log into Supabase.');
190
+ await execa('npx', ['supabase', 'login'], { stdio: 'inherit', cwd: projectPath });
193
191
 
194
- await fs.copy(TEMPLATE_DIR, projectDir, {
195
- dereference: true,
196
- filter: (src) => {
197
- const relativePath = relative(TEMPLATE_DIR, src);
198
- if (!relativePath) {
199
- return true;
200
- }
192
+ clack.note('Now, please select your NextBlock project in the browser.');
193
+ await execa('npx', ['supabase', 'link'], { stdio: 'inherit', cwd: projectPath });
201
194
 
202
- const segments = relativePath.split(sep);
203
- return !segments.includes('.git') && !segments.includes('node_modules');
195
+ const configTomlPath = resolve(projectPath, 'supabase', 'config.toml');
196
+ if (!(await fs.pathExists(configTomlPath))) {
197
+ throw new Error('supabase/config.toml not found. Please rerun the wizard after linking.');
198
+ }
199
+ const configToml = await fs.readFile(configTomlPath, 'utf8');
200
+ const projectIdMatch = configToml.match(/project_id\s*=\s*"([^"]+)"/);
201
+ if (!projectIdMatch?.[1]) {
202
+ throw new Error('Could not parse project_id from supabase/config.toml. Please ensure Supabase is linked.');
203
+ }
204
+ const projectId = projectIdMatch[1];
205
+
206
+ clack.note('Please go to your Supabase project dashboard to get the following secrets.');
207
+ const supabaseKeys = await clack.group(
208
+ {
209
+ dbPassword: () =>
210
+ clack.password({
211
+ message: 'What is your Database Password? (Settings > Database > Connection Parameters)',
212
+ validate: (val) => (!val ? 'Password is required' : undefined),
213
+ }),
214
+ anonKey: () =>
215
+ clack.password({
216
+ message: 'What is your Project API Key (anon key)? (Settings > API > Project API Keys)',
217
+ validate: (val) => (!val ? 'Anon Key is required' : undefined),
218
+ }),
219
+ serviceKey: () =>
220
+ clack.password({
221
+ message: 'What is your Service Role Key (service_role key)? (Settings > API > Project API Keys)',
222
+ validate: (val) => (!val ? 'Service Role Key is required' : undefined),
223
+ }),
204
224
  },
205
- });
206
- }
225
+ { onCancel: () => handleWizardCancel('Setup cancelled.') },
226
+ );
207
227
 
208
- async function removeBackups(projectDir) {
209
- const backupDir = resolve(projectDir, 'backup');
210
- if (await fs.pathExists(backupDir)) {
211
- await fs.remove(backupDir);
212
- }
213
- }
228
+ clack.note('Generating local secrets...');
229
+ const revalidationToken = crypto.randomBytes(32).toString('hex');
230
+ const supabaseUrl = `https://${projectId}.supabase.co`;
214
231
 
215
- async function ensureGitignore(projectDir) {
216
- const gitignorePath = resolve(projectDir, '.gitignore');
217
- const npmIgnorePath = resolve(projectDir, '.npmignore');
218
- const repoGitignorePath = resolve(REPO_ROOT, '.gitignore');
232
+ const dbHost = `db.${projectId}.supabase.co`;
233
+ const dbUser = 'postgres';
234
+ const dbPassword = supabaseKeys.dbPassword;
235
+ const dbName = 'postgres';
219
236
 
220
- const defaultLines = [
221
- '# Dependencies',
222
- 'node_modules',
223
- '',
224
- '# Next.js build output',
225
- '.next',
226
- 'out',
227
- '',
228
- '# Production',
229
- 'build',
230
- 'dist',
231
- '',
232
- '# Logs',
233
- 'logs',
234
- '*.log',
235
- 'npm-debug.log*',
236
- 'yarn-debug.log*',
237
- 'yarn-error.log*',
238
- 'pnpm-debug.log*',
237
+ const postgresUrl = `postgresql://${dbUser}:${dbPassword}@${dbHost}:5432/${dbName}`;
238
+
239
+ const envPath = resolve(projectPath, '.env');
240
+ const envLines = [
241
+ '# NextBlock core',
242
+ 'NEXT_PUBLIC_URL=http://localhost:3000',
239
243
  '',
240
- '# Environment',
241
- '.env.local',
242
- '.env.development.local',
243
- '.env.test.local',
244
- '.env.production.local',
244
+ '# Supabase',
245
+ `SUPABASE_PROJECT_ID=${projectId}`,
246
+ `NEXT_PUBLIC_SUPABASE_URL=${supabaseUrl}`,
247
+ `NEXT_PUBLIC_SUPABASE_ANON_KEY=${supabaseKeys.anonKey}`,
248
+ `SUPABASE_SERVICE_ROLE_KEY=${supabaseKeys.serviceKey}`,
249
+ `POSTGRES_URL=${postgresUrl}`,
245
250
  '',
246
- '# Backups',
247
- 'backup/',
251
+ '# Revalidation',
252
+ `REVALIDATE_SECRET_TOKEN=${revalidationToken}`,
248
253
  '',
249
- '# Misc',
250
- '.DS_Store',
251
254
  ];
252
255
 
253
- let repoLines = [];
254
- if (await fs.pathExists(repoGitignorePath)) {
255
- const raw = await fs.readFile(repoGitignorePath, 'utf8');
256
- repoLines = raw
257
- .replace(/\r\n/g, '\n')
258
- .split('\n')
259
- .map((line) => line.replace(/\s+$/, '').replace(/apps\/nextblock\//g, ''))
260
- .map((line) => (line.trim() === '' ? '' : line));
261
- }
262
-
263
- let content = '';
264
-
265
- if (await fs.pathExists(gitignorePath)) {
266
- content = await fs.readFile(gitignorePath, 'utf8');
267
- } else if (await fs.pathExists(npmIgnorePath)) {
268
- await fs.move(npmIgnorePath, gitignorePath, { overwrite: true });
269
- content = await fs.readFile(gitignorePath, 'utf8');
270
- } else {
271
- content = defaultLines.join('\n') + '\n';
272
- }
273
-
274
- const lines =
275
- content === ''
276
- ? []
277
- : content
278
- .replace(/\r\n/g, '\n')
279
- .split('\n')
280
- .map((line) => line.replace(/\s+$/, ''));
281
-
282
- const existing = new Set(lines);
283
- let updated = false;
284
-
285
- const mergeLine = (line) => {
286
- if (line === undefined || line === null) {
287
- return;
288
- }
289
- if (line === '') {
290
- if (lines.length === 0 || lines[lines.length - 1] === '') {
291
- return;
292
- }
293
- lines.push('');
294
- updated = true;
295
- return;
296
- }
297
- if (!existing.has(line)) {
298
- lines.push(line);
299
- existing.add(line);
300
- updated = true;
301
- }
302
- };
303
-
304
- for (const line of repoLines) {
305
- mergeLine(line);
306
- }
307
-
308
- mergeLine('');
309
-
310
- for (const line of defaultLines) {
311
- mergeLine(line);
312
- }
313
-
314
- const normalized = [];
315
- for (const line of lines) {
316
- if (line === '') {
317
- if (normalized.length === 0 || normalized[normalized.length - 1] === '') {
318
- continue;
319
- }
320
- normalized.push('');
321
- } else {
322
- normalized.push(line);
323
- }
324
- }
325
-
326
- if (normalized.length === 0 || normalized[normalized.length - 1] !== '') {
327
- normalized.push('');
328
- }
329
-
330
- const nextContent = normalized.join('\n');
331
-
332
- if (updated || content !== nextContent) {
333
- await fs.writeFile(gitignorePath, nextContent);
334
- }
335
- }
336
-
337
- async function ensureEnvExample(projectDir) {
338
- const destination = resolve(projectDir, '.env.example');
339
- if (await fs.pathExists(destination)) {
340
- return;
341
- }
342
-
343
- const templatePaths = [
344
- resolve(TEMPLATE_DIR, '.env.example'),
345
- resolve(REPO_ROOT, '.env.example'),
346
- resolve(REPO_ROOT, '.env.exemple'),
347
- ];
348
-
349
- for (const candidate of templatePaths) {
350
- if (await fs.pathExists(candidate)) {
351
- await fs.copy(candidate, destination);
352
- return;
353
- }
354
- }
355
-
356
- const placeholder = `# Environment variables for NextBlock CMS
357
- NEXT_PUBLIC_SUPABASE_URL=
358
- NEXT_PUBLIC_SUPABASE_ANON_KEY=
359
- SUPABASE_SERVICE_ROLE_KEY=
360
- SUPABASE_JWT_SECRET=
361
- NEXT_PUBLIC_URL=http://localhost:3000
362
- `;
363
-
364
- await fs.writeFile(destination, placeholder);
365
- }
366
-
367
- async function ensureClientComponents(projectDir) {
368
- const relativePaths = [
369
- 'components/env-var-warning.tsx',
370
- 'app/providers.tsx',
371
- 'app/ToasterProvider.tsx',
372
- 'context/AuthContext.tsx',
373
- 'context/CurrentContentContext.tsx',
374
- 'context/LanguageContext.tsx',
375
- ];
376
-
377
- for (const relativePath of relativePaths) {
378
- const absolutePath = resolve(projectDir, relativePath);
379
- if (!(await fs.pathExists(absolutePath))) {
380
- continue;
256
+ let canWriteEnv = true;
257
+ if (await fs.pathExists(envPath)) {
258
+ const overwrite = await clack.confirm({
259
+ message: '.env already exists. Overwrite with generated values?',
260
+ initialValue: false,
261
+ });
262
+ if (clack.isCancel(overwrite)) {
263
+ handleWizardCancel('Setup cancelled.');
381
264
  }
382
-
383
- const original = await fs.readFile(absolutePath, 'utf8');
384
- const trimmed = original.trimStart();
385
- if (
386
- trimmed.startsWith("'use client'") ||
387
- trimmed.startsWith('"use client"') ||
388
- trimmed.startsWith('/* @client */')
389
- ) {
390
- continue;
265
+ if (!overwrite) {
266
+ canWriteEnv = false;
267
+ clack.note('Keeping existing .env. Add/merge the generated values manually.');
391
268
  }
392
-
393
- await fs.writeFile(absolutePath, `'use client';\n\n${original}`);
394
- }
395
- }
396
-
397
- async function ensureClientProviders(projectDir) {
398
- const providersPath = resolve(projectDir, 'app/providers.tsx');
399
- if (!(await fs.pathExists(providersPath))) {
400
- return;
401
269
  }
402
270
 
403
- let content = await fs.readFile(providersPath, 'utf8');
404
- const wrapperImportStatement = "import { TranslationsProvider } from '@nextblock-cms/utils';";
405
- const existingImportRegex =
406
- /import\s+\{\s*TranslationsProvider\s*\}\s*from\s*['"]@nextblock-cms\/utils['"];?/;
407
- const legacyImportRegex =
408
- /import\s+\{\s*TranslationsProvider\s*\}\s*from\s*['"]@\/lib\/client-translations['"];?/;
409
-
410
- if (existingImportRegex.test(content) || legacyImportRegex.test(content)) {
411
- content = content
412
- .replace(existingImportRegex, wrapperImportStatement)
413
- .replace(legacyImportRegex, wrapperImportStatement);
414
- } else if (!content.includes(wrapperImportStatement)) {
415
- const lines = content.split(/\r?\n/);
416
- const firstImport = lines.findIndex((line) => line.startsWith('import'));
417
- const insertIndex = firstImport === -1 ? 0 : firstImport + 1;
418
- lines.splice(insertIndex, 0, wrapperImportStatement);
419
- content = lines.join('\n');
271
+ if (canWriteEnv) {
272
+ await fs.writeFile(envPath, envLines.join('\n'));
273
+ clack.success('Supabase configuration saved to .env');
420
274
  }
421
275
 
422
- await fs.writeFile(providersPath, content);
423
-
424
- const wrapperPath = resolve(projectDir, 'lib/client-translations.tsx');
425
- if (await fs.pathExists(wrapperPath)) {
426
- await fs.remove(wrapperPath);
276
+ clack.note('Setting up your database...');
277
+ const dbPushSpinner = clack.spinner();
278
+ dbPushSpinner.start('Pushing database schema... (This may take a minute)');
279
+ try {
280
+ process.env.POSTGRES_URL = postgresUrl;
281
+ await execa('npx', ['supabase', 'db', 'push'], { stdio: 'inherit', cwd: projectPath });
282
+ dbPushSpinner.stop('Database schema pushed successfully!');
283
+ } catch (error) {
284
+ dbPushSpinner.stop('Database push failed. Please run `npx supabase db push` manually.');
285
+ if (error instanceof Error) {
286
+ clack.note(error.message);
427
287
  }
428
288
  }
429
289
 
430
- async function ensureEditorUtils(projectDir) {
431
- const exists = await fs.pathExists(EDITOR_UTILS_SOURCE_DIR);
432
- if (!exists) {
433
- return [];
434
- }
435
-
436
- const entries = await fs.readdir(EDITOR_UTILS_SOURCE_DIR);
437
- const utilNames = entries.filter((name) => name.endsWith('.ts')).map((name) => name.replace(/\.ts$/, ''));
438
-
439
- if (utilNames.length === 0) {
440
- return [];
441
- }
442
-
443
- const destinationDir = resolve(projectDir, 'lib/editor/utils');
444
- await fs.ensureDir(destinationDir);
445
-
446
- for (const utilName of utilNames) {
447
- const sourcePath = resolve(EDITOR_UTILS_SOURCE_DIR, `${utilName}.ts`);
448
- const destinationPath = resolve(destinationDir, `${utilName}.ts`);
449
- await fs.copy(sourcePath, destinationPath);
450
- }
451
-
452
- return utilNames;
453
- }
454
-
455
- async function sanitizeBlockEditorImports(projectDir) {
456
- const blockEditorPath = resolve(projectDir, 'app/cms/blocks/components/BlockEditorArea.tsx');
457
- if (!(await fs.pathExists(blockEditorPath))) {
458
- return;
459
- }
460
-
461
- const content = await fs.readFile(blockEditorPath, 'utf8');
462
- const replacements = [
463
- { pattern: /(\.\.\/editors\/[A-Za-z0-9_-]+)\.js/g, replacement: '$1.tsx' },
464
- { pattern: /(\.\.\/actions)\.js/g, replacement: '$1.ts' },
465
- ];
466
-
467
- const updated = replacements.reduce(
468
- (current, { pattern, replacement }) => current.replace(pattern, replacement),
469
- content,
470
- );
471
-
472
- if (updated !== content) {
473
- await fs.writeFile(blockEditorPath, updated);
474
- }
475
- }
476
-
477
- async function sanitizeUiImports(projectDir) {
478
- const searchDirs = ['app', 'components', 'context', 'lib'];
479
- const validExtensions = new Set(['.js', '.jsx', '.ts', '.tsx']);
480
- const files = [];
481
-
482
- for (const relativeDir of searchDirs) {
483
- const absoluteDir = resolve(projectDir, relativeDir);
484
- if (await fs.pathExists(absoluteDir)) {
485
- await collectFiles(absoluteDir, files, validExtensions);
486
- }
290
+ const setupR2 = await clack.confirm({
291
+ message: 'Do you want to set up Cloudflare R2 for media storage now? (Optional)',
292
+ });
293
+ if (clack.isCancel(setupR2)) {
294
+ handleWizardCancel('Setup cancelled.');
487
295
  }
296
+ if (setupR2) {
297
+ clack.note('I will open your browser to the R2 dashboard.\nYou need to create a bucket and an R2 API Token.');
298
+ await open('https://dash.cloudflare.com/?to=/:account/r2', { wait: false });
299
+
300
+ const r2Keys = await clack.group(
301
+ {
302
+ accountId: () =>
303
+ clack.text({
304
+ message: 'R2: Paste your Cloudflare Account ID:',
305
+ validate: (val) => (!val ? 'Account ID is required' : undefined),
306
+ }),
307
+ bucketName: () =>
308
+ clack.text({
309
+ message: 'R2: Paste your Bucket Name:',
310
+ validate: (val) => (!val ? 'Bucket name is required' : undefined),
311
+ }),
312
+ accessKey: () =>
313
+ clack.password({
314
+ message: 'R2: Paste your Access Key ID:',
315
+ validate: (val) => (!val ? 'Access Key ID is required' : undefined),
316
+ }),
317
+ secretKey: () =>
318
+ clack.password({
319
+ message: 'R2: Paste your Secret Access Key:',
320
+ validate: (val) => (!val ? 'Secret Access Key is required' : undefined),
321
+ }),
322
+ publicBaseUrl: () =>
323
+ clack.text({
324
+ message: 'R2: Public Base URL (e.g., https://pub-xxx.r2.dev/your-bucket):',
325
+ validate: (val) => (!val ? 'Public base URL is required' : undefined),
326
+ }),
327
+ },
328
+ { onCancel: () => handleWizardCancel('Setup cancelled.') },
329
+ );
488
330
 
489
- for (const filePath of files) {
490
- const original = await fs.readFile(filePath, 'utf8');
491
- const updated = original.replace(/@nextblock-cms\/ui\/(?!styles\/)[A-Za-z0-9/_-]+/g, '@nextblock-cms/ui');
492
- if (updated !== original) {
493
- await fs.writeFile(filePath, updated);
494
- }
495
- }
496
- }
331
+ const r2Env = [
332
+ '# Cloudflare R2',
333
+ `NEXT_PUBLIC_R2_BASE_URL=${r2Keys.publicBaseUrl}`,
334
+ `R2_ACCOUNT_ID=${r2Keys.accountId}`,
335
+ `R2_BUCKET_NAME=${r2Keys.bucketName}`,
336
+ `R2_ACCESS_KEY_ID=${r2Keys.accessKey}`,
337
+ `R2_SECRET_ACCESS_KEY=${r2Keys.secretKey}`,
338
+ '',
339
+ ].join('\n');
497
340
 
498
- async function collectFiles(directory, accumulator, extensions) {
499
- const entries = await fs.readdir(directory, { withFileTypes: true });
500
- for (const entry of entries) {
501
- const fullPath = resolve(directory, entry.name);
502
- if (entry.isDirectory()) {
503
- await collectFiles(fullPath, accumulator, extensions);
341
+ if (canWriteEnv) {
342
+ await fs.appendFile(envPath, r2Env);
343
+ clack.success('Cloudflare R2 configuration saved!');
504
344
  } else {
505
- const dotIndex = entry.name.lastIndexOf('.');
506
- if (dotIndex !== -1) {
507
- const ext = entry.name.slice(dotIndex);
508
- if (extensions.has(ext)) {
509
- accumulator.push(fullPath);
510
- }
511
- }
512
- }
513
- }
514
- }
515
-
516
- async function ensureUiProxies(projectDir) {
517
- const proxiesDir = resolve(projectDir, 'lib/ui');
518
- await fs.ensureDir(proxiesDir);
519
-
520
- const proxyContent = "export * from '@nextblock-cms/ui';\n";
521
-
522
- for (const moduleName of UI_PROXY_MODULES) {
523
- const proxyPath = resolve(proxiesDir, `${moduleName}.ts`);
524
- if (!(await fs.pathExists(proxyPath))) {
525
- await fs.outputFile(proxyPath, proxyContent);
345
+ clack.note('Add the following R2 values to your existing .env:\n' + r2Env);
526
346
  }
527
347
  }
528
- }
529
-
530
- async function sanitizeLayout(projectDir) {
531
- await ensureGlobalStyles(projectDir);
532
- await ensureEditorStyles(projectDir);
533
-
534
- const layoutPath = resolve(projectDir, 'app/layout.tsx');
535
- if (!(await fs.pathExists(layoutPath))) {
536
- return;
537
- }
538
-
539
- const requiredImports = [
540
- "import '@nextblock-cms/ui/styles/globals.css';",
541
- "import '@nextblock-cms/editor/styles/editor.css';",
542
- ];
543
-
544
- const content = await fs.readFile(layoutPath, 'utf8');
545
- let updated = content.replace(
546
- /import\s+['"]\.\/globals\.css['"];?\s*/g,
547
- '',
548
- );
549
- updated = updated.replace(
550
- /import\s+['"]\.\/editor\.css['"];?\s*/g,
551
- '',
552
- );
553
348
 
554
- const missingImports = requiredImports.filter((statement) => !updated.includes(statement));
555
- if (missingImports.length > 0) {
556
- updated = `${missingImports.join('\n')}\n${updated}`;
557
- }
558
-
559
- if (updated !== content) {
560
- await fs.writeFile(layoutPath, updated);
561
- }
562
- }
563
-
564
- async function ensureGlobalStyles(projectDir) {
565
- const destination = resolve(projectDir, 'app/globals.css');
566
-
567
- if (!(await fs.pathExists(destination))) {
568
- return;
569
- }
570
-
571
- const content = (await fs.readFile(destination, 'utf8')).trim();
572
- if (
573
- content === '' ||
574
- content.startsWith('/* Project-level overrides') ||
575
- content.includes('@tailwind base')
576
- ) {
577
- await fs.remove(destination);
578
- }
579
- }
580
-
581
- async function ensureEditorStyles(projectDir) {
582
- const stylesDir = resolve(projectDir, 'app');
583
- const editorPath = resolve(stylesDir, 'editor.css');
584
- const dragHandlePath = resolve(stylesDir, 'drag-handle.css');
585
-
586
- for (const filePath of [editorPath, dragHandlePath]) {
587
- if (await fs.pathExists(filePath)) {
588
- const content = (await fs.readFile(filePath, 'utf8')).trim();
589
- if (
590
- content === '' ||
591
- content.startsWith('/* Editor styles placeholder') ||
592
- content.includes("@nextblock-cms/editor/styles")
593
- ) {
594
- await fs.remove(filePath);
595
- }
596
- }
349
+ const setupSMTP = await clack.confirm({
350
+ message: 'Do you want to set up an SMTP server for emails now? (Optional)',
351
+ });
352
+ if (clack.isCancel(setupSMTP)) {
353
+ handleWizardCancel('Setup cancelled.');
597
354
  }
598
- }
599
-
600
- async function sanitizeTailwindConfig(projectDir) {
601
- const tailwindConfigPath = resolve(projectDir, 'tailwind.config.js');
602
- const content = `/** @type {import('tailwindcss').Config} */
603
- module.exports = {
604
- darkMode: ['class'],
605
- content: [
606
- './app/**/*.{js,ts,jsx,tsx,mdx}',
607
- './components/**/*.{js,ts,jsx,tsx,mdx}',
608
- './context/**/*.{js,ts,jsx,tsx,mdx}',
609
- './lib/**/*.{js,ts,jsx,tsx,mdx}',
610
- './node_modules/@nextblock-cms/ui/**/*.{js,ts,jsx,tsx}',
611
- './node_modules/@nextblock-cms/editor/**/*.{js,ts,jsx,tsx}',
612
- ],
613
- safelist: [
614
- 'animate-enter',
615
- 'animate-leave',
616
- 'dark',
617
- 'text-primary',
618
- 'text-secondary',
619
- 'text-accent',
620
- 'text-muted',
621
- 'text-destructive',
622
- 'text-background',
623
- ],
624
- prefix: '',
625
- theme: {
626
- container: {
627
- center: true,
628
- padding: '2rem',
629
- screens: {
630
- '2xl': '1400px',
631
- },
632
- },
633
- extend: {
634
- colors: {
635
- border: 'hsl(var(--border))',
636
- input: 'hsl(var(--input))',
637
- ring: 'hsl(var(--ring))',
638
- background: 'hsl(var(--background))',
639
- foreground: 'hsl(var(--foreground))',
640
- primary: {
641
- DEFAULT: 'hsl(var(--primary))',
642
- foreground: 'hsl(var(--primary-foreground))',
643
- },
644
- secondary: {
645
- DEFAULT: 'hsl(var(--secondary))',
646
- foreground: 'hsl(var(--secondary-foreground))',
647
- },
648
- destructive: {
649
- DEFAULT: 'hsl(var(--destructive))',
650
- foreground: 'hsl(var(--destructive-foreground))',
651
- },
652
- muted: {
653
- DEFAULT: 'hsl(var(--muted))',
654
- foreground: 'hsl(var(--muted-foreground))',
655
- },
656
- warning: {
657
- DEFAULT: 'hsl(var(--warning))',
658
- foreground: 'hsl(var(--warning-foreground))',
659
- },
660
- accent: {
661
- DEFAULT: 'hsl(var(--accent))',
662
- foreground: 'hsl(var(--accent-foreground))',
663
- },
664
- popover: {
665
- DEFAULT: 'hsl(var(--popover))',
666
- foreground: 'hsl(var(--popover-foreground))',
667
- },
668
- card: {
669
- DEFAULT: 'hsl(var(--card))',
670
- foreground: 'hsl(var(--card-foreground))',
671
- },
672
- },
673
- borderRadius: {
674
- lg: 'var(--radius)',
675
- md: 'calc(var(--radius) - 2px)',
676
- sm: 'calc(var(--radius) - 4px)',
677
- },
678
- keyframes: {
679
- 'accordion-down': {
680
- from: { height: '0' },
681
- to: { height: 'var(--radix-accordion-content-height)' },
682
- },
683
- 'accordion-up': {
684
- from: { height: 'var(--radix-accordion-content-height)' },
685
- to: { height: '0' },
686
- },
355
+ if (setupSMTP) {
356
+ const smtpKeys = await clack.group(
357
+ {
358
+ host: () =>
359
+ clack.text({
360
+ message: 'SMTP: Host (e.g., smtp.resend.com):',
361
+ validate: (val) => (!val ? 'SMTP host is required' : undefined),
362
+ }),
363
+ port: () =>
364
+ clack.text({
365
+ message: 'SMTP: Port (e.g., 465):',
366
+ validate: (val) => (!val ? 'SMTP port is required' : undefined),
367
+ }),
368
+ user: () =>
369
+ clack.text({
370
+ message: 'SMTP: User (e.g., apikey):',
371
+ validate: (val) => (!val ? 'SMTP user is required' : undefined),
372
+ }),
373
+ pass: () =>
374
+ clack.password({
375
+ message: 'SMTP: Password:',
376
+ validate: (val) => (!val ? 'SMTP password is required' : undefined),
377
+ }),
378
+ fromEmail: () =>
379
+ clack.text({
380
+ message: 'SMTP: From Email (e.g., onboarding@my.site):',
381
+ validate: (val) => (!val ? 'From email is required' : undefined),
382
+ }),
383
+ fromName: () =>
384
+ clack.text({
385
+ message: 'SMTP: From Name (e.g., NextBlock):',
386
+ validate: (val) => (!val ? 'From name is required' : undefined),
387
+ }),
687
388
  },
688
- animation: {
689
- 'accordion-down': 'accordion-down 0.2s ease-out',
690
- 'accordion-up': 'accordion-up 0.2s ease-out',
691
- },
692
- },
693
- },
694
- plugins: [require('tailwindcss-animate')],
695
- };
696
- `;
697
-
698
- await fs.writeFile(tailwindConfigPath, content);
699
- }
700
-
701
- async function normalizeTsconfig(projectDir) {
702
- const tsconfigPath = resolve(projectDir, 'tsconfig.json');
703
- if (!(await fs.pathExists(tsconfigPath))) {
704
- return;
705
- }
706
-
707
- const tsconfig = await fs.readJSON(tsconfigPath);
708
- if ('extends' in tsconfig) {
709
- delete tsconfig.extends;
710
- }
711
-
712
- if ('references' in tsconfig) {
713
- delete tsconfig.references;
714
- }
715
- const defaultInclude = new Set([
716
- 'next-env.d.ts',
717
- '**/*.ts',
718
- '**/*.tsx',
719
- '**/*.js',
720
- '**/*.jsx',
721
- '.next/types/**/*.ts',
722
- ]);
723
-
724
- if (Array.isArray(tsconfig.include)) {
725
- for (const entry of tsconfig.include) {
726
- if (typeof entry === 'string' && !entry.includes('../')) {
727
- defaultInclude.add(entry);
728
- }
729
- }
730
- }
731
-
732
- tsconfig.include = Array.from(defaultInclude);
733
-
734
- const defaultExclude = new Set(['node_modules']);
735
- if (Array.isArray(tsconfig.exclude)) {
736
- for (const entry of tsconfig.exclude) {
737
- if (typeof entry === 'string' && !entry.includes('../')) {
738
- defaultExclude.add(entry);
739
- }
740
- }
741
- }
742
-
743
- tsconfig.exclude = Array.from(defaultExclude);
744
-
745
- tsconfig.compilerOptions = {
746
- ...(tsconfig.compilerOptions ?? {}),
747
- baseUrl: '.',
748
- skipLibCheck: true,
749
- };
750
-
751
- const compilerOptions = tsconfig.compilerOptions;
752
- compilerOptions.paths = {
753
- ...(compilerOptions.paths ?? {}),
754
- '@/*': ['./*'],
755
- '@nextblock-cms/ui/*': ['./lib/ui/*'],
756
- '@nextblock-cms/editor/utils/*': ['./lib/editor/utils/*'],
757
- };
758
-
759
- await fs.writeJSON(tsconfigPath, tsconfig, { spaces: 2 });
760
- }
761
-
762
- async function sanitizeNextConfig(projectDir, editorUtilNames = []) {
763
- const nextConfigPath = resolve(projectDir, 'next.config.js');
764
- const content = buildNextConfigContent(editorUtilNames);
765
- await fs.writeFile(nextConfigPath, content);
766
- }
767
-
768
- async function transformPackageJson(projectDir) {
769
- const packageJsonPath = resolve(projectDir, 'package.json');
770
- if (!(await fs.pathExists(packageJsonPath))) {
771
- return;
772
- }
773
-
774
- const packageJson = await fs.readJSON(packageJsonPath);
775
- const projectName = basename(projectDir);
389
+ { onCancel: () => handleWizardCancel('Setup cancelled.') },
390
+ );
776
391
 
777
- if (projectName) {
778
- packageJson.name = projectName;
779
- }
392
+ const smtpEnv = [
393
+ '# Email SMTP Configuration',
394
+ `SMTP_HOST=${smtpKeys.host}`,
395
+ `SMTP_PORT=${smtpKeys.port}`,
396
+ `SMTP_USER=${smtpKeys.user}`,
397
+ `SMTP_PASS=${smtpKeys.pass}`,
398
+ `SMTP_FROM_EMAIL=${smtpKeys.fromEmail}`,
399
+ `SMTP_FROM_NAME=${smtpKeys.fromName}`,
400
+ '',
401
+ ].join('\n');
780
402
 
781
- packageJson.version = packageJson.version ?? '0.1.0';
782
- packageJson.private = packageJson.private ?? true;
783
-
784
- packageJson.dependencies = packageJson.dependencies ?? {};
785
-
786
- for (const [pkgName, manifestPath] of Object.entries(PACKAGE_VERSION_SOURCES)) {
787
- if (pkgName in packageJson.dependencies) {
788
- const current = packageJson.dependencies[pkgName];
789
- if (typeof current === 'string' && current.startsWith('workspace:')) {
790
- let versionSpecifier = 'latest';
791
- try {
792
- const manifest = await fs.readJSON(manifestPath);
793
- if (manifest.version) {
794
- versionSpecifier = `^${manifest.version}`;
795
- }
796
- } catch {
797
- versionSpecifier = 'latest';
798
- }
799
-
800
- packageJson.dependencies[pkgName] = versionSpecifier;
801
- }
403
+ if (canWriteEnv) {
404
+ await fs.appendFile(envPath, smtpEnv);
405
+ clack.success('SMTP configuration saved!');
406
+ } else {
407
+ clack.note('Add the following SMTP values to your existing .env:\n' + smtpEnv);
802
408
  }
803
409
  }
804
410
 
805
- await fs.writeJSON(packageJsonPath, packageJson, { spaces: 2 });
411
+ clack.outro(
412
+ `🎉 Your NextBlock project ${projectName ? `"${projectName}" ` : ''}is ready!\n\nNext steps:\n 1. cd ${projectName || projectPath}\n 2. npm run dev`,
413
+ );
806
414
  }
807
415
 
808
- async function installDependencies(projectDir) {
809
- const npmCommand = IS_WINDOWS ? 'npm.cmd' : 'npm';
810
- console.log(chalk.blue('Installing dependencies with npm...'));
811
- await runCommand(npmCommand, ['install'], { cwd: projectDir });
812
- console.log(chalk.green('Dependencies installed.'));
416
+ function handleWizardCancel(message) {
417
+ clack.cancel(message ?? 'Setup cancelled.');
418
+ process.exit(1);
813
419
  }
814
420
 
815
- async function initializeGit(projectDir) {
816
- const gitDirectory = resolve(projectDir, '.git');
817
- if (await fs.pathExists(gitDirectory)) {
421
+ async function ensureEmptyDirectory(projectDir) {
422
+ const exists = await fs.pathExists(projectDir);
423
+ if (!exists) {
818
424
  return;
819
425
  }
820
-
821
- try {
822
- console.log(chalk.blue('Initializing Git repository...'));
823
- await runCommand('git', ['init'], { cwd: projectDir });
824
- console.log(chalk.green('Git repository initialized.'));
825
- } catch (error) {
826
- console.warn(
827
- chalk.yellow(
828
- `Skipping Git initialization: ${error instanceof Error ? error.message : String(error)}`,
829
- ),
830
- );
831
- }
832
- }
833
-
834
- function runCommand(command, args, options = {}) {
835
- return new Promise((resolve, reject) => {
836
- const child = spawn(command, args, {
837
- stdio: 'inherit',
838
- shell: IS_WINDOWS,
839
- ...options,
840
- });
841
-
842
- child.on('error', (error) => {
843
- reject(error);
844
- });
845
-
846
- child.on('close', (code) => {
847
- if (code === 0) {
848
- resolve();
849
- } else {
850
- reject(new Error(`${command} exited with code ${code}`));
851
- }
852
- });
853
- });
854
- }
855
-
856
- function buildNextConfigContent(editorUtilNames) {
857
- const aliasLines = [];
858
-
859
- for (const moduleName of UI_PROXY_MODULES) {
860
- aliasLines.push(
861
- " '@nextblock-cms/ui/" + moduleName + "': path.join(process.cwd(), 'lib/ui/" + moduleName + "'),",
862
- );
863
- }
864
-
865
- for (const moduleName of editorUtilNames) {
866
- aliasLines.push(
867
- " '@nextblock-cms/editor/utils/" +
868
- moduleName +
869
- "': path.join(process.cwd(), 'lib/editor/utils/" +
870
- moduleName +
871
- "'),",
872
- );
426
+
427
+ const contents = await fs.readdir(projectDir);
428
+ if (contents.length > 0) {
429
+ throw new Error(`Directory "${projectDir}" already exists and is not empty.`);
430
+ }
431
+ }
432
+
433
+ async function copyTemplateTo(projectDir) {
434
+ const templateExists = await fs.pathExists(TEMPLATE_DIR);
435
+ if (!templateExists) {
436
+ throw new Error(
437
+ `Template directory not found at ${TEMPLATE_DIR}. Run "npm run sync:create-nextblock" to populate it.`,
438
+ );
439
+ }
440
+
441
+ await fs.ensureDir(projectDir);
442
+
443
+ await fs.copy(TEMPLATE_DIR, projectDir, {
444
+ dereference: true,
445
+ filter: (src) => {
446
+ const relativePath = relative(TEMPLATE_DIR, src);
447
+ if (!relativePath) {
448
+ return true;
449
+ }
450
+
451
+ const segments = relativePath.split(sep);
452
+ return !segments.includes('.git') && !segments.includes('node_modules');
453
+ },
454
+ });
455
+ }
456
+
457
+ async function removeBackups(projectDir) {
458
+ const backupDir = resolve(projectDir, 'backup');
459
+ if (await fs.pathExists(backupDir)) {
460
+ await fs.remove(backupDir);
461
+ }
462
+ const backupsDir = resolve(projectDir, 'backups');
463
+ if (await fs.pathExists(backupsDir)) {
464
+ await fs.remove(backupsDir);
465
+ }
466
+ }
467
+
468
+ async function ensureGitignore(projectDir) {
469
+ const gitignorePath = resolve(projectDir, '.gitignore');
470
+ const npmIgnorePath = resolve(projectDir, '.npmignore');
471
+ const repoGitignorePath = resolve(REPO_ROOT, '.gitignore');
472
+
473
+ const defaultLines = [
474
+ '# Dependencies',
475
+ 'node_modules',
476
+ '',
477
+ '# Next.js build output',
478
+ '.next',
479
+ 'out',
480
+ '',
481
+ '# Production',
482
+ 'build',
483
+ 'dist',
484
+ '',
485
+ '# Logs',
486
+ 'logs',
487
+ '*.log',
488
+ 'npm-debug.log*',
489
+ 'yarn-debug.log*',
490
+ 'yarn-error.log*',
491
+ 'pnpm-debug.log*',
492
+ '',
493
+ '# Environment',
494
+ '.env.local',
495
+ '.env.development.local',
496
+ '.env.test.local',
497
+ '.env.production.local',
498
+ '',
499
+ '# Backups',
500
+ 'backup/',
501
+ 'backups/',
502
+ '',
503
+ '# Misc',
504
+ '.DS_Store',
505
+ ];
506
+
507
+ let repoLines = [];
508
+ if (await fs.pathExists(repoGitignorePath)) {
509
+ const raw = await fs.readFile(repoGitignorePath, 'utf8');
510
+ repoLines = raw
511
+ .replace(/\r\n/g, '\n')
512
+ .split('\n')
513
+ .map((line) => line.replace(/\s+$/, '').replace(/apps\/nextblock\//g, ''))
514
+ .map((line) => (line.trim() === '' ? '' : line));
515
+ }
516
+
517
+ let content = '';
518
+
519
+ if (await fs.pathExists(gitignorePath)) {
520
+ content = await fs.readFile(gitignorePath, 'utf8');
521
+ } else if (await fs.pathExists(npmIgnorePath)) {
522
+ await fs.move(npmIgnorePath, gitignorePath, { overwrite: true });
523
+ content = await fs.readFile(gitignorePath, 'utf8');
524
+ } else {
525
+ content = defaultLines.join('\n') + '\n';
526
+ }
527
+
528
+ const lines =
529
+ content === ''
530
+ ? []
531
+ : content
532
+ .replace(/\r\n/g, '\n')
533
+ .split('\n')
534
+ .map((line) => line.replace(/\s+$/, ''));
535
+
536
+ const existing = new Set(lines);
537
+ let updated = false;
538
+
539
+ const mergeLine = (line) => {
540
+ if (line === undefined || line === null) {
541
+ return;
542
+ }
543
+ if (line === '') {
544
+ if (lines.length === 0 || lines[lines.length - 1] === '') {
545
+ return;
546
+ }
547
+ lines.push('');
548
+ updated = true;
549
+ return;
550
+ }
551
+ if (!existing.has(line)) {
552
+ lines.push(line);
553
+ existing.add(line);
554
+ updated = true;
555
+ }
556
+ };
557
+
558
+ for (const line of repoLines) {
559
+ mergeLine(line);
560
+ }
561
+
562
+ mergeLine('');
563
+
564
+ for (const line of defaultLines) {
565
+ mergeLine(line);
566
+ }
567
+
568
+ const normalized = [];
569
+ for (const line of lines) {
570
+ if (line === '') {
571
+ if (normalized.length === 0 || normalized[normalized.length - 1] === '') {
572
+ continue;
573
+ }
574
+ normalized.push('');
575
+ } else {
576
+ normalized.push(line);
577
+ }
578
+ }
579
+
580
+ if (normalized.length === 0 || normalized[normalized.length - 1] !== '') {
581
+ normalized.push('');
582
+ }
583
+
584
+ const nextContent = normalized.join('\n');
585
+
586
+ if (updated || content !== nextContent) {
587
+ await fs.writeFile(gitignorePath, nextContent);
588
+ }
589
+ }
590
+
591
+ async function ensureEnvExample(projectDir) {
592
+ const destination = resolve(projectDir, '.env.example');
593
+ if (await fs.pathExists(destination)) {
594
+ return;
595
+ }
596
+
597
+ const templatePaths = [
598
+ resolve(TEMPLATE_DIR, '.env.example'),
599
+ resolve(REPO_ROOT, '.env.example'),
600
+ resolve(REPO_ROOT, '.env.exemple'),
601
+ ];
602
+
603
+ for (const candidate of templatePaths) {
604
+ if (await fs.pathExists(candidate)) {
605
+ await fs.copy(candidate, destination);
606
+ return;
607
+ }
873
608
  }
874
609
 
875
- const lines = [
876
- '//@ts-check',
877
- '',
878
- "const path = require('path');",
879
- "const webpack = require('webpack');",
880
- '',
881
- '/**',
882
- " * @type {import('next').NextConfig}",
883
- ' **/',
884
- 'const nextConfig = {',
885
- " outputFileTracingRoot: path.join(__dirname),",
886
- ' env: {',
887
- " NEXT_PUBLIC_SUPABASE_URL: process.env.NEXT_PUBLIC_SUPABASE_URL,",
888
- " NEXT_PUBLIC_SUPABASE_ANON_KEY: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,",
889
- ' },',
890
- ' images: {',
891
- " formats: ['image/avif', 'image/webp'],",
892
- ' imageSizes: [16, 32, 48, 64, 96, 128, 256, 384, 512],',
893
- ' deviceSizes: [320, 480, 640, 750, 828, 1080, 1200, 1440, 1920, 2048, 2560],',
894
- ' minimumCacheTTL: 31536000,',
895
- " dangerouslyAllowSVG: false,",
896
- " contentSecurityPolicy: \"default-src 'self'; script-src 'none'; sandbox;\",",
897
- ' remotePatterns: [',
898
- " { protocol: 'https', hostname: 'pub-a31e3f1a87d144898aeb489a8221f92e.r2.dev' },",
899
- " { protocol: 'https', hostname: 'e260676f72b0b18314b868f136ed72ae.r2.cloudflarestorage.com' },",
900
- ' ...(process.env.NEXT_PUBLIC_URL',
901
- ' ? [',
902
- ' {',
903
- " protocol: /** @type {'http' | 'https'} */ (new URL(process.env.NEXT_PUBLIC_URL).protocol.slice(0, -1)),",
904
- " hostname: new URL(process.env.NEXT_PUBLIC_URL).hostname,",
905
- ' },',
906
- ' ]',
907
- ' : []),',
908
- ' ],',
909
- ' },',
910
- ' experimental: {',
911
- " optimizeCss: true,",
912
- " cssChunking: 'strict',",
913
- ' },',
914
- " transpilePackages: ['@nextblock-cms/utils', '@nextblock-cms/ui', '@nextblock-cms/editor'],",
915
- ' webpack: (config, { isServer }) => {',
916
- ' config.resolve = config.resolve || {};',
917
- ' config.resolve.alias = {',
918
- ' ...(config.resolve.alias ?? {}),',
919
- ];
920
-
921
- if (aliasLines.length > 0) {
922
- lines.push(...aliasLines);
923
- }
924
-
925
- lines.push(' };', '');
926
-
927
- if (editorUtilNames.length > 0) {
928
- lines.push(
929
- ' const editorUtilsShims = ' + JSON.stringify(editorUtilNames) + ';',
930
- ' config.plugins = config.plugins || [];',
931
- ' for (const utilName of editorUtilsShims) {',
932
- " const shimPath = path.join(process.cwd(), 'lib/editor/utils', utilName);",
933
- ' config.plugins.push(',
934
- " new webpack.NormalModuleReplacementPlugin(new RegExp('^@nextblock-cms/editor/utils/' + utilName + '$'), shimPath),",
935
- ' );',
936
- ' config.plugins.push(',
937
- " new webpack.NormalModuleReplacementPlugin(new RegExp('^./utils/' + utilName + '$'), shimPath),",
938
- ' );',
939
- ' }',
940
- '',
941
- );
942
- }
610
+ const placeholder = `# Environment variables for NextBlock CMS
611
+ NEXT_PUBLIC_URL=
612
+ # Vercel / Supabase
613
+ SUPABASE_PROJECT_ID=
614
+ POSTGRES_URL=
615
+ NEXT_PUBLIC_SUPABASE_URL=
616
+ NEXT_PUBLIC_SUPABASE_ANON_KEY=
617
+ SUPABASE_SERVICE_ROLE_KEY=
943
618
 
944
- lines.push(
945
- ' if (!isServer) {',
946
- ' config.module = config.module || {};',
947
- ' config.module.rules = config.module.rules || [];',
948
- ' config.module.rules.push({',
949
- " test: /\\.svg$/i,",
950
- " issuer: /\\.[jt]sx?$/,",
951
- " use: ['@svgr/webpack'],",
952
- ' });',
953
- '',
954
- ' config.optimization = {',
955
- ' ...(config.optimization ?? {}),',
956
- ' splitChunks: {',
957
- ' ...((config.optimization ?? {}).splitChunks ?? {}),',
958
- ' cacheGroups: {',
959
- ' ...(((config.optimization ?? {}).splitChunks ?? {}).cacheGroups ?? {}),',
960
- ' tiptap: {',
961
- " test: /[\\\\/]node_modules[\\\\/](@tiptap|prosemirror)[\\\\/]/,",
962
- " name: 'tiptap',",
963
- " chunks: 'async',",
964
- ' priority: 30,',
965
- ' reuseExistingChunk: true,',
966
- ' },',
967
- ' tiptapExtensions: {',
968
- " test: /[\\\\/](tiptap-extensions|RichTextEditor|MenuBar|MediaLibraryModal)[\\\\/]/,",
969
- " name: 'tiptap-extensions',",
970
- " chunks: 'async',",
971
- ' priority: 25,',
972
- ' reuseExistingChunk: true,',
973
- ' },',
974
- ' },',
975
- ' },',
976
- ' };',
977
- ' }',
978
- '',
979
- ' return config;',
980
- ' },',
981
- ' turbopack: {',
982
- ' // Turbopack-specific options can be configured here if needed.',
983
- ' },',
984
- ' compiler: {',
985
- " removeConsole: process.env.NODE_ENV === 'production',",
986
- ' },',
987
- '};',
988
- '',
989
- 'module.exports = nextConfig;',
990
- );
619
+ # Cloudflare
620
+ NEXT_PUBLIC_R2_BASE_URL=
621
+ R2_ACCESS_KEY_ID=
622
+ R2_SECRET_ACCESS_KEY=
623
+ R2_BUCKET_NAME=
624
+ R2_ACCOUNT_ID=
625
+
626
+ REVALIDATE_SECRET_TOKEN=
627
+
628
+ # Email SMTP Configuration
629
+ SMTP_HOST=
630
+ SMTP_PORT=
631
+ SMTP_USER=
632
+ SMTP_PASS=
633
+ SMTP_FROM_EMAIL=
634
+ SMTP_FROM_NAME=
635
+ `;
991
636
 
992
- return lines.join('\n');
637
+ await fs.writeFile(destination, placeholder);
993
638
  }
639
+
640
+ async function ensureClientComponents(projectDir) {
641
+ const relativePaths = [
642
+ 'components/env-var-warning.tsx',
643
+ 'app/providers.tsx',
644
+ 'app/ToasterProvider.tsx',
645
+ 'context/AuthContext.tsx',
646
+ 'context/CurrentContentContext.tsx',
647
+ 'context/LanguageContext.tsx',
648
+ ];
649
+
650
+ for (const relativePath of relativePaths) {
651
+ const absolutePath = resolve(projectDir, relativePath);
652
+ if (!(await fs.pathExists(absolutePath))) {
653
+ continue;
654
+ }
655
+
656
+ const original = await fs.readFile(absolutePath, 'utf8');
657
+ const trimmed = original.trimStart();
658
+ if (
659
+ trimmed.startsWith("'use client'") ||
660
+ trimmed.startsWith('"use client"') ||
661
+ trimmed.startsWith('/* @client */')
662
+ ) {
663
+ continue;
664
+ }
665
+
666
+ await fs.writeFile(absolutePath, `'use client';\n\n${original}`);
667
+ }
668
+ }
669
+
670
+ async function ensureClientProviders(projectDir) {
671
+ const providersPath = resolve(projectDir, 'app/providers.tsx');
672
+ if (!(await fs.pathExists(providersPath))) {
673
+ return;
674
+ }
675
+
676
+ let content = await fs.readFile(providersPath, 'utf8');
677
+ const wrapperImportStatement = "import { TranslationsProvider } from '@nextblock-cms/utils';";
678
+ const existingImportRegex =
679
+ /import\s+\{\s*TranslationsProvider\s*\}\s*from\s*['"]@nextblock-cms\/utils['"];?/;
680
+ const legacyImportRegex =
681
+ /import\s+\{\s*TranslationsProvider\s*\}\s*from\s*['"]@\/lib\/client-translations['"];?/;
682
+
683
+ if (existingImportRegex.test(content) || legacyImportRegex.test(content)) {
684
+ content = content
685
+ .replace(existingImportRegex, wrapperImportStatement)
686
+ .replace(legacyImportRegex, wrapperImportStatement);
687
+ } else if (!content.includes(wrapperImportStatement)) {
688
+ const lines = content.split(/\r?\n/);
689
+ const firstImport = lines.findIndex((line) => line.startsWith('import'));
690
+ const insertIndex = firstImport === -1 ? 0 : firstImport + 1;
691
+ lines.splice(insertIndex, 0, wrapperImportStatement);
692
+ content = lines.join('\n');
693
+ }
694
+
695
+ await fs.writeFile(providersPath, content);
696
+
697
+ const wrapperPath = resolve(projectDir, 'lib/client-translations.tsx');
698
+ if (await fs.pathExists(wrapperPath)) {
699
+ await fs.remove(wrapperPath);
700
+ }
701
+ }
702
+
703
+ async function ensureEditorUtils(projectDir) {
704
+ const exists = await fs.pathExists(EDITOR_UTILS_SOURCE_DIR);
705
+ if (!exists) {
706
+ return [];
707
+ }
708
+
709
+ const entries = await fs.readdir(EDITOR_UTILS_SOURCE_DIR);
710
+ const utilNames = entries.filter((name) => name.endsWith('.ts')).map((name) => name.replace(/\.ts$/, ''));
711
+
712
+ if (utilNames.length === 0) {
713
+ return [];
714
+ }
715
+
716
+ const destinationDir = resolve(projectDir, 'lib/editor/utils');
717
+ await fs.ensureDir(destinationDir);
718
+
719
+ for (const utilName of utilNames) {
720
+ const sourcePath = resolve(EDITOR_UTILS_SOURCE_DIR, `${utilName}.ts`);
721
+ const destinationPath = resolve(destinationDir, `${utilName}.ts`);
722
+ await fs.copy(sourcePath, destinationPath);
723
+ }
724
+
725
+ return utilNames;
726
+ }
727
+
728
+ async function sanitizeBlockEditorImports(projectDir) {
729
+ const blockEditorPath = resolve(projectDir, 'app/cms/blocks/components/BlockEditorArea.tsx');
730
+ if (!(await fs.pathExists(blockEditorPath))) {
731
+ return;
732
+ }
733
+
734
+ const content = await fs.readFile(blockEditorPath, 'utf8');
735
+ const replacements = [
736
+ { pattern: /(\.\.\/editors\/[A-Za-z0-9_-]+)\.js/g, replacement: '$1.tsx' },
737
+ { pattern: /(\.\.\/actions)\.js/g, replacement: '$1.ts' },
738
+ ];
739
+
740
+ const updated = replacements.reduce(
741
+ (current, { pattern, replacement }) => current.replace(pattern, replacement),
742
+ content,
743
+ );
744
+
745
+ if (updated !== content) {
746
+ await fs.writeFile(blockEditorPath, updated);
747
+ }
748
+ }
749
+
750
+ async function sanitizeUiImports(projectDir) {
751
+ const searchDirs = ['app', 'components', 'context', 'lib'];
752
+ const validExtensions = new Set(['.js', '.jsx', '.ts', '.tsx']);
753
+ const files = [];
754
+
755
+ for (const relativeDir of searchDirs) {
756
+ const absoluteDir = resolve(projectDir, relativeDir);
757
+ if (await fs.pathExists(absoluteDir)) {
758
+ await collectFiles(absoluteDir, files, validExtensions);
759
+ }
760
+ }
761
+
762
+ for (const filePath of files) {
763
+ const original = await fs.readFile(filePath, 'utf8');
764
+ const updated = original.replace(/@nextblock-cms\/ui\/(?!styles\/)[A-Za-z0-9/_-]+/g, '@nextblock-cms/ui');
765
+ if (updated !== original) {
766
+ await fs.writeFile(filePath, updated);
767
+ }
768
+ }
769
+ }
770
+
771
+ async function collectFiles(directory, accumulator, extensions) {
772
+ const entries = await fs.readdir(directory, { withFileTypes: true });
773
+ for (const entry of entries) {
774
+ const fullPath = resolve(directory, entry.name);
775
+ if (entry.isDirectory()) {
776
+ await collectFiles(fullPath, accumulator, extensions);
777
+ } else {
778
+ const dotIndex = entry.name.lastIndexOf('.');
779
+ if (dotIndex !== -1) {
780
+ const ext = entry.name.slice(dotIndex);
781
+ if (extensions.has(ext)) {
782
+ accumulator.push(fullPath);
783
+ }
784
+ }
785
+ }
786
+ }
787
+ }
788
+
789
+ async function ensureUiProxies(projectDir) {
790
+ const proxiesDir = resolve(projectDir, 'lib/ui');
791
+ await fs.ensureDir(proxiesDir);
792
+
793
+ const proxyContent = "export * from '@nextblock-cms/ui';\n";
794
+
795
+ for (const moduleName of UI_PROXY_MODULES) {
796
+ const proxyPath = resolve(proxiesDir, `${moduleName}.ts`);
797
+ if (!(await fs.pathExists(proxyPath))) {
798
+ await fs.outputFile(proxyPath, proxyContent);
799
+ }
800
+ }
801
+ }
802
+
803
+ async function sanitizeLayout(projectDir) {
804
+ await ensureGlobalStyles(projectDir);
805
+ await ensureEditorStyles(projectDir);
806
+
807
+ const layoutPath = resolve(projectDir, 'app/layout.tsx');
808
+ if (!(await fs.pathExists(layoutPath))) {
809
+ return;
810
+ }
811
+
812
+ const requiredImports = [
813
+ "import '@nextblock-cms/ui/styles/globals.css';",
814
+ "import '@nextblock-cms/editor/styles/editor.css';",
815
+ ];
816
+
817
+ const content = await fs.readFile(layoutPath, 'utf8');
818
+ let updated = content.replace(
819
+ /import\s+['"]\.\/globals\.css['"];?\s*/g,
820
+ '',
821
+ );
822
+ updated = updated.replace(
823
+ /import\s+['"]\.\/editor\.css['"];?\s*/g,
824
+ '',
825
+ );
826
+
827
+ const missingImports = requiredImports.filter((statement) => !updated.includes(statement));
828
+ if (missingImports.length > 0) {
829
+ updated = `${missingImports.join('\n')}\n${updated}`;
830
+ }
831
+
832
+ if (updated !== content) {
833
+ await fs.writeFile(layoutPath, updated);
834
+ }
835
+ }
836
+
837
+ async function ensureGlobalStyles(projectDir) {
838
+ const destination = resolve(projectDir, 'app/globals.css');
839
+
840
+ if (!(await fs.pathExists(destination))) {
841
+ return;
842
+ }
843
+
844
+ const content = (await fs.readFile(destination, 'utf8')).trim();
845
+ if (
846
+ content === '' ||
847
+ content.startsWith('/* Project-level overrides') ||
848
+ content.includes('@tailwind base')
849
+ ) {
850
+ await fs.remove(destination);
851
+ }
852
+ }
853
+
854
+ async function ensureEditorStyles(projectDir) {
855
+ const stylesDir = resolve(projectDir, 'app');
856
+ const editorPath = resolve(stylesDir, 'editor.css');
857
+ const dragHandlePath = resolve(stylesDir, 'drag-handle.css');
858
+
859
+ for (const filePath of [editorPath, dragHandlePath]) {
860
+ if (await fs.pathExists(filePath)) {
861
+ const content = (await fs.readFile(filePath, 'utf8')).trim();
862
+ if (
863
+ content === '' ||
864
+ content.startsWith('/* Editor styles placeholder') ||
865
+ content.includes("@nextblock-cms/editor/styles")
866
+ ) {
867
+ await fs.remove(filePath);
868
+ }
869
+ }
870
+ }
871
+ }
872
+
873
+ async function sanitizeTailwindConfig(projectDir) {
874
+ const tailwindConfigPath = resolve(projectDir, 'tailwind.config.js');
875
+ const content = `/** @type {import('tailwindcss').Config} */
876
+ module.exports = {
877
+ darkMode: ['class'],
878
+ content: [
879
+ './app/**/*.{js,ts,jsx,tsx,mdx}',
880
+ './components/**/*.{js,ts,jsx,tsx,mdx}',
881
+ './context/**/*.{js,ts,jsx,tsx,mdx}',
882
+ './lib/**/*.{js,ts,jsx,tsx,mdx}',
883
+ './node_modules/@nextblock-cms/ui/**/*.{js,ts,jsx,tsx}',
884
+ './node_modules/@nextblock-cms/editor/**/*.{js,ts,jsx,tsx}',
885
+ ],
886
+ safelist: [
887
+ 'animate-enter',
888
+ 'animate-leave',
889
+ 'dark',
890
+ 'text-primary',
891
+ 'text-secondary',
892
+ 'text-accent',
893
+ 'text-muted',
894
+ 'text-destructive',
895
+ 'text-background',
896
+ ],
897
+ prefix: '',
898
+ theme: {
899
+ container: {
900
+ center: true,
901
+ padding: '2rem',
902
+ screens: {
903
+ '2xl': '1400px',
904
+ },
905
+ },
906
+ extend: {
907
+ colors: {
908
+ border: 'hsl(var(--border))',
909
+ input: 'hsl(var(--input))',
910
+ ring: 'hsl(var(--ring))',
911
+ background: 'hsl(var(--background))',
912
+ foreground: 'hsl(var(--foreground))',
913
+ primary: {
914
+ DEFAULT: 'hsl(var(--primary))',
915
+ foreground: 'hsl(var(--primary-foreground))',
916
+ },
917
+ secondary: {
918
+ DEFAULT: 'hsl(var(--secondary))',
919
+ foreground: 'hsl(var(--secondary-foreground))',
920
+ },
921
+ destructive: {
922
+ DEFAULT: 'hsl(var(--destructive))',
923
+ foreground: 'hsl(var(--destructive-foreground))',
924
+ },
925
+ muted: {
926
+ DEFAULT: 'hsl(var(--muted))',
927
+ foreground: 'hsl(var(--muted-foreground))',
928
+ },
929
+ warning: {
930
+ DEFAULT: 'hsl(var(--warning))',
931
+ foreground: 'hsl(var(--warning-foreground))',
932
+ },
933
+ accent: {
934
+ DEFAULT: 'hsl(var(--accent))',
935
+ foreground: 'hsl(var(--accent-foreground))',
936
+ },
937
+ popover: {
938
+ DEFAULT: 'hsl(var(--popover))',
939
+ foreground: 'hsl(var(--popover-foreground))',
940
+ },
941
+ card: {
942
+ DEFAULT: 'hsl(var(--card))',
943
+ foreground: 'hsl(var(--card-foreground))',
944
+ },
945
+ },
946
+ borderRadius: {
947
+ lg: 'var(--radius)',
948
+ md: 'calc(var(--radius) - 2px)',
949
+ sm: 'calc(var(--radius) - 4px)',
950
+ },
951
+ keyframes: {
952
+ 'accordion-down': {
953
+ from: { height: '0' },
954
+ to: { height: 'var(--radix-accordion-content-height)' },
955
+ },
956
+ 'accordion-up': {
957
+ from: { height: 'var(--radix-accordion-content-height)' },
958
+ to: { height: '0' },
959
+ },
960
+ },
961
+ animation: {
962
+ 'accordion-down': 'accordion-down 0.2s ease-out',
963
+ 'accordion-up': 'accordion-up 0.2s ease-out',
964
+ },
965
+ },
966
+ },
967
+ plugins: [require('tailwindcss-animate')],
968
+ };
969
+ `;
970
+
971
+ await fs.writeFile(tailwindConfigPath, content);
972
+ }
973
+
974
+ async function normalizeTsconfig(projectDir) {
975
+ const tsconfigPath = resolve(projectDir, 'tsconfig.json');
976
+ if (!(await fs.pathExists(tsconfigPath))) {
977
+ return;
978
+ }
979
+
980
+ const tsconfig = await fs.readJSON(tsconfigPath);
981
+ if ('extends' in tsconfig) {
982
+ delete tsconfig.extends;
983
+ }
984
+
985
+ if ('references' in tsconfig) {
986
+ delete tsconfig.references;
987
+ }
988
+ const defaultInclude = new Set([
989
+ 'next-env.d.ts',
990
+ '**/*.ts',
991
+ '**/*.tsx',
992
+ '**/*.js',
993
+ '**/*.jsx',
994
+ '.next/types/**/*.ts',
995
+ ]);
996
+
997
+ if (Array.isArray(tsconfig.include)) {
998
+ for (const entry of tsconfig.include) {
999
+ if (typeof entry === 'string' && !entry.includes('../')) {
1000
+ defaultInclude.add(entry);
1001
+ }
1002
+ }
1003
+ }
1004
+
1005
+ tsconfig.include = Array.from(defaultInclude);
1006
+
1007
+ const defaultExclude = new Set(['node_modules']);
1008
+ if (Array.isArray(tsconfig.exclude)) {
1009
+ for (const entry of tsconfig.exclude) {
1010
+ if (typeof entry === 'string' && !entry.includes('../')) {
1011
+ defaultExclude.add(entry);
1012
+ }
1013
+ }
1014
+ }
1015
+
1016
+ tsconfig.exclude = Array.from(defaultExclude);
1017
+
1018
+ tsconfig.compilerOptions = {
1019
+ ...(tsconfig.compilerOptions ?? {}),
1020
+ baseUrl: '.',
1021
+ skipLibCheck: true,
1022
+ };
1023
+
1024
+ const compilerOptions = tsconfig.compilerOptions;
1025
+ compilerOptions.paths = {
1026
+ ...(compilerOptions.paths ?? {}),
1027
+ '@/*': ['./*'],
1028
+ '@nextblock-cms/ui/*': ['./lib/ui/*'],
1029
+ '@nextblock-cms/editor/utils/*': ['./lib/editor/utils/*'],
1030
+ };
1031
+
1032
+ await fs.writeJSON(tsconfigPath, tsconfig, { spaces: 2 });
1033
+ }
1034
+
1035
+ async function sanitizeNextConfig(projectDir, editorUtilNames = []) {
1036
+ const nextConfigPath = resolve(projectDir, 'next.config.js');
1037
+ const content = buildNextConfigContent(editorUtilNames);
1038
+ await fs.writeFile(nextConfigPath, content);
1039
+ }
1040
+
1041
+ async function transformPackageJson(projectDir) {
1042
+ const packageJsonPath = resolve(projectDir, 'package.json');
1043
+ if (!(await fs.pathExists(packageJsonPath))) {
1044
+ return;
1045
+ }
1046
+
1047
+ const packageJson = await fs.readJSON(packageJsonPath);
1048
+ const projectName = basename(projectDir);
1049
+
1050
+ if (projectName) {
1051
+ packageJson.name = projectName;
1052
+ }
1053
+
1054
+ packageJson.version = packageJson.version ?? '0.1.0';
1055
+ packageJson.private = packageJson.private ?? true;
1056
+
1057
+ packageJson.dependencies = packageJson.dependencies ?? {};
1058
+
1059
+ for (const [pkgName, manifestPath] of Object.entries(PACKAGE_VERSION_SOURCES)) {
1060
+ if (pkgName in packageJson.dependencies) {
1061
+ const current = packageJson.dependencies[pkgName];
1062
+ if (typeof current === 'string' && current.startsWith('workspace:')) {
1063
+ let versionSpecifier = 'latest';
1064
+ try {
1065
+ const manifest = await fs.readJSON(manifestPath);
1066
+ if (manifest.version) {
1067
+ versionSpecifier = `^${manifest.version}`;
1068
+ }
1069
+ } catch {
1070
+ versionSpecifier = 'latest';
1071
+ }
1072
+
1073
+ packageJson.dependencies[pkgName] = versionSpecifier;
1074
+ }
1075
+ }
1076
+ }
1077
+
1078
+ await fs.writeJSON(packageJsonPath, packageJson, { spaces: 2 });
1079
+ }
1080
+
1081
+ async function installDependencies(projectDir) {
1082
+ const npmCommand = IS_WINDOWS ? 'npm.cmd' : 'npm';
1083
+ console.log(chalk.blue('Installing dependencies with npm...'));
1084
+ await runCommand(npmCommand, ['install'], { cwd: projectDir });
1085
+ console.log(chalk.green('Dependencies installed.'));
1086
+ }
1087
+
1088
+ async function initializeGit(projectDir) {
1089
+ const gitDirectory = resolve(projectDir, '.git');
1090
+ if (await fs.pathExists(gitDirectory)) {
1091
+ return;
1092
+ }
1093
+
1094
+ try {
1095
+ console.log(chalk.blue('Initializing Git repository...'));
1096
+ await runCommand('git', ['init'], { cwd: projectDir });
1097
+ console.log(chalk.green('Git repository initialized.'));
1098
+ } catch (error) {
1099
+ console.warn(
1100
+ chalk.yellow(
1101
+ `Skipping Git initialization: ${error instanceof Error ? error.message : String(error)}`,
1102
+ ),
1103
+ );
1104
+ }
1105
+ }
1106
+
1107
+ function runCommand(command, args, options = {}) {
1108
+ return new Promise((resolve, reject) => {
1109
+ const child = spawn(command, args, {
1110
+ stdio: 'inherit',
1111
+ shell: IS_WINDOWS,
1112
+ ...options,
1113
+ });
1114
+
1115
+ child.on('error', (error) => {
1116
+ reject(error);
1117
+ });
1118
+
1119
+ child.on('close', (code) => {
1120
+ if (code === 0) {
1121
+ resolve();
1122
+ } else {
1123
+ reject(new Error(`${command} exited with code ${code}`));
1124
+ }
1125
+ });
1126
+ });
1127
+ }
1128
+
1129
+ function buildNextConfigContent(editorUtilNames) {
1130
+ const aliasLines = [];
1131
+
1132
+ for (const moduleName of UI_PROXY_MODULES) {
1133
+ aliasLines.push(
1134
+ " '@nextblock-cms/ui/" + moduleName + "': path.join(process.cwd(), 'lib/ui/" + moduleName + "'),",
1135
+ );
1136
+ }
1137
+
1138
+ for (const moduleName of editorUtilNames) {
1139
+ aliasLines.push(
1140
+ " '@nextblock-cms/editor/utils/" +
1141
+ moduleName +
1142
+ "': path.join(process.cwd(), 'lib/editor/utils/" +
1143
+ moduleName +
1144
+ "'),",
1145
+ );
1146
+ }
1147
+
1148
+ const lines = [
1149
+ '//@ts-check',
1150
+ '',
1151
+ "const path = require('path');",
1152
+ "const webpack = require('webpack');",
1153
+ '',
1154
+ '/**',
1155
+ " * @type {import('next').NextConfig}",
1156
+ ' **/',
1157
+ 'const nextConfig = {',
1158
+ " outputFileTracingRoot: path.join(__dirname),",
1159
+ ' env: {',
1160
+ " NEXT_PUBLIC_SUPABASE_URL: process.env.NEXT_PUBLIC_SUPABASE_URL,",
1161
+ " NEXT_PUBLIC_SUPABASE_ANON_KEY: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,",
1162
+ ' },',
1163
+ ' images: {',
1164
+ " formats: ['image/avif', 'image/webp'],",
1165
+ ' imageSizes: [16, 32, 48, 64, 96, 128, 256, 384, 512],',
1166
+ ' deviceSizes: [320, 480, 640, 750, 828, 1080, 1200, 1440, 1920, 2048, 2560],',
1167
+ ' minimumCacheTTL: 31536000,',
1168
+ " dangerouslyAllowSVG: false,",
1169
+ " contentSecurityPolicy: \"default-src 'self'; script-src 'none'; sandbox;\",",
1170
+ ' remotePatterns: [',
1171
+ " { protocol: 'https', hostname: 'pub-a31e3f1a87d144898aeb489a8221f92e.r2.dev' },",
1172
+ " { protocol: 'https', hostname: 'e260676f72b0b18314b868f136ed72ae.r2.cloudflarestorage.com' },",
1173
+ ' ...(process.env.NEXT_PUBLIC_URL',
1174
+ ' ? [',
1175
+ ' {',
1176
+ " protocol: /** @type {'http' | 'https'} */ (new URL(process.env.NEXT_PUBLIC_URL).protocol.slice(0, -1)),",
1177
+ " hostname: new URL(process.env.NEXT_PUBLIC_URL).hostname,",
1178
+ ' },',
1179
+ ' ]',
1180
+ ' : []),',
1181
+ ' ],',
1182
+ ' },',
1183
+ ' experimental: {',
1184
+ " optimizeCss: true,",
1185
+ " cssChunking: 'strict',",
1186
+ ' },',
1187
+ " transpilePackages: ['@nextblock-cms/utils', '@nextblock-cms/ui', '@nextblock-cms/editor'],",
1188
+ ' webpack: (config, { isServer }) => {',
1189
+ ' config.resolve = config.resolve || {};',
1190
+ ' config.resolve.alias = {',
1191
+ ' ...(config.resolve.alias ?? {}),',
1192
+ ];
1193
+
1194
+ if (aliasLines.length > 0) {
1195
+ lines.push(...aliasLines);
1196
+ }
1197
+
1198
+ lines.push(' };', '');
1199
+
1200
+ if (editorUtilNames.length > 0) {
1201
+ lines.push(
1202
+ ' const editorUtilsShims = ' + JSON.stringify(editorUtilNames) + ';',
1203
+ ' config.plugins = config.plugins || [];',
1204
+ ' for (const utilName of editorUtilsShims) {',
1205
+ " const shimPath = path.join(process.cwd(), 'lib/editor/utils', utilName);",
1206
+ ' config.plugins.push(',
1207
+ " new webpack.NormalModuleReplacementPlugin(new RegExp('^@nextblock-cms/editor/utils/' + utilName + '$'), shimPath),",
1208
+ ' );',
1209
+ ' config.plugins.push(',
1210
+ " new webpack.NormalModuleReplacementPlugin(new RegExp('^./utils/' + utilName + '$'), shimPath),",
1211
+ ' );',
1212
+ ' }',
1213
+ '',
1214
+ );
1215
+ }
1216
+
1217
+ lines.push(
1218
+ ' if (!isServer) {',
1219
+ ' config.module = config.module || {};',
1220
+ ' config.module.rules = config.module.rules || [];',
1221
+ ' config.module.rules.push({',
1222
+ " test: /\\.svg$/i,",
1223
+ " issuer: /\\.[jt]sx?$/,",
1224
+ " use: ['@svgr/webpack'],",
1225
+ ' });',
1226
+ '',
1227
+ ' config.optimization = {',
1228
+ ' ...(config.optimization ?? {}),',
1229
+ ' splitChunks: {',
1230
+ ' ...((config.optimization ?? {}).splitChunks ?? {}),',
1231
+ ' cacheGroups: {',
1232
+ ' ...(((config.optimization ?? {}).splitChunks ?? {}).cacheGroups ?? {}),',
1233
+ ' tiptap: {',
1234
+ " test: /[\\\\/]node_modules[\\\\/](@tiptap|prosemirror)[\\\\/]/,",
1235
+ " name: 'tiptap',",
1236
+ " chunks: 'async',",
1237
+ ' priority: 30,',
1238
+ ' reuseExistingChunk: true,',
1239
+ ' },',
1240
+ ' tiptapExtensions: {',
1241
+ " test: /[\\\\/](tiptap-extensions|RichTextEditor|MenuBar|MediaLibraryModal)[\\\\/]/,",
1242
+ " name: 'tiptap-extensions',",
1243
+ " chunks: 'async',",
1244
+ ' priority: 25,',
1245
+ ' reuseExistingChunk: true,',
1246
+ ' },',
1247
+ ' },',
1248
+ ' },',
1249
+ ' };',
1250
+ ' }',
1251
+ '',
1252
+ ' return config;',
1253
+ ' },',
1254
+ ' turbopack: {',
1255
+ ' // Turbopack-specific options can be configured here if needed.',
1256
+ ' },',
1257
+ ' compiler: {',
1258
+ " removeConsole: process.env.NODE_ENV === 'production',",
1259
+ ' },',
1260
+ '};',
1261
+ '',
1262
+ 'module.exports = nextConfig;',
1263
+ );
1264
+
1265
+ return lines.join('\n');
1266
+ }