create-nextblock 0.0.4 → 0.2.0
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 +1193 -920
- package/package.json +6 -2
- package/scripts/sync-template.js +279 -276
- package/templates/nextblock-template/.env.example +1 -14
- package/templates/nextblock-template/README.md +1 -1
- package/templates/nextblock-template/app/[slug]/PageClientContent.tsx +67 -40
- package/templates/nextblock-template/app/[slug]/page.tsx +45 -10
- package/templates/nextblock-template/app/[slug]/page.utils.ts +92 -45
- package/templates/nextblock-template/app/api/revalidate/route.ts +15 -15
- package/templates/nextblock-template/app/{blog → article}/[slug]/PostClientContent.tsx +45 -43
- package/templates/nextblock-template/app/{blog → article}/[slug]/page.tsx +108 -98
- package/templates/nextblock-template/app/{blog → article}/[slug]/page.utils.ts +10 -3
- package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +25 -19
- package/templates/nextblock-template/app/cms/blocks/actions.ts +1 -1
- package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +1 -1
- package/templates/nextblock-template/app/cms/posts/actions.ts +47 -44
- package/templates/nextblock-template/app/cms/posts/page.tsx +2 -2
- package/templates/nextblock-template/app/cms/settings/languages/actions.ts +16 -15
- package/templates/nextblock-template/app/layout.tsx +9 -9
- package/templates/nextblock-template/app/lib/sitemap-utils.ts +52 -52
- package/templates/nextblock-template/app/sitemap.xml/route.ts +2 -2
- package/templates/nextblock-template/components/ResponsiveNav.tsx +22 -16
- package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -7
- package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +25 -26
- package/templates/nextblock-template/package.json +1 -1
- package/templates/nextblock-template/proxy.ts +4 -4
- package/templates/nextblock-template/public/images/NBcover.webp +0 -0
- package/templates/nextblock-template/public/images/developer.webp +0 -0
- package/templates/nextblock-template/public/images/nextblock-logo-small.webp +0 -0
- package/templates/nextblock-template/public/images/nx-graph.webp +0 -0
- package/templates/nextblock-template/public/images/programmer-upscaled.webp +0 -0
- package/templates/nextblock-template/scripts/backup.js +142 -47
- package/templates/nextblock-template/scripts/restore-working.js +102 -0
- package/templates/nextblock-template/scripts/restore.js +434 -0
- package/templates/nextblock-template/app/blog/page.tsx +0 -77
- package/templates/nextblock-template/backup/backup_2025-06-19.sql +0 -8057
- package/templates/nextblock-template/backup/backup_2025-06-20.sql +0 -8159
- package/templates/nextblock-template/backup/backup_2025-07-08.sql +0 -8411
- package/templates/nextblock-template/backup/backup_2025-07-09.sql +0 -8442
- package/templates/nextblock-template/backup/backup_2025-07-10.sql +0 -8442
- package/templates/nextblock-template/backup/backup_2025-10-01.sql +0 -8803
- package/templates/nextblock-template/backup/backup_2025-10-02.sql +0 -9749
package/bin/create-nextblock.js
CHANGED
|
@@ -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
|
-
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
'
|
|
22
|
-
'
|
|
23
|
-
'
|
|
24
|
-
'
|
|
25
|
-
'
|
|
26
|
-
'
|
|
27
|
-
'
|
|
28
|
-
'
|
|
29
|
-
'
|
|
30
|
-
'
|
|
31
|
-
'
|
|
32
|
-
'
|
|
33
|
-
'
|
|
34
|
-
'
|
|
35
|
-
'
|
|
36
|
-
'
|
|
37
|
-
'
|
|
38
|
-
'
|
|
39
|
-
'
|
|
40
|
-
'
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
'@nextblock-cms/
|
|
46
|
-
'@nextblock-cms/
|
|
47
|
-
'@nextblock-cms/
|
|
48
|
-
'@nextblock-cms/
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
.
|
|
54
|
-
.
|
|
55
|
-
.
|
|
56
|
-
.option('-
|
|
57
|
-
.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
console.log(
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
131
|
-
|
|
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
|
|
137
|
+
console.log(chalk.yellow('Skipping interactive setup wizard because --yes was provided.'));
|
|
149
138
|
}
|
|
150
139
|
|
|
151
|
-
await
|
|
152
|
-
console.log(chalk.green('
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
|
173
|
-
const
|
|
174
|
-
|
|
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
|
-
|
|
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
|
-
|
|
188
|
+
clack.step('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
|
-
|
|
195
|
-
|
|
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
|
-
|
|
203
|
-
|
|
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
|
-
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
await fs.remove(backupDir);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
228
|
+
clack.step('Generating local secrets...');
|
|
229
|
+
const revalidationToken = crypto.randomBytes(32).toString('hex');
|
|
230
|
+
const supabaseUrl = `https://${projectId}.supabase.co`;
|
|
214
231
|
|
|
215
|
-
|
|
216
|
-
const
|
|
217
|
-
const
|
|
218
|
-
const
|
|
232
|
+
const dbHost = `db.${projectId}.supabase.co`;
|
|
233
|
+
const dbUser = 'postgres';
|
|
234
|
+
const dbPassword = supabaseKeys.dbPassword;
|
|
235
|
+
const dbName = 'postgres';
|
|
219
236
|
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
'#
|
|
225
|
-
'
|
|
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
|
-
'#
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
'#
|
|
247
|
-
|
|
251
|
+
'# Revalidation',
|
|
252
|
+
`REVALIDATE_SECRET_TOKEN=${revalidationToken}`,
|
|
248
253
|
'',
|
|
249
|
-
'# Misc',
|
|
250
|
-
'.DS_Store',
|
|
251
254
|
];
|
|
252
255
|
|
|
253
|
-
let
|
|
254
|
-
if (await fs.pathExists(
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
384
|
-
|
|
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
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
276
|
+
clack.step('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
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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
|
-
|
|
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
|
|
555
|
-
|
|
556
|
-
|
|
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
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
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
|
-
|
|
689
|
-
|
|
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
|
-
|
|
778
|
-
|
|
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
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
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
|
-
|
|
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
|
-
|
|
809
|
-
|
|
810
|
-
|
|
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
|
|
816
|
-
const
|
|
817
|
-
if (
|
|
421
|
+
async function ensureEmptyDirectory(projectDir) {
|
|
422
|
+
const exists = await fs.pathExists(projectDir);
|
|
423
|
+
if (!exists) {
|
|
818
424
|
return;
|
|
819
425
|
}
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
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
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
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
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
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
|
-
|
|
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
|
+
}
|