create-nextblock 0.2.77 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/create-nextblock.js +740 -459
- 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 +191 -151
- 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 -116
- 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);
|
|
@@ -52,17 +52,20 @@ const PACKAGE_VERSION_SOURCES = {
|
|
|
52
52
|
'@nextblock-cms/sdk': resolve(REPO_ROOT, 'libs/sdk/package.json'),
|
|
53
53
|
};
|
|
54
54
|
|
|
55
|
+
program.name('create-nextblock').description('NextBlock™ CMS CLI');
|
|
56
|
+
|
|
55
57
|
program
|
|
56
|
-
.
|
|
57
|
-
.description('Bootstrap a NextBlock CMS project')
|
|
58
|
-
.argument(
|
|
59
|
-
'[project-directory]',
|
|
60
|
-
'The name of the project directory to create',
|
|
61
|
-
)
|
|
58
|
+
.command('create [project-directory]', { isDefault: true })
|
|
59
|
+
.description('Bootstrap a NextBlock™ CMS project')
|
|
62
60
|
.option('--skip-install', 'Skip installing dependencies')
|
|
63
61
|
.option('-y, --yes', 'Skip all interactive prompts and use defaults')
|
|
64
62
|
.action(handleCommand);
|
|
65
63
|
|
|
64
|
+
program
|
|
65
|
+
.command('activate [module]')
|
|
66
|
+
.description('Activate a premium NextBlock™ CMS module')
|
|
67
|
+
.action(handleActivateCommand);
|
|
68
|
+
|
|
66
69
|
await program.parseAsync(process.argv).catch((error) => {
|
|
67
70
|
console.error(
|
|
68
71
|
chalk.red(error instanceof Error ? error.message : String(error)),
|
|
@@ -180,7 +183,7 @@ async function handleCommand(projectDirectory, options) {
|
|
|
180
183
|
|
|
181
184
|
console.log(
|
|
182
185
|
chalk.green(
|
|
183
|
-
`\nSuccess! Your NextBlock CMS project "${projectName}" is ready.\n`,
|
|
186
|
+
`\nSuccess! Your NextBlock™ CMS project "${projectName}" is ready.\n`,
|
|
184
187
|
),
|
|
185
188
|
);
|
|
186
189
|
console.log(chalk.cyan('Next step:'));
|
|
@@ -195,482 +198,778 @@ async function handleCommand(projectDirectory, options) {
|
|
|
195
198
|
}
|
|
196
199
|
}
|
|
197
200
|
|
|
198
|
-
async function
|
|
199
|
-
|
|
200
|
-
|
|
201
|
+
async function handleActivateCommand(moduleName) {
|
|
202
|
+
if (!moduleName || moduleName !== 'ecommerce') {
|
|
203
|
+
console.error(
|
|
204
|
+
chalk.red('Invalid module name. Supported modules: ecommerce'),
|
|
205
|
+
);
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
201
208
|
|
|
202
|
-
clack.intro(
|
|
209
|
+
clack.intro(`🚀 Activating NextBlock™ module: ${moduleName}`);
|
|
203
210
|
|
|
204
|
-
const
|
|
205
|
-
await fs.ensureDir(supabaseDir);
|
|
206
|
-
await resetSupabaseProjectRef(projectPath);
|
|
211
|
+
const projectPath = process.cwd();
|
|
207
212
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
'
|
|
213
|
+
// 1. Install NPM package
|
|
214
|
+
clack.note(`Installing @nextblock-cms/${moduleName}...`);
|
|
215
|
+
|
|
216
|
+
await execa(
|
|
217
|
+
'npm',
|
|
218
|
+
['install', `@nextblock-cms/ecommerce@npm:@nextblock-cms/ecom@latest`],
|
|
219
|
+
{ cwd: projectPath, stdio: 'inherit' },
|
|
213
220
|
);
|
|
221
|
+
clack.note('NPM package installed!');
|
|
222
|
+
|
|
223
|
+
// 2. Inject Route Wrappers
|
|
224
|
+
clack.note('Injecting route wrappers...');
|
|
225
|
+
|
|
226
|
+
const routesToInject = {
|
|
227
|
+
'app/cms/orders/page.tsx': `import { OrdersPage as OrdersPageUI } from '@nextblock-cms/ecommerce';
|
|
228
|
+
import { verifyPackageOnline } from '@nextblock-cms/db/server';
|
|
229
|
+
import { redirect } from 'next/navigation';
|
|
230
|
+
|
|
231
|
+
export default async function OrdersPage() {
|
|
232
|
+
const isOnline = await verifyPackageOnline('ecommerce');
|
|
233
|
+
if (!isOnline) {
|
|
234
|
+
redirect('/cms/settings/packages');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return <OrdersPageUI />;
|
|
238
|
+
}`,
|
|
239
|
+
'app/cms/orders/[id]/page.tsx': `import { OrderDetailPage as OrderDetailPageUI } from '@nextblock-cms/ecommerce';
|
|
240
|
+
import { verifyPackageOnline } from '@nextblock-cms/db/server';
|
|
241
|
+
import { redirect } from 'next/navigation';
|
|
242
|
+
|
|
243
|
+
export default async function OrderDetailPage({
|
|
244
|
+
params,
|
|
245
|
+
}: {
|
|
246
|
+
params: Promise<{ id: string }>;
|
|
247
|
+
}) {
|
|
248
|
+
const isOnline = await verifyPackageOnline('ecommerce');
|
|
249
|
+
if (!isOnline) {
|
|
250
|
+
redirect('/cms/settings/packages');
|
|
251
|
+
}
|
|
252
|
+
const resolvedParams = await params;
|
|
253
|
+
return <OrderDetailPageUI params={resolvedParams} />;
|
|
254
|
+
}`,
|
|
255
|
+
'app/cms/products/page.tsx': `import { ProductsPage as ProductsPageUI } from '@nextblock-cms/ecommerce';
|
|
256
|
+
import { verifyPackageOnline } from '@nextblock-cms/db/server';
|
|
257
|
+
import { redirect } from 'next/navigation';
|
|
258
|
+
|
|
259
|
+
export default async function ProductsPage() {
|
|
260
|
+
const isOnline = await verifyPackageOnline('ecommerce');
|
|
261
|
+
if (!isOnline) {
|
|
262
|
+
redirect('/cms/settings/packages');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return <ProductsPageUI />;
|
|
266
|
+
}`,
|
|
267
|
+
'app/cms/products/new/page.tsx': `import { NewProductPage as NewProductPageUI } from '@nextblock-cms/ecommerce';
|
|
268
|
+
import { verifyPackageOnline } from '@nextblock-cms/db/server';
|
|
269
|
+
import { redirect } from 'next/navigation';
|
|
214
270
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
271
|
+
export default async function NewProductPage() {
|
|
272
|
+
const isOnline = await verifyPackageOnline('ecommerce');
|
|
273
|
+
if (!isOnline) {
|
|
274
|
+
redirect('/cms/settings/packages');
|
|
275
|
+
}
|
|
218
276
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
277
|
+
return <NewProductPageUI />;
|
|
278
|
+
}`,
|
|
279
|
+
'app/cms/products/[id]/edit/page.tsx': `import { EditProductPage as EditProductPageUI } from '@nextblock-cms/ecommerce';
|
|
280
|
+
import { verifyPackageOnline } from '@nextblock-cms/db/server';
|
|
281
|
+
import { redirect } from 'next/navigation';
|
|
282
|
+
|
|
283
|
+
export default async function EditProductPage({ params }: { params: Promise<{ id: string }> }) {
|
|
284
|
+
const isOnline = await verifyPackageOnline('ecommerce');
|
|
285
|
+
if (!isOnline) {
|
|
286
|
+
redirect('/cms/settings/packages');
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const resolvedParams = await params;
|
|
290
|
+
return <EditProductPageUI params={resolvedParams} />;
|
|
291
|
+
}`,
|
|
292
|
+
'app/cms/payments/page.tsx': `import { PaymentsPage as PaymentsPageUI } from '@nextblock-cms/ecommerce';
|
|
293
|
+
import { verifyPackageOnline } from '@nextblock-cms/db/server';
|
|
294
|
+
import { redirect } from 'next/navigation';
|
|
295
|
+
|
|
296
|
+
export default async function PaymentsPage() {
|
|
297
|
+
const isOnline = await verifyPackageOnline('ecommerce');
|
|
298
|
+
if (!isOnline) {
|
|
299
|
+
redirect('/cms/settings/packages');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return <PaymentsPageUI />;
|
|
303
|
+
}`,
|
|
304
|
+
'app/cms/coupons/page.tsx': `import { CouponsPage as CouponsPageUI } from '@nextblock-cms/ecommerce/server';
|
|
305
|
+
import { verifyPackageOnline } from '@nextblock-cms/db/server';
|
|
306
|
+
import { redirect } from 'next/navigation';
|
|
307
|
+
|
|
308
|
+
export default async function CouponsPage({
|
|
309
|
+
searchParams,
|
|
310
|
+
}: {
|
|
311
|
+
searchParams: Promise<{ status?: string; q?: string }>;
|
|
312
|
+
}) {
|
|
313
|
+
const isOnline = await verifyPackageOnline('ecommerce');
|
|
314
|
+
if (!isOnline) {
|
|
315
|
+
redirect('/cms/settings/packages');
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return <CouponsPageUI searchParams={await searchParams} />;
|
|
319
|
+
}`,
|
|
320
|
+
'app/cms/coupons/[id]/edit/page.tsx': `import { EditCouponPage as EditCouponPageUI } from '@nextblock-cms/ecommerce/server';
|
|
321
|
+
import { verifyPackageOnline } from '@nextblock-cms/db/server';
|
|
322
|
+
import { redirect } from 'next/navigation';
|
|
323
|
+
|
|
324
|
+
export default async function EditCouponPage({
|
|
325
|
+
params,
|
|
326
|
+
}: {
|
|
327
|
+
params: Promise<{ id: string }>;
|
|
328
|
+
}) {
|
|
329
|
+
const isOnline = await verifyPackageOnline('ecommerce');
|
|
330
|
+
if (!isOnline) {
|
|
331
|
+
redirect('/cms/settings/packages');
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return <EditCouponPageUI params={params} />;
|
|
335
|
+
}`,
|
|
336
|
+
'app/checkout/success/page.tsx': `import { CheckoutSuccessPage as CheckoutSuccessPageUI } from '@nextblock-cms/ecommerce';
|
|
337
|
+
import { verifyPackageOnline } from '@nextblock-cms/db/server';
|
|
338
|
+
import { notFound } from 'next/navigation';
|
|
339
|
+
|
|
340
|
+
export default async function CheckoutSuccessPage() {
|
|
341
|
+
const isOnline = await verifyPackageOnline('ecommerce');
|
|
342
|
+
if (!isOnline) {
|
|
343
|
+
notFound();
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return <CheckoutSuccessPageUI />;
|
|
347
|
+
}`,
|
|
348
|
+
'app/api/checkout/route.ts': `import { NextResponse } from 'next/server';
|
|
349
|
+
import { getPaymentProvider } from '@nextblock-cms/ecommerce/server';
|
|
350
|
+
import { createClient, verifyPackageOnline } from '@nextblock-cms/db/server';
|
|
351
|
+
import { normalizeCustomerAddress } from '@nextblock-cms/ecommerce';
|
|
352
|
+
|
|
353
|
+
function resolveProviderFromItem(item) {
|
|
354
|
+
if (item?.provider === 'stripe' || item?.provider === 'freemius') {
|
|
355
|
+
return item.provider;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (item?.payment_provider === 'stripe' || item?.payment_provider === 'freemius') {
|
|
359
|
+
return item.payment_provider;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (item?.product_type === 'digital') {
|
|
363
|
+
return 'freemius';
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (item?.product_type === 'physical') {
|
|
367
|
+
return 'stripe';
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (item?.freemius_product_id) {
|
|
371
|
+
return 'freemius';
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export async function POST(req: Request) {
|
|
378
|
+
try {
|
|
379
|
+
const isOnline = await verifyPackageOnline('ecommerce');
|
|
380
|
+
if (!isOnline) {
|
|
381
|
+
return NextResponse.json({ error: 'Ecommerce module license is inactive' }, { status: 403 });
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const {
|
|
385
|
+
items,
|
|
386
|
+
customerEmail,
|
|
387
|
+
customerPhone,
|
|
388
|
+
billingAddress,
|
|
389
|
+
shippingAddress,
|
|
390
|
+
shippingMethodId,
|
|
391
|
+
currencyCode,
|
|
392
|
+
locale,
|
|
393
|
+
couponCode,
|
|
394
|
+
couponContextItems,
|
|
395
|
+
} = await req.json();
|
|
396
|
+
|
|
397
|
+
if (!items || !Array.isArray(items)) {
|
|
398
|
+
return NextResponse.json({ error: 'Invalid items data' }, { status: 400 });
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const providerNames = Array.from(
|
|
402
|
+
new Set(items.map((item) => resolveProviderFromItem(item)).filter(Boolean))
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
if (providerNames.length === 0) {
|
|
406
|
+
return NextResponse.json(
|
|
407
|
+
{ error: 'Each checkout request must include provider-aware cart items.' },
|
|
408
|
+
{ status: 400 }
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (providerNames.length > 1) {
|
|
413
|
+
return NextResponse.json(
|
|
414
|
+
{ error: 'Mixed-provider carts must be checked out in separate steps.' },
|
|
415
|
+
{ status: 400 }
|
|
416
|
+
);
|
|
226
417
|
}
|
|
227
|
-
|
|
228
|
-
|
|
418
|
+
|
|
419
|
+
const providerName = providerNames[0];
|
|
420
|
+
|
|
421
|
+
if (providerName === 'freemius' && items.length !== 1) {
|
|
422
|
+
return NextResponse.json(
|
|
423
|
+
{ error: 'Freemius items must be checked out one at a time.' },
|
|
424
|
+
{ status: 400 }
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (!billingAddress) {
|
|
429
|
+
return NextResponse.json({ error: 'Billing address is required' }, { status: 400 });
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const supabase = createClient();
|
|
433
|
+
const provider = getPaymentProvider(providerName);
|
|
434
|
+
|
|
435
|
+
const { data: { user } } = await supabase.auth.getUser();
|
|
436
|
+
const userId = user?.id;
|
|
437
|
+
const resolvedCustomerEmail = user?.email || customerEmail || null;
|
|
438
|
+
|
|
439
|
+
const { url, error, errorKey, errorParams, errorStatus, customProps } =
|
|
440
|
+
await provider.createCheckoutSession({
|
|
441
|
+
items,
|
|
442
|
+
customerEmail: resolvedCustomerEmail,
|
|
443
|
+
customerPhone,
|
|
444
|
+
userId,
|
|
445
|
+
billingAddress: normalizeCustomerAddress(billingAddress) ?? billingAddress,
|
|
446
|
+
shippingAddress:
|
|
447
|
+
providerName === 'stripe'
|
|
448
|
+
? normalizeCustomerAddress(shippingAddress)
|
|
449
|
+
: null,
|
|
450
|
+
shippingMethodId: providerName === 'stripe' ? shippingMethodId : null,
|
|
451
|
+
currencyCode: typeof currencyCode === 'string' ? currencyCode : null,
|
|
452
|
+
locale: typeof locale === 'string' ? locale : null,
|
|
453
|
+
couponCode: typeof couponCode === 'string' ? couponCode : null,
|
|
454
|
+
couponContextItems: Array.isArray(couponContextItems) ? couponContextItems : items,
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
if (error) {
|
|
458
|
+
console.error('Checkout Error:', error);
|
|
459
|
+
return NextResponse.json(
|
|
460
|
+
{ error, errorKey, errorParams },
|
|
461
|
+
{ status: errorStatus ?? 500 }
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return NextResponse.json({ url, customProps });
|
|
466
|
+
} catch (err: any) {
|
|
467
|
+
console.error('Checkout API Error:', err);
|
|
468
|
+
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
|
|
229
469
|
}
|
|
470
|
+
}`,
|
|
471
|
+
};
|
|
230
472
|
|
|
231
|
-
|
|
473
|
+
for (const [routePath, content] of Object.entries(routesToInject)) {
|
|
474
|
+
const fullPath = resolve(projectPath, routePath);
|
|
475
|
+
await fs.ensureDir(dirname(fullPath));
|
|
476
|
+
await fs.writeFile(fullPath, content);
|
|
477
|
+
}
|
|
232
478
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
479
|
+
clack.outro(
|
|
480
|
+
'✅ Ecommerce module activated successfully! You can now use the storefront features.',
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// clack validator that rejects empty/whitespace-only input with a labelled message.
|
|
485
|
+
function requiredValue(label) {
|
|
486
|
+
return (value) =>
|
|
487
|
+
value && String(value).trim() ? undefined : `${label} is required`;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Read the current value of a `KEY=` line from an .env file body (handles quotes),
|
|
491
|
+
// so re-runs can reuse already-generated secrets instead of regenerating them.
|
|
492
|
+
function readEnvValue(envContent, key) {
|
|
493
|
+
for (const line of envContent.split(/\r?\n/)) {
|
|
494
|
+
if (line.startsWith(key)) {
|
|
495
|
+
return line.slice(key.length).trim().replace(/^"(.*)"$/, '$1');
|
|
242
496
|
}
|
|
243
|
-
projectId = manual.trim();
|
|
244
497
|
}
|
|
245
|
-
|
|
498
|
+
return '';
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function generateSecret() {
|
|
502
|
+
return crypto.randomBytes(32).toString('hex');
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
async function runSetupWizard(projectDir, projectName) {
|
|
506
|
+
const projectPath = resolve(projectDir);
|
|
507
|
+
process.chdir(projectPath);
|
|
508
|
+
|
|
509
|
+
clack.intro('🚀 NextBlock™ CMS setup wizard');
|
|
510
|
+
|
|
511
|
+
// 0. Prerequisites — mirror `npm run setup` (tools/scripts/setup.mjs). Make sure the
|
|
512
|
+
// developer has everything BEFORE we start prompting.
|
|
513
|
+
clack.note(
|
|
514
|
+
[
|
|
515
|
+
'1. A Supabase project https://supabase.com/dashboard',
|
|
516
|
+
' • Reference ID — Project Settings > General > "Reference ID"',
|
|
517
|
+
' • Connection string — Connect (top bar) > Direct connection > URI',
|
|
518
|
+
' • anon + service_role keys — Project Settings > API Keys',
|
|
519
|
+
' • Personal Access Token — Account > Access Tokens > Generate new token',
|
|
520
|
+
'',
|
|
521
|
+
'2. A Cloudflare R2 bucket https://dash.cloudflare.com > R2',
|
|
522
|
+
' • Create a bucket, then enable its Public Development URL (Bucket > Settings > General)',
|
|
523
|
+
' • Create an R2 API token (Object Read & Write); copy the Access Key ID + Secret (shown once)',
|
|
524
|
+
'',
|
|
525
|
+
'3. SMTP credentials SMTP2GO works very well: https://www.smtp2go.com',
|
|
526
|
+
' • Required so Supabase can email the confirmation link your first admin needs to sign in',
|
|
527
|
+
].join('\n'),
|
|
528
|
+
'Before you continue, have all of the following ready',
|
|
529
|
+
);
|
|
246
530
|
|
|
247
|
-
const
|
|
248
|
-
message: '
|
|
249
|
-
initialValue:
|
|
250
|
-
validate: (val) => (!val ? 'URL is required' : undefined),
|
|
531
|
+
const ready = await clack.confirm({
|
|
532
|
+
message: 'Do you have your Supabase, Cloudflare R2, and SMTP details ready?',
|
|
533
|
+
initialValue: true,
|
|
251
534
|
});
|
|
252
|
-
if (clack.isCancel(
|
|
535
|
+
if (clack.isCancel(ready)) {
|
|
253
536
|
handleWizardCancel('Setup cancelled.');
|
|
254
537
|
}
|
|
255
|
-
|
|
538
|
+
if (!ready) {
|
|
539
|
+
clack.note(
|
|
540
|
+
'No problem — your project files are ready. Gather the items above, then copy\n.env.example to .env.local and fill it in. Full guide: docs/05-DEVELOPER-GUIDE.md',
|
|
541
|
+
'Setup paused',
|
|
542
|
+
);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
256
545
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
546
|
+
await fs.ensureDir(resolve(projectPath, 'supabase'));
|
|
547
|
+
|
|
548
|
+
// 1. Supabase — same questions/order as setup.mjs. Nothing is masked: you are pasting
|
|
549
|
+
// keys you just copied, and seeing them makes paste mistakes easy to spot.
|
|
550
|
+
clack.note('Get these from https://supabase.com/dashboard', 'Supabase project');
|
|
551
|
+
const supabase = await clack.group(
|
|
262
552
|
{
|
|
553
|
+
projectId: () =>
|
|
554
|
+
clack.text({
|
|
555
|
+
message: 'Project ID (Project Settings > General > "Reference ID"):',
|
|
556
|
+
validate: requiredValue('Project Reference ID'),
|
|
557
|
+
}),
|
|
263
558
|
postgresUrl: () =>
|
|
264
559
|
clack.text({
|
|
265
560
|
message:
|
|
266
|
-
'
|
|
561
|
+
'Connection String (Connect > Direct connection > URI — replace [YOUR-PASSWORD] with your DB password):',
|
|
267
562
|
placeholder: 'postgresql://...',
|
|
268
|
-
validate: (
|
|
269
|
-
!val ? 'Connection string is required' : undefined,
|
|
563
|
+
validate: requiredValue('Connection string'),
|
|
270
564
|
}),
|
|
271
565
|
anonKey: () =>
|
|
272
|
-
clack.
|
|
273
|
-
message:
|
|
274
|
-
|
|
275
|
-
validate: (val) => (!val ? 'Anon Key is required' : undefined),
|
|
566
|
+
clack.text({
|
|
567
|
+
message: 'Project API Key — anon / public (Project Settings > API Keys):',
|
|
568
|
+
validate: requiredValue('Anon key'),
|
|
276
569
|
}),
|
|
277
570
|
serviceKey: () =>
|
|
278
|
-
clack.
|
|
279
|
-
message:
|
|
280
|
-
|
|
281
|
-
validate: (val) =>
|
|
282
|
-
!val ? 'Service Role Key is required' : undefined,
|
|
571
|
+
clack.text({
|
|
572
|
+
message: 'Service Role Key — service_role (Project Settings > API Keys):',
|
|
573
|
+
validate: requiredValue('Service role key'),
|
|
283
574
|
}),
|
|
284
575
|
accessToken: () =>
|
|
285
|
-
clack.
|
|
576
|
+
clack.text({
|
|
286
577
|
message:
|
|
287
|
-
'
|
|
288
|
-
validate: (
|
|
289
|
-
|
|
578
|
+
'Personal Access Token (Account > Access Tokens > Generate new token):',
|
|
579
|
+
validate: requiredValue('Access token'),
|
|
580
|
+
}),
|
|
581
|
+
siteUrl: () =>
|
|
582
|
+
clack.text({
|
|
583
|
+
// Standalone `npm run dev` is plain `next dev` on :3000 (NOT `nx serve` on :4200),
|
|
584
|
+
// so the local default differs from the monorepo setup wizard on purpose.
|
|
585
|
+
message: 'Public site URL [NEXT_PUBLIC_URL]:',
|
|
586
|
+
initialValue: 'http://localhost:3000',
|
|
587
|
+
validate: requiredValue('Site URL'),
|
|
290
588
|
}),
|
|
291
589
|
},
|
|
292
590
|
{ onCancel: () => handleWizardCancel('Setup cancelled.') },
|
|
293
591
|
);
|
|
294
592
|
|
|
295
|
-
|
|
296
|
-
const
|
|
593
|
+
const projectId = supabase.projectId.trim();
|
|
594
|
+
const postgresUrl = supabase.postgresUrl.trim();
|
|
595
|
+
const siteUrl = supabase.siteUrl.trim().replace(/\/+$/, '');
|
|
297
596
|
const supabaseUrl = `https://${projectId}.supabase.co`;
|
|
298
597
|
|
|
299
|
-
|
|
598
|
+
// Extract the database password from the connection string; prompt if it is missing
|
|
599
|
+
// or still the [YOUR-PASSWORD] placeholder.
|
|
300
600
|
let dbPassword = '';
|
|
301
601
|
try {
|
|
302
|
-
|
|
303
|
-
dbPassword = parsedUrl.password;
|
|
602
|
+
dbPassword = decodeURIComponent(new URL(postgresUrl).password);
|
|
304
603
|
} catch {
|
|
305
|
-
//
|
|
604
|
+
// Fall through to the manual prompt below.
|
|
306
605
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
const passwordPrompt = await clack.password({
|
|
606
|
+
if (!dbPassword || /YOUR-PASSWORD/i.test(dbPassword)) {
|
|
607
|
+
const passwordPrompt = await clack.text({
|
|
310
608
|
message:
|
|
311
|
-
'Could not
|
|
312
|
-
validate: (
|
|
609
|
+
'Could not read the DB password from the URI. Enter your Postgres database password:',
|
|
610
|
+
validate: requiredValue('Database password'),
|
|
313
611
|
});
|
|
314
612
|
if (clack.isCancel(passwordPrompt)) {
|
|
315
613
|
handleWizardCancel('Setup cancelled.');
|
|
316
614
|
}
|
|
317
|
-
dbPassword = passwordPrompt;
|
|
615
|
+
dbPassword = passwordPrompt.trim();
|
|
318
616
|
}
|
|
319
617
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
618
|
+
// 2. Cloudflare R2 — required. Powers media uploads, image processing, and backups.
|
|
619
|
+
clack.note('https://dash.cloudflare.com > R2', 'Cloudflare R2 storage');
|
|
620
|
+
const r2 = await clack.group(
|
|
621
|
+
{
|
|
622
|
+
accountId: () =>
|
|
623
|
+
clack.text({
|
|
624
|
+
message: 'R2 Account ID (R2 overview > Account details):',
|
|
625
|
+
validate: requiredValue('R2 Account ID'),
|
|
626
|
+
}),
|
|
627
|
+
bucketName: () =>
|
|
628
|
+
clack.text({
|
|
629
|
+
message: 'R2 Bucket Name:',
|
|
630
|
+
validate: requiredValue('R2 Bucket Name'),
|
|
631
|
+
}),
|
|
632
|
+
publicBaseUrl: () =>
|
|
633
|
+
clack.text({
|
|
634
|
+
message:
|
|
635
|
+
'R2 Public Development URL (Bucket > Settings > Public Development URL, e.g. https://pub-xxxx.r2.dev):',
|
|
636
|
+
validate: requiredValue('R2 Public Development URL'),
|
|
637
|
+
}),
|
|
638
|
+
accessKey: () =>
|
|
639
|
+
clack.text({
|
|
640
|
+
message: 'R2 Access Key ID (R2 > Manage API Tokens):',
|
|
641
|
+
validate: requiredValue('R2 Access Key ID'),
|
|
642
|
+
}),
|
|
643
|
+
secretKey: () =>
|
|
644
|
+
clack.text({
|
|
645
|
+
message:
|
|
646
|
+
'R2 Secret Access Key (shown only once when the token is created):',
|
|
647
|
+
validate: requiredValue('R2 Secret Access Key'),
|
|
648
|
+
}),
|
|
649
|
+
},
|
|
650
|
+
{ onCancel: () => handleWizardCancel('Setup cancelled.') },
|
|
651
|
+
);
|
|
652
|
+
|
|
653
|
+
// 3. SMTP — required. Sends the sign-up confirmation email your first admin needs.
|
|
654
|
+
clack.note('SMTP2GO works very well: https://www.smtp2go.com', 'SMTP email');
|
|
655
|
+
const smtp = await clack.group(
|
|
656
|
+
{
|
|
657
|
+
host: () =>
|
|
658
|
+
clack.text({
|
|
659
|
+
message: 'SMTP Host (e.g. mail.smtp2go.com):',
|
|
660
|
+
validate: requiredValue('SMTP Host'),
|
|
661
|
+
}),
|
|
662
|
+
port: () =>
|
|
663
|
+
clack.text({
|
|
664
|
+
message: 'SMTP Port (465 = SSL, 587 = STARTTLS):',
|
|
665
|
+
initialValue: '465',
|
|
666
|
+
validate: requiredValue('SMTP Port'),
|
|
667
|
+
}),
|
|
668
|
+
user: () =>
|
|
669
|
+
clack.text({
|
|
670
|
+
message: 'SMTP User:',
|
|
671
|
+
validate: requiredValue('SMTP User'),
|
|
672
|
+
}),
|
|
673
|
+
pass: () =>
|
|
674
|
+
clack.text({
|
|
675
|
+
message: 'SMTP Password:',
|
|
676
|
+
validate: requiredValue('SMTP Password'),
|
|
677
|
+
}),
|
|
678
|
+
fromEmail: () =>
|
|
679
|
+
clack.text({
|
|
680
|
+
message: 'From Email (the address confirmation emails are sent from):',
|
|
681
|
+
validate: requiredValue('From Email'),
|
|
682
|
+
}),
|
|
683
|
+
fromName: () =>
|
|
684
|
+
clack.text({
|
|
685
|
+
message: 'From Name (e.g. NextBlock):',
|
|
686
|
+
validate: requiredValue('From Name'),
|
|
687
|
+
}),
|
|
688
|
+
},
|
|
689
|
+
{ onCancel: () => handleWizardCancel('Setup cancelled.') },
|
|
690
|
+
);
|
|
691
|
+
|
|
692
|
+
const smtpValues = {
|
|
693
|
+
host: smtp.host,
|
|
694
|
+
port: smtp.port,
|
|
695
|
+
user: smtp.user,
|
|
696
|
+
pass: smtp.pass,
|
|
697
|
+
fromEmail: smtp.fromEmail,
|
|
698
|
+
fromName: smtp.fromName,
|
|
333
699
|
};
|
|
334
|
-
const envLines = [
|
|
335
|
-
`NEXT_PUBLIC_URL=${siteUrl}`,
|
|
336
|
-
'# Vercel / Supabase',
|
|
337
|
-
`SUPABASE_PROJECT_ID=${projectId}`,
|
|
338
|
-
`NEXT_PUBLIC_SUPABASE_URL=${supabaseUrl}`,
|
|
339
|
-
`NEXT_PUBLIC_SUPABASE_ANON_KEY=${supabaseKeys.anonKey}`,
|
|
340
|
-
`SUPABASE_SERVICE_ROLE_KEY=${supabaseKeys.serviceKey}`,
|
|
341
|
-
`SUPABASE_ACCESS_TOKEN=${supabaseKeys.accessToken}`,
|
|
342
|
-
`POSTGRES_URL=${postgresUrl}`,
|
|
343
|
-
'',
|
|
344
|
-
'# Revalidation',
|
|
345
|
-
`REVALIDATE_SECRET_TOKEN=${revalidationToken}`,
|
|
346
|
-
'',
|
|
347
|
-
];
|
|
348
700
|
|
|
349
|
-
|
|
701
|
+
// 4. Write .env.local with everything we collected. Mirror setup.mjs: seed from the
|
|
702
|
+
// template .env.example when present, replace keys line-by-line, append any missing,
|
|
703
|
+
// and reuse already-generated secrets so re-runs are idempotent. .env.local is what
|
|
704
|
+
// `next dev` loads first and is covered by the generated .gitignore.
|
|
705
|
+
clack.note('Writing .env.local...');
|
|
706
|
+
const envPath = resolve(projectPath, '.env.local');
|
|
707
|
+
const envExamplePath = resolve(projectPath, '.env.example');
|
|
708
|
+
let envContent = '';
|
|
350
709
|
if (await fs.pathExists(envPath)) {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
710
|
+
envContent = await fs.readFile(envPath, 'utf8');
|
|
711
|
+
} else if (await fs.pathExists(envExamplePath)) {
|
|
712
|
+
envContent = await fs.readFile(envExamplePath, 'utf8');
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const cronSecret = readEnvValue(envContent, 'CRON_SECRET=') || generateSecret();
|
|
716
|
+
const draftSecret =
|
|
717
|
+
readEnvValue(envContent, 'DRAFT_MODE_SECRET=') || generateSecret();
|
|
718
|
+
const revalidateSecret =
|
|
719
|
+
readEnvValue(envContent, 'REVALIDATE_SECRET_TOKEN=') || generateSecret();
|
|
720
|
+
|
|
721
|
+
const replacements = {
|
|
722
|
+
'SUPABASE_PROJECT_ID=': `SUPABASE_PROJECT_ID=${projectId}`,
|
|
723
|
+
'POSTGRES_URL=': `POSTGRES_URL=${postgresUrl}`,
|
|
724
|
+
'POSTGRES_PASSWORD=': `POSTGRES_PASSWORD="${dbPassword}"`,
|
|
725
|
+
'NEXT_PUBLIC_SUPABASE_URL=': `NEXT_PUBLIC_SUPABASE_URL=${supabaseUrl}`,
|
|
726
|
+
'NEXT_PUBLIC_SUPABASE_ANON_KEY=': `NEXT_PUBLIC_SUPABASE_ANON_KEY=${supabase.anonKey}`,
|
|
727
|
+
'SUPABASE_SERVICE_ROLE_KEY=': `SUPABASE_SERVICE_ROLE_KEY=${supabase.serviceKey}`,
|
|
728
|
+
'SUPABASE_ACCESS_TOKEN=': `SUPABASE_ACCESS_TOKEN=${supabase.accessToken}`,
|
|
729
|
+
'NEXT_PUBLIC_URL=': `NEXT_PUBLIC_URL=${siteUrl}`,
|
|
730
|
+
'CRON_SECRET=': `CRON_SECRET=${cronSecret}`,
|
|
731
|
+
'DRAFT_MODE_SECRET=': `DRAFT_MODE_SECRET=${draftSecret}`,
|
|
732
|
+
'REVALIDATE_SECRET_TOKEN=': `REVALIDATE_SECRET_TOKEN=${revalidateSecret}`,
|
|
733
|
+
// The R2 public URL is consumed under two names (next/image remotePatterns + CSP, and
|
|
734
|
+
// media URL resolution) — write the same value to both, matching setup.mjs.
|
|
735
|
+
'NEXT_PUBLIC_R2_PUBLIC_URL=': `NEXT_PUBLIC_R2_PUBLIC_URL=${r2.publicBaseUrl}`,
|
|
736
|
+
'NEXT_PUBLIC_R2_BASE_URL=': `NEXT_PUBLIC_R2_BASE_URL=${r2.publicBaseUrl}`,
|
|
737
|
+
'R2_ACCOUNT_ID=': `R2_ACCOUNT_ID=${r2.accountId}`,
|
|
738
|
+
'R2_BUCKET_NAME=': `R2_BUCKET_NAME=${r2.bucketName}`,
|
|
739
|
+
'R2_ACCESS_KEY_ID=': `R2_ACCESS_KEY_ID=${r2.accessKey}`,
|
|
740
|
+
'R2_SECRET_ACCESS_KEY=': `R2_SECRET_ACCESS_KEY=${r2.secretKey}`,
|
|
741
|
+
'SMTP_HOST=': `SMTP_HOST=${smtpValues.host}`,
|
|
742
|
+
'SMTP_PORT=': `SMTP_PORT=${smtpValues.port}`,
|
|
743
|
+
'SMTP_USER=': `SMTP_USER=${smtpValues.user}`,
|
|
744
|
+
'SMTP_PASS=': `SMTP_PASS=${smtpValues.pass}`,
|
|
745
|
+
'SMTP_FROM_EMAIL=': `SMTP_FROM_EMAIL=${smtpValues.fromEmail}`,
|
|
746
|
+
'SMTP_FROM_NAME=': `SMTP_FROM_NAME=${smtpValues.fromName}`,
|
|
747
|
+
'SUPABASE_AUTH_RATE_LIMIT_EMAIL_SENT=':
|
|
748
|
+
'SUPABASE_AUTH_RATE_LIMIT_EMAIL_SENT=30',
|
|
749
|
+
};
|
|
750
|
+
|
|
751
|
+
const appliedKeys = new Set();
|
|
752
|
+
const updatedLines = envContent.split(/\r?\n/).map((line) => {
|
|
753
|
+
for (const [key, value] of Object.entries(replacements)) {
|
|
754
|
+
if (line.startsWith(key)) {
|
|
755
|
+
appliedKeys.add(key);
|
|
756
|
+
return value;
|
|
757
|
+
}
|
|
363
758
|
}
|
|
364
|
-
|
|
759
|
+
return line;
|
|
760
|
+
});
|
|
365
761
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
762
|
+
// Append any keys missing from the seed so nothing is silently dropped.
|
|
763
|
+
for (const [key, value] of Object.entries(replacements)) {
|
|
764
|
+
if (!appliedKeys.has(key)) {
|
|
765
|
+
updatedLines.push(value);
|
|
766
|
+
}
|
|
369
767
|
}
|
|
370
768
|
|
|
371
|
-
|
|
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
|
-
}
|
|
769
|
+
await fs.writeFile(envPath, updatedLines.join('\n'), 'utf8');
|
|
770
|
+
clack.note(
|
|
771
|
+
'Supabase, R2, SMTP, site URL, and generated secrets saved to .env.local',
|
|
772
|
+
);
|
|
385
773
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
);
|
|
390
|
-
} else {
|
|
391
|
-
const supabaseBin = await getSupabaseBinary(projectPath);
|
|
392
|
-
const command = supabaseBin === 'npx' ? 'npx' : supabaseBin;
|
|
774
|
+
// 5. Materialize Supabase assets (migrations, config.toml, branded auth email
|
|
775
|
+
// templates) out of the installed @nextblock-cms/db package.
|
|
776
|
+
await ensureSupabaseAssets(projectPath, { required: true });
|
|
393
777
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
778
|
+
// 6. Link the project and apply the schema. These are the standalone equivalents of the
|
|
779
|
+
// monorepo `npm run db:link` + `npm run db:migrate:fresh` (which do not exist in a
|
|
780
|
+
// generated project): we drive the Supabase CLI directly, authenticating with the
|
|
781
|
+
// access token so no browser login is required.
|
|
782
|
+
const supabaseBin = await getSupabaseBinary(projectPath);
|
|
783
|
+
const command = supabaseBin === 'npx' ? 'npx' : supabaseBin;
|
|
784
|
+
const sbArgs = (args) => (supabaseBin === 'npx' ? ['supabase', ...args] : args);
|
|
785
|
+
const supabaseEnv = {
|
|
786
|
+
...process.env,
|
|
787
|
+
SUPABASE_ACCESS_TOKEN: supabase.accessToken,
|
|
788
|
+
SUPABASE_DB_PASSWORD: dbPassword,
|
|
789
|
+
POSTGRES_URL: postgresUrl,
|
|
790
|
+
// Available for env() substitution in supabase config.toml during `config push`.
|
|
791
|
+
NEXT_PUBLIC_URL: siteUrl,
|
|
792
|
+
};
|
|
399
793
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
794
|
+
const applySchema = await clack.confirm({
|
|
795
|
+
message:
|
|
796
|
+
'Apply the database schema to the linked project now? (Safe for a new database; does not delete existing data.)',
|
|
797
|
+
initialValue: true,
|
|
798
|
+
});
|
|
799
|
+
if (clack.isCancel(applySchema)) {
|
|
800
|
+
handleWizardCancel('Setup cancelled.');
|
|
801
|
+
}
|
|
404
802
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
803
|
+
const dbSpinner = clack.spinner();
|
|
804
|
+
dbSpinner.start('Linking to your Supabase project...');
|
|
805
|
+
try {
|
|
806
|
+
await execa(
|
|
807
|
+
command,
|
|
808
|
+
sbArgs(['link', '--project-ref', projectId, '--password', dbPassword]),
|
|
809
|
+
{ stdio: 'inherit', cwd: projectPath, env: supabaseEnv },
|
|
810
|
+
);
|
|
410
811
|
|
|
411
|
-
|
|
812
|
+
if (applySchema) {
|
|
813
|
+
dbSpinner.message('Pushing database schema...');
|
|
814
|
+
await execa(command, sbArgs(['db', 'push', '--include-all']), {
|
|
412
815
|
stdio: ['pipe', 'inherit', 'inherit'],
|
|
413
816
|
cwd: projectPath,
|
|
414
817
|
input: 'y\n', // Auto-confirm the push prompt
|
|
415
|
-
env:
|
|
416
|
-
...process.env,
|
|
417
|
-
SUPABASE_DB_PASSWORD: dbPassword,
|
|
418
|
-
},
|
|
818
|
+
env: supabaseEnv,
|
|
419
819
|
});
|
|
420
820
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
const configPushArgs =
|
|
424
|
-
supabaseBin === 'npx'
|
|
425
|
-
? ['supabase', 'config', 'push']
|
|
426
|
-
: ['config', 'push'];
|
|
427
|
-
|
|
428
|
-
await execa(command, configPushArgs, {
|
|
821
|
+
dbSpinner.message('Pushing Supabase config (auth settings)...');
|
|
822
|
+
await execa(command, sbArgs(['config', 'push']), {
|
|
429
823
|
stdio: ['pipe', 'inherit', 'inherit'],
|
|
430
824
|
cwd: projectPath,
|
|
431
|
-
env:
|
|
432
|
-
...process.env,
|
|
433
|
-
SUPABASE_DB_PASSWORD: dbPassword,
|
|
434
|
-
// Ensure NEXT_PUBLIC_URL is available for env() substitution in config.toml
|
|
435
|
-
NEXT_PUBLIC_URL: siteUrl,
|
|
436
|
-
},
|
|
825
|
+
env: supabaseEnv,
|
|
437
826
|
});
|
|
438
827
|
|
|
439
|
-
|
|
828
|
+
dbSpinner.stop('Database schema and config applied.');
|
|
829
|
+
} else {
|
|
830
|
+
dbSpinner.stop(
|
|
831
|
+
'Linked. Skipped schema push — run `npx supabase db push --include-all` when ready.',
|
|
832
|
+
);
|
|
440
833
|
}
|
|
441
834
|
} catch (error) {
|
|
442
|
-
|
|
443
|
-
'Database
|
|
835
|
+
dbSpinner.stop(
|
|
836
|
+
'Database setup failed. You can run `npx supabase db push --include-all` manually.',
|
|
444
837
|
);
|
|
445
838
|
if (error instanceof Error) {
|
|
446
839
|
clack.note(error.message);
|
|
447
840
|
}
|
|
448
841
|
}
|
|
449
842
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
843
|
+
// 7. Sync hosted Supabase Auth: custom SMTP + branded email templates. SMTP and the
|
|
844
|
+
// access token are required, so this always runs (matching setup.mjs). This is what
|
|
845
|
+
// lets Supabase email your first admin their confirmation link.
|
|
846
|
+
await enableSupabaseSmtpConfig(projectPath);
|
|
847
|
+
await configureHostedSupabaseAuth(projectPath, {
|
|
848
|
+
projectId,
|
|
849
|
+
siteUrl,
|
|
850
|
+
accessToken: supabase.accessToken,
|
|
851
|
+
smtpValues,
|
|
456
852
|
});
|
|
457
|
-
if (clack.isCancel(setupR2)) {
|
|
458
|
-
handleWizardCancel('Setup cancelled.');
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
let r2Values = {
|
|
462
|
-
publicBaseUrl: '',
|
|
463
|
-
accountId: '',
|
|
464
|
-
bucketName: '',
|
|
465
|
-
accessKey: '',
|
|
466
|
-
secretKey: '',
|
|
467
|
-
};
|
|
468
|
-
|
|
469
|
-
if (setupR2) {
|
|
470
|
-
clack.note(
|
|
471
|
-
'I will open your browser to the R2 dashboard.\nYou need to create a bucket and an R2 API Token.',
|
|
472
|
-
);
|
|
473
|
-
await open('https://dash.cloudflare.com/?to=/:account/r2', { wait: false });
|
|
474
|
-
|
|
475
|
-
const r2Keys = await clack.group(
|
|
476
|
-
{
|
|
477
|
-
accountId: () =>
|
|
478
|
-
clack.text({
|
|
479
|
-
message:
|
|
480
|
-
'R2: Paste your Cloudflare Account ID (Overview > Account Details - Bottom right):',
|
|
481
|
-
validate: (val) => (!val ? 'Account ID is required' : undefined),
|
|
482
|
-
}),
|
|
483
|
-
bucketName: () =>
|
|
484
|
-
clack.text({
|
|
485
|
-
message: 'R2: Paste your Bucket Name:',
|
|
486
|
-
validate: (val) => (!val ? 'Bucket name is required' : undefined),
|
|
487
|
-
}),
|
|
488
|
-
accessKey: () =>
|
|
489
|
-
clack.password({
|
|
490
|
-
message: 'R2: Paste your Access Key ID (create API tokens):',
|
|
491
|
-
validate: (val) => (!val ? 'Access Key ID is required' : undefined),
|
|
492
|
-
}),
|
|
493
|
-
secretKey: () =>
|
|
494
|
-
clack.password({
|
|
495
|
-
message: 'R2: Paste your Secret Access Key:',
|
|
496
|
-
validate: (val) =>
|
|
497
|
-
!val ? 'Secret Access Key is required' : undefined,
|
|
498
|
-
}),
|
|
499
|
-
publicBaseUrl: () =>
|
|
500
|
-
clack.text({
|
|
501
|
-
message:
|
|
502
|
-
'R2: Public Base URL (Bucket > Settings > Public Development URL-Enable: e.g., https://pub-xxx.r2.dev)',
|
|
503
|
-
validate: (val) =>
|
|
504
|
-
!val ? 'Public base URL is required' : undefined,
|
|
505
|
-
}),
|
|
506
|
-
},
|
|
507
|
-
{ onCancel: () => handleWizardCancel('Setup cancelled.') },
|
|
508
|
-
);
|
|
509
853
|
|
|
510
|
-
|
|
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)',
|
|
854
|
+
// 8. Optional premium modules (CLI-specific; requires a license + registry access).
|
|
855
|
+
const setupPremium = await clack.confirm({
|
|
856
|
+
message: 'Do you have a license and want to install premium modules now?',
|
|
857
|
+
initialValue: false,
|
|
542
858
|
});
|
|
543
|
-
if (clack.isCancel(
|
|
859
|
+
if (clack.isCancel(setupPremium)) {
|
|
544
860
|
handleWizardCancel('Setup cancelled.');
|
|
545
861
|
}
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
pass: '',
|
|
552
|
-
fromEmail: '',
|
|
553
|
-
fromName: '',
|
|
554
|
-
};
|
|
555
|
-
|
|
556
|
-
if (setupSMTP) {
|
|
557
|
-
const smtpKeys = await clack.group(
|
|
862
|
+
if (setupPremium) {
|
|
863
|
+
clack.note('Installing @nextblock-cms/ecommerce...');
|
|
864
|
+
await execa(
|
|
865
|
+
'npm',
|
|
866
|
+
['install', '@nextblock-cms/ecommerce@npm:@nextblock-cms/ecom@latest'],
|
|
558
867
|
{
|
|
559
|
-
|
|
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
|
-
}),
|
|
868
|
+
cwd: projectPath,
|
|
869
|
+
stdio: 'inherit',
|
|
589
870
|
},
|
|
590
|
-
{ onCancel: () => handleWizardCancel('Setup cancelled.') },
|
|
591
871
|
);
|
|
592
|
-
|
|
593
|
-
smtpValues = {
|
|
594
|
-
host: smtpKeys.host,
|
|
595
|
-
port: smtpKeys.port,
|
|
596
|
-
user: smtpKeys.user,
|
|
597
|
-
pass: smtpKeys.pass,
|
|
598
|
-
fromEmail: smtpKeys.fromEmail,
|
|
599
|
-
fromName: smtpKeys.fromName,
|
|
600
|
-
};
|
|
872
|
+
clack.note('Premium module installed!');
|
|
601
873
|
}
|
|
602
874
|
|
|
603
|
-
clack.
|
|
604
|
-
|
|
875
|
+
clack.outro(
|
|
876
|
+
[
|
|
877
|
+
`🎉 Your NextBlock™ project ${projectName ? `"${projectName}" ` : ''}is ready!`,
|
|
878
|
+
'',
|
|
879
|
+
'Next steps:',
|
|
880
|
+
` 1. Start the app: cd ${projectName} && npm run dev → ${siteUrl}`,
|
|
881
|
+
` 2. Create your account: open ${siteUrl}/sign-up`,
|
|
882
|
+
' The FIRST account to sign up automatically becomes the ADMIN.',
|
|
883
|
+
' 3. Confirm your email: click the link sent to your inbox',
|
|
884
|
+
` 4. Sign in — you'll land in the CMS at ${siteUrl}/cms/dashboard`,
|
|
885
|
+
].join('\n'),
|
|
605
886
|
);
|
|
606
|
-
|
|
607
|
-
message:
|
|
608
|
-
'Do you have a GitHub Personal Access Token (PAT) for premium modules?',
|
|
609
|
-
initialValue: false,
|
|
610
|
-
});
|
|
887
|
+
}
|
|
611
888
|
|
|
612
|
-
|
|
613
|
-
|
|
889
|
+
async function enableSupabaseSmtpConfig(projectDir) {
|
|
890
|
+
const configPath = resolve(projectDir, 'supabase', 'config.toml');
|
|
891
|
+
|
|
892
|
+
if (!(await fs.pathExists(configPath))) {
|
|
893
|
+
return;
|
|
614
894
|
}
|
|
615
895
|
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
return 'Token must start with ghp_ or github_pat_';
|
|
624
|
-
}
|
|
625
|
-
},
|
|
626
|
-
});
|
|
896
|
+
const smtpBlock = `# [auth.email.smtp]
|
|
897
|
+
# host = "env(SMTP_HOST)"
|
|
898
|
+
# port = 587
|
|
899
|
+
# user = "env(SMTP_USER)"
|
|
900
|
+
# pass = "env(SMTP_PASS)"
|
|
901
|
+
# admin_email = "env(SMTP_FROM_EMAIL)"
|
|
902
|
+
# sender_name = "env(SMTP_FROM_NAME)"`;
|
|
627
903
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
904
|
+
const enabledSmtpBlock = `[auth.email.smtp]
|
|
905
|
+
host = "env(SMTP_HOST)"
|
|
906
|
+
port = 587
|
|
907
|
+
user = "env(SMTP_USER)"
|
|
908
|
+
pass = "env(SMTP_PASS)"
|
|
909
|
+
admin_email = "env(SMTP_FROM_EMAIL)"
|
|
910
|
+
sender_name = "env(SMTP_FROM_NAME)"`;
|
|
633
911
|
|
|
634
|
-
|
|
635
|
-
const npmrcPath = resolve(projectPath, '.npmrc');
|
|
636
|
-
const npmrcContent = [
|
|
637
|
-
'@nextblock-cms:registry=https://npm.pkg.github.com',
|
|
638
|
-
`//npm.pkg.github.com/:_authToken=${pat}`,
|
|
639
|
-
'',
|
|
640
|
-
].join('\n');
|
|
912
|
+
const configContents = await fs.readFile(configPath, 'utf8');
|
|
641
913
|
|
|
642
|
-
|
|
643
|
-
|
|
914
|
+
if (configContents.includes(enabledSmtpBlock)) {
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
644
917
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
clack.note('Premium module installed!');
|
|
918
|
+
if (!configContents.includes(smtpBlock)) {
|
|
919
|
+
throw new Error(
|
|
920
|
+
`Could not find the SMTP placeholder block in ${configPath}.`,
|
|
921
|
+
);
|
|
650
922
|
}
|
|
651
923
|
|
|
652
|
-
await
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
if (
|
|
664
|
-
clack.note('SMTP configuration saved!');
|
|
665
|
-
} else if (canWriteEnv) {
|
|
924
|
+
await fs.writeFile(
|
|
925
|
+
configPath,
|
|
926
|
+
configContents.replace(smtpBlock, enabledSmtpBlock),
|
|
927
|
+
'utf8',
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
async function configureHostedSupabaseAuth(
|
|
932
|
+
projectDir,
|
|
933
|
+
{ projectId, siteUrl, accessToken, smtpValues },
|
|
934
|
+
) {
|
|
935
|
+
if (!projectId || !siteUrl || !accessToken) {
|
|
666
936
|
clack.note(
|
|
667
|
-
'
|
|
937
|
+
'Skipped hosted Supabase Auth sync because the project ref, site URL, or access token is missing.',
|
|
668
938
|
);
|
|
939
|
+
return;
|
|
669
940
|
}
|
|
670
941
|
|
|
671
|
-
clack.
|
|
672
|
-
|
|
673
|
-
|
|
942
|
+
const spinner = clack.spinner();
|
|
943
|
+
spinner.start('Syncing hosted Supabase Auth SMTP and branded email templates...');
|
|
944
|
+
|
|
945
|
+
try {
|
|
946
|
+
await execa('node', ['tools/configure-supabase-auth.js'], {
|
|
947
|
+
cwd: projectDir,
|
|
948
|
+
env: {
|
|
949
|
+
...process.env,
|
|
950
|
+
SUPABASE_PROJECT_ID: projectId,
|
|
951
|
+
NEXT_PUBLIC_URL: siteUrl,
|
|
952
|
+
SUPABASE_ACCESS_TOKEN: accessToken,
|
|
953
|
+
SMTP_HOST: smtpValues.host,
|
|
954
|
+
SMTP_PORT: smtpValues.port,
|
|
955
|
+
SMTP_USER: smtpValues.user,
|
|
956
|
+
SMTP_PASS: smtpValues.pass,
|
|
957
|
+
SMTP_FROM_EMAIL: smtpValues.fromEmail,
|
|
958
|
+
SMTP_FROM_NAME: smtpValues.fromName,
|
|
959
|
+
SUPABASE_AUTH_RATE_LIMIT_EMAIL_SENT:
|
|
960
|
+
process.env.SUPABASE_AUTH_RATE_LIMIT_EMAIL_SENT || '30',
|
|
961
|
+
},
|
|
962
|
+
});
|
|
963
|
+
spinner.stop('Hosted Supabase Auth configured.');
|
|
964
|
+
} catch (error) {
|
|
965
|
+
spinner.stop(
|
|
966
|
+
'Hosted Supabase Auth sync skipped. You can rerun it later with npm run configure:supabase-auth.',
|
|
967
|
+
);
|
|
968
|
+
clack.note(
|
|
969
|
+
error instanceof Error ? error.message : String(error),
|
|
970
|
+
'Supabase Auth Sync',
|
|
971
|
+
);
|
|
972
|
+
}
|
|
674
973
|
}
|
|
675
974
|
|
|
676
975
|
function handleWizardCancel(message) {
|
|
@@ -875,23 +1174,30 @@ async function ensureEnvExample(projectDir) {
|
|
|
875
1174
|
}
|
|
876
1175
|
}
|
|
877
1176
|
|
|
878
|
-
const placeholder = `# Environment variables for NextBlock CMS
|
|
1177
|
+
const placeholder = `# Environment variables for NextBlock™ CMS
|
|
879
1178
|
NEXT_PUBLIC_URL=
|
|
880
|
-
|
|
1179
|
+
|
|
1180
|
+
# Supabase — the setup wizard fills this whole block.
|
|
881
1181
|
SUPABASE_PROJECT_ID=
|
|
882
1182
|
POSTGRES_URL=
|
|
1183
|
+
POSTGRES_PASSWORD=
|
|
883
1184
|
NEXT_PUBLIC_SUPABASE_URL=
|
|
884
1185
|
NEXT_PUBLIC_SUPABASE_ANON_KEY=
|
|
885
1186
|
SUPABASE_SERVICE_ROLE_KEY=
|
|
1187
|
+
SUPABASE_ACCESS_TOKEN=
|
|
1188
|
+
|
|
1189
|
+
# Auto-generated by the setup wizard.
|
|
1190
|
+
CRON_SECRET=
|
|
1191
|
+
DRAFT_MODE_SECRET=
|
|
1192
|
+
REVALIDATE_SECRET_TOKEN=
|
|
886
1193
|
|
|
887
|
-
# Cloudflare
|
|
1194
|
+
# Cloudflare R2 — setup writes the public URL to both keys.
|
|
1195
|
+
NEXT_PUBLIC_R2_PUBLIC_URL=
|
|
888
1196
|
NEXT_PUBLIC_R2_BASE_URL=
|
|
1197
|
+
R2_ACCOUNT_ID=
|
|
1198
|
+
R2_BUCKET_NAME=
|
|
889
1199
|
R2_ACCESS_KEY_ID=
|
|
890
1200
|
R2_SECRET_ACCESS_KEY=
|
|
891
|
-
R2_BUCKET_NAME=
|
|
892
|
-
R2_ACCOUNT_ID=
|
|
893
|
-
|
|
894
|
-
REVALIDATE_SECRET_TOKEN=
|
|
895
1201
|
|
|
896
1202
|
# Email SMTP Configuration
|
|
897
1203
|
SMTP_HOST=
|
|
@@ -900,6 +1206,7 @@ SMTP_USER=
|
|
|
900
1206
|
SMTP_PASS=
|
|
901
1207
|
SMTP_FROM_EMAIL=
|
|
902
1208
|
SMTP_FROM_NAME=
|
|
1209
|
+
SUPABASE_AUTH_RATE_LIMIT_EMAIL_SENT=30
|
|
903
1210
|
`;
|
|
904
1211
|
|
|
905
1212
|
await fs.writeFile(destination, placeholder);
|
|
@@ -949,6 +1256,19 @@ async function ensureSupabaseAssets(projectDir, options = {}) {
|
|
|
949
1256
|
migrationsCopied = true;
|
|
950
1257
|
}
|
|
951
1258
|
|
|
1259
|
+
// Branded Auth email templates. configure-supabase-auth.js resolves the supabase dir by
|
|
1260
|
+
// requiring a templates/ subdir, and uploads these via the Management API. Without them
|
|
1261
|
+
// the hosted-auth + SMTP sync silently skips and the first admin never gets a
|
|
1262
|
+
// confirmation email — so copy them alongside config.toml + migrations.
|
|
1263
|
+
const sourceTemplates = resolve(packageSupabaseDir, 'templates');
|
|
1264
|
+
const destTemplates = resolve(destSupabaseDir, 'templates');
|
|
1265
|
+
if (await fs.pathExists(sourceTemplates)) {
|
|
1266
|
+
await fs.copy(sourceTemplates, destTemplates, {
|
|
1267
|
+
overwrite: true,
|
|
1268
|
+
errorOnExist: false,
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
|
|
952
1272
|
if (required) {
|
|
953
1273
|
if (!configCopied) {
|
|
954
1274
|
throw new Error(
|
|
@@ -1027,32 +1347,6 @@ async function resolvePackageSupabaseDir(projectDir) {
|
|
|
1027
1347
|
return { dir: null, triedPaths };
|
|
1028
1348
|
}
|
|
1029
1349
|
|
|
1030
|
-
async function readSupabaseProjectRef(projectDir) {
|
|
1031
|
-
const projectRefPath = resolve(
|
|
1032
|
-
projectDir,
|
|
1033
|
-
'supabase',
|
|
1034
|
-
'.temp',
|
|
1035
|
-
'project-ref',
|
|
1036
|
-
);
|
|
1037
|
-
if (await fs.pathExists(projectRefPath)) {
|
|
1038
|
-
const value = (await fs.readFile(projectRefPath, 'utf8')).trim();
|
|
1039
|
-
if (/^[a-z0-9]{20,}$/i.test(value)) {
|
|
1040
|
-
return value;
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
return null;
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
async function resetSupabaseProjectRef(projectDir) {
|
|
1048
|
-
const tempDir = resolve(projectDir, 'supabase', '.temp');
|
|
1049
|
-
await fs.ensureDir(tempDir);
|
|
1050
|
-
const projectRefPath = resolve(tempDir, 'project-ref');
|
|
1051
|
-
if (await fs.pathExists(projectRefPath)) {
|
|
1052
|
-
await fs.remove(projectRefPath);
|
|
1053
|
-
}
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
1350
|
async function ensureClientComponents(projectDir) {
|
|
1057
1351
|
const relativePaths = [
|
|
1058
1352
|
'components/env-var-warning.tsx',
|
|
@@ -1237,7 +1531,6 @@ async function sanitizeLayout(projectDir) {
|
|
|
1237
1531
|
|
|
1238
1532
|
const requiredImports = [
|
|
1239
1533
|
"import '@nextblock-cms/ui/styles/globals.css';",
|
|
1240
|
-
"import '@nextblock-cms/editor/styles/editor.css';",
|
|
1241
1534
|
];
|
|
1242
1535
|
|
|
1243
1536
|
const content = await fs.readFile(layoutPath, 'utf8');
|
|
@@ -1559,35 +1852,6 @@ function runCommand(command, args, options = {}) {
|
|
|
1559
1852
|
});
|
|
1560
1853
|
}
|
|
1561
1854
|
|
|
1562
|
-
async function runSupabaseCli(args, options = {}) {
|
|
1563
|
-
const { cwd } = options;
|
|
1564
|
-
const supabaseBin = await getSupabaseBinary(cwd);
|
|
1565
|
-
const command = supabaseBin === 'npx' ? 'npx' : supabaseBin;
|
|
1566
|
-
const cmdArgs = supabaseBin === 'npx' ? ['supabase', ...args] : args;
|
|
1567
|
-
|
|
1568
|
-
return new Promise((resolve, reject) => {
|
|
1569
|
-
const child = spawn(command, cmdArgs, {
|
|
1570
|
-
cwd,
|
|
1571
|
-
shell: IS_WINDOWS,
|
|
1572
|
-
stdio: 'inherit',
|
|
1573
|
-
});
|
|
1574
|
-
|
|
1575
|
-
child.on('error', (error) => {
|
|
1576
|
-
reject(error);
|
|
1577
|
-
});
|
|
1578
|
-
|
|
1579
|
-
child.on('close', (code) => {
|
|
1580
|
-
if (code === 0) {
|
|
1581
|
-
resolve();
|
|
1582
|
-
} else {
|
|
1583
|
-
reject(
|
|
1584
|
-
new Error(`supabase ${args.join(' ')} exited with code ${code}`),
|
|
1585
|
-
);
|
|
1586
|
-
}
|
|
1587
|
-
});
|
|
1588
|
-
});
|
|
1589
|
-
}
|
|
1590
|
-
|
|
1591
1855
|
async function getSupabaseBinary(projectDir) {
|
|
1592
1856
|
const binDir = resolve(projectDir, 'node_modules', '.bin');
|
|
1593
1857
|
const ext = IS_WINDOWS ? '.cmd' : '';
|
|
@@ -1643,18 +1907,7 @@ function buildNextConfigContent(editorUtilNames) {
|
|
|
1643
1907
|
' minimumCacheTTL: 31536000,',
|
|
1644
1908
|
' dangerouslyAllowSVG: false,',
|
|
1645
1909
|
" contentSecurityPolicy: \"default-src 'self'; script-src 'none'; sandbox;\",",
|
|
1646
|
-
' remotePatterns:
|
|
1647
|
-
" { protocol: 'https', hostname: 'pub-a31e3f1a87d144898aeb489a8221f92e.r2.dev' },",
|
|
1648
|
-
" { protocol: 'https', hostname: 'e260676f72b0b18314b868f136ed72ae.r2.cloudflarestorage.com' },",
|
|
1649
|
-
' ...(process.env.NEXT_PUBLIC_URL',
|
|
1650
|
-
' ? [',
|
|
1651
|
-
' {',
|
|
1652
|
-
" protocol: /** @type {'http' | 'https'} */ (new URL(process.env.NEXT_PUBLIC_URL).protocol.slice(0, -1)),",
|
|
1653
|
-
' hostname: new URL(process.env.NEXT_PUBLIC_URL).hostname,',
|
|
1654
|
-
' },',
|
|
1655
|
-
' ]',
|
|
1656
|
-
' : []),',
|
|
1657
|
-
' ],',
|
|
1910
|
+
' remotePatterns: getRemotePatterns(),',
|
|
1658
1911
|
' },',
|
|
1659
1912
|
' experimental: {',
|
|
1660
1913
|
' optimizeCss: true,',
|
|
@@ -1736,6 +1989,34 @@ function buildNextConfigContent(editorUtilNames) {
|
|
|
1736
1989
|
'};',
|
|
1737
1990
|
'',
|
|
1738
1991
|
'module.exports = nextConfig;',
|
|
1992
|
+
'',
|
|
1993
|
+
'function getRemotePatterns() {',
|
|
1994
|
+
' /** @type {Array<{ protocol: "http" | "https", hostname: string, pathname: string }>} */',
|
|
1995
|
+
' const patterns = [];',
|
|
1996
|
+
' // Whitelist this project R2 public/base URLs and the site URL for next/image.',
|
|
1997
|
+
' const sources = [',
|
|
1998
|
+
' process.env.NEXT_PUBLIC_R2_PUBLIC_URL,',
|
|
1999
|
+
' process.env.NEXT_PUBLIC_R2_BASE_URL,',
|
|
2000
|
+
' process.env.NEXT_PUBLIC_URL,',
|
|
2001
|
+
' ];',
|
|
2002
|
+
' for (const value of sources) {',
|
|
2003
|
+
' if (!value) continue;',
|
|
2004
|
+
' try {',
|
|
2005
|
+
' const parsed = new URL(value);',
|
|
2006
|
+
" if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') continue;",
|
|
2007
|
+
' const hostname = parsed.hostname;',
|
|
2008
|
+
' if (patterns.some((pattern) => pattern.hostname === hostname)) continue;',
|
|
2009
|
+
' patterns.push({',
|
|
2010
|
+
" protocol: parsed.protocol === 'https:' ? 'https' : 'http',",
|
|
2011
|
+
' hostname,',
|
|
2012
|
+
" pathname: '/**',",
|
|
2013
|
+
' });',
|
|
2014
|
+
' } catch {',
|
|
2015
|
+
' // ignore malformed value',
|
|
2016
|
+
' }',
|
|
2017
|
+
' }',
|
|
2018
|
+
' return patterns;',
|
|
2019
|
+
'}',
|
|
1739
2020
|
);
|
|
1740
2021
|
|
|
1741
2022
|
return lines.join('\n');
|