create-nextblock 0.12.9 → 0.12.11

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 (292) hide show
  1. package/docker-template/.dockerignore +23 -23
  2. package/docker-template/.env.docker.example +69 -69
  3. package/docker-template/docker/db/init/99-jwt.sql +6 -6
  4. package/docker-template/docker/db/init/99-roles.sql +25 -25
  5. package/docker-template/docker/kong/kong.yml +112 -112
  6. package/docker-template/scripts/docker-setup.mjs +310 -310
  7. package/libs/db/tsconfig.lib.json +3 -3
  8. package/libs/editor/tsconfig.lib.json +3 -3
  9. package/libs/ui/tsconfig.lib.json +3 -3
  10. package/libs/utils/tsconfig.json +3 -3
  11. package/package.json +1 -1
  12. package/project.json +19 -19
  13. package/templates/nextblock-template/.browserslistrc +11 -11
  14. package/templates/nextblock-template/.dockerignore +23 -23
  15. package/templates/nextblock-template/README.md +34 -34
  16. package/templates/nextblock-template/app/(auth-pages)/layout.tsx +9 -9
  17. package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +27 -27
  18. package/templates/nextblock-template/app/(auth-pages)/sign-up/SignUpForm.tsx +141 -141
  19. package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +46 -46
  20. package/templates/nextblock-template/app/(auth-pages)/two-factor/actions.ts +91 -91
  21. package/templates/nextblock-template/app/(auth-pages)/two-factor/components/TwoFactorForm.tsx +123 -123
  22. package/templates/nextblock-template/app/(auth-pages)/two-factor/page.tsx +51 -51
  23. package/templates/nextblock-template/app/ToasterProvider.tsx +17 -17
  24. package/templates/nextblock-template/app/[slug]/pageClientActions.ts +7 -7
  25. package/templates/nextblock-template/app/actions/consent.ts +57 -57
  26. package/templates/nextblock-template/app/actions/interactions.test.ts +301 -301
  27. package/templates/nextblock-template/app/actions/interactions.ts +373 -373
  28. package/templates/nextblock-template/app/actions/postActions.ts +146 -146
  29. package/templates/nextblock-template/app/actions/twoFactorEmail.ts +22 -22
  30. package/templates/nextblock-template/app/api/ai/cortex/build-widget/route.ts +153 -153
  31. package/templates/nextblock-template/app/api/ai/generate-blocks/route.ts +96 -96
  32. package/templates/nextblock-template/app/api/ai/global-agent/route.ts +964 -964
  33. package/templates/nextblock-template/app/api/brand/email-logo/route.ts +48 -48
  34. package/templates/nextblock-template/app/api/cms/check-updates/route.ts +44 -44
  35. package/templates/nextblock-template/app/api/cms/full-backup/export/route.ts +33 -33
  36. package/templates/nextblock-template/app/api/cms/full-backup/restore/route.ts +63 -63
  37. package/templates/nextblock-template/app/api/cron/reset-sandbox/route.ts +3490 -3490
  38. package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +5597 -5597
  39. package/templates/nextblock-template/app/api/cron/sync-currencies/route.ts +39 -39
  40. package/templates/nextblock-template/app/api/custom-blocks/db-relations/route.ts +92 -92
  41. package/templates/nextblock-template/app/api/custom-blocks/editor-definitions/route.ts +43 -43
  42. package/templates/nextblock-template/app/api/media/library/route.ts +69 -69
  43. package/templates/nextblock-template/app/api/media/r2-presigned/route.ts +53 -53
  44. package/templates/nextblock-template/app/api/media/record/route.ts +160 -160
  45. package/templates/nextblock-template/app/api/search/route.ts +43 -43
  46. package/templates/nextblock-template/app/article/[slug]/PostClientContent.tsx +441 -441
  47. package/templates/nextblock-template/app/auth/callback/route.ts +31 -31
  48. package/templates/nextblock-template/app/cart/page.tsx +7 -7
  49. package/templates/nextblock-template/app/cms/blocks/components/CustomBlockEditorPreview.tsx +160 -160
  50. package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +149 -149
  51. package/templates/nextblock-template/app/cms/blocks/editors/DynamicCustomBlockEditor.tsx +167 -167
  52. package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +29 -29
  53. package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +90 -90
  54. package/templates/nextblock-template/app/cms/components/ConnectGitHubButton.tsx +122 -122
  55. package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -58
  56. package/templates/nextblock-template/app/cms/components/FeatureImageField.tsx +244 -244
  57. package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +36 -36
  58. package/templates/nextblock-template/app/cms/components/SystemAlertsBanner.tsx +112 -112
  59. package/templates/nextblock-template/app/cms/components/TwoFactorReminderBanner.tsx +45 -45
  60. package/templates/nextblock-template/app/cms/components/github-connect-actions.ts +102 -102
  61. package/templates/nextblock-template/app/cms/components/system-alerts-actions.ts +31 -31
  62. package/templates/nextblock-template/app/cms/custom-blocks/[id]/edit/page.tsx +66 -66
  63. package/templates/nextblock-template/app/cms/custom-blocks/actions.ts +519 -519
  64. package/templates/nextblock-template/app/cms/custom-blocks/components/BlockComposer.tsx +1522 -1522
  65. package/templates/nextblock-template/app/cms/custom-blocks/components/BlocksLibraryTransferControls.tsx +256 -256
  66. package/templates/nextblock-template/app/cms/custom-blocks/components/DBRelationSelect.tsx +384 -384
  67. package/templates/nextblock-template/app/cms/custom-blocks/components/ImageR2Picker.tsx +221 -221
  68. package/templates/nextblock-template/app/cms/custom-blocks/new/page.tsx +12 -12
  69. package/templates/nextblock-template/app/cms/custom-blocks/page.tsx +438 -438
  70. package/templates/nextblock-template/app/cms/dashboard/actions.ts +228 -228
  71. package/templates/nextblock-template/app/cms/dashboard/components/DashboardOnboarding.tsx +130 -130
  72. package/templates/nextblock-template/app/cms/import-export/ContentTransferControls.tsx +431 -391
  73. package/templates/nextblock-template/app/cms/import-export/actions.ts +226 -226
  74. package/templates/nextblock-template/app/cms/interactions/InteractionsModerationClient.tsx +408 -408
  75. package/templates/nextblock-template/app/cms/interactions/page.tsx +51 -51
  76. package/templates/nextblock-template/app/cms/layout.tsx +73 -73
  77. package/templates/nextblock-template/app/cms/media/components/FolderNavigator.tsx +273 -273
  78. package/templates/nextblock-template/app/cms/media/components/FolderTree.tsx +122 -122
  79. package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +26 -26
  80. package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +69 -69
  81. package/templates/nextblock-template/app/cms/media/components/MediaPickerDialog.tsx +149 -149
  82. package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +101 -101
  83. package/templates/nextblock-template/app/cms/navigation/components/NavigationMenuDnd.tsx +3 -3
  84. package/templates/nextblock-template/app/cms/pages/page.tsx +18 -17
  85. package/templates/nextblock-template/app/cms/posts/page.tsx +19 -18
  86. package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -12
  87. package/templates/nextblock-template/app/cms/products/categories/page.tsx +12 -12
  88. package/templates/nextblock-template/app/cms/products/inventory/page.tsx +13 -13
  89. package/templates/nextblock-template/app/cms/products/new/page.tsx +135 -135
  90. package/templates/nextblock-template/app/cms/products/page.tsx +39 -27
  91. package/templates/nextblock-template/app/cms/products/productFormData.ts +133 -133
  92. package/templates/nextblock-template/app/cms/products/settings/page.tsx +5 -5
  93. package/templates/nextblock-template/app/cms/promotions/PromotionsWorkspace.tsx +456 -456
  94. package/templates/nextblock-template/app/cms/promotions/actions.ts +115 -115
  95. package/templates/nextblock-template/app/cms/promotions/page.tsx +31 -31
  96. package/templates/nextblock-template/app/cms/revisions/service.ts +19 -19
  97. package/templates/nextblock-template/app/cms/revisions/utils.ts +132 -132
  98. package/templates/nextblock-template/app/cms/settings/backup-restore/BackupRestoreWorkspace.tsx +1004 -1004
  99. package/templates/nextblock-template/app/cms/settings/backup-restore/page.tsx +29 -29
  100. package/templates/nextblock-template/app/cms/settings/bot-protection/actions.ts +93 -93
  101. package/templates/nextblock-template/app/cms/settings/bot-protection/components/BotProtectionForm.tsx +129 -129
  102. package/templates/nextblock-template/app/cms/settings/bot-protection/page.tsx +24 -24
  103. package/templates/nextblock-template/app/cms/settings/cortex-ai/SandboxCortexAiSettingsClient.tsx +497 -497
  104. package/templates/nextblock-template/app/cms/settings/cortex-ai/StoredCortexAiSettingsClient.tsx +410 -410
  105. package/templates/nextblock-template/app/cms/settings/cortex-ai/actions.ts +246 -246
  106. package/templates/nextblock-template/app/cms/settings/cortex-ai/page.tsx +80 -80
  107. package/templates/nextblock-template/app/cms/settings/currencies/actions.ts +331 -331
  108. package/templates/nextblock-template/app/cms/settings/currencies/page.tsx +494 -494
  109. package/templates/nextblock-template/app/cms/settings/email/actions.ts +64 -64
  110. package/templates/nextblock-template/app/cms/settings/email/components/EmailForm.tsx +181 -181
  111. package/templates/nextblock-template/app/cms/settings/email/page.tsx +28 -28
  112. package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -767
  113. package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +93 -93
  114. package/templates/nextblock-template/app/cms/settings/google-analytics/actions.ts +60 -60
  115. package/templates/nextblock-template/app/cms/settings/google-analytics/components/GoogleAnalyticsForm.tsx +129 -129
  116. package/templates/nextblock-template/app/cms/settings/google-analytics/page.tsx +26 -26
  117. package/templates/nextblock-template/app/cms/settings/languages/actions.ts +16 -16
  118. package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +7 -7
  119. package/templates/nextblock-template/app/cms/settings/logos/components/BrandingSettingsForm.tsx +339 -339
  120. package/templates/nextblock-template/app/cms/settings/logos/components/DeleteLogoButton.tsx +21 -21
  121. package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +20 -20
  122. package/templates/nextblock-template/app/cms/settings/logos/components/SetActiveLogoButton.tsx +42 -42
  123. package/templates/nextblock-template/app/cms/settings/logos/components/SiteSeoSettingsForm.tsx +133 -133
  124. package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +8 -8
  125. package/templates/nextblock-template/app/cms/settings/packages/package-card.tsx +122 -122
  126. package/templates/nextblock-template/app/cms/settings/privacy/actions.ts +52 -52
  127. package/templates/nextblock-template/app/cms/settings/privacy/components/PrivacyForm.tsx +148 -148
  128. package/templates/nextblock-template/app/cms/settings/privacy/page.tsx +27 -27
  129. package/templates/nextblock-template/app/cms/settings/registration/actions.ts +44 -44
  130. package/templates/nextblock-template/app/cms/settings/registration/components/RegistrationForm.tsx +65 -65
  131. package/templates/nextblock-template/app/cms/settings/registration/page.tsx +27 -27
  132. package/templates/nextblock-template/app/cms/settings/security/actions.ts +284 -284
  133. package/templates/nextblock-template/app/cms/settings/security/components/SecurityPanel.tsx +522 -522
  134. package/templates/nextblock-template/app/cms/settings/security/page.tsx +33 -33
  135. package/templates/nextblock-template/app/cms/settings/taxes/page.tsx +21 -21
  136. package/templates/nextblock-template/app/cms/shipping/page.tsx +20 -20
  137. package/templates/nextblock-template/app/cms/users/actions.ts +222 -222
  138. package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +12 -12
  139. package/templates/nextblock-template/app/cms/users/components/UserForm.tsx +44 -44
  140. package/templates/nextblock-template/app/lib/site-settings.ts +105 -105
  141. package/templates/nextblock-template/app/lib/sitemap-utils.ts +253 -253
  142. package/templates/nextblock-template/app/profile/ProfileAccountSidebar.tsx +73 -73
  143. package/templates/nextblock-template/app/profile/ProfilePageHeader.tsx +16 -16
  144. package/templates/nextblock-template/app/profile/ProfilePageMissingState.tsx +9 -9
  145. package/templates/nextblock-template/app/profile/account-data.ts +37 -37
  146. package/templates/nextblock-template/app/profile/account-links.ts +22 -22
  147. package/templates/nextblock-template/app/profile/account-types.ts +11 -11
  148. package/templates/nextblock-template/app/profile/orders/CustomerOrdersPageClient.tsx +124 -124
  149. package/templates/nextblock-template/app/profile/orders/[id]/CustomerOrderDetailPageClient.tsx +79 -79
  150. package/templates/nextblock-template/app/profile/orders/[id]/page.tsx +32 -32
  151. package/templates/nextblock-template/app/profile/orders/page.tsx +19 -19
  152. package/templates/nextblock-template/app/profile/page.tsx +42 -42
  153. package/templates/nextblock-template/app/profile/password/PasswordSettingsPageClient.tsx +128 -128
  154. package/templates/nextblock-template/app/profile/password/actions.ts +59 -59
  155. package/templates/nextblock-template/app/profile/password/page.tsx +27 -27
  156. package/templates/nextblock-template/app/setup/SetupWizard.tsx +678 -678
  157. package/templates/nextblock-template/app/setup/layout.tsx +13 -13
  158. package/templates/nextblock-template/app/setup/page.tsx +111 -111
  159. package/templates/nextblock-template/app/sitemap.ts +130 -130
  160. package/templates/nextblock-template/components/CartDrawerLoader.tsx +7 -7
  161. package/templates/nextblock-template/components/DeferredCartDrawer.tsx +23 -23
  162. package/templates/nextblock-template/components/DeferredGoogleAnalytics.tsx +70 -70
  163. package/templates/nextblock-template/components/DeferredGoogleTagManager.tsx +70 -70
  164. package/templates/nextblock-template/components/DeferredSpeedInsights.tsx +69 -69
  165. package/templates/nextblock-template/components/FeatureImageHero.tsx +47 -47
  166. package/templates/nextblock-template/components/FooterNavigation.tsx +32 -32
  167. package/templates/nextblock-template/components/GitHubLoginButton.tsx +15 -15
  168. package/templates/nextblock-template/components/HtmlScriptExecutor.tsx +47 -47
  169. package/templates/nextblock-template/components/LanguageSwitcher.tsx +2 -2
  170. package/templates/nextblock-template/components/PostCommentsSection.tsx +369 -369
  171. package/templates/nextblock-template/components/ProductReviewsSection.tsx +419 -419
  172. package/templates/nextblock-template/components/PublicEnvBootstrap.tsx +30 -30
  173. package/templates/nextblock-template/components/ResponsiveNav.tsx +14 -14
  174. package/templates/nextblock-template/components/auth/AuthBotProtection.tsx +182 -182
  175. package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +12 -12
  176. package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -12
  177. package/templates/nextblock-template/components/blocks/ecommerceRendererLoaders.ts +23 -23
  178. package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +249 -249
  179. package/templates/nextblock-template/components/blocks/types.ts +7 -7
  180. package/templates/nextblock-template/components/env-var-warning.tsx +3 -3
  181. package/templates/nextblock-template/components/form-message.tsx +32 -32
  182. package/templates/nextblock-template/components/privacy/ConsentBanner.tsx +170 -170
  183. package/templates/nextblock-template/components/privacy/ConsentGatedAnalytics.tsx +70 -70
  184. package/templates/nextblock-template/components/renderers/CachedDynamicLayoutEngine.tsx +28 -28
  185. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.test.tsx +166 -166
  186. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.tsx +464 -464
  187. package/templates/nextblock-template/components/submit-button.tsx +23 -23
  188. package/templates/nextblock-template/components/theme-switcher.tsx +8 -8
  189. package/templates/nextblock-template/components/visual-editing/DeferredVisualEditing.tsx +21 -21
  190. package/templates/nextblock-template/context/AuthContext.tsx +23 -23
  191. package/templates/nextblock-template/context/LanguageContext.tsx +16 -16
  192. package/templates/nextblock-template/context/language-rest-client.ts +32 -32
  193. package/templates/nextblock-template/docker/db/init/99-jwt.sql +6 -6
  194. package/templates/nextblock-template/docker/db/init/99-roles.sql +25 -25
  195. package/templates/nextblock-template/docker/kong/kong.yml +112 -112
  196. package/templates/nextblock-template/docs/01-PROJECT-OVERVIEW.md +94 -94
  197. package/templates/nextblock-template/docs/02-ECOMMERCE-CAPABILITIES.md +364 -364
  198. package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +202 -202
  199. package/templates/nextblock-template/docs/04-DATABASE-AND-AUTH.md +246 -246
  200. package/templates/nextblock-template/docs/06-CLI-AND-SCAFFOLDING.md +176 -176
  201. package/templates/nextblock-template/docs/07-BLOCK-SDK-AND-EXTENSIBILITY.md +146 -146
  202. package/templates/nextblock-template/docs/08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md +1319 -1319
  203. package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +222 -222
  204. package/templates/nextblock-template/docs/11-SELF-HOSTED-DOCKER.md +173 -173
  205. package/templates/nextblock-template/docs/12-VERCEL-DEPLOYMENT.md +170 -170
  206. package/templates/nextblock-template/docs/13-STAYING-UP-TO-DATE.md +151 -151
  207. package/templates/nextblock-template/docs/README.md +39 -39
  208. package/templates/nextblock-template/docs/TECHNICAL_SPECIFICATION.md +12505 -12505
  209. package/templates/nextblock-template/hooks/use-hotkeys.ts +21 -21
  210. package/templates/nextblock-template/hooks/useGlobalSearch.ts +101 -101
  211. package/templates/nextblock-template/index.d.ts +7 -7
  212. package/templates/nextblock-template/lib/app-secrets.ts +39 -39
  213. package/templates/nextblock-template/lib/auth/cookies.ts +47 -47
  214. package/templates/nextblock-template/lib/auth/crypto.ts +45 -45
  215. package/templates/nextblock-template/lib/auth/trustedDevices.ts +92 -92
  216. package/templates/nextblock-template/lib/auth/twoFactor.ts +208 -208
  217. package/templates/nextblock-template/lib/auth-redirects.ts +46 -46
  218. package/templates/nextblock-template/lib/blocks/README.md +13 -13
  219. package/templates/nextblock-template/lib/blocks/blockRegistry.ts +6 -6
  220. package/templates/nextblock-template/lib/blocks/blockTypes.ts +18 -18
  221. package/templates/nextblock-template/lib/botProtection/verify.ts +134 -134
  222. package/templates/nextblock-template/lib/cms-transfer/csv.test.ts +100 -77
  223. package/templates/nextblock-template/lib/cms-transfer/csv.ts +403 -399
  224. package/templates/nextblock-template/lib/cms-transfer/server.ts +2243 -2243
  225. package/templates/nextblock-template/lib/cms-transfer/types.ts +145 -145
  226. package/templates/nextblock-template/lib/config/email-settings.ts +217 -217
  227. package/templates/nextblock-template/lib/custom-block-definitions.ts +87 -87
  228. package/templates/nextblock-template/lib/custom-block-r2-upload-shared.ts +178 -178
  229. package/templates/nextblock-template/lib/custom-block-r2-upload.test.ts +140 -140
  230. package/templates/nextblock-template/lib/custom-block-r2-upload.ts +88 -88
  231. package/templates/nextblock-template/lib/custom-block-relation-registry.ts +256 -256
  232. package/templates/nextblock-template/lib/custom-block-relations.test.ts +227 -227
  233. package/templates/nextblock-template/lib/custom-block-relations.ts +279 -279
  234. package/templates/nextblock-template/lib/custom-block-safelist.ts +14 -14
  235. package/templates/nextblock-template/lib/editor/dynamic-extension-core.test.ts +172 -172
  236. package/templates/nextblock-template/lib/editor/dynamic-extension-core.ts +213 -213
  237. package/templates/nextblock-template/lib/editor/dynamic-extension-loader.ts +22 -22
  238. package/templates/nextblock-template/lib/editor/dynamic-extensions.tsx +193 -193
  239. package/templates/nextblock-template/lib/email/branding-format.test.ts +133 -133
  240. package/templates/nextblock-template/lib/email/branding-format.ts +123 -123
  241. package/templates/nextblock-template/lib/email/branding.ts +76 -76
  242. package/templates/nextblock-template/lib/full-backup/manifest.test.ts +121 -121
  243. package/templates/nextblock-template/lib/full-backup/manifest.ts +206 -206
  244. package/templates/nextblock-template/lib/full-backup/server.ts +743 -743
  245. package/templates/nextblock-template/lib/logos/active-logo.ts +53 -53
  246. package/templates/nextblock-template/lib/media/resolveMediaUrl.ts +54 -54
  247. package/templates/nextblock-template/lib/onboarding/actions.ts +31 -31
  248. package/templates/nextblock-template/lib/onboarding/status.ts +222 -222
  249. package/templates/nextblock-template/lib/posts/readTime.ts +60 -60
  250. package/templates/nextblock-template/lib/privacy/consent-client.ts +57 -57
  251. package/templates/nextblock-template/lib/privacy/contact-emails.ts +64 -64
  252. package/templates/nextblock-template/lib/privacy/settings.ts +115 -115
  253. package/templates/nextblock-template/lib/privacy/types.ts +69 -69
  254. package/templates/nextblock-template/lib/promotions/server.test.ts +74 -74
  255. package/templates/nextblock-template/lib/promotions/server.ts +741 -741
  256. package/templates/nextblock-template/lib/resolve-block-relations.test.ts +142 -142
  257. package/templates/nextblock-template/lib/resolve-block-relations.ts +255 -255
  258. package/templates/nextblock-template/lib/search/server.ts +585 -585
  259. package/templates/nextblock-template/lib/search/types.ts +27 -27
  260. package/templates/nextblock-template/lib/setup/actions.ts +460 -460
  261. package/templates/nextblock-template/lib/setup/env-status.ts +125 -125
  262. package/templates/nextblock-template/lib/setup/env-write.ts +111 -111
  263. package/templates/nextblock-template/lib/setup/migrations-bundle.ts +37 -37
  264. package/templates/nextblock-template/lib/setup/provisioning.ts +59 -59
  265. package/templates/nextblock-template/lib/setup/schema-apply.ts +408 -408
  266. package/templates/nextblock-template/lib/setup/system-config.ts +105 -105
  267. package/templates/nextblock-template/lib/setup/types.ts +18 -18
  268. package/templates/nextblock-template/lib/site-url.ts +48 -48
  269. package/templates/nextblock-template/lib/storage/provider.ts +66 -66
  270. package/templates/nextblock-template/lib/storage/supabase-storage.ts +103 -103
  271. package/templates/nextblock-template/lib/updates/check-upstream.ts +441 -441
  272. package/templates/nextblock-template/lib/updates/github-device.ts +206 -206
  273. package/templates/nextblock-template/lib/updates/repo-identity.ts +56 -56
  274. package/templates/nextblock-template/package.json +1 -1
  275. package/templates/nextblock-template/postcss.config.js +6 -6
  276. package/templates/nextblock-template/scripts/backup.js +115 -115
  277. package/templates/nextblock-template/scripts/docker-setup.mjs +310 -310
  278. package/templates/nextblock-template/scripts/restore.js +385 -385
  279. package/templates/nextblock-template/scripts/verify-cortex-ai-build-widget.tsx +98 -98
  280. package/templates/nextblock-template/scripts/verify-cortex-ai-generate-blocks.ts +62 -62
  281. package/templates/nextblock-template/scripts/verify-cortex-ai-global-tools.ts +537 -537
  282. package/templates/nextblock-template/scripts/verify-cortex-ai-routing.ts +58 -58
  283. package/templates/nextblock-template/scripts/verify-custom-block-definitions.ts +188 -188
  284. package/templates/nextblock-template/scripts/verify-dynamic-custom-block-extensions.ts +123 -123
  285. package/templates/nextblock-template/scripts/verify-dynamic-layout-engine.tsx +133 -133
  286. package/templates/nextblock-template/scripts/verify-milestone-2-custom-blocks.ts +65 -65
  287. package/templates/nextblock-template/tailwind.config.js +25 -25
  288. package/templates/nextblock-template/tools/build-migrate.mjs +209 -209
  289. package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -282
  290. package/templates/nextblock-template/types/jsdom.d.ts +6 -6
  291. package/tsconfig.base.json +3 -3
  292. package/templates/nextblock-template/tsconfig.tsbuildinfo +0 -1
@@ -1,46 +1,46 @@
1
- import type { Database } from '@nextblock-cms/db';
2
-
3
- type AuthProfile = Pick<Database['public']['Tables']['profiles']['Row'], 'role' | 'full_name'> | null;
4
-
5
- const DASHBOARD_PATH = '/cms/dashboard';
6
- const HOME_PATH = '/';
7
- const PROFILE_PATH = '/profile';
8
- const PASSWORD_RESET_PATH = '/profile/password';
9
-
10
- function isSafeInternalPath(path?: string | null) {
11
- return Boolean(path && path.startsWith('/') && !path.startsWith('//'));
12
- }
13
-
14
- function isDashboardPath(path: string) {
15
- return path === DASHBOARD_PATH || path.startsWith(`${DASHBOARD_PATH}/`);
16
- }
17
-
18
- export function isCustomerProfileComplete(profile: AuthProfile) {
19
- return Boolean(profile?.full_name?.trim());
20
- }
21
-
22
- export function resolvePostAuthRedirect(
23
- profile: AuthProfile,
24
- requestedPath?: string | null
25
- ) {
26
- const safePath = isSafeInternalPath(requestedPath) ? requestedPath : null;
27
- const role = profile?.role ?? null;
28
-
29
- if (safePath === PASSWORD_RESET_PATH) {
30
- return PASSWORD_RESET_PATH;
31
- }
32
-
33
- if (role === 'ADMIN' || role === 'WRITER') {
34
- return safePath ?? DASHBOARD_PATH;
35
- }
36
-
37
- if (!isCustomerProfileComplete(profile)) {
38
- return PROFILE_PATH;
39
- }
40
-
41
- if (safePath && !isDashboardPath(safePath)) {
42
- return safePath;
43
- }
44
-
45
- return HOME_PATH;
46
- }
1
+ import type { Database } from '@nextblock-cms/db';
2
+
3
+ type AuthProfile = Pick<Database['public']['Tables']['profiles']['Row'], 'role' | 'full_name'> | null;
4
+
5
+ const DASHBOARD_PATH = '/cms/dashboard';
6
+ const HOME_PATH = '/';
7
+ const PROFILE_PATH = '/profile';
8
+ const PASSWORD_RESET_PATH = '/profile/password';
9
+
10
+ function isSafeInternalPath(path?: string | null) {
11
+ return Boolean(path && path.startsWith('/') && !path.startsWith('//'));
12
+ }
13
+
14
+ function isDashboardPath(path: string) {
15
+ return path === DASHBOARD_PATH || path.startsWith(`${DASHBOARD_PATH}/`);
16
+ }
17
+
18
+ export function isCustomerProfileComplete(profile: AuthProfile) {
19
+ return Boolean(profile?.full_name?.trim());
20
+ }
21
+
22
+ export function resolvePostAuthRedirect(
23
+ profile: AuthProfile,
24
+ requestedPath?: string | null
25
+ ) {
26
+ const safePath = isSafeInternalPath(requestedPath) ? requestedPath : null;
27
+ const role = profile?.role ?? null;
28
+
29
+ if (safePath === PASSWORD_RESET_PATH) {
30
+ return PASSWORD_RESET_PATH;
31
+ }
32
+
33
+ if (role === 'ADMIN' || role === 'WRITER') {
34
+ return safePath ?? DASHBOARD_PATH;
35
+ }
36
+
37
+ if (!isCustomerProfileComplete(profile)) {
38
+ return PROFILE_PATH;
39
+ }
40
+
41
+ if (safePath && !isDashboardPath(safePath)) {
42
+ return safePath;
43
+ }
44
+
45
+ return HOME_PATH;
46
+ }
@@ -1,13 +1,13 @@
1
- # NextBlock™ Block System
2
-
3
- The built-in block registry for the app lives in this directory. The current
4
- source of truth is `blockRegistry.ts`, which defines block types, schemas,
5
- defaults, and editor or renderer wiring for the CMS.
6
-
7
- ## Read This First
8
-
9
- - [CMS and Editor](../../../../docs/03-CMS-AND-EDITOR.md)
10
- - [Block SDK and Extensibility](../../../../docs/07-BLOCK-SDK-AND-EXTENSIBILITY.md)
11
- - [Custom Blocks (Data-Driven CRUD)](../../../../docs/10-CUSTOM-BLOCKS.md)
12
-
13
- Use those docs instead of maintaining a second long-form block tutorial here.
1
+ # NextBlock™ Block System
2
+
3
+ The built-in block registry for the app lives in this directory. The current
4
+ source of truth is `blockRegistry.ts`, which defines block types, schemas,
5
+ defaults, and editor or renderer wiring for the CMS.
6
+
7
+ ## Read This First
8
+
9
+ - [CMS and Editor](../../../../docs/03-CMS-AND-EDITOR.md)
10
+ - [Block SDK and Extensibility](../../../../docs/07-BLOCK-SDK-AND-EXTENSIBILITY.md)
11
+ - [Custom Blocks (Data-Driven CRUD)](../../../../docs/10-CUSTOM-BLOCKS.md)
12
+
13
+ Use those docs instead of maintaining a second long-form block tutorial here.
@@ -1,8 +1,8 @@
1
- import { z } from '../zod-config';
2
- import { TestimonialBlockConfig, TestimonialBlockContent } from '../../components/blocks/TestimonialBlock';
3
- import { ProductGridBlockSchema, ProductGridBlockContent, FeaturedProductBlockSchema, FeaturedProductBlockContent, CartBlockSchema, CartBlockContent, CheckoutBlockSchema, CheckoutBlockContent, ProductDetailsBlockSchema, ProductDetailsBlockContent } from './ecommerce-block-schemas';
4
- import { availableBlockTypes, type BlockType } from './blockTypes';
5
- export { availableBlockTypes, type BlockType } from './blockTypes';
1
+ import { z } from '../zod-config';
2
+ import { TestimonialBlockConfig, TestimonialBlockContent } from '../../components/blocks/TestimonialBlock';
3
+ import { ProductGridBlockSchema, ProductGridBlockContent, FeaturedProductBlockSchema, FeaturedProductBlockContent, CartBlockSchema, CartBlockContent, CheckoutBlockSchema, CheckoutBlockContent, ProductDetailsBlockSchema, ProductDetailsBlockContent } from './ecommerce-block-schemas';
4
+ import { availableBlockTypes, type BlockType } from './blockTypes';
5
+ export { availableBlockTypes, type BlockType } from './blockTypes';
6
6
 
7
7
  /**
8
8
  * Block Registry System
@@ -12,7 +12,7 @@ export { availableBlockTypes, type BlockType } from './blockTypes';
12
12
  * their initial content, editor components, renderer components, and Zod schemas.
13
13
  */
14
14
 
15
- // --- Zod Schemas & Inferred Types ---
15
+ // --- Zod Schemas & Inferred Types ---
16
16
 
17
17
  export const TextBlockSchema = z.object({
18
18
  html_content: z.string().describe('Raw HTML content for the text block'),
@@ -1,18 +1,18 @@
1
- export const availableBlockTypes = [
2
- "text",
3
- "heading",
4
- "image",
5
- "button",
6
- "posts_grid",
7
- "video_embed",
8
- "section",
9
- "form",
10
- "testimonial",
11
- "product_grid",
12
- "featured_product",
13
- "cart",
14
- "checkout",
15
- "product_details",
16
- ] as const;
17
-
18
- export type BlockType = (typeof availableBlockTypes)[number];
1
+ export const availableBlockTypes = [
2
+ "text",
3
+ "heading",
4
+ "image",
5
+ "button",
6
+ "posts_grid",
7
+ "video_embed",
8
+ "section",
9
+ "form",
10
+ "testimonial",
11
+ "product_grid",
12
+ "featured_product",
13
+ "cart",
14
+ "checkout",
15
+ "product_details",
16
+ ] as const;
17
+
18
+ export type BlockType = (typeof availableBlockTypes)[number];
@@ -1,134 +1,134 @@
1
- // lib/botProtection/verify.ts
2
- //
3
- // Shared server-side bot-protection verification: the honeypot check plus
4
- // Cloudflare Turnstile / Google reCAPTCHA token verification. This is the single
5
- // source of truth used by BOTH the page contact-form handler
6
- // (app/actions/formActions.ts) and the account-signup action (app/actions.ts).
7
- //
8
- // Server-only: it reads the RLS-bypassing service-role client, which throws if
9
- // imported into a Client Component.
10
-
11
- import { getServiceRoleSupabaseClient } from '@nextblock-cms/db/server';
12
-
13
- export type BotProtectionProvider = 'none' | 'turnstile' | 'recaptcha';
14
-
15
- // Shared field names — the client widgets emit these, the verifier reads them.
16
- export const HONEYPOT_FIELD = 'verification_secondary_email';
17
- export const TURNSTILE_TOKEN_FIELD = 'cf-turnstile-response';
18
- export const RECAPTCHA_TOKEN_FIELD = 'g-recaptcha-response';
19
-
20
- export type BotProtectionResult =
21
- // Passed (or nothing configured beyond the honeypot).
22
- | { ok: true }
23
- // The honeypot was filled — almost certainly a bot. Callers should silently
24
- // discard the submission and fake a success so the bot learns nothing.
25
- | { ok: false; reason: 'honeypot' }
26
- // The captcha was missing, failed, or could not be checked. `message` is a
27
- // human-readable explanation safe to surface to the user.
28
- | { ok: false; reason: 'captcha'; message: string };
29
-
30
- type VerifyOptions = {
31
- // A block/form may pin a specific provider; otherwise the site-wide setting
32
- // (site_settings.bot_protection_public.provider) is used.
33
- botProtectionProvider?: BotProtectionProvider;
34
- };
35
-
36
- /**
37
- * Verify a submitted FormData against the configured bot protection.
38
- *
39
- * Phase 1 (honeypot) needs no network/DB and always runs — it is the always-on
40
- * baseline. Phase 2 (captcha) reads the global provider + secret from
41
- * `site_settings` and calls the provider's siteverify endpoint.
42
- */
43
- export async function verifyBotProtection(
44
- formData: FormData,
45
- options?: VerifyOptions
46
- ): Promise<BotProtectionResult> {
47
- // Phase 1: Honeypot validation
48
- const honeypot = formData.get(HONEYPOT_FIELD);
49
- if (honeypot && typeof honeypot === 'string' && honeypot.length > 0) {
50
- console.warn('[Bot Protection] Honeypot triggered. Discarding submission from bot.');
51
- return { ok: false, reason: 'honeypot' };
52
- }
53
-
54
- // Phase 2: Advanced captcha verification
55
- try {
56
- const supabase = getServiceRoleSupabaseClient();
57
-
58
- const { data: publicSetting } = await supabase
59
- .from('site_settings')
60
- .select('value')
61
- .eq('key', 'bot_protection_public')
62
- .maybeSingle();
63
-
64
- const { data: secretSetting } = await supabase
65
- .from('site_settings')
66
- .select('value')
67
- .eq('key', 'bot_protection_secret')
68
- .maybeSingle();
69
-
70
- const publicVal = (publicSetting?.value || {}) as Record<string, any>;
71
- const secretVal = (secretSetting?.value || {}) as Record<string, any>;
72
-
73
- const pinnedProvider =
74
- options?.botProtectionProvider === 'turnstile' || options?.botProtectionProvider === 'recaptcha'
75
- ? options.botProtectionProvider
76
- : undefined;
77
- const provider: BotProtectionProvider = pinnedProvider || publicVal.provider || 'none';
78
- const secretKey =
79
- secretVal.secretKey ||
80
- (provider === 'turnstile'
81
- ? process.env.TURNSTILE_SECRET_KEY
82
- : process.env.RECAPTCHA_SECRET_KEY) ||
83
- '';
84
-
85
- if (provider === 'turnstile') {
86
- const token = formData.get(TURNSTILE_TOKEN_FIELD) as string;
87
- if (!token) {
88
- return { ok: false, reason: 'captcha', message: 'Security verification token is missing. Please try again.' };
89
- }
90
- if (!secretKey) {
91
- console.error('[Bot Protection] Turnstile secret key is not configured.');
92
- return { ok: false, reason: 'captcha', message: 'Bot protection is misconfigured. Please contact support.' };
93
- }
94
-
95
- const res = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
96
- method: 'POST',
97
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
98
- body: `secret=${encodeURIComponent(secretKey)}&response=${encodeURIComponent(token)}`,
99
- });
100
-
101
- const outcome = await res.json();
102
- if (!outcome.success) {
103
- console.warn('[Bot Protection] Turnstile verification failed:', outcome);
104
- return { ok: false, reason: 'captcha', message: 'Security verification failed. Please try again.' };
105
- }
106
- } else if (provider === 'recaptcha') {
107
- const token = formData.get(RECAPTCHA_TOKEN_FIELD) as string;
108
- if (!token) {
109
- return { ok: false, reason: 'captcha', message: 'Security verification token is missing. Please try again.' };
110
- }
111
- if (!secretKey) {
112
- console.error('[Bot Protection] reCAPTCHA secret key is not configured.');
113
- return { ok: false, reason: 'captcha', message: 'Bot protection is misconfigured. Please contact support.' };
114
- }
115
-
116
- const res = await fetch('https://www.google.com/recaptcha/api/siteverify', {
117
- method: 'POST',
118
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
119
- body: `secret=${encodeURIComponent(secretKey)}&response=${encodeURIComponent(token)}`,
120
- });
121
-
122
- const outcome = await res.json();
123
- if (!outcome.success || outcome.score < 0.5) {
124
- console.warn('[Bot Protection] reCAPTCHA verification failed:', outcome);
125
- return { ok: false, reason: 'captcha', message: 'Security verification failed. Please try again.' };
126
- }
127
- }
128
- } catch (error) {
129
- console.error('[Bot Protection] Error during validation:', error);
130
- return { ok: false, reason: 'captcha', message: 'Sorry, security verification could not be completed at this time.' };
131
- }
132
-
133
- return { ok: true };
134
- }
1
+ // lib/botProtection/verify.ts
2
+ //
3
+ // Shared server-side bot-protection verification: the honeypot check plus
4
+ // Cloudflare Turnstile / Google reCAPTCHA token verification. This is the single
5
+ // source of truth used by BOTH the page contact-form handler
6
+ // (app/actions/formActions.ts) and the account-signup action (app/actions.ts).
7
+ //
8
+ // Server-only: it reads the RLS-bypassing service-role client, which throws if
9
+ // imported into a Client Component.
10
+
11
+ import { getServiceRoleSupabaseClient } from '@nextblock-cms/db/server';
12
+
13
+ export type BotProtectionProvider = 'none' | 'turnstile' | 'recaptcha';
14
+
15
+ // Shared field names — the client widgets emit these, the verifier reads them.
16
+ export const HONEYPOT_FIELD = 'verification_secondary_email';
17
+ export const TURNSTILE_TOKEN_FIELD = 'cf-turnstile-response';
18
+ export const RECAPTCHA_TOKEN_FIELD = 'g-recaptcha-response';
19
+
20
+ export type BotProtectionResult =
21
+ // Passed (or nothing configured beyond the honeypot).
22
+ | { ok: true }
23
+ // The honeypot was filled — almost certainly a bot. Callers should silently
24
+ // discard the submission and fake a success so the bot learns nothing.
25
+ | { ok: false; reason: 'honeypot' }
26
+ // The captcha was missing, failed, or could not be checked. `message` is a
27
+ // human-readable explanation safe to surface to the user.
28
+ | { ok: false; reason: 'captcha'; message: string };
29
+
30
+ type VerifyOptions = {
31
+ // A block/form may pin a specific provider; otherwise the site-wide setting
32
+ // (site_settings.bot_protection_public.provider) is used.
33
+ botProtectionProvider?: BotProtectionProvider;
34
+ };
35
+
36
+ /**
37
+ * Verify a submitted FormData against the configured bot protection.
38
+ *
39
+ * Phase 1 (honeypot) needs no network/DB and always runs — it is the always-on
40
+ * baseline. Phase 2 (captcha) reads the global provider + secret from
41
+ * `site_settings` and calls the provider's siteverify endpoint.
42
+ */
43
+ export async function verifyBotProtection(
44
+ formData: FormData,
45
+ options?: VerifyOptions
46
+ ): Promise<BotProtectionResult> {
47
+ // Phase 1: Honeypot validation
48
+ const honeypot = formData.get(HONEYPOT_FIELD);
49
+ if (honeypot && typeof honeypot === 'string' && honeypot.length > 0) {
50
+ console.warn('[Bot Protection] Honeypot triggered. Discarding submission from bot.');
51
+ return { ok: false, reason: 'honeypot' };
52
+ }
53
+
54
+ // Phase 2: Advanced captcha verification
55
+ try {
56
+ const supabase = getServiceRoleSupabaseClient();
57
+
58
+ const { data: publicSetting } = await supabase
59
+ .from('site_settings')
60
+ .select('value')
61
+ .eq('key', 'bot_protection_public')
62
+ .maybeSingle();
63
+
64
+ const { data: secretSetting } = await supabase
65
+ .from('site_settings')
66
+ .select('value')
67
+ .eq('key', 'bot_protection_secret')
68
+ .maybeSingle();
69
+
70
+ const publicVal = (publicSetting?.value || {}) as Record<string, any>;
71
+ const secretVal = (secretSetting?.value || {}) as Record<string, any>;
72
+
73
+ const pinnedProvider =
74
+ options?.botProtectionProvider === 'turnstile' || options?.botProtectionProvider === 'recaptcha'
75
+ ? options.botProtectionProvider
76
+ : undefined;
77
+ const provider: BotProtectionProvider = pinnedProvider || publicVal.provider || 'none';
78
+ const secretKey =
79
+ secretVal.secretKey ||
80
+ (provider === 'turnstile'
81
+ ? process.env.TURNSTILE_SECRET_KEY
82
+ : process.env.RECAPTCHA_SECRET_KEY) ||
83
+ '';
84
+
85
+ if (provider === 'turnstile') {
86
+ const token = formData.get(TURNSTILE_TOKEN_FIELD) as string;
87
+ if (!token) {
88
+ return { ok: false, reason: 'captcha', message: 'Security verification token is missing. Please try again.' };
89
+ }
90
+ if (!secretKey) {
91
+ console.error('[Bot Protection] Turnstile secret key is not configured.');
92
+ return { ok: false, reason: 'captcha', message: 'Bot protection is misconfigured. Please contact support.' };
93
+ }
94
+
95
+ const res = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
96
+ method: 'POST',
97
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
98
+ body: `secret=${encodeURIComponent(secretKey)}&response=${encodeURIComponent(token)}`,
99
+ });
100
+
101
+ const outcome = await res.json();
102
+ if (!outcome.success) {
103
+ console.warn('[Bot Protection] Turnstile verification failed:', outcome);
104
+ return { ok: false, reason: 'captcha', message: 'Security verification failed. Please try again.' };
105
+ }
106
+ } else if (provider === 'recaptcha') {
107
+ const token = formData.get(RECAPTCHA_TOKEN_FIELD) as string;
108
+ if (!token) {
109
+ return { ok: false, reason: 'captcha', message: 'Security verification token is missing. Please try again.' };
110
+ }
111
+ if (!secretKey) {
112
+ console.error('[Bot Protection] reCAPTCHA secret key is not configured.');
113
+ return { ok: false, reason: 'captcha', message: 'Bot protection is misconfigured. Please contact support.' };
114
+ }
115
+
116
+ const res = await fetch('https://www.google.com/recaptcha/api/siteverify', {
117
+ method: 'POST',
118
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
119
+ body: `secret=${encodeURIComponent(secretKey)}&response=${encodeURIComponent(token)}`,
120
+ });
121
+
122
+ const outcome = await res.json();
123
+ if (!outcome.success || outcome.score < 0.5) {
124
+ console.warn('[Bot Protection] reCAPTCHA verification failed:', outcome);
125
+ return { ok: false, reason: 'captcha', message: 'Security verification failed. Please try again.' };
126
+ }
127
+ }
128
+ } catch (error) {
129
+ console.error('[Bot Protection] Error during validation:', error);
130
+ return { ok: false, reason: 'captcha', message: 'Sorry, security verification could not be completed at this time.' };
131
+ }
132
+
133
+ return { ok: true };
134
+ }
@@ -1,77 +1,100 @@
1
- import { describe, expect, it } from "vitest";
2
-
3
- import {
4
- getCsvHeaders,
5
- getTemplateCsv,
6
- majorToMinor,
7
- makeUniqueSku,
8
- makeUniqueSlug,
9
- minorToMajor,
10
- normalizeBlocksFromFields,
11
- parseCsv,
12
- stringifyCsv,
13
- } from "./csv";
14
-
15
- describe("cms transfer csv helpers", () => {
16
- it("builds templates with the expected content columns", () => {
17
- expect(getCsvHeaders("pages")).toContain("blocks_json");
18
- expect(getCsvHeaders("products")).toContain("description_html");
19
- expect(getCsvHeaders("products")).toContain("description_blocks_json");
20
- expect(getTemplateCsv("products")).toContain("EXAMPLE-SKU");
21
- });
22
-
23
- it("round-trips JSON columns through CSV escaping", () => {
24
- const csv = stringifyCsv(
25
- [
26
- {
27
- language_code: "en",
28
- title: "Landing",
29
- blocks_json: JSON.stringify([
30
- { block_type: "text", content: { html_content: "<p>Hello, world</p>" }, order: 0 },
31
- ]),
32
- },
33
- ],
34
- ["language_code", "title", "blocks_json"]
35
- );
36
-
37
- const parsed = parseCsv(csv);
38
-
39
- expect(parsed.errors).toEqual([]);
40
- expect(parsed.rows[0].blocks_json).toContain("html_content");
41
- expect(JSON.parse(parsed.rows[0].blocks_json)).toHaveLength(1);
42
- });
43
-
44
- it("converts plain HTML into a text block fallback", () => {
45
- const errors: Array<{ row: number; message: string }> = [];
46
- const blocks = normalizeBlocksFromFields({
47
- html: "<p>Imported description</p>",
48
- languageId: 1,
49
- rowNumber: 2,
50
- errors,
51
- });
52
-
53
- expect(errors).toEqual([]);
54
- expect(blocks).toEqual([
55
- {
56
- language_id: 1,
57
- block_type: "text",
58
- content: { html_content: "<p>Imported description</p>" },
59
- order: 0,
60
- },
61
- ]);
62
- });
63
-
64
- it("generates unique slugs and SKUs for create-new imports", () => {
65
- expect(makeUniqueSlug("Example Page", new Set(["example-page"]))).toBe(
66
- "example-page-copy"
67
- );
68
- expect(makeUniqueSku("sku-1", new Set(["SKU-1", "SKU-1-COPY"]))).toBe(
69
- "SKU-1-COPY-2"
70
- );
71
- });
72
-
73
- it("converts product prices between major and minor units", () => {
74
- expect(majorToMinor(19.99)).toBe(1999);
75
- expect(minorToMajor(1999)).toBe(19.99);
76
- });
77
- });
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import {
4
+ getCsvHeaders,
5
+ getTemplateCsv,
6
+ majorToMinor,
7
+ makeUniqueSku,
8
+ makeUniqueSlug,
9
+ minorToMajor,
10
+ normalizeBlocksFromFields,
11
+ parseCsv,
12
+ stringifyCsv,
13
+ } from "./csv";
14
+
15
+ describe("cms transfer csv helpers", () => {
16
+ it("builds templates with the expected content columns", () => {
17
+ expect(getCsvHeaders("pages")).toContain("blocks_json");
18
+ expect(getCsvHeaders("products")).toContain("description_html");
19
+ expect(getCsvHeaders("products")).toContain("description_blocks_json");
20
+ expect(getTemplateCsv("products")).toContain("EXAMPLE-SKU");
21
+ });
22
+
23
+ it("round-trips JSON columns through CSV escaping", () => {
24
+ const csv = stringifyCsv(
25
+ [
26
+ {
27
+ language_code: "en",
28
+ title: "Landing",
29
+ blocks_json: JSON.stringify([
30
+ { block_type: "text", content: { html_content: "<p>Hello, world</p>" }, order: 0 },
31
+ ]),
32
+ },
33
+ ],
34
+ ["language_code", "title", "blocks_json"]
35
+ );
36
+
37
+ const parsed = parseCsv(csv);
38
+
39
+ expect(parsed.errors).toEqual([]);
40
+ expect(parsed.rows[0].blocks_json).toContain("html_content");
41
+ expect(JSON.parse(parsed.rows[0].blocks_json)).toHaveLength(1);
42
+ });
43
+
44
+ it("round-trips special characters like the symbol", () => {
45
+ const csv = stringifyCsv(
46
+ [{ language_code: "en", title: "NextBlock™ — Café © 2026" }],
47
+ ["language_code", "title"]
48
+ );
49
+
50
+ const parsed = parseCsv(csv);
51
+
52
+ expect(parsed.errors).toEqual([]);
53
+ expect(parsed.rows[0].title).toBe("NextBlock™ — Café © 2026");
54
+ });
55
+
56
+ it("strips a leading UTF-8 BOM so the first header still matches", () => {
57
+ const csv = stringifyCsv([{ id: "1", title: "NextBlock™" }], ["id", "title"]);
58
+ // Spreadsheet apps (and our own export) prefix the file with a BOM.
59
+ const parsed = parseCsv(String.fromCharCode(0xfeff) + csv);
60
+
61
+ expect(parsed.errors).toEqual([]);
62
+ expect(Object.keys(parsed.rows[0])).toContain("id");
63
+ expect(parsed.rows[0].id).toBe("1");
64
+ expect(parsed.rows[0].title).toBe("NextBlock™");
65
+ });
66
+
67
+ it("converts plain HTML into a text block fallback", () => {
68
+ const errors: Array<{ row: number; message: string }> = [];
69
+ const blocks = normalizeBlocksFromFields({
70
+ html: "<p>Imported description</p>",
71
+ languageId: 1,
72
+ rowNumber: 2,
73
+ errors,
74
+ });
75
+
76
+ expect(errors).toEqual([]);
77
+ expect(blocks).toEqual([
78
+ {
79
+ language_id: 1,
80
+ block_type: "text",
81
+ content: { html_content: "<p>Imported description</p>" },
82
+ order: 0,
83
+ },
84
+ ]);
85
+ });
86
+
87
+ it("generates unique slugs and SKUs for create-new imports", () => {
88
+ expect(makeUniqueSlug("Example Page", new Set(["example-page"]))).toBe(
89
+ "example-page-copy"
90
+ );
91
+ expect(makeUniqueSku("sku-1", new Set(["SKU-1", "SKU-1-COPY"]))).toBe(
92
+ "SKU-1-COPY-2"
93
+ );
94
+ });
95
+
96
+ it("converts product prices between major and minor units", () => {
97
+ expect(majorToMinor(19.99)).toBe(1999);
98
+ expect(minorToMajor(1999)).toBe(19.99);
99
+ });
100
+ });