create-nextblock 0.2.78 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/create-nextblock.js +793 -472
- package/package.json +1 -2
- package/scripts/sync-template.js +18 -1
- package/templates/nextblock-template/.browserslistrc +11 -0
- package/templates/nextblock-template/.swcrc +30 -30
- package/templates/nextblock-template/README.md +23 -114
- package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +27 -28
- package/templates/nextblock-template/app/(auth-pages)/sign-in/page.tsx +50 -25
- package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +111 -56
- package/templates/nextblock-template/app/(auth-pages)/two-factor/actions.ts +91 -0
- package/templates/nextblock-template/app/(auth-pages)/two-factor/components/TwoFactorForm.tsx +118 -0
- package/templates/nextblock-template/app/(auth-pages)/two-factor/page.tsx +51 -0
- package/templates/nextblock-template/app/.well-known/ucp/route.ts +16 -0
- package/templates/nextblock-template/app/[slug]/PageClientContent.tsx +48 -28
- package/templates/nextblock-template/app/[slug]/page.tsx +63 -6
- package/templates/nextblock-template/app/[slug]/page.utils.ts +374 -157
- package/templates/nextblock-template/app/[slug]/pageClientActions.ts +7 -0
- package/templates/nextblock-template/app/actions/consent.ts +57 -0
- package/templates/nextblock-template/app/actions/formActions.ts +130 -11
- package/templates/nextblock-template/app/actions/languageActions.ts +31 -30
- package/templates/nextblock-template/app/actions/package-actions.ts +183 -0
- package/templates/nextblock-template/app/actions/postActions.ts +146 -48
- package/templates/nextblock-template/app/actions/twoFactorEmail.ts +21 -0
- package/templates/nextblock-template/app/actions/visualEditingActions.test.ts +179 -0
- package/templates/nextblock-template/app/actions/visualEditingActions.ts +345 -0
- package/templates/nextblock-template/app/actions.ts +67 -12
- package/templates/nextblock-template/app/api/ai/cortex/build-widget/route.ts +153 -0
- package/templates/nextblock-template/app/api/ai/generate-blocks/route.ts +96 -0
- package/templates/nextblock-template/app/api/ai/global-agent/route.ts +965 -0
- package/templates/nextblock-template/app/api/checkout/freemius/sync/route.ts +29 -0
- package/templates/nextblock-template/app/api/checkout/route.ts +146 -0
- package/templates/nextblock-template/app/api/cms/full-backup/export/route.ts +33 -0
- package/templates/nextblock-template/app/api/cms/full-backup/restore/route.ts +63 -0
- package/templates/nextblock-template/app/api/cron/reset-sandbox/route.ts +3413 -17
- package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +7830 -0
- package/templates/nextblock-template/app/api/cron/sync-currencies/route.ts +35 -0
- package/templates/nextblock-template/app/api/custom-blocks/db-relations/route.ts +92 -0
- package/templates/nextblock-template/app/api/custom-blocks/editor-definitions/route.ts +43 -0
- package/templates/nextblock-template/app/api/draft/disable/route.ts +25 -0
- package/templates/nextblock-template/app/api/draft/route.ts +93 -0
- package/templates/nextblock-template/app/api/draft/start/route.ts +77 -0
- package/templates/nextblock-template/app/api/media/library/route.ts +65 -0
- package/templates/nextblock-template/app/api/media/r2-presigned/route.ts +53 -0
- package/templates/nextblock-template/app/api/media/record/route.ts +160 -0
- package/templates/nextblock-template/app/api/search/route.ts +43 -0
- package/templates/nextblock-template/app/api/visual-editing/block-draft/route.ts +47 -0
- package/templates/nextblock-template/app/api/visual-editing/product-draft/route.ts +47 -0
- package/templates/nextblock-template/app/api/webhooks/freemius/route.ts +34 -0
- package/templates/nextblock-template/app/api/webhooks/stripe/route.ts +27 -0
- package/templates/nextblock-template/app/article/[slug]/PostClientContent.tsx +392 -128
- package/templates/nextblock-template/app/article/[slug]/page.tsx +179 -127
- package/templates/nextblock-template/app/article/[slug]/page.utils.ts +262 -77
- package/templates/nextblock-template/app/auth/callback/route.ts +31 -58
- package/templates/nextblock-template/app/cart/page.tsx +7 -0
- package/templates/nextblock-template/app/checkout/UcpCartHydrator.tsx +20 -0
- package/templates/nextblock-template/app/checkout/page.tsx +52 -0
- package/templates/nextblock-template/app/checkout/success/actions.ts +136 -0
- package/templates/nextblock-template/app/checkout/success/page.tsx +186 -0
- package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +163 -33
- package/templates/nextblock-template/app/cms/blocks/actions.ts +424 -235
- package/templates/nextblock-template/app/cms/blocks/components/BackgroundSelector.tsx +212 -151
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +41 -20
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +152 -19
- package/templates/nextblock-template/app/cms/blocks/components/BlockTypeCard.tsx +25 -17
- package/templates/nextblock-template/app/cms/blocks/components/BlockTypeSelector.tsx +200 -18
- package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +33 -16
- package/templates/nextblock-template/app/cms/blocks/components/CustomBlockEditorPreview.tsx +160 -0
- package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +37 -18
- package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +149 -67
- package/templates/nextblock-template/app/cms/blocks/components/SectionConfigPanel.tsx +108 -31
- package/templates/nextblock-template/app/cms/blocks/editors/DynamicCustomBlockEditor.tsx +167 -0
- package/templates/nextblock-template/app/cms/blocks/editors/FeaturedProductBlockEditor.tsx +31 -0
- package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +2 -2
- package/templates/nextblock-template/app/cms/blocks/editors/HeadingBlockEditor.tsx +1 -1
- package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +29 -29
- package/templates/nextblock-template/app/cms/blocks/editors/PostsGridBlockEditor.tsx +14 -18
- package/templates/nextblock-template/app/cms/blocks/editors/ProductGridBlockEditor.tsx +41 -0
- package/templates/nextblock-template/app/cms/blocks/editors/SectionBlockEditor.tsx +318 -118
- package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +98 -21
- package/templates/nextblock-template/app/cms/blocks/editors/VideoEmbedBlockEditor.tsx +1 -1
- package/templates/nextblock-template/app/cms/components/ContentLanguageSwitcher.tsx +27 -9
- package/templates/nextblock-template/app/cms/components/CopyContentFromLanguage.tsx +1 -1
- package/templates/nextblock-template/app/cms/components/CortexAiActiveContext.tsx +23 -0
- package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -0
- package/templates/nextblock-template/app/cms/components/CortexGlobalAgentChat.tsx +1507 -0
- package/templates/nextblock-template/app/cms/components/DraftStatusActions.tsx +145 -0
- package/templates/nextblock-template/app/cms/components/FeatureImageField.tsx +244 -0
- package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +38 -24
- package/templates/nextblock-template/app/cms/coupons/[id]/edit/page.tsx +16 -0
- package/templates/nextblock-template/app/cms/coupons/page.tsx +16 -0
- package/templates/nextblock-template/app/cms/custom-blocks/[id]/edit/page.tsx +66 -0
- package/templates/nextblock-template/app/cms/custom-blocks/actions.ts +519 -0
- package/templates/nextblock-template/app/cms/custom-blocks/components/BlockComposer.tsx +1522 -0
- package/templates/nextblock-template/app/cms/custom-blocks/components/BlocksLibraryTransferControls.tsx +256 -0
- package/templates/nextblock-template/app/cms/custom-blocks/components/DBRelationSelect.tsx +384 -0
- package/templates/nextblock-template/app/cms/custom-blocks/components/ImageR2Picker.tsx +221 -0
- package/templates/nextblock-template/app/cms/custom-blocks/new/page.tsx +12 -0
- package/templates/nextblock-template/app/cms/custom-blocks/page.tsx +438 -0
- package/templates/nextblock-template/app/cms/dashboard/actions.ts +228 -98
- package/templates/nextblock-template/app/cms/dashboard/components/DashboardComponents.tsx +200 -0
- package/templates/nextblock-template/app/cms/dashboard/page.tsx +182 -154
- package/templates/nextblock-template/app/cms/import-export/ContentTransferControls.tsx +391 -0
- package/templates/nextblock-template/app/cms/import-export/actions.ts +226 -0
- package/templates/nextblock-template/app/cms/layout.tsx +29 -10
- package/templates/nextblock-template/app/cms/media/UploadFolderContext.tsx +22 -22
- package/templates/nextblock-template/app/cms/media/actions.ts +45 -124
- package/templates/nextblock-template/app/cms/media/components/DeleteMediaButtonClient.tsx +1 -1
- package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +26 -26
- package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +69 -64
- package/templates/nextblock-template/app/cms/media/components/MediaPickerDialog.tsx +227 -158
- package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +101 -89
- package/templates/nextblock-template/app/cms/media/page.tsx +1 -1
- package/templates/nextblock-template/app/cms/navigation/components/NavigationItemForm.tsx +2 -2
- package/templates/nextblock-template/app/cms/orders/[id]/MarkPaidButton.tsx +44 -0
- package/templates/nextblock-template/app/cms/orders/[id]/page.tsx +16 -0
- package/templates/nextblock-template/app/cms/orders/actions.ts +201 -0
- package/templates/nextblock-template/app/cms/orders/page.tsx +20 -0
- package/templates/nextblock-template/app/cms/orders/types.ts +20 -0
- package/templates/nextblock-template/app/cms/pages/[id]/edit/EditPageClient.tsx +156 -121
- package/templates/nextblock-template/app/cms/pages/[id]/edit/page.tsx +79 -26
- package/templates/nextblock-template/app/cms/pages/actions.ts +54 -38
- package/templates/nextblock-template/app/cms/pages/components/DeletePageButtonClient.tsx +1 -1
- package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +267 -116
- package/templates/nextblock-template/app/cms/pages/page.tsx +25 -18
- package/templates/nextblock-template/app/cms/payments/page.tsx +16 -0
- package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +132 -90
- package/templates/nextblock-template/app/cms/posts/actions.ts +71 -72
- package/templates/nextblock-template/app/cms/posts/components/DeletePostButtonClient.tsx +1 -1
- package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +256 -245
- package/templates/nextblock-template/app/cms/posts/new/page.tsx +1 -1
- package/templates/nextblock-template/app/cms/posts/page.tsx +20 -13
- package/templates/nextblock-template/app/cms/products/ClientNotionEditor.tsx +16 -0
- package/templates/nextblock-template/app/cms/products/ProductFormClientShell.tsx +56 -0
- package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +292 -0
- package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -0
- package/templates/nextblock-template/app/cms/products/categories/page.tsx +12 -0
- package/templates/nextblock-template/app/cms/products/inventory/page.tsx +13 -0
- package/templates/nextblock-template/app/cms/products/new/page.tsx +143 -0
- package/templates/nextblock-template/app/cms/products/page.tsx +42 -0
- package/templates/nextblock-template/app/cms/products/productFormData.ts +133 -0
- package/templates/nextblock-template/app/cms/products/settings/page.tsx +5 -0
- package/templates/nextblock-template/app/cms/promotions/PromotionsWorkspace.tsx +456 -0
- package/templates/nextblock-template/app/cms/promotions/actions.ts +115 -0
- package/templates/nextblock-template/app/cms/promotions/page.tsx +31 -0
- package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +2 -2
- package/templates/nextblock-template/app/cms/revisions/actions.ts +285 -285
- package/templates/nextblock-template/app/cms/revisions/service.ts +19 -16
- package/templates/nextblock-template/app/cms/revisions/utils.ts +8 -3
- package/templates/nextblock-template/app/cms/settings/backup-restore/BackupRestoreWorkspace.tsx +1004 -0
- package/templates/nextblock-template/app/cms/settings/backup-restore/page.tsx +29 -0
- package/templates/nextblock-template/app/cms/settings/bot-protection/actions.ts +93 -0
- package/templates/nextblock-template/app/cms/settings/bot-protection/components/BotProtectionForm.tsx +129 -0
- package/templates/nextblock-template/app/cms/settings/bot-protection/page.tsx +24 -0
- package/templates/nextblock-template/app/cms/settings/copyright/actions.ts +1 -1
- package/templates/nextblock-template/app/cms/settings/copyright/components/CopyrightForm.tsx +2 -2
- package/templates/nextblock-template/app/cms/settings/copyright/page.tsx +1 -1
- package/templates/nextblock-template/app/cms/settings/cortex-ai/SandboxCortexAiSettingsClient.tsx +496 -0
- package/templates/nextblock-template/app/cms/settings/cortex-ai/StoredCortexAiSettingsClient.tsx +410 -0
- package/templates/nextblock-template/app/cms/settings/cortex-ai/actions.ts +248 -0
- package/templates/nextblock-template/app/cms/settings/cortex-ai/page.tsx +80 -0
- package/templates/nextblock-template/app/cms/settings/currencies/actions.ts +331 -0
- package/templates/nextblock-template/app/cms/settings/currencies/page.tsx +494 -0
- package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -0
- package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +203 -44
- package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +93 -242
- package/templates/nextblock-template/app/cms/settings/global-css/actions.ts +65 -0
- package/templates/nextblock-template/app/cms/settings/global-css/components/GlobalCssForm.tsx +46 -0
- package/templates/nextblock-template/app/cms/settings/global-css/page.tsx +24 -0
- package/templates/nextblock-template/app/cms/settings/languages/components/DeleteLanguageButton.tsx +1 -1
- package/templates/nextblock-template/app/cms/settings/languages/components/LanguageForm.tsx +2 -2
- package/templates/nextblock-template/app/cms/settings/languages/page.tsx +1 -1
- package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +7 -7
- package/templates/nextblock-template/app/cms/settings/logos/actions.ts +82 -6
- package/templates/nextblock-template/app/cms/settings/logos/components/BrandingSettingsForm.tsx +339 -0
- package/templates/nextblock-template/app/cms/settings/logos/components/DeleteLogoButton.tsx +21 -18
- package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +20 -16
- package/templates/nextblock-template/app/cms/settings/logos/components/SiteSeoSettingsForm.tsx +133 -0
- package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +8 -8
- package/templates/nextblock-template/app/cms/settings/logos/page.tsx +120 -82
- package/templates/nextblock-template/app/cms/settings/logos/types.ts +8 -8
- package/templates/nextblock-template/app/cms/settings/packages/activation-form.tsx +84 -0
- package/templates/nextblock-template/app/cms/settings/packages/package-card.tsx +122 -0
- package/templates/nextblock-template/app/cms/settings/packages/page.tsx +49 -0
- package/templates/nextblock-template/app/cms/settings/privacy/actions.ts +53 -0
- package/templates/nextblock-template/app/cms/settings/privacy/components/PrivacyForm.tsx +196 -0
- package/templates/nextblock-template/app/cms/settings/privacy/page.tsx +26 -0
- package/templates/nextblock-template/app/cms/settings/security/actions.ts +251 -0
- package/templates/nextblock-template/app/cms/settings/security/components/SecurityPanel.tsx +453 -0
- package/templates/nextblock-template/app/cms/settings/security/page.tsx +13 -0
- package/templates/nextblock-template/app/cms/settings/taxes/page.tsx +21 -0
- package/templates/nextblock-template/app/cms/shipping/page.tsx +20 -0
- package/templates/nextblock-template/app/cms/users/[id]/edit/page.tsx +28 -23
- package/templates/nextblock-template/app/cms/users/actions.ts +105 -40
- package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +1 -1
- package/templates/nextblock-template/app/cms/users/components/UserForm.tsx +65 -152
- package/templates/nextblock-template/app/cms/users/page.tsx +15 -10
- package/templates/nextblock-template/app/globals.css +9 -0
- package/templates/nextblock-template/app/layout.tsx +372 -120
- package/templates/nextblock-template/app/lib/seo.test.ts +52 -0
- package/templates/nextblock-template/app/lib/seo.ts +279 -0
- package/templates/nextblock-template/app/lib/site-settings.ts +87 -0
- package/templates/nextblock-template/app/lib/sitemap-utils.ts +224 -39
- package/templates/nextblock-template/app/lib/ucp/protocol.ts +190 -0
- package/templates/nextblock-template/app/lib/ucp/server.test.ts +56 -0
- package/templates/nextblock-template/app/lib/ucp/server.ts +1914 -0
- package/templates/nextblock-template/app/page.tsx +165 -73
- package/templates/nextblock-template/app/product/[slug]/page.tsx +433 -0
- package/templates/nextblock-template/app/profile/ProfileAccountSidebar.tsx +73 -0
- package/templates/nextblock-template/app/profile/ProfilePageHeader.tsx +16 -0
- package/templates/nextblock-template/app/profile/ProfilePageMissingState.tsx +9 -0
- package/templates/nextblock-template/app/profile/account-data.ts +37 -0
- package/templates/nextblock-template/app/profile/account-links.ts +22 -0
- package/templates/nextblock-template/app/profile/account-types.ts +11 -0
- package/templates/nextblock-template/app/profile/orders/CustomerOrdersPageClient.tsx +124 -0
- package/templates/nextblock-template/app/profile/orders/[id]/CustomerOrderDetailPageClient.tsx +79 -0
- package/templates/nextblock-template/app/profile/orders/[id]/page.tsx +32 -0
- package/templates/nextblock-template/app/profile/orders/page.tsx +19 -0
- package/templates/nextblock-template/app/profile/page.tsx +51 -0
- package/templates/nextblock-template/app/profile/password/PasswordSettingsPageClient.tsx +128 -0
- package/templates/nextblock-template/app/profile/password/actions.ts +59 -0
- package/templates/nextblock-template/app/profile/password/page.tsx +27 -0
- package/templates/nextblock-template/app/providers.tsx +55 -17
- package/templates/nextblock-template/app/robots.txt/route.ts +11 -1
- package/templates/nextblock-template/app/sitemap.ts +128 -0
- package/templates/nextblock-template/app/ucp/v1/carts/[id]/cancel/route.ts +38 -0
- package/templates/nextblock-template/app/ucp/v1/carts/[id]/route.ts +68 -0
- package/templates/nextblock-template/app/ucp/v1/carts/route.ts +35 -0
- package/templates/nextblock-template/app/ucp/v1/catalog/lookup/route.ts +35 -0
- package/templates/nextblock-template/app/ucp/v1/catalog/product/route.ts +35 -0
- package/templates/nextblock-template/app/ucp/v1/catalog/search/route.ts +34 -0
- package/templates/nextblock-template/components/AppShell.tsx +154 -0
- package/templates/nextblock-template/components/BlockRenderer.tsx +210 -64
- package/templates/nextblock-template/components/CartDrawerLoader.tsx +7 -0
- package/templates/nextblock-template/components/CartTranslator.tsx +210 -0
- package/templates/nextblock-template/components/CurrentContentSetter.tsx +25 -0
- package/templates/nextblock-template/components/DeferredCartDrawer.tsx +23 -0
- package/templates/nextblock-template/components/DeferredCartTranslator.tsx +51 -0
- package/templates/nextblock-template/components/DeferredGlobalSearch.tsx +68 -0
- package/templates/nextblock-template/components/DeferredGoogleTagManager.tsx +70 -0
- package/templates/nextblock-template/components/DeferredSpeedInsights.tsx +69 -0
- package/templates/nextblock-template/components/FeatureImageHero.tsx +47 -0
- package/templates/nextblock-template/components/GitHubLoginButton.tsx +36 -0
- package/templates/nextblock-template/components/GlobalSearch.tsx +557 -0
- package/templates/nextblock-template/components/Header.tsx +49 -41
- package/templates/nextblock-template/components/LanguageSwitcher.tsx +55 -32
- package/templates/nextblock-template/components/ResponsiveNav.tsx +138 -43
- package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +12 -8
- package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -55
- package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +42 -37
- package/templates/nextblock-template/components/blocks/TestimonialBlock.tsx +6 -2
- package/templates/nextblock-template/components/blocks/ecommerceRendererLoaders.ts +23 -0
- package/templates/nextblock-template/components/blocks/publicRendererLoaders.ts +25 -0
- package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -84
- package/templates/nextblock-template/components/blocks/renderers/CartBlockRenderer.tsx +17 -0
- package/templates/nextblock-template/components/blocks/renderers/CheckoutBlockRenderer.tsx +19 -0
- package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +262 -8
- package/templates/nextblock-template/components/blocks/renderers/FeaturedProductBlockRenderer.tsx +22 -0
- package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +320 -37
- package/templates/nextblock-template/components/blocks/renderers/HeadingBlockRenderer.tsx +11 -8
- package/templates/nextblock-template/components/blocks/renderers/ImageBlockRenderer.tsx +12 -3
- package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +18 -13
- package/templates/nextblock-template/components/blocks/renderers/ProductDetailsBlockRenderer.tsx +90 -0
- package/templates/nextblock-template/components/blocks/renderers/ProductGridBlockRenderer.tsx +31 -0
- package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +424 -55
- package/templates/nextblock-template/components/blocks/renderers/SectionSlider.tsx +137 -0
- package/templates/nextblock-template/components/blocks/renderers/TestimonialBlockRenderer.tsx +57 -0
- package/templates/nextblock-template/components/blocks/renderers/TextBlockRenderer.tsx +37 -22
- package/templates/nextblock-template/components/blocks/renderers/VideoEmbedBlockRenderer.tsx +23 -15
- package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +1 -3
- package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +1 -3
- package/templates/nextblock-template/components/blocks/types.ts +7 -6
- package/templates/nextblock-template/components/env-var-warning.tsx +3 -3
- package/templates/nextblock-template/components/form-message.tsx +32 -26
- package/templates/nextblock-template/components/header-auth.tsx +69 -17
- package/templates/nextblock-template/components/privacy/ConsentBanner.tsx +127 -0
- package/templates/nextblock-template/components/privacy/ConsentGatedAnalytics.tsx +59 -0
- package/templates/nextblock-template/components/renderers/CachedDynamicLayoutEngine.tsx +28 -0
- package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.test.tsx +166 -0
- package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.tsx +464 -0
- package/templates/nextblock-template/components/theme-switcher.tsx +8 -8
- package/templates/nextblock-template/components/visual-editing/DeferredVisualEditing.tsx +21 -0
- package/templates/nextblock-template/components/visual-editing/NextblockVisualEditing.tsx +1172 -0
- package/templates/nextblock-template/context/AuthContext.tsx +23 -90
- package/templates/nextblock-template/context/CurrentContentContext.tsx +10 -4
- package/templates/nextblock-template/context/LanguageContext.tsx +16 -16
- package/templates/nextblock-template/context/language-rest-client.ts +31 -0
- package/templates/nextblock-template/docs/01-PROJECT-OVERVIEW.md +94 -0
- package/templates/nextblock-template/docs/02-ECOMMERCE-CAPABILITIES.md +364 -0
- package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +202 -0
- package/templates/nextblock-template/docs/04-DATABASE-AND-AUTH.md +252 -0
- package/templates/nextblock-template/docs/05-DEVELOPER-GUIDE.md +238 -0
- package/templates/nextblock-template/docs/06-CLI-AND-SCAFFOLDING.md +125 -0
- package/templates/nextblock-template/docs/07-BLOCK-SDK-AND-EXTENSIBILITY.md +146 -0
- package/templates/nextblock-template/docs/08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md +1319 -0
- package/templates/nextblock-template/docs/09-LIVE-DRAFT-MODE.md +104 -0
- package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +222 -0
- package/templates/nextblock-template/docs/README.md +34 -0
- package/templates/nextblock-template/docs/TECHNICAL_SPECIFICATION.md +12507 -0
- package/templates/nextblock-template/hooks/use-hotkeys.ts +21 -14
- package/templates/nextblock-template/hooks/useGlobalSearch.ts +101 -0
- package/templates/nextblock-template/index.d.ts +2 -0
- package/templates/nextblock-template/lib/ai-block-generation.ts +339 -0
- package/templates/nextblock-template/lib/ai-client.ts +247 -0
- package/templates/nextblock-template/lib/ai-config.ts +81 -0
- package/templates/nextblock-template/lib/ai-cortex-widget-builder.ts +125 -0
- package/templates/nextblock-template/lib/ai-global-agent-custom-block-tools.ts +363 -0
- package/templates/nextblock-template/lib/ai-global-agent-db-tools.test.ts +405 -0
- package/templates/nextblock-template/lib/ai-global-agent-db-tools.ts +1228 -0
- package/templates/nextblock-template/lib/ai-global-agent-ecommerce.ts +5 -0
- package/templates/nextblock-template/lib/ai-global-agent-tools-stats.test.ts +223 -0
- package/templates/nextblock-template/lib/ai-global-agent-tools.test.ts +2183 -0
- package/templates/nextblock-template/lib/ai-global-agent-tools.ts +4807 -0
- package/templates/nextblock-template/lib/ai-key-crypto.test.ts +70 -0
- package/templates/nextblock-template/lib/ai-key-crypto.ts +132 -0
- package/templates/nextblock-template/lib/ai-model-catalog.test.ts +49 -0
- package/templates/nextblock-template/lib/ai-model-catalog.ts +41 -0
- package/templates/nextblock-template/lib/ai-model-registry.test.ts +231 -0
- package/templates/nextblock-template/lib/ai-model-registry.ts +522 -0
- package/templates/nextblock-template/lib/auth/cookies.ts +47 -0
- package/templates/nextblock-template/lib/auth/crypto.ts +42 -0
- package/templates/nextblock-template/lib/auth/trustedDevices.ts +92 -0
- package/templates/nextblock-template/lib/auth/twoFactor.ts +167 -0
- package/templates/nextblock-template/lib/auth-redirects.ts +46 -0
- package/templates/nextblock-template/lib/blocks/FeaturedProductBlock.tsx +94 -0
- package/templates/nextblock-template/lib/blocks/ProductGridBlock.tsx +137 -0
- package/templates/nextblock-template/lib/blocks/README.md +13 -670
- package/templates/nextblock-template/lib/blocks/blockRegistry.ts +138 -56
- package/templates/nextblock-template/lib/blocks/blockTypes.ts +18 -0
- package/templates/nextblock-template/lib/blocks/ecommerce-block-schemas.ts +31 -0
- package/templates/nextblock-template/lib/cms-transfer/csv.test.ts +77 -0
- package/templates/nextblock-template/lib/cms-transfer/csv.ts +399 -0
- package/templates/nextblock-template/lib/cms-transfer/server.ts +2243 -0
- package/templates/nextblock-template/lib/cms-transfer/types.ts +145 -0
- package/templates/nextblock-template/lib/cortex-widget-registry.test.ts +199 -0
- package/templates/nextblock-template/lib/cortex-widget-registry.ts +88 -0
- package/templates/nextblock-template/lib/cortex-widget-schema.test.tsx +237 -0
- package/templates/nextblock-template/lib/cortex-widget-schema.ts +393 -0
- package/templates/nextblock-template/lib/custom-block-definitions.ts +87 -0
- package/templates/nextblock-template/lib/custom-block-r2-upload-shared.ts +178 -0
- package/templates/nextblock-template/lib/custom-block-r2-upload.test.ts +140 -0
- package/templates/nextblock-template/lib/custom-block-r2-upload.ts +68 -0
- package/templates/nextblock-template/lib/custom-block-relation-registry.ts +256 -0
- package/templates/nextblock-template/lib/custom-block-relations.test.ts +227 -0
- package/templates/nextblock-template/lib/custom-block-relations.ts +279 -0
- package/templates/nextblock-template/lib/custom-block-safelist.ts +14 -0
- package/templates/nextblock-template/lib/editor/dynamic-extension-core.test.ts +172 -0
- package/templates/nextblock-template/lib/editor/dynamic-extension-core.ts +213 -0
- package/templates/nextblock-template/lib/editor/dynamic-extension-loader.ts +22 -0
- package/templates/nextblock-template/lib/editor/dynamic-extensions.tsx +193 -0
- package/templates/nextblock-template/lib/full-backup/manifest.test.ts +121 -0
- package/templates/nextblock-template/lib/full-backup/manifest.ts +206 -0
- package/templates/nextblock-template/lib/full-backup/server.ts +743 -0
- package/templates/nextblock-template/lib/media/resolveMediaUrl.ts +45 -0
- package/templates/nextblock-template/lib/posts/readTime.ts +60 -0
- package/templates/nextblock-template/lib/privacy/consent-client.ts +57 -0
- package/templates/nextblock-template/lib/privacy/settings.ts +103 -0
- package/templates/nextblock-template/lib/privacy/types.ts +67 -0
- package/templates/nextblock-template/lib/promotions/server.test.ts +74 -0
- package/templates/nextblock-template/lib/promotions/server.ts +741 -0
- package/templates/nextblock-template/lib/resolve-block-relations.test.ts +142 -0
- package/templates/nextblock-template/lib/resolve-block-relations.ts +255 -0
- package/templates/nextblock-template/lib/search/server.ts +585 -0
- package/templates/nextblock-template/lib/search/types.ts +27 -0
- package/templates/nextblock-template/lib/visual-editing/draft-content.test.ts +105 -0
- package/templates/nextblock-template/lib/visual-editing/draft-content.ts +380 -0
- package/templates/nextblock-template/lib/visual-editing/draft-route.test.ts +42 -0
- package/templates/nextblock-template/lib/visual-editing/draft-route.ts +82 -0
- package/templates/nextblock-template/lib/visual-editing/edit-info.test.ts +143 -0
- package/templates/nextblock-template/lib/visual-editing/edit-info.ts +94 -0
- package/templates/nextblock-template/lib/visual-editing/mutations.ts +190 -0
- package/templates/nextblock-template/lib/visual-editing/product-drafts.test.ts +81 -0
- package/templates/nextblock-template/lib/visual-editing/product-drafts.ts +511 -0
- package/templates/nextblock-template/lib/visual-editing/types.ts +122 -0
- package/templates/nextblock-template/lib/zod-config.ts +5 -0
- package/templates/nextblock-template/next.config.js +190 -66
- package/templates/nextblock-template/package.json +34 -30
- package/templates/nextblock-template/proxy.ts +435 -253
- package/templates/nextblock-template/public/images/NBcover.webp +0 -0
- package/templates/nextblock-template/public/images/cap.webp +0 -0
- package/templates/nextblock-template/public/images/commerce-plan.webp +0 -0
- package/templates/nextblock-template/public/images/commerce-square.webp +0 -0
- package/templates/nextblock-template/public/images/commerce-wide.webp +0 -0
- package/templates/nextblock-template/public/images/cortex-ai-square.webp +0 -0
- package/templates/nextblock-template/public/images/cortex-ai.webp +0 -0
- package/templates/nextblock-template/public/images/extensibility.webp +0 -0
- package/templates/nextblock-template/public/images/goals.webp +0 -0
- package/templates/nextblock-template/public/images/included.webp +0 -0
- package/templates/nextblock-template/public/images/nx-graph.webp +0 -0
- package/templates/nextblock-template/public/images/pants.webp +0 -0
- package/templates/nextblock-template/public/images/t-shirt.webp +0 -0
- package/templates/nextblock-template/scripts/validate-editor-block-schema.ts +112 -0
- package/templates/nextblock-template/scripts/verify-cortex-ai-build-widget.tsx +100 -0
- package/templates/nextblock-template/scripts/verify-cortex-ai-generate-blocks.ts +62 -0
- package/templates/nextblock-template/scripts/verify-cortex-ai-global-tools.ts +537 -0
- package/templates/nextblock-template/scripts/verify-cortex-ai-routing.ts +58 -0
- package/templates/nextblock-template/scripts/verify-custom-block-definitions.ts +188 -0
- package/templates/nextblock-template/scripts/verify-dynamic-custom-block-extensions.ts +123 -0
- package/templates/nextblock-template/scripts/verify-dynamic-layout-engine.tsx +133 -0
- package/templates/nextblock-template/scripts/verify-milestone-2-custom-blocks.ts +65 -0
- package/templates/nextblock-template/tailwind.config.js +1 -0
- package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -0
- package/templates/nextblock-template/tools/deploy-supabase.js +69 -71
- package/templates/nextblock-template/tsconfig.json +52 -66
- package/templates/nextblock-template/tsconfig.tsbuildinfo +1 -1
- package/templates/nextblock-template/types/jsdom.d.ts +6 -0
- package/templates/nextblock-template/app/force-styles.tsx +0 -31
- package/templates/nextblock-template/app/sitemap.xml/route.ts +0 -63
- package/templates/nextblock-template/components/blocks/renderers/HeroBlockRenderer.tsx +0 -273
- package/templates/nextblock-template/docs/How to Create a Custom Block.md +0 -149
- package/templates/nextblock-template/docs/cms-application-overview.md +0 -56
- package/templates/nextblock-template/docs/cms-architecture-overview.md +0 -73
- package/templates/nextblock-template/docs/files-structure.md +0 -426
- package/templates/nextblock-template/docs/tiptap-bundle-optimization-summary.md +0 -174
package/bin/create-nextblock.js
CHANGED
|
@@ -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);
|
|
@@ -19,6 +19,7 @@ const TEMPLATE_DIR = resolve(__dirname, '../templates/nextblock-template');
|
|
|
19
19
|
const REPO_ROOT = resolve(__dirname, '../../..');
|
|
20
20
|
const EDITOR_UTILS_SOURCE_DIR = resolve(REPO_ROOT, 'libs/editor/src/lib/utils');
|
|
21
21
|
const IS_WINDOWS = process.platform === 'win32';
|
|
22
|
+
const CLI_VERSION = createRequire(import.meta.url)('../package.json').version;
|
|
22
23
|
|
|
23
24
|
const UI_PROXY_MODULES = [
|
|
24
25
|
'avatar',
|
|
@@ -54,15 +55,21 @@ const PACKAGE_VERSION_SOURCES = {
|
|
|
54
55
|
|
|
55
56
|
program
|
|
56
57
|
.name('create-nextblock')
|
|
57
|
-
.description('
|
|
58
|
-
.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
)
|
|
58
|
+
.description('NextBlock™ CMS CLI')
|
|
59
|
+
.version(CLI_VERSION, '-v, --version');
|
|
60
|
+
|
|
61
|
+
program
|
|
62
|
+
.command('create [project-directory]', { isDefault: true })
|
|
63
|
+
.description('Bootstrap a NextBlock™ CMS project')
|
|
62
64
|
.option('--skip-install', 'Skip installing dependencies')
|
|
63
65
|
.option('-y, --yes', 'Skip all interactive prompts and use defaults')
|
|
64
66
|
.action(handleCommand);
|
|
65
67
|
|
|
68
|
+
program
|
|
69
|
+
.command('activate [module]')
|
|
70
|
+
.description('Activate a premium NextBlock™ CMS module')
|
|
71
|
+
.action(handleActivateCommand);
|
|
72
|
+
|
|
66
73
|
await program.parseAsync(process.argv).catch((error) => {
|
|
67
74
|
console.error(
|
|
68
75
|
chalk.red(error instanceof Error ? error.message : String(error)),
|
|
@@ -74,6 +81,46 @@ async function handleCommand(projectDirectory, options) {
|
|
|
74
81
|
const { skipInstall, yes } = options;
|
|
75
82
|
|
|
76
83
|
try {
|
|
84
|
+
console.log(chalk.bold.cyan(`\n🧱 create-nextblock v${CLI_VERSION}\n`));
|
|
85
|
+
|
|
86
|
+
// Prerequisites gate (interactive only) — shown BEFORE we ask for a name, scaffold, or
|
|
87
|
+
// install, so anyone who isn't ready can cancel without creating anything.
|
|
88
|
+
if (!yes) {
|
|
89
|
+
clack.note(
|
|
90
|
+
[
|
|
91
|
+
'1. A Supabase project https://supabase.com/dashboard',
|
|
92
|
+
' • Reference ID — Project Settings > General > "Reference ID"',
|
|
93
|
+
' • Connection string — Connect (top bar) > Direct connection > URI',
|
|
94
|
+
' • anon + service_role keys — Project Settings > API Keys',
|
|
95
|
+
' • Personal Access Token — Account > Access Tokens > Generate new token',
|
|
96
|
+
'',
|
|
97
|
+
'2. A Cloudflare R2 bucket https://dash.cloudflare.com > R2',
|
|
98
|
+
' • Create a bucket, then enable its Public Development URL (Bucket > Settings > General)',
|
|
99
|
+
' • Create an R2 API token (Object Read & Write); copy the Access Key ID + Secret (shown once)',
|
|
100
|
+
'',
|
|
101
|
+
'3. SMTP credentials SMTP2GO works very well: https://www.smtp2go.com',
|
|
102
|
+
' • Required so Supabase can email the confirmation link your first admin needs to sign in',
|
|
103
|
+
].join('\n'),
|
|
104
|
+
'Before you continue, have all of the following ready',
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const ready = await clack.confirm({
|
|
108
|
+
message:
|
|
109
|
+
'Do you have your Supabase, Cloudflare R2, and SMTP details ready?',
|
|
110
|
+
initialValue: true,
|
|
111
|
+
});
|
|
112
|
+
if (clack.isCancel(ready)) {
|
|
113
|
+
handleWizardCancel('Setup cancelled.');
|
|
114
|
+
}
|
|
115
|
+
if (!ready) {
|
|
116
|
+
clack.note(
|
|
117
|
+
'No problem — nothing was created. Gather the items above, then run\n`npm create nextblock` again. Full guide: docs/05-DEVELOPER-GUIDE.md',
|
|
118
|
+
'Come back when ready',
|
|
119
|
+
);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
77
124
|
let projectName = projectDirectory;
|
|
78
125
|
|
|
79
126
|
if (!projectName) {
|
|
@@ -159,32 +206,35 @@ async function handleCommand(projectDirectory, options) {
|
|
|
159
206
|
await ensurePublicNpmrc(projectDir);
|
|
160
207
|
console.log(chalk.green('Enforced public registry for initial install.'));
|
|
161
208
|
|
|
209
|
+
await initializeGit(projectDir);
|
|
210
|
+
|
|
162
211
|
if (!skipInstall) {
|
|
163
212
|
await installDependencies(projectDir);
|
|
164
213
|
} else {
|
|
165
214
|
console.log(chalk.yellow('Skipping dependency installation.'));
|
|
166
215
|
}
|
|
167
216
|
|
|
168
|
-
// Run setup wizard after dependencies are installed so package assets are available
|
|
217
|
+
// Run the setup wizard after dependencies are installed so package assets are available.
|
|
218
|
+
// When it runs, its own "next steps" outro (cd + npm run dev) is the final message, so we
|
|
219
|
+
// don't print a second closing block here — the whole flow completes in this one command.
|
|
169
220
|
if (!yes) {
|
|
170
221
|
await runSetupWizard(projectDir, projectName);
|
|
171
222
|
} else {
|
|
223
|
+
// Non-interactive path: nothing was configured, so point the user at their env file.
|
|
172
224
|
console.log(
|
|
173
|
-
chalk.
|
|
174
|
-
|
|
225
|
+
chalk.green(
|
|
226
|
+
`\nSuccess! Your NextBlock™ CMS project "${projectName}" is scaffolded.\n`,
|
|
175
227
|
),
|
|
176
228
|
);
|
|
229
|
+
console.log(chalk.cyan('Next steps:'));
|
|
230
|
+
console.log(chalk.cyan(` 1. cd ${projectName}`));
|
|
231
|
+
console.log(
|
|
232
|
+
chalk.gray(
|
|
233
|
+
' 2. Add your Supabase / R2 / SMTP values to .env.local (template in .env.example)',
|
|
234
|
+
),
|
|
235
|
+
);
|
|
236
|
+
console.log(chalk.cyan(' 3. npm run dev'));
|
|
177
237
|
}
|
|
178
|
-
|
|
179
|
-
await initializeGit(projectDir);
|
|
180
|
-
|
|
181
|
-
console.log(
|
|
182
|
-
chalk.green(
|
|
183
|
-
`\nSuccess! Your NextBlock CMS project "${projectName}" is ready.\n`,
|
|
184
|
-
),
|
|
185
|
-
);
|
|
186
|
-
console.log(chalk.cyan('Next step:'));
|
|
187
|
-
console.log(chalk.cyan(` cd ${projectName} && npm run dev`));
|
|
188
238
|
} catch (error) {
|
|
189
239
|
console.error(
|
|
190
240
|
chalk.red(
|
|
@@ -195,482 +245,745 @@ async function handleCommand(projectDirectory, options) {
|
|
|
195
245
|
}
|
|
196
246
|
}
|
|
197
247
|
|
|
198
|
-
async function
|
|
199
|
-
|
|
200
|
-
|
|
248
|
+
async function handleActivateCommand(moduleName) {
|
|
249
|
+
if (!moduleName || moduleName !== 'ecommerce') {
|
|
250
|
+
console.error(
|
|
251
|
+
chalk.red('Invalid module name. Supported modules: ecommerce'),
|
|
252
|
+
);
|
|
253
|
+
process.exit(1);
|
|
254
|
+
}
|
|
201
255
|
|
|
202
|
-
clack.intro(
|
|
256
|
+
clack.intro(`🚀 Activating NextBlock™ module: ${moduleName}`);
|
|
203
257
|
|
|
204
|
-
const
|
|
205
|
-
await fs.ensureDir(supabaseDir);
|
|
206
|
-
await resetSupabaseProjectRef(projectPath);
|
|
258
|
+
const projectPath = process.cwd();
|
|
207
259
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
'
|
|
260
|
+
// 1. Install NPM package
|
|
261
|
+
clack.note(`Installing @nextblock-cms/${moduleName}...`);
|
|
262
|
+
|
|
263
|
+
await execa(
|
|
264
|
+
'npm',
|
|
265
|
+
['install', `@nextblock-cms/ecommerce@npm:@nextblock-cms/ecom@latest`],
|
|
266
|
+
{ cwd: projectPath, stdio: 'inherit' },
|
|
213
267
|
);
|
|
268
|
+
clack.note('NPM package installed!');
|
|
269
|
+
|
|
270
|
+
// 2. Inject Route Wrappers
|
|
271
|
+
clack.note('Injecting route wrappers...');
|
|
272
|
+
|
|
273
|
+
const routesToInject = {
|
|
274
|
+
'app/cms/orders/page.tsx': `import { OrdersPage as OrdersPageUI } from '@nextblock-cms/ecommerce';
|
|
275
|
+
import { verifyPackageOnline } from '@nextblock-cms/db/server';
|
|
276
|
+
import { redirect } from 'next/navigation';
|
|
277
|
+
|
|
278
|
+
export default async function OrdersPage() {
|
|
279
|
+
const isOnline = await verifyPackageOnline('ecommerce');
|
|
280
|
+
if (!isOnline) {
|
|
281
|
+
redirect('/cms/settings/packages');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return <OrdersPageUI />;
|
|
285
|
+
}`,
|
|
286
|
+
'app/cms/orders/[id]/page.tsx': `import { OrderDetailPage as OrderDetailPageUI } from '@nextblock-cms/ecommerce';
|
|
287
|
+
import { verifyPackageOnline } from '@nextblock-cms/db/server';
|
|
288
|
+
import { redirect } from 'next/navigation';
|
|
289
|
+
|
|
290
|
+
export default async function OrderDetailPage({
|
|
291
|
+
params,
|
|
292
|
+
}: {
|
|
293
|
+
params: Promise<{ id: string }>;
|
|
294
|
+
}) {
|
|
295
|
+
const isOnline = await verifyPackageOnline('ecommerce');
|
|
296
|
+
if (!isOnline) {
|
|
297
|
+
redirect('/cms/settings/packages');
|
|
298
|
+
}
|
|
299
|
+
const resolvedParams = await params;
|
|
300
|
+
return <OrderDetailPageUI params={resolvedParams} />;
|
|
301
|
+
}`,
|
|
302
|
+
'app/cms/products/page.tsx': `import { ProductsPage as ProductsPageUI } from '@nextblock-cms/ecommerce';
|
|
303
|
+
import { verifyPackageOnline } from '@nextblock-cms/db/server';
|
|
304
|
+
import { redirect } from 'next/navigation';
|
|
305
|
+
|
|
306
|
+
export default async function ProductsPage() {
|
|
307
|
+
const isOnline = await verifyPackageOnline('ecommerce');
|
|
308
|
+
if (!isOnline) {
|
|
309
|
+
redirect('/cms/settings/packages');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return <ProductsPageUI />;
|
|
313
|
+
}`,
|
|
314
|
+
'app/cms/products/new/page.tsx': `import { NewProductPage as NewProductPageUI } from '@nextblock-cms/ecommerce';
|
|
315
|
+
import { verifyPackageOnline } from '@nextblock-cms/db/server';
|
|
316
|
+
import { redirect } from 'next/navigation';
|
|
214
317
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
318
|
+
export default async function NewProductPage() {
|
|
319
|
+
const isOnline = await verifyPackageOnline('ecommerce');
|
|
320
|
+
if (!isOnline) {
|
|
321
|
+
redirect('/cms/settings/packages');
|
|
322
|
+
}
|
|
218
323
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
324
|
+
return <NewProductPageUI />;
|
|
325
|
+
}`,
|
|
326
|
+
'app/cms/products/[id]/edit/page.tsx': `import { EditProductPage as EditProductPageUI } from '@nextblock-cms/ecommerce';
|
|
327
|
+
import { verifyPackageOnline } from '@nextblock-cms/db/server';
|
|
328
|
+
import { redirect } from 'next/navigation';
|
|
329
|
+
|
|
330
|
+
export default async function EditProductPage({ params }: { params: Promise<{ id: string }> }) {
|
|
331
|
+
const isOnline = await verifyPackageOnline('ecommerce');
|
|
332
|
+
if (!isOnline) {
|
|
333
|
+
redirect('/cms/settings/packages');
|
|
229
334
|
}
|
|
230
335
|
|
|
231
|
-
|
|
336
|
+
const resolvedParams = await params;
|
|
337
|
+
return <EditProductPageUI params={resolvedParams} />;
|
|
338
|
+
}`,
|
|
339
|
+
'app/cms/payments/page.tsx': `import { PaymentsPage as PaymentsPageUI } from '@nextblock-cms/ecommerce';
|
|
340
|
+
import { verifyPackageOnline } from '@nextblock-cms/db/server';
|
|
341
|
+
import { redirect } from 'next/navigation';
|
|
232
342
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
343
|
+
export default async function PaymentsPage() {
|
|
344
|
+
const isOnline = await verifyPackageOnline('ecommerce');
|
|
345
|
+
if (!isOnline) {
|
|
346
|
+
redirect('/cms/settings/packages');
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return <PaymentsPageUI />;
|
|
350
|
+
}`,
|
|
351
|
+
'app/cms/coupons/page.tsx': `import { CouponsPage as CouponsPageUI } from '@nextblock-cms/ecommerce/server';
|
|
352
|
+
import { verifyPackageOnline } from '@nextblock-cms/db/server';
|
|
353
|
+
import { redirect } from 'next/navigation';
|
|
354
|
+
|
|
355
|
+
export default async function CouponsPage({
|
|
356
|
+
searchParams,
|
|
357
|
+
}: {
|
|
358
|
+
searchParams: Promise<{ status?: string; q?: string }>;
|
|
359
|
+
}) {
|
|
360
|
+
const isOnline = await verifyPackageOnline('ecommerce');
|
|
361
|
+
if (!isOnline) {
|
|
362
|
+
redirect('/cms/settings/packages');
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return <CouponsPageUI searchParams={await searchParams} />;
|
|
366
|
+
}`,
|
|
367
|
+
'app/cms/coupons/[id]/edit/page.tsx': `import { EditCouponPage as EditCouponPageUI } from '@nextblock-cms/ecommerce/server';
|
|
368
|
+
import { verifyPackageOnline } from '@nextblock-cms/db/server';
|
|
369
|
+
import { redirect } from 'next/navigation';
|
|
370
|
+
|
|
371
|
+
export default async function EditCouponPage({
|
|
372
|
+
params,
|
|
373
|
+
}: {
|
|
374
|
+
params: Promise<{ id: string }>;
|
|
375
|
+
}) {
|
|
376
|
+
const isOnline = await verifyPackageOnline('ecommerce');
|
|
377
|
+
if (!isOnline) {
|
|
378
|
+
redirect('/cms/settings/packages');
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return <EditCouponPageUI params={params} />;
|
|
382
|
+
}`,
|
|
383
|
+
'app/checkout/success/page.tsx': `import { CheckoutSuccessPage as CheckoutSuccessPageUI } from '@nextblock-cms/ecommerce';
|
|
384
|
+
import { verifyPackageOnline } from '@nextblock-cms/db/server';
|
|
385
|
+
import { notFound } from 'next/navigation';
|
|
386
|
+
|
|
387
|
+
export default async function CheckoutSuccessPage() {
|
|
388
|
+
const isOnline = await verifyPackageOnline('ecommerce');
|
|
389
|
+
if (!isOnline) {
|
|
390
|
+
notFound();
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return <CheckoutSuccessPageUI />;
|
|
394
|
+
}`,
|
|
395
|
+
'app/api/checkout/route.ts': `import { NextResponse } from 'next/server';
|
|
396
|
+
import { getPaymentProvider } from '@nextblock-cms/ecommerce/server';
|
|
397
|
+
import { createClient, verifyPackageOnline } from '@nextblock-cms/db/server';
|
|
398
|
+
import { normalizeCustomerAddress } from '@nextblock-cms/ecommerce';
|
|
399
|
+
|
|
400
|
+
function resolveProviderFromItem(item) {
|
|
401
|
+
if (item?.provider === 'stripe' || item?.provider === 'freemius') {
|
|
402
|
+
return item.provider;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (item?.payment_provider === 'stripe' || item?.payment_provider === 'freemius') {
|
|
406
|
+
return item.payment_provider;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (item?.product_type === 'digital') {
|
|
410
|
+
return 'freemius';
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (item?.product_type === 'physical') {
|
|
414
|
+
return 'stripe';
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (item?.freemius_product_id) {
|
|
418
|
+
return 'freemius';
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export async function POST(req: Request) {
|
|
425
|
+
try {
|
|
426
|
+
const isOnline = await verifyPackageOnline('ecommerce');
|
|
427
|
+
if (!isOnline) {
|
|
428
|
+
return NextResponse.json({ error: 'Ecommerce module license is inactive' }, { status: 403 });
|
|
242
429
|
}
|
|
243
|
-
|
|
430
|
+
|
|
431
|
+
const {
|
|
432
|
+
items,
|
|
433
|
+
customerEmail,
|
|
434
|
+
customerPhone,
|
|
435
|
+
billingAddress,
|
|
436
|
+
shippingAddress,
|
|
437
|
+
shippingMethodId,
|
|
438
|
+
currencyCode,
|
|
439
|
+
locale,
|
|
440
|
+
couponCode,
|
|
441
|
+
couponContextItems,
|
|
442
|
+
} = await req.json();
|
|
443
|
+
|
|
444
|
+
if (!items || !Array.isArray(items)) {
|
|
445
|
+
return NextResponse.json({ error: 'Invalid items data' }, { status: 400 });
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const providerNames = Array.from(
|
|
449
|
+
new Set(items.map((item) => resolveProviderFromItem(item)).filter(Boolean))
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
if (providerNames.length === 0) {
|
|
453
|
+
return NextResponse.json(
|
|
454
|
+
{ error: 'Each checkout request must include provider-aware cart items.' },
|
|
455
|
+
{ status: 400 }
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (providerNames.length > 1) {
|
|
460
|
+
return NextResponse.json(
|
|
461
|
+
{ error: 'Mixed-provider carts must be checked out in separate steps.' },
|
|
462
|
+
{ status: 400 }
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const providerName = providerNames[0];
|
|
467
|
+
|
|
468
|
+
if (providerName === 'freemius' && items.length !== 1) {
|
|
469
|
+
return NextResponse.json(
|
|
470
|
+
{ error: 'Freemius items must be checked out one at a time.' },
|
|
471
|
+
{ status: 400 }
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (!billingAddress) {
|
|
476
|
+
return NextResponse.json({ error: 'Billing address is required' }, { status: 400 });
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const supabase = createClient();
|
|
480
|
+
const provider = getPaymentProvider(providerName);
|
|
481
|
+
|
|
482
|
+
const { data: { user } } = await supabase.auth.getUser();
|
|
483
|
+
const userId = user?.id;
|
|
484
|
+
const resolvedCustomerEmail = user?.email || customerEmail || null;
|
|
485
|
+
|
|
486
|
+
const { url, error, errorKey, errorParams, errorStatus, customProps } =
|
|
487
|
+
await provider.createCheckoutSession({
|
|
488
|
+
items,
|
|
489
|
+
customerEmail: resolvedCustomerEmail,
|
|
490
|
+
customerPhone,
|
|
491
|
+
userId,
|
|
492
|
+
billingAddress: normalizeCustomerAddress(billingAddress) ?? billingAddress,
|
|
493
|
+
shippingAddress:
|
|
494
|
+
providerName === 'stripe'
|
|
495
|
+
? normalizeCustomerAddress(shippingAddress)
|
|
496
|
+
: null,
|
|
497
|
+
shippingMethodId: providerName === 'stripe' ? shippingMethodId : null,
|
|
498
|
+
currencyCode: typeof currencyCode === 'string' ? currencyCode : null,
|
|
499
|
+
locale: typeof locale === 'string' ? locale : null,
|
|
500
|
+
couponCode: typeof couponCode === 'string' ? couponCode : null,
|
|
501
|
+
couponContextItems: Array.isArray(couponContextItems) ? couponContextItems : items,
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
if (error) {
|
|
505
|
+
console.error('Checkout Error:', error);
|
|
506
|
+
return NextResponse.json(
|
|
507
|
+
{ error, errorKey, errorParams },
|
|
508
|
+
{ status: errorStatus ?? 500 }
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return NextResponse.json({ url, customProps });
|
|
513
|
+
} catch (err: any) {
|
|
514
|
+
console.error('Checkout API Error:', err);
|
|
515
|
+
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
|
|
244
516
|
}
|
|
245
|
-
|
|
517
|
+
}`,
|
|
518
|
+
};
|
|
246
519
|
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
});
|
|
252
|
-
if (clack.isCancel(siteUrlPrompt)) {
|
|
253
|
-
handleWizardCancel('Setup cancelled.');
|
|
520
|
+
for (const [routePath, content] of Object.entries(routesToInject)) {
|
|
521
|
+
const fullPath = resolve(projectPath, routePath);
|
|
522
|
+
await fs.ensureDir(dirname(fullPath));
|
|
523
|
+
await fs.writeFile(fullPath, content);
|
|
254
524
|
}
|
|
255
|
-
const siteUrl = siteUrlPrompt.trim();
|
|
256
525
|
|
|
257
|
-
clack.
|
|
258
|
-
'
|
|
259
|
-
'Note: For "Access Token", go to Account > Access Tokens > Generate New Token.',
|
|
526
|
+
clack.outro(
|
|
527
|
+
'✅ Ecommerce module activated successfully! You can now use the storefront features.',
|
|
260
528
|
);
|
|
261
|
-
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// clack validator that rejects empty/whitespace-only input with a labelled message.
|
|
532
|
+
function requiredValue(label) {
|
|
533
|
+
return (value) =>
|
|
534
|
+
value && String(value).trim() ? undefined : `${label} is required`;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Read the current value of a `KEY=` line from an .env file body (handles quotes),
|
|
538
|
+
// so re-runs can reuse already-generated secrets instead of regenerating them.
|
|
539
|
+
function readEnvValue(envContent, key) {
|
|
540
|
+
for (const line of envContent.split(/\r?\n/)) {
|
|
541
|
+
if (line.startsWith(key)) {
|
|
542
|
+
return line.slice(key.length).trim().replace(/^"(.*)"$/, '$1');
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
return '';
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function generateSecret() {
|
|
549
|
+
return crypto.randomBytes(32).toString('hex');
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
async function runSetupWizard(projectDir, projectName) {
|
|
553
|
+
const projectPath = resolve(projectDir);
|
|
554
|
+
process.chdir(projectPath);
|
|
555
|
+
|
|
556
|
+
// Prerequisites + readiness were already confirmed up front in handleCommand (before any
|
|
557
|
+
// scaffolding), so the wizard goes straight to collecting configuration.
|
|
558
|
+
clack.intro('🚀 NextBlock™ CMS setup');
|
|
559
|
+
|
|
560
|
+
await fs.ensureDir(resolve(projectPath, 'supabase'));
|
|
561
|
+
|
|
562
|
+
// 1. Supabase — same questions/order as setup.mjs. Nothing is masked: you are pasting
|
|
563
|
+
// keys you just copied, and seeing them makes paste mistakes easy to spot.
|
|
564
|
+
clack.note('Get these from https://supabase.com/dashboard', 'Supabase project');
|
|
565
|
+
const supabase = await clack.group(
|
|
262
566
|
{
|
|
567
|
+
projectId: () =>
|
|
568
|
+
clack.text({
|
|
569
|
+
message: 'Project ID (Project Settings > General > "Reference ID"):',
|
|
570
|
+
validate: requiredValue('Project Reference ID'),
|
|
571
|
+
}),
|
|
263
572
|
postgresUrl: () =>
|
|
264
573
|
clack.text({
|
|
265
574
|
message:
|
|
266
|
-
'
|
|
575
|
+
'Connection String (Connect > Direct connection > URI — replace [YOUR-PASSWORD] with your DB password):',
|
|
267
576
|
placeholder: 'postgresql://...',
|
|
268
|
-
validate: (
|
|
269
|
-
!val ? 'Connection string is required' : undefined,
|
|
577
|
+
validate: requiredValue('Connection string'),
|
|
270
578
|
}),
|
|
271
579
|
anonKey: () =>
|
|
272
|
-
clack.
|
|
273
|
-
message:
|
|
274
|
-
|
|
275
|
-
validate: (val) => (!val ? 'Anon Key is required' : undefined),
|
|
580
|
+
clack.text({
|
|
581
|
+
message: 'Project API Key — anon / public (Project Settings > API Keys):',
|
|
582
|
+
validate: requiredValue('Anon key'),
|
|
276
583
|
}),
|
|
277
584
|
serviceKey: () =>
|
|
278
|
-
clack.
|
|
279
|
-
message:
|
|
280
|
-
|
|
281
|
-
validate: (val) =>
|
|
282
|
-
!val ? 'Service Role Key is required' : undefined,
|
|
585
|
+
clack.text({
|
|
586
|
+
message: 'Service Role Key — service_role (Project Settings > API Keys):',
|
|
587
|
+
validate: requiredValue('Service role key'),
|
|
283
588
|
}),
|
|
284
589
|
accessToken: () =>
|
|
285
|
-
clack.
|
|
590
|
+
clack.text({
|
|
286
591
|
message:
|
|
287
|
-
'
|
|
288
|
-
validate: (
|
|
289
|
-
|
|
592
|
+
'Personal Access Token (Account > Access Tokens > Generate new token):',
|
|
593
|
+
validate: requiredValue('Access token'),
|
|
594
|
+
}),
|
|
595
|
+
siteUrl: () =>
|
|
596
|
+
clack.text({
|
|
597
|
+
// Standalone `npm run dev` is plain `next dev` on :3000 (NOT `nx serve` on :4200),
|
|
598
|
+
// so the local default differs from the monorepo setup wizard on purpose.
|
|
599
|
+
message: 'Public site URL [NEXT_PUBLIC_URL]:',
|
|
600
|
+
initialValue: 'http://localhost:3000',
|
|
601
|
+
validate: requiredValue('Site URL'),
|
|
290
602
|
}),
|
|
291
603
|
},
|
|
292
604
|
{ onCancel: () => handleWizardCancel('Setup cancelled.') },
|
|
293
605
|
);
|
|
294
606
|
|
|
295
|
-
|
|
296
|
-
const
|
|
607
|
+
const projectId = supabase.projectId.trim();
|
|
608
|
+
const postgresUrl = supabase.postgresUrl.trim();
|
|
609
|
+
const siteUrl = supabase.siteUrl.trim().replace(/\/+$/, '');
|
|
297
610
|
const supabaseUrl = `https://${projectId}.supabase.co`;
|
|
298
611
|
|
|
299
|
-
|
|
612
|
+
// Extract the database password from the connection string; prompt if it is missing
|
|
613
|
+
// or still the [YOUR-PASSWORD] placeholder.
|
|
300
614
|
let dbPassword = '';
|
|
301
615
|
try {
|
|
302
|
-
|
|
303
|
-
dbPassword = parsedUrl.password;
|
|
616
|
+
dbPassword = decodeURIComponent(new URL(postgresUrl).password);
|
|
304
617
|
} catch {
|
|
305
|
-
//
|
|
618
|
+
// Fall through to the manual prompt below.
|
|
306
619
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
const passwordPrompt = await clack.password({
|
|
620
|
+
if (!dbPassword || /YOUR-PASSWORD/i.test(dbPassword)) {
|
|
621
|
+
const passwordPrompt = await clack.text({
|
|
310
622
|
message:
|
|
311
|
-
'Could not
|
|
312
|
-
validate: (
|
|
623
|
+
'Could not read the DB password from the URI. Enter your Postgres database password:',
|
|
624
|
+
validate: requiredValue('Database password'),
|
|
313
625
|
});
|
|
314
626
|
if (clack.isCancel(passwordPrompt)) {
|
|
315
627
|
handleWizardCancel('Setup cancelled.');
|
|
316
628
|
}
|
|
317
|
-
dbPassword = passwordPrompt;
|
|
629
|
+
dbPassword = passwordPrompt.trim();
|
|
318
630
|
}
|
|
319
631
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
632
|
+
// 2. Cloudflare R2 — required. Powers media uploads, image processing, and backups.
|
|
633
|
+
clack.note('https://dash.cloudflare.com > R2', 'Cloudflare R2 storage');
|
|
634
|
+
const r2 = await clack.group(
|
|
635
|
+
{
|
|
636
|
+
accountId: () =>
|
|
637
|
+
clack.text({
|
|
638
|
+
message: 'R2 Account ID (R2 overview > Account details):',
|
|
639
|
+
validate: requiredValue('R2 Account ID'),
|
|
640
|
+
}),
|
|
641
|
+
bucketName: () =>
|
|
642
|
+
clack.text({
|
|
643
|
+
message: 'R2 Bucket Name:',
|
|
644
|
+
validate: requiredValue('R2 Bucket Name'),
|
|
645
|
+
}),
|
|
646
|
+
publicBaseUrl: () =>
|
|
647
|
+
clack.text({
|
|
648
|
+
message:
|
|
649
|
+
'R2 Public Development URL (Bucket > Settings > Public Development URL, e.g. https://pub-xxxx.r2.dev):',
|
|
650
|
+
validate: requiredValue('R2 Public Development URL'),
|
|
651
|
+
}),
|
|
652
|
+
accessKey: () =>
|
|
653
|
+
clack.text({
|
|
654
|
+
message: 'R2 Access Key ID (R2 > Manage API Tokens):',
|
|
655
|
+
validate: requiredValue('R2 Access Key ID'),
|
|
656
|
+
}),
|
|
657
|
+
secretKey: () =>
|
|
658
|
+
clack.text({
|
|
659
|
+
message:
|
|
660
|
+
'R2 Secret Access Key (shown only once when the token is created):',
|
|
661
|
+
validate: requiredValue('R2 Secret Access Key'),
|
|
662
|
+
}),
|
|
663
|
+
},
|
|
664
|
+
{ onCancel: () => handleWizardCancel('Setup cancelled.') },
|
|
665
|
+
);
|
|
666
|
+
|
|
667
|
+
// 3. SMTP — required. Sends the sign-up confirmation email your first admin needs.
|
|
668
|
+
clack.note('SMTP2GO works very well: https://www.smtp2go.com', 'SMTP email');
|
|
669
|
+
const smtp = await clack.group(
|
|
670
|
+
{
|
|
671
|
+
host: () =>
|
|
672
|
+
clack.text({
|
|
673
|
+
message: 'SMTP Host (e.g. mail.smtp2go.com):',
|
|
674
|
+
validate: requiredValue('SMTP Host'),
|
|
675
|
+
}),
|
|
676
|
+
port: () =>
|
|
677
|
+
clack.text({
|
|
678
|
+
message: 'SMTP Port (465 = SSL, 587 = STARTTLS):',
|
|
679
|
+
initialValue: '465',
|
|
680
|
+
validate: requiredValue('SMTP Port'),
|
|
681
|
+
}),
|
|
682
|
+
user: () =>
|
|
683
|
+
clack.text({
|
|
684
|
+
message: 'SMTP User:',
|
|
685
|
+
validate: requiredValue('SMTP User'),
|
|
686
|
+
}),
|
|
687
|
+
pass: () =>
|
|
688
|
+
clack.text({
|
|
689
|
+
message: 'SMTP Password:',
|
|
690
|
+
validate: requiredValue('SMTP Password'),
|
|
691
|
+
}),
|
|
692
|
+
fromEmail: () =>
|
|
693
|
+
clack.text({
|
|
694
|
+
message: 'From Email (the address confirmation emails are sent from):',
|
|
695
|
+
validate: requiredValue('From Email'),
|
|
696
|
+
}),
|
|
697
|
+
fromName: () =>
|
|
698
|
+
clack.text({
|
|
699
|
+
message: 'From Name (e.g. NextBlock):',
|
|
700
|
+
validate: requiredValue('From Name'),
|
|
701
|
+
}),
|
|
702
|
+
},
|
|
703
|
+
{ onCancel: () => handleWizardCancel('Setup cancelled.') },
|
|
704
|
+
);
|
|
705
|
+
|
|
706
|
+
const smtpValues = {
|
|
707
|
+
host: smtp.host,
|
|
708
|
+
port: smtp.port,
|
|
709
|
+
user: smtp.user,
|
|
710
|
+
pass: smtp.pass,
|
|
711
|
+
fromEmail: smtp.fromEmail,
|
|
712
|
+
fromName: smtp.fromName,
|
|
333
713
|
};
|
|
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
714
|
|
|
349
|
-
|
|
715
|
+
// 4. Write .env.local with everything we collected. Mirror setup.mjs: seed from the
|
|
716
|
+
// template .env.example when present, replace keys line-by-line, append any missing,
|
|
717
|
+
// and reuse already-generated secrets so re-runs are idempotent. .env.local is what
|
|
718
|
+
// `next dev` loads first and is covered by the generated .gitignore.
|
|
719
|
+
clack.note('Writing .env.local...');
|
|
720
|
+
const envPath = resolve(projectPath, '.env.local');
|
|
721
|
+
const envExamplePath = resolve(projectPath, '.env.example');
|
|
722
|
+
let envContent = '';
|
|
350
723
|
if (await fs.pathExists(envPath)) {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
724
|
+
envContent = await fs.readFile(envPath, 'utf8');
|
|
725
|
+
} else if (await fs.pathExists(envExamplePath)) {
|
|
726
|
+
envContent = await fs.readFile(envExamplePath, 'utf8');
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
const cronSecret = readEnvValue(envContent, 'CRON_SECRET=') || generateSecret();
|
|
730
|
+
const draftSecret =
|
|
731
|
+
readEnvValue(envContent, 'DRAFT_MODE_SECRET=') || generateSecret();
|
|
732
|
+
const revalidateSecret =
|
|
733
|
+
readEnvValue(envContent, 'REVALIDATE_SECRET_TOKEN=') || generateSecret();
|
|
734
|
+
|
|
735
|
+
const replacements = {
|
|
736
|
+
'SUPABASE_PROJECT_ID=': `SUPABASE_PROJECT_ID=${projectId}`,
|
|
737
|
+
'POSTGRES_URL=': `POSTGRES_URL=${postgresUrl}`,
|
|
738
|
+
'POSTGRES_PASSWORD=': `POSTGRES_PASSWORD="${dbPassword}"`,
|
|
739
|
+
'NEXT_PUBLIC_SUPABASE_URL=': `NEXT_PUBLIC_SUPABASE_URL=${supabaseUrl}`,
|
|
740
|
+
'NEXT_PUBLIC_SUPABASE_ANON_KEY=': `NEXT_PUBLIC_SUPABASE_ANON_KEY=${supabase.anonKey}`,
|
|
741
|
+
'SUPABASE_SERVICE_ROLE_KEY=': `SUPABASE_SERVICE_ROLE_KEY=${supabase.serviceKey}`,
|
|
742
|
+
'SUPABASE_ACCESS_TOKEN=': `SUPABASE_ACCESS_TOKEN=${supabase.accessToken}`,
|
|
743
|
+
'NEXT_PUBLIC_URL=': `NEXT_PUBLIC_URL=${siteUrl}`,
|
|
744
|
+
'CRON_SECRET=': `CRON_SECRET=${cronSecret}`,
|
|
745
|
+
'DRAFT_MODE_SECRET=': `DRAFT_MODE_SECRET=${draftSecret}`,
|
|
746
|
+
'REVALIDATE_SECRET_TOKEN=': `REVALIDATE_SECRET_TOKEN=${revalidateSecret}`,
|
|
747
|
+
// The R2 public URL is consumed under two names (next/image remotePatterns + CSP, and
|
|
748
|
+
// media URL resolution) — write the same value to both, matching setup.mjs.
|
|
749
|
+
'NEXT_PUBLIC_R2_PUBLIC_URL=': `NEXT_PUBLIC_R2_PUBLIC_URL=${r2.publicBaseUrl}`,
|
|
750
|
+
'NEXT_PUBLIC_R2_BASE_URL=': `NEXT_PUBLIC_R2_BASE_URL=${r2.publicBaseUrl}`,
|
|
751
|
+
'R2_ACCOUNT_ID=': `R2_ACCOUNT_ID=${r2.accountId}`,
|
|
752
|
+
'R2_BUCKET_NAME=': `R2_BUCKET_NAME=${r2.bucketName}`,
|
|
753
|
+
'R2_ACCESS_KEY_ID=': `R2_ACCESS_KEY_ID=${r2.accessKey}`,
|
|
754
|
+
'R2_SECRET_ACCESS_KEY=': `R2_SECRET_ACCESS_KEY=${r2.secretKey}`,
|
|
755
|
+
'SMTP_HOST=': `SMTP_HOST=${smtpValues.host}`,
|
|
756
|
+
'SMTP_PORT=': `SMTP_PORT=${smtpValues.port}`,
|
|
757
|
+
'SMTP_USER=': `SMTP_USER=${smtpValues.user}`,
|
|
758
|
+
'SMTP_PASS=': `SMTP_PASS=${smtpValues.pass}`,
|
|
759
|
+
'SMTP_FROM_EMAIL=': `SMTP_FROM_EMAIL=${smtpValues.fromEmail}`,
|
|
760
|
+
'SMTP_FROM_NAME=': `SMTP_FROM_NAME=${smtpValues.fromName}`,
|
|
761
|
+
'SUPABASE_AUTH_RATE_LIMIT_EMAIL_SENT=':
|
|
762
|
+
'SUPABASE_AUTH_RATE_LIMIT_EMAIL_SENT=30',
|
|
763
|
+
};
|
|
764
|
+
|
|
765
|
+
const appliedKeys = new Set();
|
|
766
|
+
const updatedLines = envContent.split(/\r?\n/).map((line) => {
|
|
767
|
+
for (const [key, value] of Object.entries(replacements)) {
|
|
768
|
+
if (line.startsWith(key)) {
|
|
769
|
+
appliedKeys.add(key);
|
|
770
|
+
return value;
|
|
771
|
+
}
|
|
363
772
|
}
|
|
364
|
-
|
|
773
|
+
return line;
|
|
774
|
+
});
|
|
365
775
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
776
|
+
// Append any keys missing from the seed so nothing is silently dropped.
|
|
777
|
+
for (const [key, value] of Object.entries(replacements)) {
|
|
778
|
+
if (!appliedKeys.has(key)) {
|
|
779
|
+
updatedLines.push(value);
|
|
780
|
+
}
|
|
369
781
|
}
|
|
370
782
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
-
}
|
|
783
|
+
await fs.writeFile(envPath, updatedLines.join('\n'), 'utf8');
|
|
784
|
+
clack.note(
|
|
785
|
+
'Supabase, R2, SMTP, site URL, and generated secrets saved to .env.local',
|
|
786
|
+
);
|
|
385
787
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
);
|
|
390
|
-
} else {
|
|
391
|
-
const supabaseBin = await getSupabaseBinary(projectPath);
|
|
392
|
-
const command = supabaseBin === 'npx' ? 'npx' : supabaseBin;
|
|
788
|
+
// 5. Materialize Supabase assets (migrations, config.toml, branded auth email
|
|
789
|
+
// templates) out of the installed @nextblock-cms/db package.
|
|
790
|
+
await ensureSupabaseAssets(projectPath, { required: true });
|
|
393
791
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
792
|
+
// 6. Link the project and apply the schema. These are the standalone equivalents of the
|
|
793
|
+
// monorepo `npm run db:link` + `npm run db:migrate:fresh` (which do not exist in a
|
|
794
|
+
// generated project): we drive the Supabase CLI directly, authenticating with the
|
|
795
|
+
// access token so no browser login is required.
|
|
796
|
+
const supabaseBin = await getSupabaseBinary(projectPath);
|
|
797
|
+
const command = supabaseBin === 'npx' ? 'npx' : supabaseBin;
|
|
798
|
+
const sbArgs = (args) => (supabaseBin === 'npx' ? ['supabase', ...args] : args);
|
|
799
|
+
const supabaseEnv = {
|
|
800
|
+
...process.env,
|
|
801
|
+
SUPABASE_ACCESS_TOKEN: supabase.accessToken,
|
|
802
|
+
SUPABASE_DB_PASSWORD: dbPassword,
|
|
803
|
+
POSTGRES_URL: postgresUrl,
|
|
804
|
+
// Available for env() substitution in supabase config.toml during `config push`.
|
|
805
|
+
NEXT_PUBLIC_URL: siteUrl,
|
|
806
|
+
};
|
|
399
807
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
808
|
+
const applySchema = await clack.confirm({
|
|
809
|
+
message:
|
|
810
|
+
'Apply the database schema to the linked project now? (Safe for a new database; does not delete existing data.)',
|
|
811
|
+
initialValue: true,
|
|
812
|
+
});
|
|
813
|
+
if (clack.isCancel(applySchema)) {
|
|
814
|
+
handleWizardCancel('Setup cancelled.');
|
|
815
|
+
}
|
|
404
816
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
817
|
+
const dbSpinner = clack.spinner();
|
|
818
|
+
dbSpinner.start('Linking to your Supabase project...');
|
|
819
|
+
try {
|
|
820
|
+
await execa(
|
|
821
|
+
command,
|
|
822
|
+
sbArgs(['link', '--project-ref', projectId, '--password', dbPassword]),
|
|
823
|
+
{ stdio: 'inherit', cwd: projectPath, env: supabaseEnv },
|
|
824
|
+
);
|
|
410
825
|
|
|
411
|
-
|
|
826
|
+
if (applySchema) {
|
|
827
|
+
dbSpinner.message('Pushing database schema...');
|
|
828
|
+
await execa(command, sbArgs(['db', 'push', '--include-all']), {
|
|
412
829
|
stdio: ['pipe', 'inherit', 'inherit'],
|
|
413
830
|
cwd: projectPath,
|
|
414
831
|
input: 'y\n', // Auto-confirm the push prompt
|
|
415
|
-
env:
|
|
416
|
-
...process.env,
|
|
417
|
-
SUPABASE_DB_PASSWORD: dbPassword,
|
|
418
|
-
},
|
|
832
|
+
env: supabaseEnv,
|
|
419
833
|
});
|
|
420
834
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
const configPushArgs =
|
|
424
|
-
supabaseBin === 'npx'
|
|
425
|
-
? ['supabase', 'config', 'push']
|
|
426
|
-
: ['config', 'push'];
|
|
427
|
-
|
|
428
|
-
await execa(command, configPushArgs, {
|
|
835
|
+
dbSpinner.message('Pushing Supabase config (auth settings)...');
|
|
836
|
+
await execa(command, sbArgs(['config', 'push']), {
|
|
429
837
|
stdio: ['pipe', 'inherit', 'inherit'],
|
|
430
838
|
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
|
-
},
|
|
839
|
+
env: supabaseEnv,
|
|
437
840
|
});
|
|
438
841
|
|
|
439
|
-
|
|
842
|
+
dbSpinner.stop('Database schema and config applied.');
|
|
843
|
+
} else {
|
|
844
|
+
dbSpinner.stop(
|
|
845
|
+
'Linked. Skipped schema push — run `npx supabase db push --include-all` when ready.',
|
|
846
|
+
);
|
|
440
847
|
}
|
|
441
848
|
} catch (error) {
|
|
442
|
-
|
|
443
|
-
'Database
|
|
849
|
+
dbSpinner.stop(
|
|
850
|
+
'Database setup failed. You can run `npx supabase db push --include-all` manually.',
|
|
444
851
|
);
|
|
445
852
|
if (error instanceof Error) {
|
|
446
853
|
clack.note(error.message);
|
|
447
854
|
}
|
|
448
855
|
}
|
|
449
856
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
857
|
+
// 7. Sync hosted Supabase Auth: custom SMTP + branded email templates. SMTP and the
|
|
858
|
+
// access token are required, so this always runs (matching setup.mjs). This is what
|
|
859
|
+
// lets Supabase email your first admin their confirmation link.
|
|
860
|
+
await enableSupabaseSmtpConfig(projectPath);
|
|
861
|
+
await configureHostedSupabaseAuth(projectPath, {
|
|
862
|
+
projectId,
|
|
863
|
+
siteUrl,
|
|
864
|
+
accessToken: supabase.accessToken,
|
|
865
|
+
smtpValues,
|
|
456
866
|
});
|
|
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
867
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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)',
|
|
868
|
+
// 8. Optional premium modules (CLI-specific; requires a license + registry access).
|
|
869
|
+
const setupPremium = await clack.confirm({
|
|
870
|
+
message: 'Do you have a license and want to install premium modules now?',
|
|
871
|
+
initialValue: false,
|
|
542
872
|
});
|
|
543
|
-
if (clack.isCancel(
|
|
873
|
+
if (clack.isCancel(setupPremium)) {
|
|
544
874
|
handleWizardCancel('Setup cancelled.');
|
|
545
875
|
}
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
pass: '',
|
|
552
|
-
fromEmail: '',
|
|
553
|
-
fromName: '',
|
|
554
|
-
};
|
|
555
|
-
|
|
556
|
-
if (setupSMTP) {
|
|
557
|
-
const smtpKeys = await clack.group(
|
|
876
|
+
if (setupPremium) {
|
|
877
|
+
clack.note('Installing @nextblock-cms/ecommerce...');
|
|
878
|
+
await execa(
|
|
879
|
+
'npm',
|
|
880
|
+
['install', '@nextblock-cms/ecommerce@npm:@nextblock-cms/ecom@latest'],
|
|
558
881
|
{
|
|
559
|
-
|
|
560
|
-
|
|
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
|
-
}),
|
|
882
|
+
cwd: projectPath,
|
|
883
|
+
stdio: 'inherit',
|
|
589
884
|
},
|
|
590
|
-
{ onCancel: () => handleWizardCancel('Setup cancelled.') },
|
|
591
885
|
);
|
|
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
|
-
};
|
|
886
|
+
clack.note('Premium module installed!');
|
|
601
887
|
}
|
|
602
888
|
|
|
603
|
-
clack.
|
|
604
|
-
|
|
889
|
+
clack.outro(
|
|
890
|
+
[
|
|
891
|
+
`🎉 Your NextBlock™ project ${projectName ? `"${projectName}" ` : ''}is ready!`,
|
|
892
|
+
'',
|
|
893
|
+
'Next steps:',
|
|
894
|
+
` 1. Start the app: cd ${projectName} && npm run dev → ${siteUrl}`,
|
|
895
|
+
` 2. Create your account: open ${siteUrl}/sign-up`,
|
|
896
|
+
' The FIRST account to sign up automatically becomes the ADMIN.',
|
|
897
|
+
' 3. Confirm your email: click the link sent to your inbox',
|
|
898
|
+
` 4. Sign in — you'll land in the CMS at ${siteUrl}/cms/dashboard`,
|
|
899
|
+
].join('\n'),
|
|
605
900
|
);
|
|
606
|
-
|
|
607
|
-
message:
|
|
608
|
-
'Do you have a GitHub Personal Access Token (PAT) for premium modules?',
|
|
609
|
-
initialValue: false,
|
|
610
|
-
});
|
|
901
|
+
}
|
|
611
902
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
}
|
|
903
|
+
async function enableSupabaseSmtpConfig(projectDir) {
|
|
904
|
+
const configPath = resolve(projectDir, 'supabase', 'config.toml');
|
|
615
905
|
|
|
616
|
-
if (
|
|
617
|
-
|
|
618
|
-
|
|
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
|
-
});
|
|
906
|
+
if (!(await fs.pathExists(configPath))) {
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
627
909
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
910
|
+
const smtpBlock = `# [auth.email.smtp]
|
|
911
|
+
# host = "env(SMTP_HOST)"
|
|
912
|
+
# port = 587
|
|
913
|
+
# user = "env(SMTP_USER)"
|
|
914
|
+
# pass = "env(SMTP_PASS)"
|
|
915
|
+
# admin_email = "env(SMTP_FROM_EMAIL)"
|
|
916
|
+
# sender_name = "env(SMTP_FROM_NAME)"`;
|
|
631
917
|
|
|
632
|
-
|
|
918
|
+
const enabledSmtpBlock = `[auth.email.smtp]
|
|
919
|
+
host = "env(SMTP_HOST)"
|
|
920
|
+
port = 587
|
|
921
|
+
user = "env(SMTP_USER)"
|
|
922
|
+
pass = "env(SMTP_PASS)"
|
|
923
|
+
admin_email = "env(SMTP_FROM_EMAIL)"
|
|
924
|
+
sender_name = "env(SMTP_FROM_NAME)"`;
|
|
633
925
|
|
|
634
|
-
|
|
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');
|
|
926
|
+
const configContents = await fs.readFile(configPath, 'utf8');
|
|
641
927
|
|
|
642
|
-
|
|
643
|
-
|
|
928
|
+
if (configContents.includes(enabledSmtpBlock)) {
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
644
931
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
clack.note('Premium module installed!');
|
|
932
|
+
if (!configContents.includes(smtpBlock)) {
|
|
933
|
+
throw new Error(
|
|
934
|
+
`Could not find the SMTP placeholder block in ${configPath}.`,
|
|
935
|
+
);
|
|
650
936
|
}
|
|
651
937
|
|
|
652
|
-
await
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
if (
|
|
664
|
-
clack.note('SMTP configuration saved!');
|
|
665
|
-
} else if (canWriteEnv) {
|
|
938
|
+
await fs.writeFile(
|
|
939
|
+
configPath,
|
|
940
|
+
configContents.replace(smtpBlock, enabledSmtpBlock),
|
|
941
|
+
'utf8',
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
async function configureHostedSupabaseAuth(
|
|
946
|
+
projectDir,
|
|
947
|
+
{ projectId, siteUrl, accessToken, smtpValues },
|
|
948
|
+
) {
|
|
949
|
+
if (!projectId || !siteUrl || !accessToken) {
|
|
666
950
|
clack.note(
|
|
667
|
-
'
|
|
951
|
+
'Skipped hosted Supabase Auth sync because the project ref, site URL, or access token is missing.',
|
|
668
952
|
);
|
|
953
|
+
return;
|
|
669
954
|
}
|
|
670
955
|
|
|
671
|
-
clack.
|
|
672
|
-
|
|
673
|
-
|
|
956
|
+
const spinner = clack.spinner();
|
|
957
|
+
spinner.start('Syncing hosted Supabase Auth SMTP and branded email templates...');
|
|
958
|
+
|
|
959
|
+
try {
|
|
960
|
+
await execa('node', ['tools/configure-supabase-auth.js'], {
|
|
961
|
+
cwd: projectDir,
|
|
962
|
+
env: {
|
|
963
|
+
...process.env,
|
|
964
|
+
SUPABASE_PROJECT_ID: projectId,
|
|
965
|
+
NEXT_PUBLIC_URL: siteUrl,
|
|
966
|
+
SUPABASE_ACCESS_TOKEN: accessToken,
|
|
967
|
+
SMTP_HOST: smtpValues.host,
|
|
968
|
+
SMTP_PORT: smtpValues.port,
|
|
969
|
+
SMTP_USER: smtpValues.user,
|
|
970
|
+
SMTP_PASS: smtpValues.pass,
|
|
971
|
+
SMTP_FROM_EMAIL: smtpValues.fromEmail,
|
|
972
|
+
SMTP_FROM_NAME: smtpValues.fromName,
|
|
973
|
+
SUPABASE_AUTH_RATE_LIMIT_EMAIL_SENT:
|
|
974
|
+
process.env.SUPABASE_AUTH_RATE_LIMIT_EMAIL_SENT || '30',
|
|
975
|
+
},
|
|
976
|
+
});
|
|
977
|
+
spinner.stop('Hosted Supabase Auth configured.');
|
|
978
|
+
} catch (error) {
|
|
979
|
+
spinner.stop(
|
|
980
|
+
'Hosted Supabase Auth sync skipped. You can rerun it later with npm run configure:supabase-auth.',
|
|
981
|
+
);
|
|
982
|
+
clack.note(
|
|
983
|
+
error instanceof Error ? error.message : String(error),
|
|
984
|
+
'Supabase Auth Sync',
|
|
985
|
+
);
|
|
986
|
+
}
|
|
674
987
|
}
|
|
675
988
|
|
|
676
989
|
function handleWizardCancel(message) {
|
|
@@ -875,23 +1188,30 @@ async function ensureEnvExample(projectDir) {
|
|
|
875
1188
|
}
|
|
876
1189
|
}
|
|
877
1190
|
|
|
878
|
-
const placeholder = `# Environment variables for NextBlock CMS
|
|
1191
|
+
const placeholder = `# Environment variables for NextBlock™ CMS
|
|
879
1192
|
NEXT_PUBLIC_URL=
|
|
880
|
-
|
|
1193
|
+
|
|
1194
|
+
# Supabase — the setup wizard fills this whole block.
|
|
881
1195
|
SUPABASE_PROJECT_ID=
|
|
882
1196
|
POSTGRES_URL=
|
|
1197
|
+
POSTGRES_PASSWORD=
|
|
883
1198
|
NEXT_PUBLIC_SUPABASE_URL=
|
|
884
1199
|
NEXT_PUBLIC_SUPABASE_ANON_KEY=
|
|
885
1200
|
SUPABASE_SERVICE_ROLE_KEY=
|
|
1201
|
+
SUPABASE_ACCESS_TOKEN=
|
|
1202
|
+
|
|
1203
|
+
# Auto-generated by the setup wizard.
|
|
1204
|
+
CRON_SECRET=
|
|
1205
|
+
DRAFT_MODE_SECRET=
|
|
1206
|
+
REVALIDATE_SECRET_TOKEN=
|
|
886
1207
|
|
|
887
|
-
# Cloudflare
|
|
1208
|
+
# Cloudflare R2 — setup writes the public URL to both keys.
|
|
1209
|
+
NEXT_PUBLIC_R2_PUBLIC_URL=
|
|
888
1210
|
NEXT_PUBLIC_R2_BASE_URL=
|
|
1211
|
+
R2_ACCOUNT_ID=
|
|
1212
|
+
R2_BUCKET_NAME=
|
|
889
1213
|
R2_ACCESS_KEY_ID=
|
|
890
1214
|
R2_SECRET_ACCESS_KEY=
|
|
891
|
-
R2_BUCKET_NAME=
|
|
892
|
-
R2_ACCOUNT_ID=
|
|
893
|
-
|
|
894
|
-
REVALIDATE_SECRET_TOKEN=
|
|
895
1215
|
|
|
896
1216
|
# Email SMTP Configuration
|
|
897
1217
|
SMTP_HOST=
|
|
@@ -900,6 +1220,7 @@ SMTP_USER=
|
|
|
900
1220
|
SMTP_PASS=
|
|
901
1221
|
SMTP_FROM_EMAIL=
|
|
902
1222
|
SMTP_FROM_NAME=
|
|
1223
|
+
SUPABASE_AUTH_RATE_LIMIT_EMAIL_SENT=30
|
|
903
1224
|
`;
|
|
904
1225
|
|
|
905
1226
|
await fs.writeFile(destination, placeholder);
|
|
@@ -949,6 +1270,19 @@ async function ensureSupabaseAssets(projectDir, options = {}) {
|
|
|
949
1270
|
migrationsCopied = true;
|
|
950
1271
|
}
|
|
951
1272
|
|
|
1273
|
+
// Branded Auth email templates. configure-supabase-auth.js resolves the supabase dir by
|
|
1274
|
+
// requiring a templates/ subdir, and uploads these via the Management API. Without them
|
|
1275
|
+
// the hosted-auth + SMTP sync silently skips and the first admin never gets a
|
|
1276
|
+
// confirmation email — so copy them alongside config.toml + migrations.
|
|
1277
|
+
const sourceTemplates = resolve(packageSupabaseDir, 'templates');
|
|
1278
|
+
const destTemplates = resolve(destSupabaseDir, 'templates');
|
|
1279
|
+
if (await fs.pathExists(sourceTemplates)) {
|
|
1280
|
+
await fs.copy(sourceTemplates, destTemplates, {
|
|
1281
|
+
overwrite: true,
|
|
1282
|
+
errorOnExist: false,
|
|
1283
|
+
});
|
|
1284
|
+
}
|
|
1285
|
+
|
|
952
1286
|
if (required) {
|
|
953
1287
|
if (!configCopied) {
|
|
954
1288
|
throw new Error(
|
|
@@ -1027,32 +1361,6 @@ async function resolvePackageSupabaseDir(projectDir) {
|
|
|
1027
1361
|
return { dir: null, triedPaths };
|
|
1028
1362
|
}
|
|
1029
1363
|
|
|
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
1364
|
async function ensureClientComponents(projectDir) {
|
|
1057
1365
|
const relativePaths = [
|
|
1058
1366
|
'components/env-var-warning.tsx',
|
|
@@ -1237,7 +1545,6 @@ async function sanitizeLayout(projectDir) {
|
|
|
1237
1545
|
|
|
1238
1546
|
const requiredImports = [
|
|
1239
1547
|
"import '@nextblock-cms/ui/styles/globals.css';",
|
|
1240
|
-
"import '@nextblock-cms/editor/styles/editor.css';",
|
|
1241
1548
|
];
|
|
1242
1549
|
|
|
1243
1550
|
const content = await fs.readFile(layoutPath, 'utf8');
|
|
@@ -1508,6 +1815,32 @@ async function transformPackageJson(projectDir) {
|
|
|
1508
1815
|
}
|
|
1509
1816
|
}
|
|
1510
1817
|
|
|
1818
|
+
// Mirror the monorepo's defensive dependency overrides into the generated project so a
|
|
1819
|
+
// fresh `npm install` reproduces the "0 vulnerabilities" posture and silences deprecated
|
|
1820
|
+
// transitive deps (e.g. uuid@10). Read live from the repo root when available (local dev /
|
|
1821
|
+
// `npm run test-create`); fall back to this baked-in set in the published CLI where the
|
|
1822
|
+
// monorepo root is not on disk. Keep the fallback in sync with the root package.json.
|
|
1823
|
+
const FALLBACK_OVERRIDES = {
|
|
1824
|
+
postcss: '^8.5.12',
|
|
1825
|
+
qs: '^6.15.2',
|
|
1826
|
+
uuid: '^11.1.1',
|
|
1827
|
+
glob: '^10.4.5',
|
|
1828
|
+
'whatwg-encoding': 'npm:@exodus/bytes@latest',
|
|
1829
|
+
'node-domexception': 'npm:domexception@latest',
|
|
1830
|
+
keygrip: 'npm:keygrip@latest',
|
|
1831
|
+
};
|
|
1832
|
+
let rootOverrides = FALLBACK_OVERRIDES;
|
|
1833
|
+
try {
|
|
1834
|
+
const rootPkg = await fs.readJSON(resolve(REPO_ROOT, 'package.json'));
|
|
1835
|
+
if (rootPkg?.overrides && Object.keys(rootPkg.overrides).length > 0) {
|
|
1836
|
+
rootOverrides = rootPkg.overrides;
|
|
1837
|
+
}
|
|
1838
|
+
} catch {
|
|
1839
|
+
// Published CLI: repo root package.json not present — keep the baked-in fallback.
|
|
1840
|
+
}
|
|
1841
|
+
// Project-specific overrides (if any) win over the inherited defaults.
|
|
1842
|
+
packageJson.overrides = { ...rootOverrides, ...(packageJson.overrides ?? {}) };
|
|
1843
|
+
|
|
1511
1844
|
await fs.writeJSON(packageJsonPath, packageJson, { spaces: 2 });
|
|
1512
1845
|
}
|
|
1513
1846
|
|
|
@@ -1559,35 +1892,6 @@ function runCommand(command, args, options = {}) {
|
|
|
1559
1892
|
});
|
|
1560
1893
|
}
|
|
1561
1894
|
|
|
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
1895
|
async function getSupabaseBinary(projectDir) {
|
|
1592
1896
|
const binDir = resolve(projectDir, 'node_modules', '.bin');
|
|
1593
1897
|
const ext = IS_WINDOWS ? '.cmd' : '';
|
|
@@ -1643,18 +1947,7 @@ function buildNextConfigContent(editorUtilNames) {
|
|
|
1643
1947
|
' minimumCacheTTL: 31536000,',
|
|
1644
1948
|
' dangerouslyAllowSVG: false,',
|
|
1645
1949
|
" 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
|
-
' ],',
|
|
1950
|
+
' remotePatterns: getRemotePatterns(),',
|
|
1658
1951
|
' },',
|
|
1659
1952
|
' experimental: {',
|
|
1660
1953
|
' optimizeCss: true,',
|
|
@@ -1736,6 +2029,34 @@ function buildNextConfigContent(editorUtilNames) {
|
|
|
1736
2029
|
'};',
|
|
1737
2030
|
'',
|
|
1738
2031
|
'module.exports = nextConfig;',
|
|
2032
|
+
'',
|
|
2033
|
+
'function getRemotePatterns() {',
|
|
2034
|
+
' /** @type {Array<{ protocol: "http" | "https", hostname: string, pathname: string }>} */',
|
|
2035
|
+
' const patterns = [];',
|
|
2036
|
+
' // Whitelist this project R2 public/base URLs and the site URL for next/image.',
|
|
2037
|
+
' const sources = [',
|
|
2038
|
+
' process.env.NEXT_PUBLIC_R2_PUBLIC_URL,',
|
|
2039
|
+
' process.env.NEXT_PUBLIC_R2_BASE_URL,',
|
|
2040
|
+
' process.env.NEXT_PUBLIC_URL,',
|
|
2041
|
+
' ];',
|
|
2042
|
+
' for (const value of sources) {',
|
|
2043
|
+
' if (!value) continue;',
|
|
2044
|
+
' try {',
|
|
2045
|
+
' const parsed = new URL(value);',
|
|
2046
|
+
" if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') continue;",
|
|
2047
|
+
' const hostname = parsed.hostname;',
|
|
2048
|
+
' if (patterns.some((pattern) => pattern.hostname === hostname)) continue;',
|
|
2049
|
+
' patterns.push({',
|
|
2050
|
+
" protocol: parsed.protocol === 'https:' ? 'https' : 'http',",
|
|
2051
|
+
' hostname,',
|
|
2052
|
+
" pathname: '/**',",
|
|
2053
|
+
' });',
|
|
2054
|
+
' } catch {',
|
|
2055
|
+
' // ignore malformed value',
|
|
2056
|
+
' }',
|
|
2057
|
+
' }',
|
|
2058
|
+
' return patterns;',
|
|
2059
|
+
'}',
|
|
1739
2060
|
);
|
|
1740
2061
|
|
|
1741
2062
|
return lines.join('\n');
|