create-nextblock 0.2.77 → 0.8.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.
Files changed (413) hide show
  1. package/bin/create-nextblock.js +740 -459
  2. package/package.json +1 -2
  3. package/scripts/sync-template.js +18 -1
  4. package/templates/nextblock-template/.browserslistrc +11 -0
  5. package/templates/nextblock-template/.swcrc +30 -30
  6. package/templates/nextblock-template/README.md +23 -114
  7. package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +27 -28
  8. package/templates/nextblock-template/app/(auth-pages)/sign-in/page.tsx +50 -25
  9. package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +111 -56
  10. package/templates/nextblock-template/app/(auth-pages)/two-factor/actions.ts +91 -0
  11. package/templates/nextblock-template/app/(auth-pages)/two-factor/components/TwoFactorForm.tsx +118 -0
  12. package/templates/nextblock-template/app/(auth-pages)/two-factor/page.tsx +51 -0
  13. package/templates/nextblock-template/app/.well-known/ucp/route.ts +16 -0
  14. package/templates/nextblock-template/app/[slug]/PageClientContent.tsx +48 -28
  15. package/templates/nextblock-template/app/[slug]/page.tsx +63 -6
  16. package/templates/nextblock-template/app/[slug]/page.utils.ts +374 -157
  17. package/templates/nextblock-template/app/[slug]/pageClientActions.ts +7 -0
  18. package/templates/nextblock-template/app/actions/consent.ts +57 -0
  19. package/templates/nextblock-template/app/actions/formActions.ts +130 -11
  20. package/templates/nextblock-template/app/actions/languageActions.ts +31 -30
  21. package/templates/nextblock-template/app/actions/package-actions.ts +183 -0
  22. package/templates/nextblock-template/app/actions/postActions.ts +146 -48
  23. package/templates/nextblock-template/app/actions/twoFactorEmail.ts +21 -0
  24. package/templates/nextblock-template/app/actions/visualEditingActions.test.ts +179 -0
  25. package/templates/nextblock-template/app/actions/visualEditingActions.ts +345 -0
  26. package/templates/nextblock-template/app/actions.ts +67 -12
  27. package/templates/nextblock-template/app/api/ai/cortex/build-widget/route.ts +153 -0
  28. package/templates/nextblock-template/app/api/ai/generate-blocks/route.ts +96 -0
  29. package/templates/nextblock-template/app/api/ai/global-agent/route.ts +965 -0
  30. package/templates/nextblock-template/app/api/checkout/freemius/sync/route.ts +29 -0
  31. package/templates/nextblock-template/app/api/checkout/route.ts +146 -0
  32. package/templates/nextblock-template/app/api/cms/full-backup/export/route.ts +33 -0
  33. package/templates/nextblock-template/app/api/cms/full-backup/restore/route.ts +63 -0
  34. package/templates/nextblock-template/app/api/cron/reset-sandbox/route.ts +3413 -17
  35. package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +7830 -0
  36. package/templates/nextblock-template/app/api/cron/sync-currencies/route.ts +35 -0
  37. package/templates/nextblock-template/app/api/custom-blocks/db-relations/route.ts +92 -0
  38. package/templates/nextblock-template/app/api/custom-blocks/editor-definitions/route.ts +43 -0
  39. package/templates/nextblock-template/app/api/draft/disable/route.ts +25 -0
  40. package/templates/nextblock-template/app/api/draft/route.ts +93 -0
  41. package/templates/nextblock-template/app/api/draft/start/route.ts +77 -0
  42. package/templates/nextblock-template/app/api/media/library/route.ts +65 -0
  43. package/templates/nextblock-template/app/api/media/r2-presigned/route.ts +53 -0
  44. package/templates/nextblock-template/app/api/media/record/route.ts +160 -0
  45. package/templates/nextblock-template/app/api/search/route.ts +43 -0
  46. package/templates/nextblock-template/app/api/visual-editing/block-draft/route.ts +47 -0
  47. package/templates/nextblock-template/app/api/visual-editing/product-draft/route.ts +47 -0
  48. package/templates/nextblock-template/app/api/webhooks/freemius/route.ts +34 -0
  49. package/templates/nextblock-template/app/api/webhooks/stripe/route.ts +27 -0
  50. package/templates/nextblock-template/app/article/[slug]/PostClientContent.tsx +392 -128
  51. package/templates/nextblock-template/app/article/[slug]/page.tsx +179 -127
  52. package/templates/nextblock-template/app/article/[slug]/page.utils.ts +262 -77
  53. package/templates/nextblock-template/app/auth/callback/route.ts +31 -58
  54. package/templates/nextblock-template/app/cart/page.tsx +7 -0
  55. package/templates/nextblock-template/app/checkout/UcpCartHydrator.tsx +20 -0
  56. package/templates/nextblock-template/app/checkout/page.tsx +52 -0
  57. package/templates/nextblock-template/app/checkout/success/actions.ts +136 -0
  58. package/templates/nextblock-template/app/checkout/success/page.tsx +186 -0
  59. package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +163 -33
  60. package/templates/nextblock-template/app/cms/blocks/actions.ts +424 -235
  61. package/templates/nextblock-template/app/cms/blocks/components/BackgroundSelector.tsx +212 -151
  62. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +41 -20
  63. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +152 -19
  64. package/templates/nextblock-template/app/cms/blocks/components/BlockTypeCard.tsx +25 -17
  65. package/templates/nextblock-template/app/cms/blocks/components/BlockTypeSelector.tsx +200 -18
  66. package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +33 -16
  67. package/templates/nextblock-template/app/cms/blocks/components/CustomBlockEditorPreview.tsx +160 -0
  68. package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +37 -18
  69. package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +149 -67
  70. package/templates/nextblock-template/app/cms/blocks/components/SectionConfigPanel.tsx +108 -31
  71. package/templates/nextblock-template/app/cms/blocks/editors/DynamicCustomBlockEditor.tsx +167 -0
  72. package/templates/nextblock-template/app/cms/blocks/editors/FeaturedProductBlockEditor.tsx +31 -0
  73. package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +2 -2
  74. package/templates/nextblock-template/app/cms/blocks/editors/HeadingBlockEditor.tsx +1 -1
  75. package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +29 -29
  76. package/templates/nextblock-template/app/cms/blocks/editors/PostsGridBlockEditor.tsx +14 -18
  77. package/templates/nextblock-template/app/cms/blocks/editors/ProductGridBlockEditor.tsx +41 -0
  78. package/templates/nextblock-template/app/cms/blocks/editors/SectionBlockEditor.tsx +318 -118
  79. package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +98 -21
  80. package/templates/nextblock-template/app/cms/blocks/editors/VideoEmbedBlockEditor.tsx +1 -1
  81. package/templates/nextblock-template/app/cms/components/ContentLanguageSwitcher.tsx +27 -9
  82. package/templates/nextblock-template/app/cms/components/CopyContentFromLanguage.tsx +1 -1
  83. package/templates/nextblock-template/app/cms/components/CortexAiActiveContext.tsx +23 -0
  84. package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -0
  85. package/templates/nextblock-template/app/cms/components/CortexGlobalAgentChat.tsx +1507 -0
  86. package/templates/nextblock-template/app/cms/components/DraftStatusActions.tsx +145 -0
  87. package/templates/nextblock-template/app/cms/components/FeatureImageField.tsx +244 -0
  88. package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +38 -24
  89. package/templates/nextblock-template/app/cms/coupons/[id]/edit/page.tsx +16 -0
  90. package/templates/nextblock-template/app/cms/coupons/page.tsx +16 -0
  91. package/templates/nextblock-template/app/cms/custom-blocks/[id]/edit/page.tsx +66 -0
  92. package/templates/nextblock-template/app/cms/custom-blocks/actions.ts +519 -0
  93. package/templates/nextblock-template/app/cms/custom-blocks/components/BlockComposer.tsx +1522 -0
  94. package/templates/nextblock-template/app/cms/custom-blocks/components/BlocksLibraryTransferControls.tsx +256 -0
  95. package/templates/nextblock-template/app/cms/custom-blocks/components/DBRelationSelect.tsx +384 -0
  96. package/templates/nextblock-template/app/cms/custom-blocks/components/ImageR2Picker.tsx +221 -0
  97. package/templates/nextblock-template/app/cms/custom-blocks/new/page.tsx +12 -0
  98. package/templates/nextblock-template/app/cms/custom-blocks/page.tsx +438 -0
  99. package/templates/nextblock-template/app/cms/dashboard/actions.ts +228 -98
  100. package/templates/nextblock-template/app/cms/dashboard/components/DashboardComponents.tsx +200 -0
  101. package/templates/nextblock-template/app/cms/dashboard/page.tsx +191 -151
  102. package/templates/nextblock-template/app/cms/import-export/ContentTransferControls.tsx +391 -0
  103. package/templates/nextblock-template/app/cms/import-export/actions.ts +226 -0
  104. package/templates/nextblock-template/app/cms/layout.tsx +29 -10
  105. package/templates/nextblock-template/app/cms/media/UploadFolderContext.tsx +22 -22
  106. package/templates/nextblock-template/app/cms/media/actions.ts +45 -124
  107. package/templates/nextblock-template/app/cms/media/components/DeleteMediaButtonClient.tsx +1 -1
  108. package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +26 -26
  109. package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +69 -64
  110. package/templates/nextblock-template/app/cms/media/components/MediaPickerDialog.tsx +227 -158
  111. package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +101 -89
  112. package/templates/nextblock-template/app/cms/media/page.tsx +1 -1
  113. package/templates/nextblock-template/app/cms/navigation/components/NavigationItemForm.tsx +2 -2
  114. package/templates/nextblock-template/app/cms/orders/[id]/MarkPaidButton.tsx +44 -0
  115. package/templates/nextblock-template/app/cms/orders/[id]/page.tsx +16 -0
  116. package/templates/nextblock-template/app/cms/orders/actions.ts +201 -0
  117. package/templates/nextblock-template/app/cms/orders/page.tsx +20 -0
  118. package/templates/nextblock-template/app/cms/orders/types.ts +20 -0
  119. package/templates/nextblock-template/app/cms/pages/[id]/edit/EditPageClient.tsx +156 -121
  120. package/templates/nextblock-template/app/cms/pages/[id]/edit/page.tsx +79 -26
  121. package/templates/nextblock-template/app/cms/pages/actions.ts +54 -38
  122. package/templates/nextblock-template/app/cms/pages/components/DeletePageButtonClient.tsx +1 -1
  123. package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +267 -116
  124. package/templates/nextblock-template/app/cms/pages/page.tsx +25 -18
  125. package/templates/nextblock-template/app/cms/payments/page.tsx +16 -0
  126. package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +132 -90
  127. package/templates/nextblock-template/app/cms/posts/actions.ts +71 -72
  128. package/templates/nextblock-template/app/cms/posts/components/DeletePostButtonClient.tsx +1 -1
  129. package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +256 -245
  130. package/templates/nextblock-template/app/cms/posts/new/page.tsx +1 -1
  131. package/templates/nextblock-template/app/cms/posts/page.tsx +20 -13
  132. package/templates/nextblock-template/app/cms/products/ClientNotionEditor.tsx +16 -0
  133. package/templates/nextblock-template/app/cms/products/ProductFormClientShell.tsx +56 -0
  134. package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +292 -0
  135. package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -0
  136. package/templates/nextblock-template/app/cms/products/categories/page.tsx +12 -0
  137. package/templates/nextblock-template/app/cms/products/inventory/page.tsx +13 -0
  138. package/templates/nextblock-template/app/cms/products/new/page.tsx +143 -0
  139. package/templates/nextblock-template/app/cms/products/page.tsx +42 -0
  140. package/templates/nextblock-template/app/cms/products/productFormData.ts +133 -0
  141. package/templates/nextblock-template/app/cms/products/settings/page.tsx +5 -0
  142. package/templates/nextblock-template/app/cms/promotions/PromotionsWorkspace.tsx +456 -0
  143. package/templates/nextblock-template/app/cms/promotions/actions.ts +115 -0
  144. package/templates/nextblock-template/app/cms/promotions/page.tsx +31 -0
  145. package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +2 -2
  146. package/templates/nextblock-template/app/cms/revisions/actions.ts +285 -285
  147. package/templates/nextblock-template/app/cms/revisions/service.ts +19 -16
  148. package/templates/nextblock-template/app/cms/revisions/utils.ts +8 -3
  149. package/templates/nextblock-template/app/cms/settings/backup-restore/BackupRestoreWorkspace.tsx +1004 -0
  150. package/templates/nextblock-template/app/cms/settings/backup-restore/page.tsx +29 -0
  151. package/templates/nextblock-template/app/cms/settings/bot-protection/actions.ts +93 -0
  152. package/templates/nextblock-template/app/cms/settings/bot-protection/components/BotProtectionForm.tsx +129 -0
  153. package/templates/nextblock-template/app/cms/settings/bot-protection/page.tsx +24 -0
  154. package/templates/nextblock-template/app/cms/settings/copyright/actions.ts +1 -1
  155. package/templates/nextblock-template/app/cms/settings/copyright/components/CopyrightForm.tsx +2 -2
  156. package/templates/nextblock-template/app/cms/settings/copyright/page.tsx +1 -1
  157. package/templates/nextblock-template/app/cms/settings/cortex-ai/SandboxCortexAiSettingsClient.tsx +496 -0
  158. package/templates/nextblock-template/app/cms/settings/cortex-ai/StoredCortexAiSettingsClient.tsx +410 -0
  159. package/templates/nextblock-template/app/cms/settings/cortex-ai/actions.ts +248 -0
  160. package/templates/nextblock-template/app/cms/settings/cortex-ai/page.tsx +80 -0
  161. package/templates/nextblock-template/app/cms/settings/currencies/actions.ts +331 -0
  162. package/templates/nextblock-template/app/cms/settings/currencies/page.tsx +494 -0
  163. package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -0
  164. package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +203 -44
  165. package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +93 -242
  166. package/templates/nextblock-template/app/cms/settings/global-css/actions.ts +65 -0
  167. package/templates/nextblock-template/app/cms/settings/global-css/components/GlobalCssForm.tsx +46 -0
  168. package/templates/nextblock-template/app/cms/settings/global-css/page.tsx +24 -0
  169. package/templates/nextblock-template/app/cms/settings/languages/components/DeleteLanguageButton.tsx +1 -1
  170. package/templates/nextblock-template/app/cms/settings/languages/components/LanguageForm.tsx +2 -2
  171. package/templates/nextblock-template/app/cms/settings/languages/page.tsx +1 -1
  172. package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +7 -7
  173. package/templates/nextblock-template/app/cms/settings/logos/actions.ts +82 -6
  174. package/templates/nextblock-template/app/cms/settings/logos/components/BrandingSettingsForm.tsx +339 -0
  175. package/templates/nextblock-template/app/cms/settings/logos/components/DeleteLogoButton.tsx +21 -18
  176. package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +20 -16
  177. package/templates/nextblock-template/app/cms/settings/logos/components/SiteSeoSettingsForm.tsx +133 -0
  178. package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +8 -8
  179. package/templates/nextblock-template/app/cms/settings/logos/page.tsx +120 -82
  180. package/templates/nextblock-template/app/cms/settings/logos/types.ts +8 -8
  181. package/templates/nextblock-template/app/cms/settings/packages/activation-form.tsx +84 -0
  182. package/templates/nextblock-template/app/cms/settings/packages/package-card.tsx +122 -0
  183. package/templates/nextblock-template/app/cms/settings/packages/page.tsx +49 -0
  184. package/templates/nextblock-template/app/cms/settings/privacy/actions.ts +53 -0
  185. package/templates/nextblock-template/app/cms/settings/privacy/components/PrivacyForm.tsx +196 -0
  186. package/templates/nextblock-template/app/cms/settings/privacy/page.tsx +26 -0
  187. package/templates/nextblock-template/app/cms/settings/security/actions.ts +251 -0
  188. package/templates/nextblock-template/app/cms/settings/security/components/SecurityPanel.tsx +453 -0
  189. package/templates/nextblock-template/app/cms/settings/security/page.tsx +13 -0
  190. package/templates/nextblock-template/app/cms/settings/taxes/page.tsx +21 -0
  191. package/templates/nextblock-template/app/cms/shipping/page.tsx +20 -0
  192. package/templates/nextblock-template/app/cms/users/[id]/edit/page.tsx +28 -23
  193. package/templates/nextblock-template/app/cms/users/actions.ts +105 -40
  194. package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +1 -1
  195. package/templates/nextblock-template/app/cms/users/components/UserForm.tsx +65 -152
  196. package/templates/nextblock-template/app/cms/users/page.tsx +15 -10
  197. package/templates/nextblock-template/app/globals.css +9 -0
  198. package/templates/nextblock-template/app/layout.tsx +372 -116
  199. package/templates/nextblock-template/app/lib/seo.test.ts +52 -0
  200. package/templates/nextblock-template/app/lib/seo.ts +279 -0
  201. package/templates/nextblock-template/app/lib/site-settings.ts +87 -0
  202. package/templates/nextblock-template/app/lib/sitemap-utils.ts +224 -39
  203. package/templates/nextblock-template/app/lib/ucp/protocol.ts +190 -0
  204. package/templates/nextblock-template/app/lib/ucp/server.test.ts +56 -0
  205. package/templates/nextblock-template/app/lib/ucp/server.ts +1914 -0
  206. package/templates/nextblock-template/app/page.tsx +165 -73
  207. package/templates/nextblock-template/app/product/[slug]/page.tsx +433 -0
  208. package/templates/nextblock-template/app/profile/ProfileAccountSidebar.tsx +73 -0
  209. package/templates/nextblock-template/app/profile/ProfilePageHeader.tsx +16 -0
  210. package/templates/nextblock-template/app/profile/ProfilePageMissingState.tsx +9 -0
  211. package/templates/nextblock-template/app/profile/account-data.ts +37 -0
  212. package/templates/nextblock-template/app/profile/account-links.ts +22 -0
  213. package/templates/nextblock-template/app/profile/account-types.ts +11 -0
  214. package/templates/nextblock-template/app/profile/orders/CustomerOrdersPageClient.tsx +124 -0
  215. package/templates/nextblock-template/app/profile/orders/[id]/CustomerOrderDetailPageClient.tsx +79 -0
  216. package/templates/nextblock-template/app/profile/orders/[id]/page.tsx +32 -0
  217. package/templates/nextblock-template/app/profile/orders/page.tsx +19 -0
  218. package/templates/nextblock-template/app/profile/page.tsx +51 -0
  219. package/templates/nextblock-template/app/profile/password/PasswordSettingsPageClient.tsx +128 -0
  220. package/templates/nextblock-template/app/profile/password/actions.ts +59 -0
  221. package/templates/nextblock-template/app/profile/password/page.tsx +27 -0
  222. package/templates/nextblock-template/app/providers.tsx +55 -17
  223. package/templates/nextblock-template/app/robots.txt/route.ts +11 -1
  224. package/templates/nextblock-template/app/sitemap.ts +128 -0
  225. package/templates/nextblock-template/app/ucp/v1/carts/[id]/cancel/route.ts +38 -0
  226. package/templates/nextblock-template/app/ucp/v1/carts/[id]/route.ts +68 -0
  227. package/templates/nextblock-template/app/ucp/v1/carts/route.ts +35 -0
  228. package/templates/nextblock-template/app/ucp/v1/catalog/lookup/route.ts +35 -0
  229. package/templates/nextblock-template/app/ucp/v1/catalog/product/route.ts +35 -0
  230. package/templates/nextblock-template/app/ucp/v1/catalog/search/route.ts +34 -0
  231. package/templates/nextblock-template/components/AppShell.tsx +154 -0
  232. package/templates/nextblock-template/components/BlockRenderer.tsx +210 -64
  233. package/templates/nextblock-template/components/CartDrawerLoader.tsx +7 -0
  234. package/templates/nextblock-template/components/CartTranslator.tsx +210 -0
  235. package/templates/nextblock-template/components/CurrentContentSetter.tsx +25 -0
  236. package/templates/nextblock-template/components/DeferredCartDrawer.tsx +23 -0
  237. package/templates/nextblock-template/components/DeferredCartTranslator.tsx +51 -0
  238. package/templates/nextblock-template/components/DeferredGlobalSearch.tsx +68 -0
  239. package/templates/nextblock-template/components/DeferredGoogleTagManager.tsx +70 -0
  240. package/templates/nextblock-template/components/DeferredSpeedInsights.tsx +69 -0
  241. package/templates/nextblock-template/components/FeatureImageHero.tsx +47 -0
  242. package/templates/nextblock-template/components/GitHubLoginButton.tsx +36 -0
  243. package/templates/nextblock-template/components/GlobalSearch.tsx +557 -0
  244. package/templates/nextblock-template/components/Header.tsx +49 -41
  245. package/templates/nextblock-template/components/LanguageSwitcher.tsx +55 -32
  246. package/templates/nextblock-template/components/ResponsiveNav.tsx +138 -43
  247. package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +12 -8
  248. package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -55
  249. package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +42 -37
  250. package/templates/nextblock-template/components/blocks/TestimonialBlock.tsx +6 -2
  251. package/templates/nextblock-template/components/blocks/ecommerceRendererLoaders.ts +23 -0
  252. package/templates/nextblock-template/components/blocks/publicRendererLoaders.ts +25 -0
  253. package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -84
  254. package/templates/nextblock-template/components/blocks/renderers/CartBlockRenderer.tsx +17 -0
  255. package/templates/nextblock-template/components/blocks/renderers/CheckoutBlockRenderer.tsx +19 -0
  256. package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +262 -8
  257. package/templates/nextblock-template/components/blocks/renderers/FeaturedProductBlockRenderer.tsx +22 -0
  258. package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +320 -37
  259. package/templates/nextblock-template/components/blocks/renderers/HeadingBlockRenderer.tsx +11 -8
  260. package/templates/nextblock-template/components/blocks/renderers/ImageBlockRenderer.tsx +12 -3
  261. package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +18 -13
  262. package/templates/nextblock-template/components/blocks/renderers/ProductDetailsBlockRenderer.tsx +90 -0
  263. package/templates/nextblock-template/components/blocks/renderers/ProductGridBlockRenderer.tsx +31 -0
  264. package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +424 -55
  265. package/templates/nextblock-template/components/blocks/renderers/SectionSlider.tsx +137 -0
  266. package/templates/nextblock-template/components/blocks/renderers/TestimonialBlockRenderer.tsx +57 -0
  267. package/templates/nextblock-template/components/blocks/renderers/TextBlockRenderer.tsx +37 -22
  268. package/templates/nextblock-template/components/blocks/renderers/VideoEmbedBlockRenderer.tsx +23 -15
  269. package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +1 -3
  270. package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +1 -3
  271. package/templates/nextblock-template/components/blocks/types.ts +7 -6
  272. package/templates/nextblock-template/components/env-var-warning.tsx +3 -3
  273. package/templates/nextblock-template/components/form-message.tsx +32 -26
  274. package/templates/nextblock-template/components/header-auth.tsx +69 -17
  275. package/templates/nextblock-template/components/privacy/ConsentBanner.tsx +127 -0
  276. package/templates/nextblock-template/components/privacy/ConsentGatedAnalytics.tsx +59 -0
  277. package/templates/nextblock-template/components/renderers/CachedDynamicLayoutEngine.tsx +28 -0
  278. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.test.tsx +166 -0
  279. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.tsx +464 -0
  280. package/templates/nextblock-template/components/theme-switcher.tsx +8 -8
  281. package/templates/nextblock-template/components/visual-editing/DeferredVisualEditing.tsx +21 -0
  282. package/templates/nextblock-template/components/visual-editing/NextblockVisualEditing.tsx +1172 -0
  283. package/templates/nextblock-template/context/AuthContext.tsx +23 -90
  284. package/templates/nextblock-template/context/CurrentContentContext.tsx +10 -4
  285. package/templates/nextblock-template/context/LanguageContext.tsx +16 -16
  286. package/templates/nextblock-template/context/language-rest-client.ts +31 -0
  287. package/templates/nextblock-template/docs/01-PROJECT-OVERVIEW.md +94 -0
  288. package/templates/nextblock-template/docs/02-ECOMMERCE-CAPABILITIES.md +364 -0
  289. package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +202 -0
  290. package/templates/nextblock-template/docs/04-DATABASE-AND-AUTH.md +252 -0
  291. package/templates/nextblock-template/docs/05-DEVELOPER-GUIDE.md +238 -0
  292. package/templates/nextblock-template/docs/06-CLI-AND-SCAFFOLDING.md +125 -0
  293. package/templates/nextblock-template/docs/07-BLOCK-SDK-AND-EXTENSIBILITY.md +146 -0
  294. package/templates/nextblock-template/docs/08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md +1319 -0
  295. package/templates/nextblock-template/docs/09-LIVE-DRAFT-MODE.md +104 -0
  296. package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +222 -0
  297. package/templates/nextblock-template/docs/README.md +34 -0
  298. package/templates/nextblock-template/docs/TECHNICAL_SPECIFICATION.md +12507 -0
  299. package/templates/nextblock-template/hooks/use-hotkeys.ts +21 -14
  300. package/templates/nextblock-template/hooks/useGlobalSearch.ts +101 -0
  301. package/templates/nextblock-template/index.d.ts +2 -0
  302. package/templates/nextblock-template/lib/ai-block-generation.ts +339 -0
  303. package/templates/nextblock-template/lib/ai-client.ts +247 -0
  304. package/templates/nextblock-template/lib/ai-config.ts +81 -0
  305. package/templates/nextblock-template/lib/ai-cortex-widget-builder.ts +125 -0
  306. package/templates/nextblock-template/lib/ai-global-agent-custom-block-tools.ts +363 -0
  307. package/templates/nextblock-template/lib/ai-global-agent-db-tools.test.ts +405 -0
  308. package/templates/nextblock-template/lib/ai-global-agent-db-tools.ts +1228 -0
  309. package/templates/nextblock-template/lib/ai-global-agent-ecommerce.ts +5 -0
  310. package/templates/nextblock-template/lib/ai-global-agent-tools-stats.test.ts +223 -0
  311. package/templates/nextblock-template/lib/ai-global-agent-tools.test.ts +2183 -0
  312. package/templates/nextblock-template/lib/ai-global-agent-tools.ts +4807 -0
  313. package/templates/nextblock-template/lib/ai-key-crypto.test.ts +70 -0
  314. package/templates/nextblock-template/lib/ai-key-crypto.ts +132 -0
  315. package/templates/nextblock-template/lib/ai-model-catalog.test.ts +49 -0
  316. package/templates/nextblock-template/lib/ai-model-catalog.ts +41 -0
  317. package/templates/nextblock-template/lib/ai-model-registry.test.ts +231 -0
  318. package/templates/nextblock-template/lib/ai-model-registry.ts +522 -0
  319. package/templates/nextblock-template/lib/auth/cookies.ts +47 -0
  320. package/templates/nextblock-template/lib/auth/crypto.ts +42 -0
  321. package/templates/nextblock-template/lib/auth/trustedDevices.ts +92 -0
  322. package/templates/nextblock-template/lib/auth/twoFactor.ts +167 -0
  323. package/templates/nextblock-template/lib/auth-redirects.ts +46 -0
  324. package/templates/nextblock-template/lib/blocks/FeaturedProductBlock.tsx +94 -0
  325. package/templates/nextblock-template/lib/blocks/ProductGridBlock.tsx +137 -0
  326. package/templates/nextblock-template/lib/blocks/README.md +13 -670
  327. package/templates/nextblock-template/lib/blocks/blockRegistry.ts +138 -56
  328. package/templates/nextblock-template/lib/blocks/blockTypes.ts +18 -0
  329. package/templates/nextblock-template/lib/blocks/ecommerce-block-schemas.ts +31 -0
  330. package/templates/nextblock-template/lib/cms-transfer/csv.test.ts +77 -0
  331. package/templates/nextblock-template/lib/cms-transfer/csv.ts +399 -0
  332. package/templates/nextblock-template/lib/cms-transfer/server.ts +2243 -0
  333. package/templates/nextblock-template/lib/cms-transfer/types.ts +145 -0
  334. package/templates/nextblock-template/lib/cortex-widget-registry.test.ts +199 -0
  335. package/templates/nextblock-template/lib/cortex-widget-registry.ts +88 -0
  336. package/templates/nextblock-template/lib/cortex-widget-schema.test.tsx +237 -0
  337. package/templates/nextblock-template/lib/cortex-widget-schema.ts +393 -0
  338. package/templates/nextblock-template/lib/custom-block-definitions.ts +87 -0
  339. package/templates/nextblock-template/lib/custom-block-r2-upload-shared.ts +178 -0
  340. package/templates/nextblock-template/lib/custom-block-r2-upload.test.ts +140 -0
  341. package/templates/nextblock-template/lib/custom-block-r2-upload.ts +68 -0
  342. package/templates/nextblock-template/lib/custom-block-relation-registry.ts +256 -0
  343. package/templates/nextblock-template/lib/custom-block-relations.test.ts +227 -0
  344. package/templates/nextblock-template/lib/custom-block-relations.ts +279 -0
  345. package/templates/nextblock-template/lib/custom-block-safelist.ts +14 -0
  346. package/templates/nextblock-template/lib/editor/dynamic-extension-core.test.ts +172 -0
  347. package/templates/nextblock-template/lib/editor/dynamic-extension-core.ts +213 -0
  348. package/templates/nextblock-template/lib/editor/dynamic-extension-loader.ts +22 -0
  349. package/templates/nextblock-template/lib/editor/dynamic-extensions.tsx +193 -0
  350. package/templates/nextblock-template/lib/full-backup/manifest.test.ts +121 -0
  351. package/templates/nextblock-template/lib/full-backup/manifest.ts +206 -0
  352. package/templates/nextblock-template/lib/full-backup/server.ts +743 -0
  353. package/templates/nextblock-template/lib/media/resolveMediaUrl.ts +45 -0
  354. package/templates/nextblock-template/lib/posts/readTime.ts +60 -0
  355. package/templates/nextblock-template/lib/privacy/consent-client.ts +57 -0
  356. package/templates/nextblock-template/lib/privacy/settings.ts +103 -0
  357. package/templates/nextblock-template/lib/privacy/types.ts +67 -0
  358. package/templates/nextblock-template/lib/promotions/server.test.ts +74 -0
  359. package/templates/nextblock-template/lib/promotions/server.ts +741 -0
  360. package/templates/nextblock-template/lib/resolve-block-relations.test.ts +142 -0
  361. package/templates/nextblock-template/lib/resolve-block-relations.ts +255 -0
  362. package/templates/nextblock-template/lib/search/server.ts +585 -0
  363. package/templates/nextblock-template/lib/search/types.ts +27 -0
  364. package/templates/nextblock-template/lib/visual-editing/draft-content.test.ts +105 -0
  365. package/templates/nextblock-template/lib/visual-editing/draft-content.ts +380 -0
  366. package/templates/nextblock-template/lib/visual-editing/draft-route.test.ts +42 -0
  367. package/templates/nextblock-template/lib/visual-editing/draft-route.ts +82 -0
  368. package/templates/nextblock-template/lib/visual-editing/edit-info.test.ts +143 -0
  369. package/templates/nextblock-template/lib/visual-editing/edit-info.ts +94 -0
  370. package/templates/nextblock-template/lib/visual-editing/mutations.ts +190 -0
  371. package/templates/nextblock-template/lib/visual-editing/product-drafts.test.ts +81 -0
  372. package/templates/nextblock-template/lib/visual-editing/product-drafts.ts +511 -0
  373. package/templates/nextblock-template/lib/visual-editing/types.ts +122 -0
  374. package/templates/nextblock-template/lib/zod-config.ts +5 -0
  375. package/templates/nextblock-template/next.config.js +190 -66
  376. package/templates/nextblock-template/package.json +34 -30
  377. package/templates/nextblock-template/proxy.ts +435 -253
  378. package/templates/nextblock-template/public/images/NBcover.webp +0 -0
  379. package/templates/nextblock-template/public/images/cap.webp +0 -0
  380. package/templates/nextblock-template/public/images/commerce-plan.webp +0 -0
  381. package/templates/nextblock-template/public/images/commerce-square.webp +0 -0
  382. package/templates/nextblock-template/public/images/commerce-wide.webp +0 -0
  383. package/templates/nextblock-template/public/images/cortex-ai-square.webp +0 -0
  384. package/templates/nextblock-template/public/images/cortex-ai.webp +0 -0
  385. package/templates/nextblock-template/public/images/extensibility.webp +0 -0
  386. package/templates/nextblock-template/public/images/goals.webp +0 -0
  387. package/templates/nextblock-template/public/images/included.webp +0 -0
  388. package/templates/nextblock-template/public/images/nx-graph.webp +0 -0
  389. package/templates/nextblock-template/public/images/pants.webp +0 -0
  390. package/templates/nextblock-template/public/images/t-shirt.webp +0 -0
  391. package/templates/nextblock-template/scripts/validate-editor-block-schema.ts +112 -0
  392. package/templates/nextblock-template/scripts/verify-cortex-ai-build-widget.tsx +100 -0
  393. package/templates/nextblock-template/scripts/verify-cortex-ai-generate-blocks.ts +62 -0
  394. package/templates/nextblock-template/scripts/verify-cortex-ai-global-tools.ts +537 -0
  395. package/templates/nextblock-template/scripts/verify-cortex-ai-routing.ts +58 -0
  396. package/templates/nextblock-template/scripts/verify-custom-block-definitions.ts +188 -0
  397. package/templates/nextblock-template/scripts/verify-dynamic-custom-block-extensions.ts +123 -0
  398. package/templates/nextblock-template/scripts/verify-dynamic-layout-engine.tsx +133 -0
  399. package/templates/nextblock-template/scripts/verify-milestone-2-custom-blocks.ts +65 -0
  400. package/templates/nextblock-template/tailwind.config.js +1 -0
  401. package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -0
  402. package/templates/nextblock-template/tools/deploy-supabase.js +69 -71
  403. package/templates/nextblock-template/tsconfig.json +52 -66
  404. package/templates/nextblock-template/tsconfig.tsbuildinfo +1 -1
  405. package/templates/nextblock-template/types/jsdom.d.ts +6 -0
  406. package/templates/nextblock-template/app/force-styles.tsx +0 -31
  407. package/templates/nextblock-template/app/sitemap.xml/route.ts +0 -63
  408. package/templates/nextblock-template/components/blocks/renderers/HeroBlockRenderer.tsx +0 -273
  409. package/templates/nextblock-template/docs/How to Create a Custom Block.md +0 -149
  410. package/templates/nextblock-template/docs/cms-application-overview.md +0 -56
  411. package/templates/nextblock-template/docs/cms-architecture-overview.md +0 -73
  412. package/templates/nextblock-template/docs/files-structure.md +0 -426
  413. package/templates/nextblock-template/docs/tiptap-bundle-optimization-summary.md +0 -174
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // eslint-disable-next-line @nx/enforce-module-boundaries
3
4
  import * as clack from '@clack/prompts';
4
5
  import { spawn } from 'node:child_process';
5
6
  import crypto from 'node:crypto';
@@ -10,7 +11,6 @@ import { execa } from 'execa';
10
11
  import { program } from 'commander';
11
12
  import chalk from 'chalk';
12
13
  import fs from 'fs-extra';
13
- import open from 'open';
14
14
 
15
15
  const DEFAULT_PROJECT_NAME = 'nextblock-cms';
16
16
  const __filename = fileURLToPath(import.meta.url);
@@ -52,17 +52,20 @@ const PACKAGE_VERSION_SOURCES = {
52
52
  '@nextblock-cms/sdk': resolve(REPO_ROOT, 'libs/sdk/package.json'),
53
53
  };
54
54
 
55
+ program.name('create-nextblock').description('NextBlock™ CMS CLI');
56
+
55
57
  program
56
- .name('create-nextblock')
57
- .description('Bootstrap a NextBlock CMS project')
58
- .argument(
59
- '[project-directory]',
60
- 'The name of the project directory to create',
61
- )
58
+ .command('create [project-directory]', { isDefault: true })
59
+ .description('Bootstrap a NextBlock CMS project')
62
60
  .option('--skip-install', 'Skip installing dependencies')
63
61
  .option('-y, --yes', 'Skip all interactive prompts and use defaults')
64
62
  .action(handleCommand);
65
63
 
64
+ program
65
+ .command('activate [module]')
66
+ .description('Activate a premium NextBlock™ CMS module')
67
+ .action(handleActivateCommand);
68
+
66
69
  await program.parseAsync(process.argv).catch((error) => {
67
70
  console.error(
68
71
  chalk.red(error instanceof Error ? error.message : String(error)),
@@ -180,7 +183,7 @@ async function handleCommand(projectDirectory, options) {
180
183
 
181
184
  console.log(
182
185
  chalk.green(
183
- `\nSuccess! Your NextBlock CMS project "${projectName}" is ready.\n`,
186
+ `\nSuccess! Your NextBlock CMS project "${projectName}" is ready.\n`,
184
187
  ),
185
188
  );
186
189
  console.log(chalk.cyan('Next step:'));
@@ -195,482 +198,778 @@ async function handleCommand(projectDirectory, options) {
195
198
  }
196
199
  }
197
200
 
198
- async function runSetupWizard(projectDir, projectName) {
199
- const projectPath = resolve(projectDir);
200
- process.chdir(projectPath);
201
+ async function handleActivateCommand(moduleName) {
202
+ if (!moduleName || moduleName !== 'ecommerce') {
203
+ console.error(
204
+ chalk.red('Invalid module name. Supported modules: ecommerce'),
205
+ );
206
+ process.exit(1);
207
+ }
201
208
 
202
- clack.intro('🚀 Welcome to the NextBlock setup wizard!');
209
+ clack.intro(`🚀 Activating NextBlock module: ${moduleName}`);
203
210
 
204
- const supabaseDir = resolve(projectPath, 'supabase');
205
- await fs.ensureDir(supabaseDir);
206
- await resetSupabaseProjectRef(projectPath);
211
+ const projectPath = process.cwd();
207
212
 
208
- clack.note(
209
- 'Before proceeding, ensure you have a Supabase project ready.\n\n' +
210
- '1. Supabase Cloud: Create a project at https://supabase.com/dashboard\n' +
211
- '2. Vercel Storage: If created via Vercel > Storage, check your .env.local snippet on Vercel for keys.',
212
- 'Supabase Prerequisites',
213
+ // 1. Install NPM package
214
+ clack.note(`Installing @nextblock-cms/${moduleName}...`);
215
+
216
+ await execa(
217
+ 'npm',
218
+ ['install', `@nextblock-cms/ecommerce@npm:@nextblock-cms/ecom@latest`],
219
+ { cwd: projectPath, stdio: 'inherit' },
213
220
  );
221
+ clack.note('NPM package installed!');
222
+
223
+ // 2. Inject Route Wrappers
224
+ clack.note('Injecting route wrappers...');
225
+
226
+ const routesToInject = {
227
+ 'app/cms/orders/page.tsx': `import { OrdersPage as OrdersPageUI } from '@nextblock-cms/ecommerce';
228
+ import { verifyPackageOnline } from '@nextblock-cms/db/server';
229
+ import { redirect } from 'next/navigation';
230
+
231
+ export default async function OrdersPage() {
232
+ const isOnline = await verifyPackageOnline('ecommerce');
233
+ if (!isOnline) {
234
+ redirect('/cms/settings/packages');
235
+ }
236
+
237
+ return <OrdersPageUI />;
238
+ }`,
239
+ 'app/cms/orders/[id]/page.tsx': `import { OrderDetailPage as OrderDetailPageUI } from '@nextblock-cms/ecommerce';
240
+ import { verifyPackageOnline } from '@nextblock-cms/db/server';
241
+ import { redirect } from 'next/navigation';
242
+
243
+ export default async function OrderDetailPage({
244
+ params,
245
+ }: {
246
+ params: Promise<{ id: string }>;
247
+ }) {
248
+ const isOnline = await verifyPackageOnline('ecommerce');
249
+ if (!isOnline) {
250
+ redirect('/cms/settings/packages');
251
+ }
252
+ const resolvedParams = await params;
253
+ return <OrderDetailPageUI params={resolvedParams} />;
254
+ }`,
255
+ 'app/cms/products/page.tsx': `import { ProductsPage as ProductsPageUI } from '@nextblock-cms/ecommerce';
256
+ import { verifyPackageOnline } from '@nextblock-cms/db/server';
257
+ import { redirect } from 'next/navigation';
258
+
259
+ export default async function ProductsPage() {
260
+ const isOnline = await verifyPackageOnline('ecommerce');
261
+ if (!isOnline) {
262
+ redirect('/cms/settings/packages');
263
+ }
264
+
265
+ return <ProductsPageUI />;
266
+ }`,
267
+ 'app/cms/products/new/page.tsx': `import { NewProductPage as NewProductPageUI } from '@nextblock-cms/ecommerce';
268
+ import { verifyPackageOnline } from '@nextblock-cms/db/server';
269
+ import { redirect } from 'next/navigation';
214
270
 
215
- clack.note('Connecting to Supabase...');
216
- clack.note('I will now open your browser to log into Supabase.');
217
- await runSupabaseCli(['login'], { cwd: projectPath });
271
+ export default async function NewProductPage() {
272
+ const isOnline = await verifyPackageOnline('ecommerce');
273
+ if (!isOnline) {
274
+ redirect('/cms/settings/packages');
275
+ }
218
276
 
219
- clack.note('Now, please select your NextBlock project when prompted.');
220
- await runSupabaseCli(['link'], { cwd: projectPath });
221
- if (process.stdin.isTTY) {
222
- try {
223
- process.stdin.setRawMode(false);
224
- } catch {
225
- // ignore
277
+ return <NewProductPageUI />;
278
+ }`,
279
+ 'app/cms/products/[id]/edit/page.tsx': `import { EditProductPage as EditProductPageUI } from '@nextblock-cms/ecommerce';
280
+ import { verifyPackageOnline } from '@nextblock-cms/db/server';
281
+ import { redirect } from 'next/navigation';
282
+
283
+ export default async function EditProductPage({ params }: { params: Promise<{ id: string }> }) {
284
+ const isOnline = await verifyPackageOnline('ecommerce');
285
+ if (!isOnline) {
286
+ redirect('/cms/settings/packages');
287
+ }
288
+
289
+ const resolvedParams = await params;
290
+ return <EditProductPageUI params={resolvedParams} />;
291
+ }`,
292
+ 'app/cms/payments/page.tsx': `import { PaymentsPage as PaymentsPageUI } from '@nextblock-cms/ecommerce';
293
+ import { verifyPackageOnline } from '@nextblock-cms/db/server';
294
+ import { redirect } from 'next/navigation';
295
+
296
+ export default async function PaymentsPage() {
297
+ const isOnline = await verifyPackageOnline('ecommerce');
298
+ if (!isOnline) {
299
+ redirect('/cms/settings/packages');
300
+ }
301
+
302
+ return <PaymentsPageUI />;
303
+ }`,
304
+ 'app/cms/coupons/page.tsx': `import { CouponsPage as CouponsPageUI } from '@nextblock-cms/ecommerce/server';
305
+ import { verifyPackageOnline } from '@nextblock-cms/db/server';
306
+ import { redirect } from 'next/navigation';
307
+
308
+ export default async function CouponsPage({
309
+ searchParams,
310
+ }: {
311
+ searchParams: Promise<{ status?: string; q?: string }>;
312
+ }) {
313
+ const isOnline = await verifyPackageOnline('ecommerce');
314
+ if (!isOnline) {
315
+ redirect('/cms/settings/packages');
316
+ }
317
+
318
+ return <CouponsPageUI searchParams={await searchParams} />;
319
+ }`,
320
+ 'app/cms/coupons/[id]/edit/page.tsx': `import { EditCouponPage as EditCouponPageUI } from '@nextblock-cms/ecommerce/server';
321
+ import { verifyPackageOnline } from '@nextblock-cms/db/server';
322
+ import { redirect } from 'next/navigation';
323
+
324
+ export default async function EditCouponPage({
325
+ params,
326
+ }: {
327
+ params: Promise<{ id: string }>;
328
+ }) {
329
+ const isOnline = await verifyPackageOnline('ecommerce');
330
+ if (!isOnline) {
331
+ redirect('/cms/settings/packages');
332
+ }
333
+
334
+ return <EditCouponPageUI params={params} />;
335
+ }`,
336
+ 'app/checkout/success/page.tsx': `import { CheckoutSuccessPage as CheckoutSuccessPageUI } from '@nextblock-cms/ecommerce';
337
+ import { verifyPackageOnline } from '@nextblock-cms/db/server';
338
+ import { notFound } from 'next/navigation';
339
+
340
+ export default async function CheckoutSuccessPage() {
341
+ const isOnline = await verifyPackageOnline('ecommerce');
342
+ if (!isOnline) {
343
+ notFound();
344
+ }
345
+
346
+ return <CheckoutSuccessPageUI />;
347
+ }`,
348
+ 'app/api/checkout/route.ts': `import { NextResponse } from 'next/server';
349
+ import { getPaymentProvider } from '@nextblock-cms/ecommerce/server';
350
+ import { createClient, verifyPackageOnline } from '@nextblock-cms/db/server';
351
+ import { normalizeCustomerAddress } from '@nextblock-cms/ecommerce';
352
+
353
+ function resolveProviderFromItem(item) {
354
+ if (item?.provider === 'stripe' || item?.provider === 'freemius') {
355
+ return item.provider;
356
+ }
357
+
358
+ if (item?.payment_provider === 'stripe' || item?.payment_provider === 'freemius') {
359
+ return item.payment_provider;
360
+ }
361
+
362
+ if (item?.product_type === 'digital') {
363
+ return 'freemius';
364
+ }
365
+
366
+ if (item?.product_type === 'physical') {
367
+ return 'stripe';
368
+ }
369
+
370
+ if (item?.freemius_product_id) {
371
+ return 'freemius';
372
+ }
373
+
374
+ return null;
375
+ }
376
+
377
+ export async function POST(req: Request) {
378
+ try {
379
+ const isOnline = await verifyPackageOnline('ecommerce');
380
+ if (!isOnline) {
381
+ return NextResponse.json({ error: 'Ecommerce module license is inactive' }, { status: 403 });
382
+ }
383
+
384
+ const {
385
+ items,
386
+ customerEmail,
387
+ customerPhone,
388
+ billingAddress,
389
+ shippingAddress,
390
+ shippingMethodId,
391
+ currencyCode,
392
+ locale,
393
+ couponCode,
394
+ couponContextItems,
395
+ } = await req.json();
396
+
397
+ if (!items || !Array.isArray(items)) {
398
+ return NextResponse.json({ error: 'Invalid items data' }, { status: 400 });
399
+ }
400
+
401
+ const providerNames = Array.from(
402
+ new Set(items.map((item) => resolveProviderFromItem(item)).filter(Boolean))
403
+ );
404
+
405
+ if (providerNames.length === 0) {
406
+ return NextResponse.json(
407
+ { error: 'Each checkout request must include provider-aware cart items.' },
408
+ { status: 400 }
409
+ );
410
+ }
411
+
412
+ if (providerNames.length > 1) {
413
+ return NextResponse.json(
414
+ { error: 'Mixed-provider carts must be checked out in separate steps.' },
415
+ { status: 400 }
416
+ );
226
417
  }
227
- process.stdin.setEncoding('utf8');
228
- process.stdin.resume();
418
+
419
+ const providerName = providerNames[0];
420
+
421
+ if (providerName === 'freemius' && items.length !== 1) {
422
+ return NextResponse.json(
423
+ { error: 'Freemius items must be checked out one at a time.' },
424
+ { status: 400 }
425
+ );
426
+ }
427
+
428
+ if (!billingAddress) {
429
+ return NextResponse.json({ error: 'Billing address is required' }, { status: 400 });
430
+ }
431
+
432
+ const supabase = createClient();
433
+ const provider = getPaymentProvider(providerName);
434
+
435
+ const { data: { user } } = await supabase.auth.getUser();
436
+ const userId = user?.id;
437
+ const resolvedCustomerEmail = user?.email || customerEmail || null;
438
+
439
+ const { url, error, errorKey, errorParams, errorStatus, customProps } =
440
+ await provider.createCheckoutSession({
441
+ items,
442
+ customerEmail: resolvedCustomerEmail,
443
+ customerPhone,
444
+ userId,
445
+ billingAddress: normalizeCustomerAddress(billingAddress) ?? billingAddress,
446
+ shippingAddress:
447
+ providerName === 'stripe'
448
+ ? normalizeCustomerAddress(shippingAddress)
449
+ : null,
450
+ shippingMethodId: providerName === 'stripe' ? shippingMethodId : null,
451
+ currencyCode: typeof currencyCode === 'string' ? currencyCode : null,
452
+ locale: typeof locale === 'string' ? locale : null,
453
+ couponCode: typeof couponCode === 'string' ? couponCode : null,
454
+ couponContextItems: Array.isArray(couponContextItems) ? couponContextItems : items,
455
+ });
456
+
457
+ if (error) {
458
+ console.error('Checkout Error:', error);
459
+ return NextResponse.json(
460
+ { error, errorKey, errorParams },
461
+ { status: errorStatus ?? 500 }
462
+ );
463
+ }
464
+
465
+ return NextResponse.json({ url, customProps });
466
+ } catch (err: any) {
467
+ console.error('Checkout API Error:', err);
468
+ return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
229
469
  }
470
+ }`,
471
+ };
230
472
 
231
- let projectId = await readSupabaseProjectRef(projectPath);
473
+ for (const [routePath, content] of Object.entries(routesToInject)) {
474
+ const fullPath = resolve(projectPath, routePath);
475
+ await fs.ensureDir(dirname(fullPath));
476
+ await fs.writeFile(fullPath, content);
477
+ }
232
478
 
233
- if (!projectId) {
234
- clack.note('I could not detect your Supabase project ref automatically.');
235
- const manual = await clack.text({
236
- message:
237
- 'Enter your Supabase project ref (from the Supabase dashboard URL or the link output, e.g., abcdefghijklmnopqrstu):',
238
- validate: (val) => (!val ? 'Project ref is required' : undefined),
239
- });
240
- if (clack.isCancel(manual)) {
241
- handleWizardCancel('Setup cancelled.');
479
+ clack.outro(
480
+ ' Ecommerce module activated successfully! You can now use the storefront features.',
481
+ );
482
+ }
483
+
484
+ // clack validator that rejects empty/whitespace-only input with a labelled message.
485
+ function requiredValue(label) {
486
+ return (value) =>
487
+ value && String(value).trim() ? undefined : `${label} is required`;
488
+ }
489
+
490
+ // Read the current value of a `KEY=` line from an .env file body (handles quotes),
491
+ // so re-runs can reuse already-generated secrets instead of regenerating them.
492
+ function readEnvValue(envContent, key) {
493
+ for (const line of envContent.split(/\r?\n/)) {
494
+ if (line.startsWith(key)) {
495
+ return line.slice(key.length).trim().replace(/^"(.*)"$/, '$1');
242
496
  }
243
- projectId = manual.trim();
244
497
  }
245
- await ensureSupabaseAssets(projectPath, { required: true });
498
+ return '';
499
+ }
500
+
501
+ function generateSecret() {
502
+ return crypto.randomBytes(32).toString('hex');
503
+ }
504
+
505
+ async function runSetupWizard(projectDir, projectName) {
506
+ const projectPath = resolve(projectDir);
507
+ process.chdir(projectPath);
508
+
509
+ clack.intro('🚀 NextBlock™ CMS setup wizard');
510
+
511
+ // 0. Prerequisites — mirror `npm run setup` (tools/scripts/setup.mjs). Make sure the
512
+ // developer has everything BEFORE we start prompting.
513
+ clack.note(
514
+ [
515
+ '1. A Supabase project https://supabase.com/dashboard',
516
+ ' • Reference ID — Project Settings > General > "Reference ID"',
517
+ ' • Connection string — Connect (top bar) > Direct connection > URI',
518
+ ' • anon + service_role keys — Project Settings > API Keys',
519
+ ' • Personal Access Token — Account > Access Tokens > Generate new token',
520
+ '',
521
+ '2. A Cloudflare R2 bucket https://dash.cloudflare.com > R2',
522
+ ' • Create a bucket, then enable its Public Development URL (Bucket > Settings > General)',
523
+ ' • Create an R2 API token (Object Read & Write); copy the Access Key ID + Secret (shown once)',
524
+ '',
525
+ '3. SMTP credentials SMTP2GO works very well: https://www.smtp2go.com',
526
+ ' • Required so Supabase can email the confirmation link your first admin needs to sign in',
527
+ ].join('\n'),
528
+ 'Before you continue, have all of the following ready',
529
+ );
246
530
 
247
- const siteUrlPrompt = await clack.text({
248
- message: 'What is the public URL of your site? (NEXT_PUBLIC_URL)',
249
- initialValue: 'http://localhost:3000',
250
- validate: (val) => (!val ? 'URL is required' : undefined),
531
+ const ready = await clack.confirm({
532
+ message: 'Do you have your Supabase, Cloudflare R2, and SMTP details ready?',
533
+ initialValue: true,
251
534
  });
252
- if (clack.isCancel(siteUrlPrompt)) {
535
+ if (clack.isCancel(ready)) {
253
536
  handleWizardCancel('Setup cancelled.');
254
537
  }
255
- const siteUrl = siteUrlPrompt.trim();
538
+ if (!ready) {
539
+ clack.note(
540
+ 'No problem — your project files are ready. Gather the items above, then copy\n.env.example to .env.local and fill it in. Full guide: docs/05-DEVELOPER-GUIDE.md',
541
+ 'Setup paused',
542
+ );
543
+ return;
544
+ }
256
545
 
257
- clack.note(
258
- 'Please go to your Supabase project dashboard to get the following secrets.\n' +
259
- 'Note: For "Access Token", go to Account > Access Tokens > Generate New Token.',
260
- );
261
- const supabaseKeys = await clack.group(
546
+ await fs.ensureDir(resolve(projectPath, 'supabase'));
547
+
548
+ // 1. Supabase same questions/order as setup.mjs. Nothing is masked: you are pasting
549
+ // keys you just copied, and seeing them makes paste mistakes easy to spot.
550
+ clack.note('Get these from https://supabase.com/dashboard', 'Supabase project');
551
+ const supabase = await clack.group(
262
552
  {
553
+ projectId: () =>
554
+ clack.text({
555
+ message: 'Project ID (Project Settings > General > "Reference ID"):',
556
+ validate: requiredValue('Project Reference ID'),
557
+ }),
263
558
  postgresUrl: () =>
264
559
  clack.text({
265
560
  message:
266
- 'What is your Connection String? (Supabase: Project Dashboard > Connect (Top Left) > Connection String > URI | Vercel: POSTGRES_URL)',
561
+ 'Connection String (Connect > Direct connection > URI replace [YOUR-PASSWORD] with your DB password):',
267
562
  placeholder: 'postgresql://...',
268
- validate: (val) =>
269
- !val ? 'Connection string is required' : undefined,
563
+ validate: requiredValue('Connection string'),
270
564
  }),
271
565
  anonKey: () =>
272
- clack.password({
273
- message:
274
- 'What is your Project API Key (anon key)? (Supabase: Project Settings > API Keys > Project Legacy API Keys | Vercel: SUPABASE_ANON_KEY)',
275
- validate: (val) => (!val ? 'Anon Key is required' : undefined),
566
+ clack.text({
567
+ message: 'Project API Key — anon / public (Project Settings > API Keys):',
568
+ validate: requiredValue('Anon key'),
276
569
  }),
277
570
  serviceKey: () =>
278
- clack.password({
279
- message:
280
- 'What is your Service Role Key (service_role key)? (Supabase: Project Settings > API Keys > Project Legacy API Keys | Vercel: SUPABASE_SERVICE_ROLE_KEY)',
281
- validate: (val) =>
282
- !val ? 'Service Role Key is required' : undefined,
571
+ clack.text({
572
+ message: 'Service Role Key — service_role (Project Settings > API Keys):',
573
+ validate: requiredValue('Service role key'),
283
574
  }),
284
575
  accessToken: () =>
285
- clack.password({
576
+ clack.text({
286
577
  message:
287
- 'What is your Personal Access Token? (Supabase: Account > Access Tokens). Required for deployment.',
288
- validate: (val) =>
289
- !val ? 'Access Token is required for deployment' : undefined,
578
+ 'Personal Access Token (Account > Access Tokens > Generate new token):',
579
+ validate: requiredValue('Access token'),
580
+ }),
581
+ siteUrl: () =>
582
+ clack.text({
583
+ // Standalone `npm run dev` is plain `next dev` on :3000 (NOT `nx serve` on :4200),
584
+ // so the local default differs from the monorepo setup wizard on purpose.
585
+ message: 'Public site URL [NEXT_PUBLIC_URL]:',
586
+ initialValue: 'http://localhost:3000',
587
+ validate: requiredValue('Site URL'),
290
588
  }),
291
589
  },
292
590
  { onCancel: () => handleWizardCancel('Setup cancelled.') },
293
591
  );
294
592
 
295
- clack.note('Generating local secrets...');
296
- const revalidationToken = crypto.randomBytes(32).toString('hex');
593
+ const projectId = supabase.projectId.trim();
594
+ const postgresUrl = supabase.postgresUrl.trim();
595
+ const siteUrl = supabase.siteUrl.trim().replace(/\/+$/, '');
297
596
  const supabaseUrl = `https://${projectId}.supabase.co`;
298
597
 
299
- const postgresUrl = supabaseKeys.postgresUrl;
598
+ // Extract the database password from the connection string; prompt if it is missing
599
+ // or still the [YOUR-PASSWORD] placeholder.
300
600
  let dbPassword = '';
301
601
  try {
302
- const parsedUrl = new URL(postgresUrl);
303
- dbPassword = parsedUrl.password;
602
+ dbPassword = decodeURIComponent(new URL(postgresUrl).password);
304
603
  } catch {
305
- // Fallback if URL parsing fails, though validation above checks for non-empty
604
+ // Fall through to the manual prompt below.
306
605
  }
307
-
308
- if (!dbPassword) {
309
- const passwordPrompt = await clack.password({
606
+ if (!dbPassword || /YOUR-PASSWORD/i.test(dbPassword)) {
607
+ const passwordPrompt = await clack.text({
310
608
  message:
311
- 'Could not extract password from URL. What is your Database Password?',
312
- validate: (val) => (!val ? 'Password is required' : undefined),
609
+ 'Could not read the DB password from the URI. Enter your Postgres database password:',
610
+ validate: requiredValue('Database password'),
313
611
  });
314
612
  if (clack.isCancel(passwordPrompt)) {
315
613
  handleWizardCancel('Setup cancelled.');
316
614
  }
317
- dbPassword = passwordPrompt;
615
+ dbPassword = passwordPrompt.trim();
318
616
  }
319
617
 
320
- const envPath = resolve(projectPath, '.env');
321
- const appendEnvBlock = async (label, lines) => {
322
- const normalized = lines.join('\n');
323
- const blockContent = normalized.endsWith('\n')
324
- ? normalized
325
- : `${normalized}\n`;
326
- if (canWriteEnv) {
327
- await fs.appendFile(envPath, blockContent);
328
- } else {
329
- clack.note(
330
- `Add the following ${label} values to your existing .env:\n${blockContent}`,
331
- );
332
- }
618
+ // 2. Cloudflare R2 — required. Powers media uploads, image processing, and backups.
619
+ clack.note('https://dash.cloudflare.com > R2', 'Cloudflare R2 storage');
620
+ const r2 = await clack.group(
621
+ {
622
+ accountId: () =>
623
+ clack.text({
624
+ message: 'R2 Account ID (R2 overview > Account details):',
625
+ validate: requiredValue('R2 Account ID'),
626
+ }),
627
+ bucketName: () =>
628
+ clack.text({
629
+ message: 'R2 Bucket Name:',
630
+ validate: requiredValue('R2 Bucket Name'),
631
+ }),
632
+ publicBaseUrl: () =>
633
+ clack.text({
634
+ message:
635
+ 'R2 Public Development URL (Bucket > Settings > Public Development URL, e.g. https://pub-xxxx.r2.dev):',
636
+ validate: requiredValue('R2 Public Development URL'),
637
+ }),
638
+ accessKey: () =>
639
+ clack.text({
640
+ message: 'R2 Access Key ID (R2 > Manage API Tokens):',
641
+ validate: requiredValue('R2 Access Key ID'),
642
+ }),
643
+ secretKey: () =>
644
+ clack.text({
645
+ message:
646
+ 'R2 Secret Access Key (shown only once when the token is created):',
647
+ validate: requiredValue('R2 Secret Access Key'),
648
+ }),
649
+ },
650
+ { onCancel: () => handleWizardCancel('Setup cancelled.') },
651
+ );
652
+
653
+ // 3. SMTP — required. Sends the sign-up confirmation email your first admin needs.
654
+ clack.note('SMTP2GO works very well: https://www.smtp2go.com', 'SMTP email');
655
+ const smtp = await clack.group(
656
+ {
657
+ host: () =>
658
+ clack.text({
659
+ message: 'SMTP Host (e.g. mail.smtp2go.com):',
660
+ validate: requiredValue('SMTP Host'),
661
+ }),
662
+ port: () =>
663
+ clack.text({
664
+ message: 'SMTP Port (465 = SSL, 587 = STARTTLS):',
665
+ initialValue: '465',
666
+ validate: requiredValue('SMTP Port'),
667
+ }),
668
+ user: () =>
669
+ clack.text({
670
+ message: 'SMTP User:',
671
+ validate: requiredValue('SMTP User'),
672
+ }),
673
+ pass: () =>
674
+ clack.text({
675
+ message: 'SMTP Password:',
676
+ validate: requiredValue('SMTP Password'),
677
+ }),
678
+ fromEmail: () =>
679
+ clack.text({
680
+ message: 'From Email (the address confirmation emails are sent from):',
681
+ validate: requiredValue('From Email'),
682
+ }),
683
+ fromName: () =>
684
+ clack.text({
685
+ message: 'From Name (e.g. NextBlock):',
686
+ validate: requiredValue('From Name'),
687
+ }),
688
+ },
689
+ { onCancel: () => handleWizardCancel('Setup cancelled.') },
690
+ );
691
+
692
+ const smtpValues = {
693
+ host: smtp.host,
694
+ port: smtp.port,
695
+ user: smtp.user,
696
+ pass: smtp.pass,
697
+ fromEmail: smtp.fromEmail,
698
+ fromName: smtp.fromName,
333
699
  };
334
- const envLines = [
335
- `NEXT_PUBLIC_URL=${siteUrl}`,
336
- '# Vercel / Supabase',
337
- `SUPABASE_PROJECT_ID=${projectId}`,
338
- `NEXT_PUBLIC_SUPABASE_URL=${supabaseUrl}`,
339
- `NEXT_PUBLIC_SUPABASE_ANON_KEY=${supabaseKeys.anonKey}`,
340
- `SUPABASE_SERVICE_ROLE_KEY=${supabaseKeys.serviceKey}`,
341
- `SUPABASE_ACCESS_TOKEN=${supabaseKeys.accessToken}`,
342
- `POSTGRES_URL=${postgresUrl}`,
343
- '',
344
- '# Revalidation',
345
- `REVALIDATE_SECRET_TOKEN=${revalidationToken}`,
346
- '',
347
- ];
348
700
 
349
- let canWriteEnv = true;
701
+ // 4. Write .env.local with everything we collected. Mirror setup.mjs: seed from the
702
+ // template .env.example when present, replace keys line-by-line, append any missing,
703
+ // and reuse already-generated secrets so re-runs are idempotent. .env.local is what
704
+ // `next dev` loads first and is covered by the generated .gitignore.
705
+ clack.note('Writing .env.local...');
706
+ const envPath = resolve(projectPath, '.env.local');
707
+ const envExamplePath = resolve(projectPath, '.env.example');
708
+ let envContent = '';
350
709
  if (await fs.pathExists(envPath)) {
351
- const overwrite = await clack.confirm({
352
- message: '.env already exists. Overwrite with generated values?',
353
- initialValue: false,
354
- });
355
- if (clack.isCancel(overwrite)) {
356
- handleWizardCancel('Setup cancelled.');
357
- }
358
- if (!overwrite) {
359
- canWriteEnv = false;
360
- clack.note(
361
- 'Keeping existing .env. Add/merge the generated values manually.',
362
- );
710
+ envContent = await fs.readFile(envPath, 'utf8');
711
+ } else if (await fs.pathExists(envExamplePath)) {
712
+ envContent = await fs.readFile(envExamplePath, 'utf8');
713
+ }
714
+
715
+ const cronSecret = readEnvValue(envContent, 'CRON_SECRET=') || generateSecret();
716
+ const draftSecret =
717
+ readEnvValue(envContent, 'DRAFT_MODE_SECRET=') || generateSecret();
718
+ const revalidateSecret =
719
+ readEnvValue(envContent, 'REVALIDATE_SECRET_TOKEN=') || generateSecret();
720
+
721
+ const replacements = {
722
+ 'SUPABASE_PROJECT_ID=': `SUPABASE_PROJECT_ID=${projectId}`,
723
+ 'POSTGRES_URL=': `POSTGRES_URL=${postgresUrl}`,
724
+ 'POSTGRES_PASSWORD=': `POSTGRES_PASSWORD="${dbPassword}"`,
725
+ 'NEXT_PUBLIC_SUPABASE_URL=': `NEXT_PUBLIC_SUPABASE_URL=${supabaseUrl}`,
726
+ 'NEXT_PUBLIC_SUPABASE_ANON_KEY=': `NEXT_PUBLIC_SUPABASE_ANON_KEY=${supabase.anonKey}`,
727
+ 'SUPABASE_SERVICE_ROLE_KEY=': `SUPABASE_SERVICE_ROLE_KEY=${supabase.serviceKey}`,
728
+ 'SUPABASE_ACCESS_TOKEN=': `SUPABASE_ACCESS_TOKEN=${supabase.accessToken}`,
729
+ 'NEXT_PUBLIC_URL=': `NEXT_PUBLIC_URL=${siteUrl}`,
730
+ 'CRON_SECRET=': `CRON_SECRET=${cronSecret}`,
731
+ 'DRAFT_MODE_SECRET=': `DRAFT_MODE_SECRET=${draftSecret}`,
732
+ 'REVALIDATE_SECRET_TOKEN=': `REVALIDATE_SECRET_TOKEN=${revalidateSecret}`,
733
+ // The R2 public URL is consumed under two names (next/image remotePatterns + CSP, and
734
+ // media URL resolution) — write the same value to both, matching setup.mjs.
735
+ 'NEXT_PUBLIC_R2_PUBLIC_URL=': `NEXT_PUBLIC_R2_PUBLIC_URL=${r2.publicBaseUrl}`,
736
+ 'NEXT_PUBLIC_R2_BASE_URL=': `NEXT_PUBLIC_R2_BASE_URL=${r2.publicBaseUrl}`,
737
+ 'R2_ACCOUNT_ID=': `R2_ACCOUNT_ID=${r2.accountId}`,
738
+ 'R2_BUCKET_NAME=': `R2_BUCKET_NAME=${r2.bucketName}`,
739
+ 'R2_ACCESS_KEY_ID=': `R2_ACCESS_KEY_ID=${r2.accessKey}`,
740
+ 'R2_SECRET_ACCESS_KEY=': `R2_SECRET_ACCESS_KEY=${r2.secretKey}`,
741
+ 'SMTP_HOST=': `SMTP_HOST=${smtpValues.host}`,
742
+ 'SMTP_PORT=': `SMTP_PORT=${smtpValues.port}`,
743
+ 'SMTP_USER=': `SMTP_USER=${smtpValues.user}`,
744
+ 'SMTP_PASS=': `SMTP_PASS=${smtpValues.pass}`,
745
+ 'SMTP_FROM_EMAIL=': `SMTP_FROM_EMAIL=${smtpValues.fromEmail}`,
746
+ 'SMTP_FROM_NAME=': `SMTP_FROM_NAME=${smtpValues.fromName}`,
747
+ 'SUPABASE_AUTH_RATE_LIMIT_EMAIL_SENT=':
748
+ 'SUPABASE_AUTH_RATE_LIMIT_EMAIL_SENT=30',
749
+ };
750
+
751
+ const appliedKeys = new Set();
752
+ const updatedLines = envContent.split(/\r?\n/).map((line) => {
753
+ for (const [key, value] of Object.entries(replacements)) {
754
+ if (line.startsWith(key)) {
755
+ appliedKeys.add(key);
756
+ return value;
757
+ }
363
758
  }
364
- }
759
+ return line;
760
+ });
365
761
 
366
- if (canWriteEnv) {
367
- await fs.writeFile(envPath, envLines.join('\n'));
368
- clack.note('Supabase configuration saved to .env');
762
+ // Append any keys missing from the seed so nothing is silently dropped.
763
+ for (const [key, value] of Object.entries(replacements)) {
764
+ if (!appliedKeys.has(key)) {
765
+ updatedLines.push(value);
766
+ }
369
767
  }
370
768
 
371
- clack.note('Setting up your database...');
372
-
373
- const dbPushSpinner = clack.spinner();
374
- dbPushSpinner.start('Pushing database schema...');
375
- try {
376
- process.env.POSTGRES_URL = postgresUrl;
377
- const migrationsDir = resolve(projectPath, 'supabase', 'migrations');
378
- const hasMigrations = async () =>
379
- (await fs.pathExists(migrationsDir)) &&
380
- (await fs.readdir(migrationsDir)).some((name) => name.endsWith('.sql'));
381
-
382
- if (!(await hasMigrations())) {
383
- await ensureSupabaseAssets(projectPath);
384
- }
769
+ await fs.writeFile(envPath, updatedLines.join('\n'), 'utf8');
770
+ clack.note(
771
+ 'Supabase, R2, SMTP, site URL, and generated secrets saved to .env.local',
772
+ );
385
773
 
386
- if (!(await hasMigrations())) {
387
- dbPushSpinner.stop(
388
- `No migrations found in ${migrationsDir}; skipping db push. Ensure @nextblock-cms/db includes supabase/migrations.`,
389
- );
390
- } else {
391
- const supabaseBin = await getSupabaseBinary(projectPath);
392
- const command = supabaseBin === 'npx' ? 'npx' : supabaseBin;
774
+ // 5. Materialize Supabase assets (migrations, config.toml, branded auth email
775
+ // templates) out of the installed @nextblock-cms/db package.
776
+ await ensureSupabaseAssets(projectPath, { required: true });
393
777
 
394
- // 1. Link the project explicitly
395
- dbPushSpinner.message('Linking to Supabase project...');
396
- const linkArgs = supabaseBin === 'npx' ? ['supabase', 'link'] : ['link'];
397
- linkArgs.push('--project-ref', projectId);
398
- linkArgs.push('--password', dbPassword);
778
+ // 6. Link the project and apply the schema. These are the standalone equivalents of the
779
+ // monorepo `npm run db:link` + `npm run db:migrate:fresh` (which do not exist in a
780
+ // generated project): we drive the Supabase CLI directly, authenticating with the
781
+ // access token so no browser login is required.
782
+ const supabaseBin = await getSupabaseBinary(projectPath);
783
+ const command = supabaseBin === 'npx' ? 'npx' : supabaseBin;
784
+ const sbArgs = (args) => (supabaseBin === 'npx' ? ['supabase', ...args] : args);
785
+ const supabaseEnv = {
786
+ ...process.env,
787
+ SUPABASE_ACCESS_TOKEN: supabase.accessToken,
788
+ SUPABASE_DB_PASSWORD: dbPassword,
789
+ POSTGRES_URL: postgresUrl,
790
+ // Available for env() substitution in supabase config.toml during `config push`.
791
+ NEXT_PUBLIC_URL: siteUrl,
792
+ };
399
793
 
400
- await execa(command, linkArgs, {
401
- stdio: 'inherit',
402
- cwd: projectPath,
403
- });
794
+ const applySchema = await clack.confirm({
795
+ message:
796
+ 'Apply the database schema to the linked project now? (Safe for a new database; does not delete existing data.)',
797
+ initialValue: true,
798
+ });
799
+ if (clack.isCancel(applySchema)) {
800
+ handleWizardCancel('Setup cancelled.');
801
+ }
404
802
 
405
- // 2. Push the schema using the linked state
406
- dbPushSpinner.message('Pushing database schema...');
407
- const pushArgs =
408
- supabaseBin === 'npx' ? ['supabase', 'db', 'push'] : ['db', 'push'];
409
- pushArgs.push('--include-all');
803
+ const dbSpinner = clack.spinner();
804
+ dbSpinner.start('Linking to your Supabase project...');
805
+ try {
806
+ await execa(
807
+ command,
808
+ sbArgs(['link', '--project-ref', projectId, '--password', dbPassword]),
809
+ { stdio: 'inherit', cwd: projectPath, env: supabaseEnv },
810
+ );
410
811
 
411
- await execa(command, pushArgs, {
812
+ if (applySchema) {
813
+ dbSpinner.message('Pushing database schema...');
814
+ await execa(command, sbArgs(['db', 'push', '--include-all']), {
412
815
  stdio: ['pipe', 'inherit', 'inherit'],
413
816
  cwd: projectPath,
414
817
  input: 'y\n', // Auto-confirm the push prompt
415
- env: {
416
- ...process.env,
417
- SUPABASE_DB_PASSWORD: dbPassword,
418
- },
818
+ env: supabaseEnv,
419
819
  });
420
820
 
421
- // 3. Push the config (for Auth settings like site_url)
422
- dbPushSpinner.message('Pushing Supabase config (auth settings)...');
423
- const configPushArgs =
424
- supabaseBin === 'npx'
425
- ? ['supabase', 'config', 'push']
426
- : ['config', 'push'];
427
-
428
- await execa(command, configPushArgs, {
821
+ dbSpinner.message('Pushing Supabase config (auth settings)...');
822
+ await execa(command, sbArgs(['config', 'push']), {
429
823
  stdio: ['pipe', 'inherit', 'inherit'],
430
824
  cwd: projectPath,
431
- env: {
432
- ...process.env,
433
- SUPABASE_DB_PASSWORD: dbPassword,
434
- // Ensure NEXT_PUBLIC_URL is available for env() substitution in config.toml
435
- NEXT_PUBLIC_URL: siteUrl,
436
- },
825
+ env: supabaseEnv,
437
826
  });
438
827
 
439
- dbPushSpinner.stop('Database schema and config pushed successfully!');
828
+ dbSpinner.stop('Database schema and config applied.');
829
+ } else {
830
+ dbSpinner.stop(
831
+ 'Linked. Skipped schema push — run `npx supabase db push --include-all` when ready.',
832
+ );
440
833
  }
441
834
  } catch (error) {
442
- dbPushSpinner.stop(
443
- 'Database push failed. Please run `npx supabase db push` manually.',
835
+ dbSpinner.stop(
836
+ 'Database setup failed. You can run `npx supabase db push --include-all` manually.',
444
837
  );
445
838
  if (error instanceof Error) {
446
839
  clack.note(error.message);
447
840
  }
448
841
  }
449
842
 
450
- clack.note(
451
- 'Optional Cloudflare R2 Setup:\nHave your Account ID, API token (Access + Secret), bucket name, and public bucket URL handy if you want media storage ready now.',
452
- );
453
- const setupR2 = await clack.confirm({
454
- message:
455
- 'Do you want to set up Cloudflare R2 for media storage now? (Optional > populate .env keys)',
843
+ // 7. Sync hosted Supabase Auth: custom SMTP + branded email templates. SMTP and the
844
+ // access token are required, so this always runs (matching setup.mjs). This is what
845
+ // lets Supabase email your first admin their confirmation link.
846
+ await enableSupabaseSmtpConfig(projectPath);
847
+ await configureHostedSupabaseAuth(projectPath, {
848
+ projectId,
849
+ siteUrl,
850
+ accessToken: supabase.accessToken,
851
+ smtpValues,
456
852
  });
457
- if (clack.isCancel(setupR2)) {
458
- handleWizardCancel('Setup cancelled.');
459
- }
460
-
461
- let r2Values = {
462
- publicBaseUrl: '',
463
- accountId: '',
464
- bucketName: '',
465
- accessKey: '',
466
- secretKey: '',
467
- };
468
-
469
- if (setupR2) {
470
- clack.note(
471
- 'I will open your browser to the R2 dashboard.\nYou need to create a bucket and an R2 API Token.',
472
- );
473
- await open('https://dash.cloudflare.com/?to=/:account/r2', { wait: false });
474
-
475
- const r2Keys = await clack.group(
476
- {
477
- accountId: () =>
478
- clack.text({
479
- message:
480
- 'R2: Paste your Cloudflare Account ID (Overview > Account Details - Bottom right):',
481
- validate: (val) => (!val ? 'Account ID is required' : undefined),
482
- }),
483
- bucketName: () =>
484
- clack.text({
485
- message: 'R2: Paste your Bucket Name:',
486
- validate: (val) => (!val ? 'Bucket name is required' : undefined),
487
- }),
488
- accessKey: () =>
489
- clack.password({
490
- message: 'R2: Paste your Access Key ID (create API tokens):',
491
- validate: (val) => (!val ? 'Access Key ID is required' : undefined),
492
- }),
493
- secretKey: () =>
494
- clack.password({
495
- message: 'R2: Paste your Secret Access Key:',
496
- validate: (val) =>
497
- !val ? 'Secret Access Key is required' : undefined,
498
- }),
499
- publicBaseUrl: () =>
500
- clack.text({
501
- message:
502
- 'R2: Public Base URL (Bucket > Settings > Public Development URL-Enable: e.g., https://pub-xxx.r2.dev)',
503
- validate: (val) =>
504
- !val ? 'Public base URL is required' : undefined,
505
- }),
506
- },
507
- { onCancel: () => handleWizardCancel('Setup cancelled.') },
508
- );
509
853
 
510
- r2Values = {
511
- publicBaseUrl: r2Keys.publicBaseUrl,
512
- accountId: r2Keys.accountId,
513
- bucketName: r2Keys.bucketName,
514
- accessKey: r2Keys.accessKey,
515
- secretKey: r2Keys.secretKey,
516
- };
517
- }
518
-
519
- await appendEnvBlock('Cloudflare R2', [
520
- '',
521
- '# Cloudflare',
522
- `NEXT_PUBLIC_R2_BASE_URL=${r2Values.publicBaseUrl}`,
523
- `R2_ACCOUNT_ID=${r2Values.accountId}`,
524
- `R2_BUCKET_NAME=${r2Values.bucketName}`,
525
- `R2_ACCESS_KEY_ID=${r2Values.accessKey}`,
526
- `R2_SECRET_ACCESS_KEY=${r2Values.secretKey}`,
527
- '',
528
- ]);
529
- if (setupR2) {
530
- clack.note('Cloudflare R2 configuration saved!');
531
- } else if (canWriteEnv) {
532
- clack.note(
533
- 'Cloudflare R2 placeholders added to .env. Configure them later when ready.',
534
- );
535
- }
536
-
537
- clack.note(
538
- 'Optional SMTP Setup:\nProvide the host, port, credentials, and from details for your email provider (e.g., Resend, Postmark) to send transactional emails immediately.',
539
- );
540
- const setupSMTP = await clack.confirm({
541
- message: 'Do you want to set up an SMTP server for emails now? (Optional)',
854
+ // 8. Optional premium modules (CLI-specific; requires a license + registry access).
855
+ const setupPremium = await clack.confirm({
856
+ message: 'Do you have a license and want to install premium modules now?',
857
+ initialValue: false,
542
858
  });
543
- if (clack.isCancel(setupSMTP)) {
859
+ if (clack.isCancel(setupPremium)) {
544
860
  handleWizardCancel('Setup cancelled.');
545
861
  }
546
-
547
- let smtpValues = {
548
- host: '',
549
- port: '',
550
- user: '',
551
- pass: '',
552
- fromEmail: '',
553
- fromName: '',
554
- };
555
-
556
- if (setupSMTP) {
557
- const smtpKeys = await clack.group(
862
+ if (setupPremium) {
863
+ clack.note('Installing @nextblock-cms/ecommerce...');
864
+ await execa(
865
+ 'npm',
866
+ ['install', '@nextblock-cms/ecommerce@npm:@nextblock-cms/ecom@latest'],
558
867
  {
559
- host: () =>
560
- clack.text({
561
- message: 'SMTP: Host (e.g., smtp.resend.com):',
562
- validate: (val) => (!val ? 'SMTP host is required' : undefined),
563
- }),
564
- port: () =>
565
- clack.text({
566
- message: 'SMTP: Port (e.g., 465):',
567
- validate: (val) => (!val ? 'SMTP port is required' : undefined),
568
- }),
569
- user: () =>
570
- clack.text({
571
- message: 'SMTP: User (e.g., apikey):',
572
- validate: (val) => (!val ? 'SMTP user is required' : undefined),
573
- }),
574
- pass: () =>
575
- clack.password({
576
- message: 'SMTP: Password:',
577
- validate: (val) => (!val ? 'SMTP password is required' : undefined),
578
- }),
579
- fromEmail: () =>
580
- clack.text({
581
- message: 'SMTP: From Email (e.g., onboarding@my.site):',
582
- validate: (val) => (!val ? 'From email is required' : undefined),
583
- }),
584
- fromName: () =>
585
- clack.text({
586
- message: 'SMTP: From Name (e.g., NextBlock):',
587
- validate: (val) => (!val ? 'From name is required' : undefined),
588
- }),
868
+ cwd: projectPath,
869
+ stdio: 'inherit',
589
870
  },
590
- { onCancel: () => handleWizardCancel('Setup cancelled.') },
591
871
  );
592
-
593
- smtpValues = {
594
- host: smtpKeys.host,
595
- port: smtpKeys.port,
596
- user: smtpKeys.user,
597
- pass: smtpKeys.pass,
598
- fromEmail: smtpKeys.fromEmail,
599
- fromName: smtpKeys.fromName,
600
- };
872
+ clack.note('Premium module installed!');
601
873
  }
602
874
 
603
- clack.note(
604
- 'Optional Premium Module Setup:\nIf you have a nextblock license, you can install the premium modules now.',
875
+ clack.outro(
876
+ [
877
+ `🎉 Your NextBlock™ project ${projectName ? `"${projectName}" ` : ''}is ready!`,
878
+ '',
879
+ 'Next steps:',
880
+ ` 1. Start the app: cd ${projectName} && npm run dev → ${siteUrl}`,
881
+ ` 2. Create your account: open ${siteUrl}/sign-up`,
882
+ ' The FIRST account to sign up automatically becomes the ADMIN.',
883
+ ' 3. Confirm your email: click the link sent to your inbox',
884
+ ` 4. Sign in — you'll land in the CMS at ${siteUrl}/cms/dashboard`,
885
+ ].join('\n'),
605
886
  );
606
- const setupPremium = await clack.confirm({
607
- message:
608
- 'Do you have a GitHub Personal Access Token (PAT) for premium modules?',
609
- initialValue: false,
610
- });
887
+ }
611
888
 
612
- if (clack.isCancel(setupPremium)) {
613
- handleWizardCancel('Setup cancelled.');
889
+ async function enableSupabaseSmtpConfig(projectDir) {
890
+ const configPath = resolve(projectDir, 'supabase', 'config.toml');
891
+
892
+ if (!(await fs.pathExists(configPath))) {
893
+ return;
614
894
  }
615
895
 
616
- if (setupPremium) {
617
- const patPrompt = await clack.text({
618
- message: 'Your GitHub Personal Access Token (PAT):',
619
- placeholder: 'ghp_ or github_pat_...',
620
- validate: (val) => {
621
- if (!val) return 'PAT is required';
622
- if (!val.startsWith('ghp_') && !val.startsWith('github_pat_')) {
623
- return 'Token must start with ghp_ or github_pat_';
624
- }
625
- },
626
- });
896
+ const smtpBlock = `# [auth.email.smtp]
897
+ # host = "env(SMTP_HOST)"
898
+ # port = 587
899
+ # user = "env(SMTP_USER)"
900
+ # pass = "env(SMTP_PASS)"
901
+ # admin_email = "env(SMTP_FROM_EMAIL)"
902
+ # sender_name = "env(SMTP_FROM_NAME)"`;
627
903
 
628
- if (clack.isCancel(patPrompt)) {
629
- handleWizardCancel('Setup cancelled.');
630
- }
631
-
632
- const pat = patPrompt.trim();
904
+ const enabledSmtpBlock = `[auth.email.smtp]
905
+ host = "env(SMTP_HOST)"
906
+ port = 587
907
+ user = "env(SMTP_USER)"
908
+ pass = "env(SMTP_PASS)"
909
+ admin_email = "env(SMTP_FROM_EMAIL)"
910
+ sender_name = "env(SMTP_FROM_NAME)"`;
633
911
 
634
- // Configure .npmrc for the project
635
- const npmrcPath = resolve(projectPath, '.npmrc');
636
- const npmrcContent = [
637
- '@nextblock-cms:registry=https://npm.pkg.github.com',
638
- `//npm.pkg.github.com/:_authToken=${pat}`,
639
- '',
640
- ].join('\n');
912
+ const configContents = await fs.readFile(configPath, 'utf8');
641
913
 
642
- await fs.writeFile(npmrcPath, npmrcContent);
643
- clack.note('Premium modules configured in .npmrc!');
914
+ if (configContents.includes(enabledSmtpBlock)) {
915
+ return;
916
+ }
644
917
 
645
- clack.note('Installing @nextblock-cms/ecom...');
646
- await runCommand('npm', ['install', '@nextblock-cms/ecom'], {
647
- cwd: projectPath,
648
- });
649
- clack.note('Premium module installed!');
918
+ if (!configContents.includes(smtpBlock)) {
919
+ throw new Error(
920
+ `Could not find the SMTP placeholder block in ${configPath}.`,
921
+ );
650
922
  }
651
923
 
652
- await appendEnvBlock('SMTP', [
653
- '',
654
- '# Email SMTP Configuration',
655
- `SMTP_HOST=${smtpValues.host}`,
656
- `SMTP_PORT=${smtpValues.port}`,
657
- `SMTP_USER=${smtpValues.user}`,
658
- `SMTP_PASS=${smtpValues.pass}`,
659
- `SMTP_FROM_EMAIL=${smtpValues.fromEmail}`,
660
- `SMTP_FROM_NAME=${smtpValues.fromName}`,
661
- '',
662
- ]);
663
- if (setupSMTP) {
664
- clack.note('SMTP configuration saved!');
665
- } else if (canWriteEnv) {
924
+ await fs.writeFile(
925
+ configPath,
926
+ configContents.replace(smtpBlock, enabledSmtpBlock),
927
+ 'utf8',
928
+ );
929
+ }
930
+
931
+ async function configureHostedSupabaseAuth(
932
+ projectDir,
933
+ { projectId, siteUrl, accessToken, smtpValues },
934
+ ) {
935
+ if (!projectId || !siteUrl || !accessToken) {
666
936
  clack.note(
667
- 'SMTP placeholders added to .env. Configure them later when ready.',
937
+ 'Skipped hosted Supabase Auth sync because the project ref, site URL, or access token is missing.',
668
938
  );
939
+ return;
669
940
  }
670
941
 
671
- clack.outro(
672
- `🎉 Your NextBlock project ${projectName ? `"${projectName}" ` : ''}is ready!`,
673
- );
942
+ const spinner = clack.spinner();
943
+ spinner.start('Syncing hosted Supabase Auth SMTP and branded email templates...');
944
+
945
+ try {
946
+ await execa('node', ['tools/configure-supabase-auth.js'], {
947
+ cwd: projectDir,
948
+ env: {
949
+ ...process.env,
950
+ SUPABASE_PROJECT_ID: projectId,
951
+ NEXT_PUBLIC_URL: siteUrl,
952
+ SUPABASE_ACCESS_TOKEN: accessToken,
953
+ SMTP_HOST: smtpValues.host,
954
+ SMTP_PORT: smtpValues.port,
955
+ SMTP_USER: smtpValues.user,
956
+ SMTP_PASS: smtpValues.pass,
957
+ SMTP_FROM_EMAIL: smtpValues.fromEmail,
958
+ SMTP_FROM_NAME: smtpValues.fromName,
959
+ SUPABASE_AUTH_RATE_LIMIT_EMAIL_SENT:
960
+ process.env.SUPABASE_AUTH_RATE_LIMIT_EMAIL_SENT || '30',
961
+ },
962
+ });
963
+ spinner.stop('Hosted Supabase Auth configured.');
964
+ } catch (error) {
965
+ spinner.stop(
966
+ 'Hosted Supabase Auth sync skipped. You can rerun it later with npm run configure:supabase-auth.',
967
+ );
968
+ clack.note(
969
+ error instanceof Error ? error.message : String(error),
970
+ 'Supabase Auth Sync',
971
+ );
972
+ }
674
973
  }
675
974
 
676
975
  function handleWizardCancel(message) {
@@ -875,23 +1174,30 @@ async function ensureEnvExample(projectDir) {
875
1174
  }
876
1175
  }
877
1176
 
878
- const placeholder = `# Environment variables for NextBlock CMS
1177
+ const placeholder = `# Environment variables for NextBlock CMS
879
1178
  NEXT_PUBLIC_URL=
880
- # Vercel / Supabase
1179
+
1180
+ # Supabase — the setup wizard fills this whole block.
881
1181
  SUPABASE_PROJECT_ID=
882
1182
  POSTGRES_URL=
1183
+ POSTGRES_PASSWORD=
883
1184
  NEXT_PUBLIC_SUPABASE_URL=
884
1185
  NEXT_PUBLIC_SUPABASE_ANON_KEY=
885
1186
  SUPABASE_SERVICE_ROLE_KEY=
1187
+ SUPABASE_ACCESS_TOKEN=
1188
+
1189
+ # Auto-generated by the setup wizard.
1190
+ CRON_SECRET=
1191
+ DRAFT_MODE_SECRET=
1192
+ REVALIDATE_SECRET_TOKEN=
886
1193
 
887
- # Cloudflare
1194
+ # Cloudflare R2 — setup writes the public URL to both keys.
1195
+ NEXT_PUBLIC_R2_PUBLIC_URL=
888
1196
  NEXT_PUBLIC_R2_BASE_URL=
1197
+ R2_ACCOUNT_ID=
1198
+ R2_BUCKET_NAME=
889
1199
  R2_ACCESS_KEY_ID=
890
1200
  R2_SECRET_ACCESS_KEY=
891
- R2_BUCKET_NAME=
892
- R2_ACCOUNT_ID=
893
-
894
- REVALIDATE_SECRET_TOKEN=
895
1201
 
896
1202
  # Email SMTP Configuration
897
1203
  SMTP_HOST=
@@ -900,6 +1206,7 @@ SMTP_USER=
900
1206
  SMTP_PASS=
901
1207
  SMTP_FROM_EMAIL=
902
1208
  SMTP_FROM_NAME=
1209
+ SUPABASE_AUTH_RATE_LIMIT_EMAIL_SENT=30
903
1210
  `;
904
1211
 
905
1212
  await fs.writeFile(destination, placeholder);
@@ -949,6 +1256,19 @@ async function ensureSupabaseAssets(projectDir, options = {}) {
949
1256
  migrationsCopied = true;
950
1257
  }
951
1258
 
1259
+ // Branded Auth email templates. configure-supabase-auth.js resolves the supabase dir by
1260
+ // requiring a templates/ subdir, and uploads these via the Management API. Without them
1261
+ // the hosted-auth + SMTP sync silently skips and the first admin never gets a
1262
+ // confirmation email — so copy them alongside config.toml + migrations.
1263
+ const sourceTemplates = resolve(packageSupabaseDir, 'templates');
1264
+ const destTemplates = resolve(destSupabaseDir, 'templates');
1265
+ if (await fs.pathExists(sourceTemplates)) {
1266
+ await fs.copy(sourceTemplates, destTemplates, {
1267
+ overwrite: true,
1268
+ errorOnExist: false,
1269
+ });
1270
+ }
1271
+
952
1272
  if (required) {
953
1273
  if (!configCopied) {
954
1274
  throw new Error(
@@ -1027,32 +1347,6 @@ async function resolvePackageSupabaseDir(projectDir) {
1027
1347
  return { dir: null, triedPaths };
1028
1348
  }
1029
1349
 
1030
- async function readSupabaseProjectRef(projectDir) {
1031
- const projectRefPath = resolve(
1032
- projectDir,
1033
- 'supabase',
1034
- '.temp',
1035
- 'project-ref',
1036
- );
1037
- if (await fs.pathExists(projectRefPath)) {
1038
- const value = (await fs.readFile(projectRefPath, 'utf8')).trim();
1039
- if (/^[a-z0-9]{20,}$/i.test(value)) {
1040
- return value;
1041
- }
1042
- }
1043
-
1044
- return null;
1045
- }
1046
-
1047
- async function resetSupabaseProjectRef(projectDir) {
1048
- const tempDir = resolve(projectDir, 'supabase', '.temp');
1049
- await fs.ensureDir(tempDir);
1050
- const projectRefPath = resolve(tempDir, 'project-ref');
1051
- if (await fs.pathExists(projectRefPath)) {
1052
- await fs.remove(projectRefPath);
1053
- }
1054
- }
1055
-
1056
1350
  async function ensureClientComponents(projectDir) {
1057
1351
  const relativePaths = [
1058
1352
  'components/env-var-warning.tsx',
@@ -1237,7 +1531,6 @@ async function sanitizeLayout(projectDir) {
1237
1531
 
1238
1532
  const requiredImports = [
1239
1533
  "import '@nextblock-cms/ui/styles/globals.css';",
1240
- "import '@nextblock-cms/editor/styles/editor.css';",
1241
1534
  ];
1242
1535
 
1243
1536
  const content = await fs.readFile(layoutPath, 'utf8');
@@ -1559,35 +1852,6 @@ function runCommand(command, args, options = {}) {
1559
1852
  });
1560
1853
  }
1561
1854
 
1562
- async function runSupabaseCli(args, options = {}) {
1563
- const { cwd } = options;
1564
- const supabaseBin = await getSupabaseBinary(cwd);
1565
- const command = supabaseBin === 'npx' ? 'npx' : supabaseBin;
1566
- const cmdArgs = supabaseBin === 'npx' ? ['supabase', ...args] : args;
1567
-
1568
- return new Promise((resolve, reject) => {
1569
- const child = spawn(command, cmdArgs, {
1570
- cwd,
1571
- shell: IS_WINDOWS,
1572
- stdio: 'inherit',
1573
- });
1574
-
1575
- child.on('error', (error) => {
1576
- reject(error);
1577
- });
1578
-
1579
- child.on('close', (code) => {
1580
- if (code === 0) {
1581
- resolve();
1582
- } else {
1583
- reject(
1584
- new Error(`supabase ${args.join(' ')} exited with code ${code}`),
1585
- );
1586
- }
1587
- });
1588
- });
1589
- }
1590
-
1591
1855
  async function getSupabaseBinary(projectDir) {
1592
1856
  const binDir = resolve(projectDir, 'node_modules', '.bin');
1593
1857
  const ext = IS_WINDOWS ? '.cmd' : '';
@@ -1643,18 +1907,7 @@ function buildNextConfigContent(editorUtilNames) {
1643
1907
  ' minimumCacheTTL: 31536000,',
1644
1908
  ' dangerouslyAllowSVG: false,',
1645
1909
  " contentSecurityPolicy: \"default-src 'self'; script-src 'none'; sandbox;\",",
1646
- ' remotePatterns: [',
1647
- " { protocol: 'https', hostname: 'pub-a31e3f1a87d144898aeb489a8221f92e.r2.dev' },",
1648
- " { protocol: 'https', hostname: 'e260676f72b0b18314b868f136ed72ae.r2.cloudflarestorage.com' },",
1649
- ' ...(process.env.NEXT_PUBLIC_URL',
1650
- ' ? [',
1651
- ' {',
1652
- " protocol: /** @type {'http' | 'https'} */ (new URL(process.env.NEXT_PUBLIC_URL).protocol.slice(0, -1)),",
1653
- ' hostname: new URL(process.env.NEXT_PUBLIC_URL).hostname,',
1654
- ' },',
1655
- ' ]',
1656
- ' : []),',
1657
- ' ],',
1910
+ ' remotePatterns: getRemotePatterns(),',
1658
1911
  ' },',
1659
1912
  ' experimental: {',
1660
1913
  ' optimizeCss: true,',
@@ -1736,6 +1989,34 @@ function buildNextConfigContent(editorUtilNames) {
1736
1989
  '};',
1737
1990
  '',
1738
1991
  'module.exports = nextConfig;',
1992
+ '',
1993
+ 'function getRemotePatterns() {',
1994
+ ' /** @type {Array<{ protocol: "http" | "https", hostname: string, pathname: string }>} */',
1995
+ ' const patterns = [];',
1996
+ ' // Whitelist this project R2 public/base URLs and the site URL for next/image.',
1997
+ ' const sources = [',
1998
+ ' process.env.NEXT_PUBLIC_R2_PUBLIC_URL,',
1999
+ ' process.env.NEXT_PUBLIC_R2_BASE_URL,',
2000
+ ' process.env.NEXT_PUBLIC_URL,',
2001
+ ' ];',
2002
+ ' for (const value of sources) {',
2003
+ ' if (!value) continue;',
2004
+ ' try {',
2005
+ ' const parsed = new URL(value);',
2006
+ " if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') continue;",
2007
+ ' const hostname = parsed.hostname;',
2008
+ ' if (patterns.some((pattern) => pattern.hostname === hostname)) continue;',
2009
+ ' patterns.push({',
2010
+ " protocol: parsed.protocol === 'https:' ? 'https' : 'http',",
2011
+ ' hostname,',
2012
+ " pathname: '/**',",
2013
+ ' });',
2014
+ ' } catch {',
2015
+ ' // ignore malformed value',
2016
+ ' }',
2017
+ ' }',
2018
+ ' return patterns;',
2019
+ '}',
1739
2020
  );
1740
2021
 
1741
2022
  return lines.join('\n');