create-nextblock 0.0.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 (206) hide show
  1. package/bin/create-nextblock.js +997 -0
  2. package/package.json +25 -0
  3. package/scripts/sync-template.js +284 -0
  4. package/templates/nextblock-template/.env.example +37 -0
  5. package/templates/nextblock-template/.swcrc +30 -0
  6. package/templates/nextblock-template/README.md +194 -0
  7. package/templates/nextblock-template/app/(auth-pages)/forgot-password/page.tsx +57 -0
  8. package/templates/nextblock-template/app/(auth-pages)/layout.tsx +9 -0
  9. package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +28 -0
  10. package/templates/nextblock-template/app/(auth-pages)/sign-in/page.tsx +67 -0
  11. package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +70 -0
  12. package/templates/nextblock-template/app/ToasterProvider.tsx +17 -0
  13. package/templates/nextblock-template/app/[slug]/PageClientContent.tsx +147 -0
  14. package/templates/nextblock-template/app/[slug]/page.tsx +145 -0
  15. package/templates/nextblock-template/app/[slug]/page.utils.ts +183 -0
  16. package/templates/nextblock-template/app/actions/email.ts +31 -0
  17. package/templates/nextblock-template/app/actions/formActions.ts +65 -0
  18. package/templates/nextblock-template/app/actions/languageActions.ts +130 -0
  19. package/templates/nextblock-template/app/actions/postActions.ts +80 -0
  20. package/templates/nextblock-template/app/actions.ts +146 -0
  21. package/templates/nextblock-template/app/api/process-image/route.ts +210 -0
  22. package/templates/nextblock-template/app/api/revalidate/route.ts +86 -0
  23. package/templates/nextblock-template/app/api/revalidate-log/route.ts +23 -0
  24. package/templates/nextblock-template/app/api/upload/presigned-url/route.ts +106 -0
  25. package/templates/nextblock-template/app/api/upload/proxy/route.ts +84 -0
  26. package/templates/nextblock-template/app/auth/callback/route.ts +58 -0
  27. package/templates/nextblock-template/app/blog/[slug]/PostClientContent.tsx +169 -0
  28. package/templates/nextblock-template/app/blog/[slug]/page.tsx +177 -0
  29. package/templates/nextblock-template/app/blog/[slug]/page.utils.ts +136 -0
  30. package/templates/nextblock-template/app/blog/page.tsx +77 -0
  31. package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +321 -0
  32. package/templates/nextblock-template/app/cms/blocks/actions.ts +434 -0
  33. package/templates/nextblock-template/app/cms/blocks/components/BackgroundSelector.tsx +348 -0
  34. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +567 -0
  35. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +98 -0
  36. package/templates/nextblock-template/app/cms/blocks/components/BlockTypeCard.tsx +58 -0
  37. package/templates/nextblock-template/app/cms/blocks/components/BlockTypeSelector.tsx +62 -0
  38. package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +276 -0
  39. package/templates/nextblock-template/app/cms/blocks/components/DeleteBlockButtonClient.tsx +47 -0
  40. package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +182 -0
  41. package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +120 -0
  42. package/templates/nextblock-template/app/cms/blocks/components/SectionConfigPanel.tsx +133 -0
  43. package/templates/nextblock-template/app/cms/blocks/components/SortableBlockItem.tsx +46 -0
  44. package/templates/nextblock-template/app/cms/blocks/editors/ButtonBlockEditor.tsx +85 -0
  45. package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +182 -0
  46. package/templates/nextblock-template/app/cms/blocks/editors/HeadingBlockEditor.tsx +111 -0
  47. package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +150 -0
  48. package/templates/nextblock-template/app/cms/blocks/editors/PostsGridBlockEditor.tsx +79 -0
  49. package/templates/nextblock-template/app/cms/blocks/editors/SectionBlockEditor.tsx +337 -0
  50. package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +81 -0
  51. package/templates/nextblock-template/app/cms/blocks/editors/VideoEmbedBlockEditor.tsx +64 -0
  52. package/templates/nextblock-template/app/cms/components/ConfirmationModal.tsx +51 -0
  53. package/templates/nextblock-template/app/cms/components/ContentLanguageSwitcher.tsx +145 -0
  54. package/templates/nextblock-template/app/cms/components/CopyContentFromLanguage.tsx +203 -0
  55. package/templates/nextblock-template/app/cms/components/LanguageFilterSelect.tsx +69 -0
  56. package/templates/nextblock-template/app/cms/dashboard/page.tsx +247 -0
  57. package/templates/nextblock-template/app/cms/layout.tsx +10 -0
  58. package/templates/nextblock-template/app/cms/media/UploadFolderContext.tsx +22 -0
  59. package/templates/nextblock-template/app/cms/media/[id]/edit/page.tsx +80 -0
  60. package/templates/nextblock-template/app/cms/media/actions.ts +577 -0
  61. package/templates/nextblock-template/app/cms/media/components/DeleteMediaButtonClient.tsx +53 -0
  62. package/templates/nextblock-template/app/cms/media/components/FolderNavigator.tsx +273 -0
  63. package/templates/nextblock-template/app/cms/media/components/FolderTree.tsx +122 -0
  64. package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +157 -0
  65. package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +275 -0
  66. package/templates/nextblock-template/app/cms/media/components/MediaImage.tsx +70 -0
  67. package/templates/nextblock-template/app/cms/media/components/MediaPickerDialog.tsx +195 -0
  68. package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +362 -0
  69. package/templates/nextblock-template/app/cms/media/page.tsx +120 -0
  70. package/templates/nextblock-template/app/cms/navigation/[id]/edit/page.tsx +101 -0
  71. package/templates/nextblock-template/app/cms/navigation/actions.ts +358 -0
  72. package/templates/nextblock-template/app/cms/navigation/components/DeleteNavItemButton.tsx +52 -0
  73. package/templates/nextblock-template/app/cms/navigation/components/NavigationItemForm.tsx +248 -0
  74. package/templates/nextblock-template/app/cms/navigation/components/NavigationLanguageSwitcher.tsx +132 -0
  75. package/templates/nextblock-template/app/cms/navigation/components/NavigationMenuDnd.tsx +701 -0
  76. package/templates/nextblock-template/app/cms/navigation/components/SortableNavItem.tsx +98 -0
  77. package/templates/nextblock-template/app/cms/navigation/new/page.tsx +26 -0
  78. package/templates/nextblock-template/app/cms/navigation/page.tsx +102 -0
  79. package/templates/nextblock-template/app/cms/navigation/utils.ts +51 -0
  80. package/templates/nextblock-template/app/cms/pages/[id]/edit/EditPageClient.tsx +121 -0
  81. package/templates/nextblock-template/app/cms/pages/[id]/edit/page.tsx +79 -0
  82. package/templates/nextblock-template/app/cms/pages/actions.ts +241 -0
  83. package/templates/nextblock-template/app/cms/pages/components/DeletePageButtonClient.tsx +47 -0
  84. package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +253 -0
  85. package/templates/nextblock-template/app/cms/pages/new/page.tsx +52 -0
  86. package/templates/nextblock-template/app/cms/pages/page.tsx +232 -0
  87. package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +183 -0
  88. package/templates/nextblock-template/app/cms/posts/actions.ts +309 -0
  89. package/templates/nextblock-template/app/cms/posts/components/DeletePostButtonClient.tsx +55 -0
  90. package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +419 -0
  91. package/templates/nextblock-template/app/cms/posts/new/page.tsx +21 -0
  92. package/templates/nextblock-template/app/cms/posts/page.tsx +192 -0
  93. package/templates/nextblock-template/app/cms/revisions/JsonDiffView.tsx +86 -0
  94. package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +201 -0
  95. package/templates/nextblock-template/app/cms/revisions/actions.ts +84 -0
  96. package/templates/nextblock-template/app/cms/revisions/service.ts +344 -0
  97. package/templates/nextblock-template/app/cms/revisions/utils.ts +127 -0
  98. package/templates/nextblock-template/app/cms/settings/copyright/actions.ts +68 -0
  99. package/templates/nextblock-template/app/cms/settings/copyright/components/CopyrightForm.tsx +78 -0
  100. package/templates/nextblock-template/app/cms/settings/copyright/page.tsx +32 -0
  101. package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +117 -0
  102. package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +216 -0
  103. package/templates/nextblock-template/app/cms/settings/languages/[id]/edit/page.tsx +77 -0
  104. package/templates/nextblock-template/app/cms/settings/languages/actions.ts +261 -0
  105. package/templates/nextblock-template/app/cms/settings/languages/components/DeleteLanguageButton.tsx +76 -0
  106. package/templates/nextblock-template/app/cms/settings/languages/components/LanguageForm.tsx +167 -0
  107. package/templates/nextblock-template/app/cms/settings/languages/new/page.tsx +34 -0
  108. package/templates/nextblock-template/app/cms/settings/languages/page.tsx +156 -0
  109. package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +19 -0
  110. package/templates/nextblock-template/app/cms/settings/logos/actions.ts +114 -0
  111. package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +177 -0
  112. package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +11 -0
  113. package/templates/nextblock-template/app/cms/settings/logos/page.tsx +118 -0
  114. package/templates/nextblock-template/app/cms/settings/logos/types.ts +8 -0
  115. package/templates/nextblock-template/app/cms/users/[id]/edit/page.tsx +91 -0
  116. package/templates/nextblock-template/app/cms/users/actions.ts +156 -0
  117. package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +71 -0
  118. package/templates/nextblock-template/app/cms/users/components/UserForm.tsx +138 -0
  119. package/templates/nextblock-template/app/cms/users/page.tsx +183 -0
  120. package/templates/nextblock-template/app/favicon.ico +0 -0
  121. package/templates/nextblock-template/app/globals.css +401 -0
  122. package/templates/nextblock-template/app/layout.tsx +191 -0
  123. package/templates/nextblock-template/app/lib/sitemap-utils.ts +68 -0
  124. package/templates/nextblock-template/app/page.tsx +109 -0
  125. package/templates/nextblock-template/app/providers.tsx +43 -0
  126. package/templates/nextblock-template/app/robots.txt/route.ts +19 -0
  127. package/templates/nextblock-template/app/sitemap.xml/route.ts +63 -0
  128. package/templates/nextblock-template/app/unauthorized/page.tsx +27 -0
  129. package/templates/nextblock-template/backup/backup_2025-06-19.sql +8057 -0
  130. package/templates/nextblock-template/backup/backup_2025-06-20.sql +8159 -0
  131. package/templates/nextblock-template/backup/backup_2025-07-08.sql +8411 -0
  132. package/templates/nextblock-template/backup/backup_2025-07-09.sql +8442 -0
  133. package/templates/nextblock-template/backup/backup_2025-07-10.sql +8442 -0
  134. package/templates/nextblock-template/backup/backup_2025-10-01.sql +8803 -0
  135. package/templates/nextblock-template/backup/backup_2025-10-02.sql +9749 -0
  136. package/templates/nextblock-template/components/BlockRenderer.tsx +119 -0
  137. package/templates/nextblock-template/components/FooterNavigation.tsx +33 -0
  138. package/templates/nextblock-template/components/Header.tsx +42 -0
  139. package/templates/nextblock-template/components/HtmlScriptExecutor.tsx +47 -0
  140. package/templates/nextblock-template/components/LanguageSwitcher.tsx +103 -0
  141. package/templates/nextblock-template/components/ResponsiveNav.tsx +372 -0
  142. package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +17 -0
  143. package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +93 -0
  144. package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +180 -0
  145. package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -0
  146. package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +69 -0
  147. package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +98 -0
  148. package/templates/nextblock-template/components/blocks/renderers/HeadingBlockRenderer.tsx +41 -0
  149. package/templates/nextblock-template/components/blocks/renderers/HeroBlockRenderer.tsx +240 -0
  150. package/templates/nextblock-template/components/blocks/renderers/ImageBlockRenderer.tsx +79 -0
  151. package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +33 -0
  152. package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +189 -0
  153. package/templates/nextblock-template/components/blocks/renderers/TextBlockRenderer.tsx +31 -0
  154. package/templates/nextblock-template/components/blocks/renderers/VideoEmbedBlockRenderer.tsx +59 -0
  155. package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +51 -0
  156. package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +40 -0
  157. package/templates/nextblock-template/components/blocks/types.ts +8 -0
  158. package/templates/nextblock-template/components/env-var-warning.tsx +33 -0
  159. package/templates/nextblock-template/components/form-message.tsx +26 -0
  160. package/templates/nextblock-template/components/header-auth.tsx +71 -0
  161. package/templates/nextblock-template/components/submit-button.tsx +23 -0
  162. package/templates/nextblock-template/components/theme-switcher.tsx +78 -0
  163. package/templates/nextblock-template/context/AuthContext.tsx +138 -0
  164. package/templates/nextblock-template/context/CurrentContentContext.tsx +42 -0
  165. package/templates/nextblock-template/context/LanguageContext.tsx +206 -0
  166. package/templates/nextblock-template/docs/cms-application-overview.md +56 -0
  167. package/templates/nextblock-template/docs/cms-architecture-overview.md +73 -0
  168. package/templates/nextblock-template/docs/files-structure.md +426 -0
  169. package/templates/nextblock-template/docs/tiptap-bundle-optimization-summary.md +174 -0
  170. package/templates/nextblock-template/eslint.config.mjs +28 -0
  171. package/templates/nextblock-template/index.d.ts +5 -0
  172. package/templates/nextblock-template/lib/blocks/README.md +670 -0
  173. package/templates/nextblock-template/lib/blocks/blockRegistry.ts +1001 -0
  174. package/templates/nextblock-template/lib/ui/ColorPicker.ts +1 -0
  175. package/templates/nextblock-template/lib/ui/ConfirmationDialog.ts +1 -0
  176. package/templates/nextblock-template/lib/ui/CustomSelectWithInput.ts +1 -0
  177. package/templates/nextblock-template/lib/ui/Skeleton.ts +1 -0
  178. package/templates/nextblock-template/lib/ui/avatar.ts +1 -0
  179. package/templates/nextblock-template/lib/ui/badge.ts +1 -0
  180. package/templates/nextblock-template/lib/ui/button.ts +1 -0
  181. package/templates/nextblock-template/lib/ui/card.ts +1 -0
  182. package/templates/nextblock-template/lib/ui/checkbox.ts +1 -0
  183. package/templates/nextblock-template/lib/ui/dialog.ts +1 -0
  184. package/templates/nextblock-template/lib/ui/dropdown-menu.ts +1 -0
  185. package/templates/nextblock-template/lib/ui/input.ts +1 -0
  186. package/templates/nextblock-template/lib/ui/label.ts +1 -0
  187. package/templates/nextblock-template/lib/ui/popover.ts +1 -0
  188. package/templates/nextblock-template/lib/ui/progress.ts +1 -0
  189. package/templates/nextblock-template/lib/ui/select.ts +1 -0
  190. package/templates/nextblock-template/lib/ui/separator.ts +1 -0
  191. package/templates/nextblock-template/lib/ui/table.ts +1 -0
  192. package/templates/nextblock-template/lib/ui/textarea.ts +1 -0
  193. package/templates/nextblock-template/lib/ui/tooltip.ts +1 -0
  194. package/templates/nextblock-template/lib/ui/ui.ts +1 -0
  195. package/templates/nextblock-template/middleware.ts +206 -0
  196. package/templates/nextblock-template/next-env.d.ts +6 -0
  197. package/templates/nextblock-template/next.config.js +99 -0
  198. package/templates/nextblock-template/package.json +52 -0
  199. package/templates/nextblock-template/postcss.config.js +6 -0
  200. package/templates/nextblock-template/project.json +7 -0
  201. package/templates/nextblock-template/public/.gitkeep +0 -0
  202. package/templates/nextblock-template/scripts/backfill-image-meta.ts +149 -0
  203. package/templates/nextblock-template/scripts/backup.js +53 -0
  204. package/templates/nextblock-template/scripts/test-bundle-optimization.js +114 -0
  205. package/templates/nextblock-template/tailwind.config.ts +19 -0
  206. package/templates/nextblock-template/tsconfig.json +62 -0
@@ -0,0 +1,997 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn } from 'node:child_process';
4
+ import { dirname, resolve, relative, sep, basename } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { program } from 'commander';
7
+ import inquirer from 'inquirer';
8
+ import chalk from 'chalk';
9
+ 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
+
124
+ await ensureGitignore(projectDir);
125
+ console.log(chalk.green('.gitignore ready.'));
126
+
127
+ await ensureEnvExample(projectDir);
128
+ console.log(chalk.green('.env.example ready.'));
129
+
130
+ await sanitizeLayout(projectDir);
131
+ console.log(chalk.green('Global styles configured.'));
132
+
133
+ await sanitizeTailwindConfig(projectDir);
134
+ console.log(chalk.green('tailwind.config.ts 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);
147
+ } else {
148
+ console.log(chalk.yellow('Skipping dependency installation.'));
149
+ }
150
+
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
+ );
164
+ } catch (error) {
165
+ console.error(
166
+ chalk.red(error instanceof Error ? error.message : 'An unexpected error occurred'),
167
+ );
168
+ process.exit(1);
169
+ }
170
+ }
171
+
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
+ }
183
+
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
+ }
191
+
192
+ await fs.ensureDir(projectDir);
193
+
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
+ }
201
+
202
+ const segments = relativePath.split(sep);
203
+ return !segments.includes('.git') && !segments.includes('node_modules');
204
+ },
205
+ });
206
+ }
207
+
208
+ async function removeBackups(projectDir) {
209
+ const backupDir = resolve(projectDir, 'backup');
210
+ if (await fs.pathExists(backupDir)) {
211
+ await fs.remove(backupDir);
212
+ }
213
+ }
214
+
215
+ async function ensureGitignore(projectDir) {
216
+ const gitignorePath = resolve(projectDir, '.gitignore');
217
+ const npmIgnorePath = resolve(projectDir, '.npmignore');
218
+ const repoGitignorePath = resolve(REPO_ROOT, '.gitignore');
219
+
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*',
239
+ '',
240
+ '# Environment',
241
+ '.env.local',
242
+ '.env.development.local',
243
+ '.env.test.local',
244
+ '.env.production.local',
245
+ '',
246
+ '# Backups',
247
+ 'backup/',
248
+ '',
249
+ '# Misc',
250
+ '.DS_Store',
251
+ ];
252
+
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;
381
+ }
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;
391
+ }
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
+ }
402
+
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');
420
+ }
421
+
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);
427
+ }
428
+ }
429
+
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
+ }
487
+ }
488
+
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
+ }
497
+
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);
504
+ } 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);
526
+ }
527
+ }
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
+
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
+ }
597
+ }
598
+ }
599
+
600
+ async function sanitizeTailwindConfig(projectDir) {
601
+ const tailwindConfigPath = resolve(projectDir, 'tailwind.config.ts');
602
+ const content = `import type { Config } from 'tailwindcss';
603
+
604
+ const config = {
605
+ darkMode: ['class'],
606
+ content: [
607
+ './app/**/*.{js,ts,jsx,tsx,mdx}',
608
+ './components/**/*.{js,ts,jsx,tsx,mdx}',
609
+ './context/**/*.{js,ts,jsx,tsx,mdx}',
610
+ './lib/**/*.{js,ts,jsx,tsx,mdx}',
611
+ './node_modules/@nextblock-cms/ui/**/*.{js,ts,jsx,tsx}',
612
+ './node_modules/@nextblock-cms/editor/**/*.{js,ts,jsx,tsx}',
613
+ ],
614
+ safelist: [
615
+ 'animate-enter',
616
+ 'animate-leave',
617
+ 'dark',
618
+ 'text-primary',
619
+ 'text-secondary',
620
+ 'text-accent',
621
+ 'text-muted',
622
+ 'text-destructive',
623
+ 'text-background',
624
+ ],
625
+ prefix: '',
626
+ theme: {
627
+ container: {
628
+ center: true,
629
+ padding: '2rem',
630
+ screens: {
631
+ '2xl': '1400px',
632
+ },
633
+ },
634
+ extend: {
635
+ colors: {
636
+ border: 'hsl(var(--border))',
637
+ input: 'hsl(var(--input))',
638
+ ring: 'hsl(var(--ring))',
639
+ background: 'hsl(var(--background))',
640
+ foreground: 'hsl(var(--foreground))',
641
+ primary: {
642
+ DEFAULT: 'hsl(var(--primary))',
643
+ foreground: 'hsl(var(--primary-foreground))',
644
+ },
645
+ secondary: {
646
+ DEFAULT: 'hsl(var(--secondary))',
647
+ foreground: 'hsl(var(--secondary-foreground))',
648
+ },
649
+ destructive: {
650
+ DEFAULT: 'hsl(var(--destructive))',
651
+ foreground: 'hsl(var(--destructive-foreground))',
652
+ },
653
+ muted: {
654
+ DEFAULT: 'hsl(var(--muted))',
655
+ foreground: 'hsl(var(--muted-foreground))',
656
+ },
657
+ warning: {
658
+ DEFAULT: 'hsl(var(--warning))',
659
+ foreground: 'hsl(var(--warning-foreground))',
660
+ },
661
+ accent: {
662
+ DEFAULT: 'hsl(var(--accent))',
663
+ foreground: 'hsl(var(--accent-foreground))',
664
+ },
665
+ popover: {
666
+ DEFAULT: 'hsl(var(--popover))',
667
+ foreground: 'hsl(var(--popover-foreground))',
668
+ },
669
+ card: {
670
+ DEFAULT: 'hsl(var(--card))',
671
+ foreground: 'hsl(var(--card-foreground))',
672
+ },
673
+ },
674
+ borderRadius: {
675
+ lg: 'var(--radius)',
676
+ md: 'calc(var(--radius) - 2px)',
677
+ sm: 'calc(var(--radius) - 4px)',
678
+ },
679
+ keyframes: {
680
+ 'accordion-down': {
681
+ from: { height: '0' },
682
+ to: { height: 'var(--radix-accordion-content-height)' },
683
+ },
684
+ 'accordion-up': {
685
+ from: { height: 'var(--radix-accordion-content-height)' },
686
+ to: { height: '0' },
687
+ },
688
+ },
689
+ animation: {
690
+ 'accordion-down': 'accordion-down 0.2s ease-out',
691
+ 'accordion-up': 'accordion-up 0.2s ease-out',
692
+ },
693
+ },
694
+ },
695
+ plugins: [require('tailwindcss-animate')],
696
+ } satisfies Config;
697
+
698
+ export default config;
699
+ `;
700
+
701
+ await fs.writeFile(tailwindConfigPath, content);
702
+ }
703
+
704
+ async function normalizeTsconfig(projectDir) {
705
+ const tsconfigPath = resolve(projectDir, 'tsconfig.json');
706
+ if (!(await fs.pathExists(tsconfigPath))) {
707
+ return;
708
+ }
709
+
710
+ const tsconfig = await fs.readJSON(tsconfigPath);
711
+ if ('extends' in tsconfig) {
712
+ delete tsconfig.extends;
713
+ }
714
+
715
+ if ('references' in tsconfig) {
716
+ delete tsconfig.references;
717
+ }
718
+ const defaultInclude = new Set([
719
+ 'next-env.d.ts',
720
+ '**/*.ts',
721
+ '**/*.tsx',
722
+ '**/*.js',
723
+ '**/*.jsx',
724
+ '.next/types/**/*.ts',
725
+ ]);
726
+
727
+ if (Array.isArray(tsconfig.include)) {
728
+ for (const entry of tsconfig.include) {
729
+ if (typeof entry === 'string' && !entry.includes('../')) {
730
+ defaultInclude.add(entry);
731
+ }
732
+ }
733
+ }
734
+
735
+ tsconfig.include = Array.from(defaultInclude);
736
+
737
+ const defaultExclude = new Set(['node_modules']);
738
+ if (Array.isArray(tsconfig.exclude)) {
739
+ for (const entry of tsconfig.exclude) {
740
+ if (typeof entry === 'string' && !entry.includes('../')) {
741
+ defaultExclude.add(entry);
742
+ }
743
+ }
744
+ }
745
+
746
+ tsconfig.exclude = Array.from(defaultExclude);
747
+
748
+ tsconfig.compilerOptions = {
749
+ ...(tsconfig.compilerOptions ?? {}),
750
+ baseUrl: '.',
751
+ skipLibCheck: true,
752
+ };
753
+
754
+ const compilerOptions = tsconfig.compilerOptions;
755
+ compilerOptions.paths = {
756
+ ...(compilerOptions.paths ?? {}),
757
+ '@/*': ['./*'],
758
+ '@nextblock-cms/ui/*': ['./lib/ui/*'],
759
+ '@nextblock-cms/editor/utils/*': ['./lib/editor/utils/*'],
760
+ };
761
+
762
+ await fs.writeJSON(tsconfigPath, tsconfig, { spaces: 2 });
763
+ }
764
+
765
+ async function sanitizeNextConfig(projectDir, editorUtilNames = []) {
766
+ const nextConfigPath = resolve(projectDir, 'next.config.js');
767
+ const content = buildNextConfigContent(editorUtilNames);
768
+ await fs.writeFile(nextConfigPath, content);
769
+ }
770
+
771
+ async function transformPackageJson(projectDir) {
772
+ const packageJsonPath = resolve(projectDir, 'package.json');
773
+ if (!(await fs.pathExists(packageJsonPath))) {
774
+ return;
775
+ }
776
+
777
+ const packageJson = await fs.readJSON(packageJsonPath);
778
+ const projectName = basename(projectDir);
779
+
780
+ if (projectName) {
781
+ packageJson.name = projectName;
782
+ }
783
+
784
+ packageJson.version = packageJson.version ?? '0.1.0';
785
+ packageJson.private = packageJson.private ?? true;
786
+
787
+ packageJson.dependencies = packageJson.dependencies ?? {};
788
+
789
+ for (const [pkgName, manifestPath] of Object.entries(PACKAGE_VERSION_SOURCES)) {
790
+ if (pkgName in packageJson.dependencies) {
791
+ const current = packageJson.dependencies[pkgName];
792
+ if (typeof current === 'string' && current.startsWith('workspace:')) {
793
+ let versionSpecifier = 'latest';
794
+ try {
795
+ const manifest = await fs.readJSON(manifestPath);
796
+ if (manifest.version) {
797
+ versionSpecifier = `^${manifest.version}`;
798
+ }
799
+ } catch {
800
+ versionSpecifier = 'latest';
801
+ }
802
+
803
+ packageJson.dependencies[pkgName] = versionSpecifier;
804
+ }
805
+ }
806
+ }
807
+
808
+ await fs.writeJSON(packageJsonPath, packageJson, { spaces: 2 });
809
+ }
810
+
811
+ async function installDependencies(projectDir) {
812
+ const npmCommand = IS_WINDOWS ? 'npm.cmd' : 'npm';
813
+ console.log(chalk.blue('Installing dependencies with npm...'));
814
+ await runCommand(npmCommand, ['install'], { cwd: projectDir });
815
+ console.log(chalk.green('Dependencies installed.'));
816
+ }
817
+
818
+ async function initializeGit(projectDir) {
819
+ const gitDirectory = resolve(projectDir, '.git');
820
+ if (await fs.pathExists(gitDirectory)) {
821
+ return;
822
+ }
823
+
824
+ try {
825
+ console.log(chalk.blue('Initializing Git repository...'));
826
+ await runCommand('git', ['init'], { cwd: projectDir });
827
+ console.log(chalk.green('Git repository initialized.'));
828
+ } catch (error) {
829
+ console.warn(
830
+ chalk.yellow(
831
+ `Skipping Git initialization: ${error instanceof Error ? error.message : String(error)}`,
832
+ ),
833
+ );
834
+ }
835
+ }
836
+
837
+ function runCommand(command, args, options = {}) {
838
+ return new Promise((resolve, reject) => {
839
+ const child = spawn(command, args, {
840
+ stdio: 'inherit',
841
+ shell: IS_WINDOWS,
842
+ ...options,
843
+ });
844
+
845
+ child.on('error', (error) => {
846
+ reject(error);
847
+ });
848
+
849
+ child.on('close', (code) => {
850
+ if (code === 0) {
851
+ resolve();
852
+ } else {
853
+ reject(new Error(`${command} exited with code ${code}`));
854
+ }
855
+ });
856
+ });
857
+ }
858
+
859
+ function buildNextConfigContent(editorUtilNames) {
860
+ const aliasLines = [];
861
+
862
+ for (const moduleName of UI_PROXY_MODULES) {
863
+ aliasLines.push(
864
+ " '@nextblock-cms/ui/" + moduleName + "': path.join(process.cwd(), 'lib/ui/" + moduleName + "'),",
865
+ );
866
+ }
867
+
868
+ for (const moduleName of editorUtilNames) {
869
+ aliasLines.push(
870
+ " '@nextblock-cms/editor/utils/" +
871
+ moduleName +
872
+ "': path.join(process.cwd(), 'lib/editor/utils/" +
873
+ moduleName +
874
+ "'),",
875
+ );
876
+ }
877
+
878
+ const lines = [
879
+ '//@ts-check',
880
+ '',
881
+ "const path = require('path');",
882
+ "const webpack = require('webpack');",
883
+ '',
884
+ '/**',
885
+ " * @type {import('next').NextConfig}",
886
+ ' **/',
887
+ 'const nextConfig = {',
888
+ " outputFileTracingRoot: path.join(__dirname),",
889
+ ' env: {',
890
+ " NEXT_PUBLIC_SUPABASE_URL: process.env.NEXT_PUBLIC_SUPABASE_URL,",
891
+ " NEXT_PUBLIC_SUPABASE_ANON_KEY: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,",
892
+ ' },',
893
+ ' images: {',
894
+ " formats: ['image/avif', 'image/webp'],",
895
+ ' imageSizes: [16, 32, 48, 64, 96, 128, 256, 384, 512],',
896
+ ' deviceSizes: [320, 480, 640, 750, 828, 1080, 1200, 1440, 1920, 2048, 2560],',
897
+ ' minimumCacheTTL: 31536000,',
898
+ " dangerouslyAllowSVG: false,",
899
+ " contentSecurityPolicy: \"default-src 'self'; script-src 'none'; sandbox;\",",
900
+ ' remotePatterns: [',
901
+ " { protocol: 'https', hostname: 'pub-a31e3f1a87d144898aeb489a8221f92e.r2.dev' },",
902
+ " { protocol: 'https', hostname: 'e260676f72b0b18314b868f136ed72ae.r2.cloudflarestorage.com' },",
903
+ ' ...(process.env.NEXT_PUBLIC_URL',
904
+ ' ? [',
905
+ ' {',
906
+ " protocol: /** @type {'http' | 'https'} */ (new URL(process.env.NEXT_PUBLIC_URL).protocol.slice(0, -1)),",
907
+ " hostname: new URL(process.env.NEXT_PUBLIC_URL).hostname,",
908
+ ' },',
909
+ ' ]',
910
+ ' : []),',
911
+ ' ],',
912
+ ' },',
913
+ ' experimental: {',
914
+ " optimizeCss: true,",
915
+ " cssChunking: 'strict',",
916
+ ' },',
917
+ " transpilePackages: ['@nextblock-cms/utils', '@nextblock-cms/ui', '@nextblock-cms/editor'],",
918
+ ' webpack: (config, { isServer }) => {',
919
+ ' config.resolve = config.resolve || {};',
920
+ ' config.resolve.alias = {',
921
+ ' ...(config.resolve.alias ?? {}),',
922
+ ];
923
+
924
+ if (aliasLines.length > 0) {
925
+ lines.push(...aliasLines);
926
+ }
927
+
928
+ lines.push(' };', '');
929
+
930
+ if (editorUtilNames.length > 0) {
931
+ lines.push(
932
+ ' const editorUtilsShims = ' + JSON.stringify(editorUtilNames) + ';',
933
+ ' config.plugins = config.plugins || [];',
934
+ ' for (const utilName of editorUtilsShims) {',
935
+ " const shimPath = path.join(process.cwd(), 'lib/editor/utils', utilName);",
936
+ ' config.plugins.push(',
937
+ " new webpack.NormalModuleReplacementPlugin(new RegExp('^@nextblock-cms/editor/utils/' + utilName + '$'), shimPath),",
938
+ ' );',
939
+ ' config.plugins.push(',
940
+ " new webpack.NormalModuleReplacementPlugin(new RegExp('^./utils/' + utilName + '$'), shimPath),",
941
+ ' );',
942
+ ' }',
943
+ '',
944
+ );
945
+ }
946
+
947
+ lines.push(
948
+ ' if (!isServer) {',
949
+ ' config.module = config.module || {};',
950
+ ' config.module.rules = config.module.rules || [];',
951
+ ' config.module.rules.push({',
952
+ " test: /\\.svg$/i,",
953
+ " issuer: /\\.[jt]sx?$/,",
954
+ " use: ['@svgr/webpack'],",
955
+ ' });',
956
+ '',
957
+ ' config.optimization = {',
958
+ ' ...(config.optimization ?? {}),',
959
+ ' splitChunks: {',
960
+ ' ...((config.optimization ?? {}).splitChunks ?? {}),',
961
+ ' cacheGroups: {',
962
+ ' ...(((config.optimization ?? {}).splitChunks ?? {}).cacheGroups ?? {}),',
963
+ ' tiptap: {',
964
+ " test: /[\\\\/]node_modules[\\\\/](@tiptap|prosemirror)[\\\\/]/,",
965
+ " name: 'tiptap',",
966
+ " chunks: 'async',",
967
+ ' priority: 30,',
968
+ ' reuseExistingChunk: true,',
969
+ ' },',
970
+ ' tiptapExtensions: {',
971
+ " test: /[\\\\/](tiptap-extensions|RichTextEditor|MenuBar|MediaLibraryModal)[\\\\/]/,",
972
+ " name: 'tiptap-extensions',",
973
+ " chunks: 'async',",
974
+ ' priority: 25,',
975
+ ' reuseExistingChunk: true,',
976
+ ' },',
977
+ ' },',
978
+ ' },',
979
+ ' };',
980
+ ' }',
981
+ '',
982
+ ' return config;',
983
+ ' },',
984
+ ' turbopack: {',
985
+ ' // Turbopack-specific options can be configured here if needed.',
986
+ ' },',
987
+ ' compiler: {',
988
+ " removeConsole: process.env.NODE_ENV === 'production',",
989
+ ' },',
990
+ '};',
991
+ '',
992
+ 'module.exports = nextConfig;',
993
+ );
994
+
995
+ return lines.join('\n');
996
+ }
997
+