create-nextdevtpl 2.6.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/README.md +29 -0
- package/catalog.json +385 -0
- package/dist/catalog.d.ts +3 -0
- package/dist/catalog.js +112 -0
- package/dist/catalog.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +115 -0
- package/dist/cli.js.map +1 -0
- package/dist/filesystem.d.ts +10 -0
- package/dist/filesystem.js +86 -0
- package/dist/filesystem.js.map +1 -0
- package/dist/generate.d.ts +5 -0
- package/dist/generate.js +758 -0
- package/dist/generate.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/selection.d.ts +2 -0
- package/dist/selection.js +111 -0
- package/dist/selection.js.map +1 -0
- package/dist/service-templates.d.ts +4 -0
- package/dist/service-templates.js +180 -0
- package/dist/service-templates.js.map +1 -0
- package/dist/types.d.ts +65 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +54 -0
- package/template/.dockerignore +14 -0
- package/template/.env.example +282 -0
- package/template/.gitattributes +17 -0
- package/template/.node-version +1 -0
- package/template/Dockerfile +45 -0
- package/template/LICENSE +21 -0
- package/template/README.md +267 -0
- package/template/biome.json +68 -0
- package/template/cloudflare/templates/logger.ts +162 -0
- package/template/cloudflare/templates/mail/server.ts +6 -0
- package/template/cloudflare/templates/mail/templates.ts +87 -0
- package/template/cloudflare/templates/mail/utils.ts +69 -0
- package/template/cloudflare/worker.mjs +25 -0
- package/template/components.json +22 -0
- package/template/compose.yaml +74 -0
- package/template/deploy/check-health.mjs +25 -0
- package/template/deploy/docker/README.md +14 -0
- package/template/deploy/server/README.md +24 -0
- package/template/deploy/server/build.sh +24 -0
- package/template/deploy/server/nextdevtpl.service +19 -0
- package/template/deploy/server/start.sh +19 -0
- package/template/deploy/vercel/README.md +16 -0
- package/template/deploy/verify-production-health.mjs +86 -0
- package/template/drizzle.config.ts +47 -0
- package/template/gitignore +62 -0
- package/template/manifest.json +35 -0
- package/template/messages/en.json +766 -0
- package/template/messages/zh.json +762 -0
- package/template/next.config.mjs +28 -0
- package/template/open-next.config.ts +3 -0
- package/template/package.json +125 -0
- package/template/pnpm-lock.yaml +15901 -0
- package/template/postcss.config.mjs +7 -0
- package/template/public/file.svg +1 -0
- package/template/public/globe.svg +1 -0
- package/template/public/logo.svg +6 -0
- package/template/public/next.svg +1 -0
- package/template/public/vercel.svg +1 -0
- package/template/public/window.svg +1 -0
- package/template/sentry.client.config.ts +10 -0
- package/template/sentry.edge.config.ts +10 -0
- package/template/sentry.server.config.ts +10 -0
- package/template/source.config.ts +59 -0
- package/template/src/adapters/ai/anthropic.ts +113 -0
- package/template/src/adapters/ai/index.ts +6 -0
- package/template/src/adapters/ai/openai-compatible.ts +88 -0
- package/template/src/adapters/ai/workers-ai.ts +63 -0
- package/template/src/adapters/index.ts +10 -0
- package/template/src/adapters/jobs/cloudflare-workflows.ts +46 -0
- package/template/src/adapters/jobs/index.ts +5 -0
- package/template/src/adapters/jobs/inngest/adapter.ts +29 -0
- package/template/src/adapters/jobs/inngest/client.ts +8 -0
- package/template/src/adapters/jobs/inngest/functions.ts +17 -0
- package/template/src/adapters/jobs/inngest/handler.ts +6 -0
- package/template/src/adapters/jobs/inngest/index.ts +4 -0
- package/template/src/adapters/mail/cloudflare-email.ts +50 -0
- package/template/src/adapters/mail/disabled.ts +17 -0
- package/template/src/adapters/mail/index.ts +7 -0
- package/template/src/adapters/mail/resend.ts +76 -0
- package/template/src/adapters/mail/smtp.ts +82 -0
- package/template/src/adapters/payment/creem.ts +269 -0
- package/template/src/adapters/payment/index.ts +2 -0
- package/template/src/adapters/payment/stripe.ts +302 -0
- package/template/src/adapters/rate-limit/cloudflare.ts +42 -0
- package/template/src/adapters/rate-limit/index.ts +6 -0
- package/template/src/adapters/rate-limit/noop.ts +27 -0
- package/template/src/adapters/rate-limit/upstash.ts +113 -0
- package/template/src/adapters/registry.ts +228 -0
- package/template/src/adapters/storage/index.ts +3 -0
- package/template/src/adapters/storage/r2-binding.ts +105 -0
- package/template/src/adapters/storage/s3-compatible.ts +149 -0
- package/template/src/app/[locale]/(admin)/admin/layout.tsx +36 -0
- package/template/src/app/[locale]/(admin)/admin/page.tsx +345 -0
- package/template/src/app/[locale]/(admin)/admin/tickets/[id]/page.tsx +261 -0
- package/template/src/app/[locale]/(admin)/admin/tickets/page.tsx +236 -0
- package/template/src/app/[locale]/(admin)/admin/users/page.tsx +557 -0
- package/template/src/app/[locale]/(auth)/forgot-password/page.tsx +9 -0
- package/template/src/app/[locale]/(auth)/layout.tsx +28 -0
- package/template/src/app/[locale]/(auth)/sign-in/page.tsx +9 -0
- package/template/src/app/[locale]/(auth)/sign-up/page.tsx +9 -0
- package/template/src/app/[locale]/(dashboard)/dashboard/credits/buy/buy-credits-view.tsx +170 -0
- package/template/src/app/[locale]/(dashboard)/dashboard/credits/buy/page.tsx +12 -0
- package/template/src/app/[locale]/(dashboard)/dashboard/credits/page.tsx +5 -0
- package/template/src/app/[locale]/(dashboard)/dashboard/loading.tsx +41 -0
- package/template/src/app/[locale]/(dashboard)/dashboard/page.tsx +299 -0
- package/template/src/app/[locale]/(dashboard)/dashboard/settings/page.tsx +38 -0
- package/template/src/app/[locale]/(dashboard)/dashboard/support/[id]/page.tsx +220 -0
- package/template/src/app/[locale]/(dashboard)/dashboard/support/new/page.tsx +183 -0
- package/template/src/app/[locale]/(dashboard)/dashboard/support/page.tsx +145 -0
- package/template/src/app/[locale]/(dashboard)/layout.tsx +35 -0
- package/template/src/app/[locale]/(marketing)/blog/[slug]/page.tsx +178 -0
- package/template/src/app/[locale]/(marketing)/blog/blog-post-card.tsx +95 -0
- package/template/src/app/[locale]/(marketing)/blog/page.tsx +127 -0
- package/template/src/app/[locale]/(marketing)/demo/plan-badges/page.tsx +36 -0
- package/template/src/app/[locale]/(marketing)/layout.tsx +15 -0
- package/template/src/app/[locale]/(marketing)/legal/[slug]/page.tsx +130 -0
- package/template/src/app/[locale]/(marketing)/page.tsx +94 -0
- package/template/src/app/[locale]/(marketing)/pseo/[slug]/page.tsx +68 -0
- package/template/src/app/[locale]/(marketing)/pseo/page.tsx +137 -0
- package/template/src/app/[locale]/docs/[[...slug]]/page.tsx +63 -0
- package/template/src/app/[locale]/docs/layout.tsx +39 -0
- package/template/src/app/[locale]/layout.tsx +80 -0
- package/template/src/app/[locale]/opengraph-image.tsx +20 -0
- package/template/src/app/[locale]/twitter-image.tsx +20 -0
- package/template/src/app/api/auth/[...all]/route.ts +20 -0
- package/template/src/app/api/health/route.ts +45 -0
- package/template/src/app/api/inngest/route.ts +12 -0
- package/template/src/app/api/jobs/credits/expire/route.ts +102 -0
- package/template/src/app/api/search/route.ts +17 -0
- package/template/src/app/api/upload/presigned/route.ts +98 -0
- package/template/src/app/api/webhooks/payment/route.ts +363 -0
- package/template/src/app/globals.css +406 -0
- package/template/src/app/icon.svg +6 -0
- package/template/src/app/image-proxy/[...path]/route.ts +137 -0
- package/template/src/app/layout.tsx +73 -0
- package/template/src/app/robots.ts +29 -0
- package/template/src/app/sitemap.ts +68 -0
- package/template/src/components/motion/reveal.tsx +49 -0
- package/template/src/components/seo/json-ld.tsx +87 -0
- package/template/src/components/seo/og-image-template.tsx +118 -0
- package/template/src/components/ui/accordion.tsx +66 -0
- package/template/src/components/ui/alert-dialog.tsx +195 -0
- package/template/src/components/ui/avatar.tsx +53 -0
- package/template/src/components/ui/badge.tsx +46 -0
- package/template/src/components/ui/button.tsx +65 -0
- package/template/src/components/ui/card.tsx +92 -0
- package/template/src/components/ui/checkbox.tsx +32 -0
- package/template/src/components/ui/collapsible.tsx +33 -0
- package/template/src/components/ui/dialog.tsx +124 -0
- package/template/src/components/ui/dropdown-menu.tsx +257 -0
- package/template/src/components/ui/form.tsx +233 -0
- package/template/src/components/ui/index.ts +2 -0
- package/template/src/components/ui/input.tsx +21 -0
- package/template/src/components/ui/label.tsx +24 -0
- package/template/src/components/ui/navigation-menu.tsx +167 -0
- package/template/src/components/ui/pagination.tsx +125 -0
- package/template/src/components/ui/popover.tsx +48 -0
- package/template/src/components/ui/progress.tsx +27 -0
- package/template/src/components/ui/radio-group.tsx +45 -0
- package/template/src/components/ui/select.tsx +190 -0
- package/template/src/components/ui/separator.tsx +28 -0
- package/template/src/components/ui/sheet.tsx +140 -0
- package/template/src/components/ui/switch.tsx +31 -0
- package/template/src/components/ui/tabs.tsx +75 -0
- package/template/src/components/ui/textarea.tsx +18 -0
- package/template/src/config/index.ts +31 -0
- package/template/src/config/nav.ts +238 -0
- package/template/src/config/payment.ts +282 -0
- package/template/src/config/site.ts +53 -0
- package/template/src/config/subscription-plan.ts +164 -0
- package/template/src/content/blog/en/hello-world.mdx +48 -0
- package/template/src/content/blog/zh/hello-world.mdx +48 -0
- package/template/src/content/blog/zh/nextdevtpl-linuxdo-intro.mdx +98 -0
- package/template/src/content/docs/en/admin-and-support.mdx +65 -0
- package/template/src/content/docs/en/authentication.mdx +71 -0
- package/template/src/content/docs/en/background-jobs.mdx +53 -0
- package/template/src/content/docs/en/configuration.mdx +71 -0
- package/template/src/content/docs/en/credits.mdx +69 -0
- package/template/src/content/docs/en/customization.mdx +95 -0
- package/template/src/content/docs/en/database.mdx +66 -0
- package/template/src/content/docs/en/deployment.mdx +62 -0
- package/template/src/content/docs/en/email.mdx +48 -0
- package/template/src/content/docs/en/index.mdx +48 -0
- package/template/src/content/docs/en/internationalization.mdx +78 -0
- package/template/src/content/docs/en/meta.json +22 -0
- package/template/src/content/docs/en/observability.mdx +69 -0
- package/template/src/content/docs/en/payments.mdx +64 -0
- package/template/src/content/docs/en/project-structure.mdx +82 -0
- package/template/src/content/docs/en/quick-start.mdx +113 -0
- package/template/src/content/docs/en/storage.mdx +51 -0
- package/template/src/content/docs/zh/admin-and-support.mdx +65 -0
- package/template/src/content/docs/zh/authentication.mdx +71 -0
- package/template/src/content/docs/zh/background-jobs.mdx +53 -0
- package/template/src/content/docs/zh/configuration.mdx +70 -0
- package/template/src/content/docs/zh/credits.mdx +69 -0
- package/template/src/content/docs/zh/customization.mdx +95 -0
- package/template/src/content/docs/zh/database.mdx +66 -0
- package/template/src/content/docs/zh/deployment.mdx +61 -0
- package/template/src/content/docs/zh/email.mdx +47 -0
- package/template/src/content/docs/zh/index.mdx +48 -0
- package/template/src/content/docs/zh/internationalization.mdx +78 -0
- package/template/src/content/docs/zh/meta.json +22 -0
- package/template/src/content/docs/zh/observability.mdx +68 -0
- package/template/src/content/docs/zh/payments.mdx +64 -0
- package/template/src/content/docs/zh/project-structure.mdx +82 -0
- package/template/src/content/docs/zh/quick-start.mdx +110 -0
- package/template/src/content/docs/zh/storage.mdx +51 -0
- package/template/src/content/legal/en/cookie-policy.mdx +78 -0
- package/template/src/content/legal/en/privacy.mdx +131 -0
- package/template/src/content/legal/en/terms.mdx +93 -0
- package/template/src/content/legal/zh/cookie-policy.mdx +78 -0
- package/template/src/content/legal/zh/privacy.mdx +131 -0
- package/template/src/content/legal/zh/terms.mdx +92 -0
- package/template/src/core/modules/contract.ts +225 -0
- package/template/src/core/modules/index.ts +1 -0
- package/template/src/core/services/ai.ts +31 -0
- package/template/src/core/services/common.ts +112 -0
- package/template/src/core/services/index.ts +8 -0
- package/template/src/core/services/jobs.ts +24 -0
- package/template/src/core/services/mail.ts +35 -0
- package/template/src/core/services/manifest.ts +51 -0
- package/template/src/core/services/payment.ts +92 -0
- package/template/src/core/services/rate-limit.ts +47 -0
- package/template/src/core/services/storage.ts +42 -0
- package/template/src/db/index.ts +71 -0
- package/template/src/db/schema/auth.ts +67 -0
- package/template/src/db/schema/credits.ts +94 -0
- package/template/src/db/schema/index.ts +5 -0
- package/template/src/db/schema/mail.ts +14 -0
- package/template/src/db/schema/subscription.ts +21 -0
- package/template/src/db/schema/support.ts +56 -0
- package/template/src/db/schema.ts +2 -0
- package/template/src/features/admin/components/admin-sidebar.tsx +213 -0
- package/template/src/features/admin/components/index.ts +2 -0
- package/template/src/features/admin/index.ts +3 -0
- package/template/src/features/admin/manifest.ts +41 -0
- package/template/src/features/analytics/components/analytics.tsx +62 -0
- package/template/src/features/analytics/components/index.ts +2 -0
- package/template/src/features/analytics/index.ts +3 -0
- package/template/src/features/analytics/manifest.ts +13 -0
- package/template/src/features/auth/components/auth-brand-panel.tsx +82 -0
- package/template/src/features/auth/components/auth-error-alert.tsx +28 -0
- package/template/src/features/auth/components/auth-footer.tsx +52 -0
- package/template/src/features/auth/components/auth-logo.tsx +30 -0
- package/template/src/features/auth/components/forgot-password-form.tsx +146 -0
- package/template/src/features/auth/components/index.ts +9 -0
- package/template/src/features/auth/components/sign-in-form.tsx +241 -0
- package/template/src/features/auth/components/sign-up-form.tsx +307 -0
- package/template/src/features/auth/index.ts +3 -0
- package/template/src/features/auth/manifest.ts +30 -0
- package/template/src/features/blog/components/blog-post-item.tsx +72 -0
- package/template/src/features/blog/components/index.ts +1 -0
- package/template/src/features/blog/data/mock-posts.ts +62 -0
- package/template/src/features/blog/index.ts +3 -0
- package/template/src/features/blog/manifest.ts +22 -0
- package/template/src/features/credits/account.ts +76 -0
- package/template/src/features/credits/actions.ts +427 -0
- package/template/src/features/credits/components/credit-balance-badge.tsx +54 -0
- package/template/src/features/credits/components/credit-usage-section.tsx +160 -0
- package/template/src/features/credits/components/index.ts +7 -0
- package/template/src/features/credits/components/transaction-history.tsx +346 -0
- package/template/src/features/credits/config.ts +70 -0
- package/template/src/features/credits/consume.ts +140 -0
- package/template/src/features/credits/core.ts +23 -0
- package/template/src/features/credits/errors.ts +26 -0
- package/template/src/features/credits/expire.ts +112 -0
- package/template/src/features/credits/grant.ts +168 -0
- package/template/src/features/credits/index.ts +6 -0
- package/template/src/features/credits/manifest.ts +36 -0
- package/template/src/features/credits/query.ts +65 -0
- package/template/src/features/credits/types.ts +48 -0
- package/template/src/features/dashboard/components/index.ts +3 -0
- package/template/src/features/dashboard/components/main-wrapper.tsx +100 -0
- package/template/src/features/dashboard/components/sidebar.tsx +341 -0
- package/template/src/features/dashboard/context/index.ts +2 -0
- package/template/src/features/dashboard/context/sidebar-context.tsx +72 -0
- package/template/src/features/dashboard/index.ts +4 -0
- package/template/src/features/dashboard/manifest.ts +28 -0
- package/template/src/features/mail/actions.ts +175 -0
- package/template/src/features/mail/index.ts +5 -0
- package/template/src/features/mail/manifest.ts +13 -0
- package/template/src/features/mail/server.ts +8 -0
- package/template/src/features/mail/templates/index.ts +11 -0
- package/template/src/features/mail/templates/primary-action-email.tsx +224 -0
- package/template/src/features/mail/templates/welcome-email.tsx +121 -0
- package/template/src/features/mail/utils.ts +190 -0
- package/template/src/features/marketing/components/animated-price.tsx +51 -0
- package/template/src/features/marketing/components/cookie-consent.tsx +255 -0
- package/template/src/features/marketing/components/cta-section.tsx +50 -0
- package/template/src/features/marketing/components/faq-section.tsx +59 -0
- package/template/src/features/marketing/components/feature-grid.tsx +80 -0
- package/template/src/features/marketing/components/footer.tsx +128 -0
- package/template/src/features/marketing/components/header.tsx +255 -0
- package/template/src/features/marketing/components/hero-section.tsx +323 -0
- package/template/src/features/marketing/components/how-it-works.tsx +115 -0
- package/template/src/features/marketing/components/index.ts +14 -0
- package/template/src/features/marketing/components/nav-menu.tsx +236 -0
- package/template/src/features/marketing/components/pricing-section.tsx +446 -0
- package/template/src/features/marketing/components/testimonials.tsx +59 -0
- package/template/src/features/marketing/components/use-cases-section.tsx +78 -0
- package/template/src/features/marketing/index.ts +3 -0
- package/template/src/features/marketing/manifest.ts +35 -0
- package/template/src/features/payment/actions.ts +182 -0
- package/template/src/features/payment/index.ts +4 -0
- package/template/src/features/payment/manifest.ts +18 -0
- package/template/src/features/payment/server.ts +7 -0
- package/template/src/features/payment/types.ts +202 -0
- package/template/src/features/pseo/components/index.ts +6 -0
- package/template/src/features/pseo/components/pseo-cta.tsx +51 -0
- package/template/src/features/pseo/components/pseo-faq.tsx +40 -0
- package/template/src/features/pseo/components/pseo-feature-grid.tsx +74 -0
- package/template/src/features/pseo/components/pseo-hero.tsx +106 -0
- package/template/src/features/pseo/components/pseo-related.tsx +62 -0
- package/template/src/features/pseo/components/pseo-use-cases.tsx +50 -0
- package/template/src/features/pseo/data/pseo-pages.json +534 -0
- package/template/src/features/pseo/index.ts +4 -0
- package/template/src/features/pseo/lib/pseo-data.ts +140 -0
- package/template/src/features/pseo/manifest.ts +22 -0
- package/template/src/features/settings/actions/delete-account.ts +50 -0
- package/template/src/features/settings/actions/index.ts +3 -0
- package/template/src/features/settings/actions/update-profile.ts +49 -0
- package/template/src/features/settings/components/billing-section.tsx +283 -0
- package/template/src/features/settings/components/index.ts +3 -0
- package/template/src/features/settings/components/profile-form.tsx +154 -0
- package/template/src/features/settings/components/security-section.tsx +158 -0
- package/template/src/features/settings/components/settings-profile-view.tsx +506 -0
- package/template/src/features/settings/index.ts +5 -0
- package/template/src/features/settings/manifest.ts +28 -0
- package/template/src/features/settings/schemas/index.ts +2 -0
- package/template/src/features/settings/schemas/update-profile.ts +21 -0
- package/template/src/features/shared/components/cookie-settings-dialog.tsx +282 -0
- package/template/src/features/shared/components/index.ts +5 -0
- package/template/src/features/shared/components/language-switcher.tsx +82 -0
- package/template/src/features/shared/components/max-width-wrapper.tsx +49 -0
- package/template/src/features/shared/components/mode-toggle.tsx +119 -0
- package/template/src/features/shared/icons/google-icon.tsx +39 -0
- package/template/src/features/shared/icons/index.ts +7 -0
- package/template/src/features/shared/index.ts +5 -0
- package/template/src/features/shared/manifest.ts +13 -0
- package/template/src/features/shared/providers.tsx +59 -0
- package/template/src/features/storage/actions.ts +107 -0
- package/template/src/features/storage/index.ts +6 -0
- package/template/src/features/storage/manifest.ts +22 -0
- package/template/src/features/storage/types.ts +143 -0
- package/template/src/features/storage/utils.ts +85 -0
- package/template/src/features/storage/validation.ts +151 -0
- package/template/src/features/subscription/actions/get-user-plan.ts +26 -0
- package/template/src/features/subscription/actions/index.ts +5 -0
- package/template/src/features/subscription/components/current-plan-badge.tsx +18 -0
- package/template/src/features/subscription/components/index.ts +6 -0
- package/template/src/features/subscription/components/plan-badge.tsx +98 -0
- package/template/src/features/subscription/index.ts +25 -0
- package/template/src/features/subscription/manifest.ts +13 -0
- package/template/src/features/subscription/services/user-plan.ts +181 -0
- package/template/src/features/support/actions/admin-ticket.ts +172 -0
- package/template/src/features/support/actions/admin-users.ts +295 -0
- package/template/src/features/support/actions/index.ts +22 -0
- package/template/src/features/support/actions/ticket.ts +12 -0
- package/template/src/features/support/actions/user-ticket.ts +153 -0
- package/template/src/features/support/components/admin-ticket-reply-form.tsx +109 -0
- package/template/src/features/support/components/admin-ticket-status-select.tsx +119 -0
- package/template/src/features/support/components/index.ts +6 -0
- package/template/src/features/support/components/ticket-message-form.tsx +99 -0
- package/template/src/features/support/components/user-role-select.tsx +107 -0
- package/template/src/features/support/index.ts +5 -0
- package/template/src/features/support/manifest.ts +53 -0
- package/template/src/features/support/schemas/index.ts +15 -0
- package/template/src/features/support/schemas/ticket.ts +81 -0
- package/template/src/hooks/index.ts +2 -0
- package/template/src/i18n/index.ts +12 -0
- package/template/src/i18n/request.ts +24 -0
- package/template/src/i18n/routing.ts +23 -0
- package/template/src/instrumentation.ts +19 -0
- package/template/src/lib/ai/index.ts +16 -0
- package/template/src/lib/api-logger.ts +24 -0
- package/template/src/lib/auth/admin.ts +54 -0
- package/template/src/lib/auth/api.ts +17 -0
- package/template/src/lib/auth/client.ts +227 -0
- package/template/src/lib/auth/edge.ts +84 -0
- package/template/src/lib/auth/errors.ts +99 -0
- package/template/src/lib/auth/index.ts +159 -0
- package/template/src/lib/auth/server.ts +46 -0
- package/template/src/lib/auth/session-context.tsx +96 -0
- package/template/src/lib/cloudflare/bindings.ts +31 -0
- package/template/src/lib/cookie-consent.ts +33 -0
- package/template/src/lib/file-utils.ts +45 -0
- package/template/src/lib/fingerprint/index.ts +73 -0
- package/template/src/lib/health/checks.ts +6 -0
- package/template/src/lib/health/core.ts +86 -0
- package/template/src/lib/health/index.ts +11 -0
- package/template/src/lib/health/types.ts +13 -0
- package/template/src/lib/index.ts +2 -0
- package/template/src/lib/logger/index.ts +258 -0
- package/template/src/lib/monitoring/index.ts +384 -0
- package/template/src/lib/rate-limit/anonymous.ts +85 -0
- package/template/src/lib/rate-limit/index.ts +111 -0
- package/template/src/lib/safe-action.ts +127 -0
- package/template/src/lib/seo/json-ld.ts +188 -0
- package/template/src/lib/source.ts +122 -0
- package/template/src/lib/utils.ts +6 -0
- package/template/src/middleware.ts +162 -0
- package/template/src/modules/index.ts +10 -0
- package/template/src/modules/presets.ts +8 -0
- package/template/src/modules/registry.ts +34 -0
- package/template/src/modules/selection.ts +45 -0
- package/template/src/services/ai.ts +49 -0
- package/template/src/services/index.ts +6 -0
- package/template/src/services/jobs.ts +3 -0
- package/template/src/services/mail.ts +7 -0
- package/template/src/services/payment.ts +3 -0
- package/template/src/services/rate-limit.ts +12 -0
- package/template/src/services/storage.ts +3 -0
- package/template/src/types/fumadocs-source.d.ts +14 -0
- package/template/src/types/index.ts +2 -0
- package/template/tsconfig.json +53 -0
- package/template/vercel.json +11 -0
- package/template/vitest.config.ts +51 -0
- package/template/wrangler.jsonc +30 -0
package/dist/generate.js
ADDED
|
@@ -0,0 +1,758 @@
|
|
|
1
|
+
import { readdir, readFile, rm } from "node:fs/promises";
|
|
2
|
+
import { basename, dirname, join } from "node:path";
|
|
3
|
+
import { parse, stringify } from "yaml";
|
|
4
|
+
import { findGeneratorAssets, loadCatalog } from "./catalog.js";
|
|
5
|
+
import { copyTemplate, installDependencies, prepareTarget, removePath, runPackageScript, writeJson, writeText, } from "./filesystem.js";
|
|
6
|
+
import { createProjectSelection } from "./selection.js";
|
|
7
|
+
import { adapterExports, serviceExportNames, serviceSources, } from "./service-templates.js";
|
|
8
|
+
const SERVICE_KINDS = [
|
|
9
|
+
"payment",
|
|
10
|
+
"storage",
|
|
11
|
+
"mail",
|
|
12
|
+
"ai",
|
|
13
|
+
"jobs",
|
|
14
|
+
"rate-limit",
|
|
15
|
+
];
|
|
16
|
+
const ENV_DEFAULTS = {
|
|
17
|
+
APP_VERSION: "local",
|
|
18
|
+
DATABASE_URL: "postgresql://postgres:password@localhost:5432/nextdevtpl",
|
|
19
|
+
BETTER_AUTH_SECRET: "replace-with-a-long-random-secret",
|
|
20
|
+
BETTER_AUTH_URL: "http://localhost:3000",
|
|
21
|
+
NEXT_PUBLIC_APP_URL: "http://localhost:3000",
|
|
22
|
+
NEXT_PUBLIC_APP_NAME: "NextDevTpl",
|
|
23
|
+
PORT: "3000",
|
|
24
|
+
POSTGRES_DB: "nextdevtpl",
|
|
25
|
+
POSTGRES_PASSWORD: "replace-with-a-long-random-password",
|
|
26
|
+
POSTGRES_USER: "nextdevtpl",
|
|
27
|
+
};
|
|
28
|
+
const CLOUDFLARE_RATE_LIMITS = [
|
|
29
|
+
{ id: "1001", limit: 100, name: "RATE_LIMIT_GLOBAL" },
|
|
30
|
+
{ id: "1002", limit: 5, name: "RATE_LIMIT_AUTH" },
|
|
31
|
+
{ id: "1003", limit: 20, name: "RATE_LIMIT_AI" },
|
|
32
|
+
{ id: "1004", limit: 10, name: "RATE_LIMIT_PAYMENT" },
|
|
33
|
+
{ id: "1005", limit: 30, name: "RATE_LIMIT_UPLOAD" },
|
|
34
|
+
{ id: "1006", limit: 3, name: "RATE_LIMIT_STRICT" },
|
|
35
|
+
];
|
|
36
|
+
function projectName(target) {
|
|
37
|
+
return (basename(target)
|
|
38
|
+
.toLowerCase()
|
|
39
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
40
|
+
.replace(/^-+|-+$/g, "") || "nextdevtpl-app");
|
|
41
|
+
}
|
|
42
|
+
function cloudflareDatabaseSource() {
|
|
43
|
+
return `import { neon } from "@neondatabase/serverless";
|
|
44
|
+
import { drizzle } from "drizzle-orm/neon-http";
|
|
45
|
+
|
|
46
|
+
import * as schema from "./schema/index";
|
|
47
|
+
|
|
48
|
+
const databaseUrl = process.env.DATABASE_URL;
|
|
49
|
+
if (!databaseUrl) {
|
|
50
|
+
throw new Error("DATABASE_URL is required");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const sql = neon(databaseUrl);
|
|
54
|
+
export const db = drizzle(sql, { schema });
|
|
55
|
+
|
|
56
|
+
export * from "./schema/index";
|
|
57
|
+
`;
|
|
58
|
+
}
|
|
59
|
+
function cloudflareMonitoringSource() {
|
|
60
|
+
return `export type SeverityLevel = "debug" | "error" | "fatal" | "info" | "log" | "warning";
|
|
61
|
+
|
|
62
|
+
export function isSentryEnabled(): boolean {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function initSentryServer(): void {}
|
|
67
|
+
export function initSentryClient(): void {}
|
|
68
|
+
|
|
69
|
+
export function captureError(
|
|
70
|
+
error: unknown,
|
|
71
|
+
context?: Record<string, unknown>
|
|
72
|
+
): void {
|
|
73
|
+
console.error("[Error]", error, context);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function captureMessage(
|
|
77
|
+
message: string,
|
|
78
|
+
level: SeverityLevel = "info",
|
|
79
|
+
context?: Record<string, unknown>
|
|
80
|
+
): void {
|
|
81
|
+
let log = console.log;
|
|
82
|
+
if (level === "error" || level === "fatal") log = console.error;
|
|
83
|
+
if (level === "warning") log = console.warn;
|
|
84
|
+
log(\`[\${level}]\`, message, context);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function setUser(_user: { id: string; email?: string; username?: string } | null): void {}
|
|
88
|
+
export function clearUser(): void {}
|
|
89
|
+
export function setTag(_key: string, _value: string): void {}
|
|
90
|
+
export function setContext(_name: string, _context: Record<string, unknown>): void {}
|
|
91
|
+
|
|
92
|
+
export function startSpan(
|
|
93
|
+
_operation: string,
|
|
94
|
+
_description?: string
|
|
95
|
+
): undefined {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function withSpan<T>(
|
|
100
|
+
_operation: string,
|
|
101
|
+
_description: string,
|
|
102
|
+
fn: () => Promise<T>
|
|
103
|
+
): Promise<T> {
|
|
104
|
+
return fn();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function withSentryAction<TInput, TOutput>(
|
|
108
|
+
action: (input: TInput) => Promise<TOutput>
|
|
109
|
+
): (input: TInput) => Promise<TOutput> {
|
|
110
|
+
return async (input: TInput) => {
|
|
111
|
+
try {
|
|
112
|
+
return await action(input);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
captureError(error, { action: action.name });
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function withSentryHandler<T extends Response>(
|
|
121
|
+
handler: (request: Request) => Promise<T>
|
|
122
|
+
): (request: Request) => Promise<T | Response> {
|
|
123
|
+
return async (request: Request) => {
|
|
124
|
+
try {
|
|
125
|
+
return await handler(request);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
captureError(error, { method: request.method, path: new URL(request.url).pathname });
|
|
128
|
+
return Response.json({ error: "Internal Server Error" }, { status: 500 });
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
`;
|
|
133
|
+
}
|
|
134
|
+
function cloudflareConfig(target, selection) {
|
|
135
|
+
const name = projectName(target);
|
|
136
|
+
const bindings = new Set(selection.bindings);
|
|
137
|
+
return {
|
|
138
|
+
$schema: "node_modules/wrangler/config-schema.json",
|
|
139
|
+
name,
|
|
140
|
+
main: "cloudflare/worker.mjs",
|
|
141
|
+
compatibility_date: new Date().toISOString().slice(0, 10),
|
|
142
|
+
compatibility_flags: ["nodejs_compat", "global_fetch_strictly_public"],
|
|
143
|
+
minify: true,
|
|
144
|
+
assets: { directory: ".open-next/assets", binding: "ASSETS" },
|
|
145
|
+
services: [{ binding: "WORKER_SELF_REFERENCE", service: name }],
|
|
146
|
+
observability: {
|
|
147
|
+
enabled: true,
|
|
148
|
+
logs: {
|
|
149
|
+
enabled: true,
|
|
150
|
+
head_sampling_rate: 1,
|
|
151
|
+
invocation_logs: true,
|
|
152
|
+
},
|
|
153
|
+
traces: { enabled: true, head_sampling_rate: 0.1 },
|
|
154
|
+
},
|
|
155
|
+
...(bindings.has("R2Bucket")
|
|
156
|
+
? { r2_buckets: [{ binding: "NEXTDEVTPL_STORAGE" }] }
|
|
157
|
+
: {}),
|
|
158
|
+
...(bindings.has("Ai") ? { ai: { binding: "AI" } } : {}),
|
|
159
|
+
...(bindings.has("Workflow")
|
|
160
|
+
? {
|
|
161
|
+
workflows: [
|
|
162
|
+
{
|
|
163
|
+
binding: "NEXTDEVTPL_WORKFLOW",
|
|
164
|
+
name: `${name}-jobs`,
|
|
165
|
+
class_name: "NextDevTplWorkflow",
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
}
|
|
169
|
+
: {}),
|
|
170
|
+
...(bindings.has("SendEmail")
|
|
171
|
+
? { send_email: [{ name: "NEXTDEVTPL_EMAIL" }] }
|
|
172
|
+
: {}),
|
|
173
|
+
...(bindings.has("RateLimit")
|
|
174
|
+
? {
|
|
175
|
+
ratelimits: CLOUDFLARE_RATE_LIMITS.map((item) => ({
|
|
176
|
+
name: item.name,
|
|
177
|
+
namespace_id: item.id,
|
|
178
|
+
simple: { limit: item.limit, period: 60 },
|
|
179
|
+
})),
|
|
180
|
+
}
|
|
181
|
+
: {}),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
async function rewriteCloudflareRuntime(target, selection) {
|
|
185
|
+
const templateRoot = join(target, "cloudflare", "templates", "mail");
|
|
186
|
+
const loggerTemplate = join(target, "cloudflare", "templates", "logger.ts");
|
|
187
|
+
const mailRoot = join(target, "src", "features", "mail");
|
|
188
|
+
await writeText(join(target, "src", "lib", "logger", "index.ts"), await readFile(loggerTemplate, "utf8"));
|
|
189
|
+
if (hasModule(selection, "mail")) {
|
|
190
|
+
await removePath(join(mailRoot, "templates"));
|
|
191
|
+
await writeText(join(mailRoot, "templates", "index.ts"), await readFile(join(templateRoot, "templates.ts"), "utf8"));
|
|
192
|
+
await writeText(join(mailRoot, "utils.ts"), await readFile(join(templateRoot, "utils.ts"), "utf8"));
|
|
193
|
+
await writeText(join(mailRoot, "server.ts"), await readFile(join(templateRoot, "server.ts"), "utf8"));
|
|
194
|
+
await writeText(join(mailRoot, "index.ts"), `export * from "./actions";\nexport { mailModule } from "./manifest";\nexport * from "./templates";\nexport * from "./utils";\n`);
|
|
195
|
+
}
|
|
196
|
+
if (hasModule(selection, "auth")) {
|
|
197
|
+
const authPath = join(target, "src", "lib", "auth", "index.ts");
|
|
198
|
+
const authSource = await readFile(authPath, "utf8");
|
|
199
|
+
const workerAuthSource = authSource
|
|
200
|
+
.replace("ResetPasswordEmail,", "createResetPasswordEmail,")
|
|
201
|
+
.replace("VerifyEmailEmail,", "createVerifyEmail,")
|
|
202
|
+
.replace("react: ResetPasswordEmail({", "content: createResetPasswordEmail({")
|
|
203
|
+
.replace("react: VerifyEmailEmail({", "content: createVerifyEmail({");
|
|
204
|
+
if (workerAuthSource === authSource ||
|
|
205
|
+
workerAuthSource.includes("react:")) {
|
|
206
|
+
throw new Error("Cloudflare mail rewrite no longer matches the auth source");
|
|
207
|
+
}
|
|
208
|
+
await writeText(authPath, workerAuthSource);
|
|
209
|
+
}
|
|
210
|
+
await removePath(join(target, "src", "app", "[locale]", "opengraph-image.tsx"));
|
|
211
|
+
await removePath(join(target, "src", "app", "[locale]", "twitter-image.tsx"));
|
|
212
|
+
await removePath(join(target, "cloudflare", "templates"));
|
|
213
|
+
}
|
|
214
|
+
function hasModule(selection, id) {
|
|
215
|
+
return selection.modules.includes(id);
|
|
216
|
+
}
|
|
217
|
+
async function pruneModules(target, selection, catalog) {
|
|
218
|
+
for (const id of selection.excludedModules) {
|
|
219
|
+
await removePath(join(target, "src", "features", id));
|
|
220
|
+
for (const route of catalog.modules[id]?.routes ?? []) {
|
|
221
|
+
await removePath(dirname(join(target, ...route.split("/"))));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (!hasModule(selection, "admin")) {
|
|
225
|
+
await removePath(join(target, "src", "app", "[locale]", "(admin)"));
|
|
226
|
+
}
|
|
227
|
+
if (!hasModule(selection, "marketing")) {
|
|
228
|
+
await removePath(join(target, "src", "app", "[locale]", "(marketing)"));
|
|
229
|
+
await removePath(join(target, "src", "app", "[locale]", "docs"));
|
|
230
|
+
await removePath(join(target, "src", "app", "[locale]", "opengraph-image.tsx"));
|
|
231
|
+
await removePath(join(target, "src", "app", "[locale]", "twitter-image.tsx"));
|
|
232
|
+
await removePath(join(target, "src", "app", "api", "search"));
|
|
233
|
+
await removePath(join(target, "src", "lib", "source.ts"));
|
|
234
|
+
await removePath(join(target, "src", "types", "fumadocs-source.d.ts"));
|
|
235
|
+
await removePath(join(target, "source.config.ts"));
|
|
236
|
+
await removePath(join(target, "src", "content"));
|
|
237
|
+
await writeText(join(target, "next.config.mjs"), `import createNextIntlPlugin from "next-intl/plugin";
|
|
238
|
+
|
|
239
|
+
const withNextIntl = createNextIntlPlugin("./src/i18n/request.ts");
|
|
240
|
+
|
|
241
|
+
/** @type {import("next").NextConfig} */
|
|
242
|
+
const nextConfig = {
|
|
243
|
+
output: "standalone",
|
|
244
|
+
serverExternalPackages: [
|
|
245
|
+
"@neondatabase/serverless",
|
|
246
|
+
"pino",
|
|
247
|
+
"pino-pretty",
|
|
248
|
+
"ws",
|
|
249
|
+
],
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
export default withNextIntl(nextConfig);
|
|
253
|
+
`);
|
|
254
|
+
}
|
|
255
|
+
if (selection.adapters.jobs !== "jobs:inngest") {
|
|
256
|
+
await removePath(join(target, "src", "app", "api", "inngest"));
|
|
257
|
+
}
|
|
258
|
+
if (!selection.adapters.ai) {
|
|
259
|
+
await removePath(join(target, "src", "lib", "ai"));
|
|
260
|
+
}
|
|
261
|
+
if (!hasModule(selection, "payment")) {
|
|
262
|
+
await removePath(join(target, "src", "config", "payment.ts"));
|
|
263
|
+
}
|
|
264
|
+
if (!hasModule(selection, "subscription")) {
|
|
265
|
+
await removePath(join(target, "src", "config", "subscription-plan.ts"));
|
|
266
|
+
}
|
|
267
|
+
const paymentExports = hasModule(selection, "payment")
|
|
268
|
+
? `export { findPlanByPriceId, getBaseUrl, getPlanPrice, getPricingConfig, getPricingPlans, PRICE_IDS, paymentConfig } from "./payment";\n`
|
|
269
|
+
: "";
|
|
270
|
+
await writeText(join(target, "src", "config", "index.ts"), `export { adminConfig, adminNav, dashboardConfig, dashboardNav, footerNav, mainNav, marketingConfig, type NavGroup, type NavItem, type ProductNavGroup, type ProductNavItem, productsNav } from "./nav";\n${paymentExports}export { type SiteConfig, siteConfig } from "./site";\n`);
|
|
271
|
+
}
|
|
272
|
+
async function rewriteAdapters(target, selection, catalog) {
|
|
273
|
+
const selectedIds = new Set(Object.values(selection.adapters));
|
|
274
|
+
const selectedSources = new Set([...selectedIds].map((id) => catalog.adapters[id]?.source));
|
|
275
|
+
selectedSources.add("src/adapters/rate-limit/noop.ts");
|
|
276
|
+
for (const adapter of Object.values(catalog.adapters)) {
|
|
277
|
+
if (!selectedSources.has(adapter.source)) {
|
|
278
|
+
await removePath(join(target, ...adapter.source.split("/")));
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
for (const kind of SERVICE_KINDS) {
|
|
282
|
+
const id = selection.adapters[kind];
|
|
283
|
+
const family = join(target, "src", "adapters", kind);
|
|
284
|
+
if (!id) {
|
|
285
|
+
await removePath(family);
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
let exports = adapterExports[id];
|
|
289
|
+
if (!exports)
|
|
290
|
+
throw new Error(`Missing adapter export template: ${id}`);
|
|
291
|
+
if (kind === "rate-limit" && id !== "rate-limit:noop") {
|
|
292
|
+
exports += adapterExports["rate-limit:noop"];
|
|
293
|
+
}
|
|
294
|
+
await writeText(join(family, "index.ts"), exports);
|
|
295
|
+
}
|
|
296
|
+
const manifests = [...selectedIds].map((id) => {
|
|
297
|
+
const item = catalog.adapters[id];
|
|
298
|
+
if (!item)
|
|
299
|
+
throw new Error(`Unknown selected adapter: ${id}`);
|
|
300
|
+
return { id, ...item };
|
|
301
|
+
});
|
|
302
|
+
const registrySource = `import { defineServiceAdapter, type ServiceAdapterManifest, validateServiceAdapterManifests } from "@/core/services";
|
|
303
|
+
|
|
304
|
+
const manifests = ${JSON.stringify(manifests, null, 2)} as const satisfies readonly ServiceAdapterManifest[];
|
|
305
|
+
export const serviceAdapterRegistry = manifests.map((manifest) => defineServiceAdapter(manifest));
|
|
306
|
+
const errors = validateServiceAdapterManifests(serviceAdapterRegistry);
|
|
307
|
+
if (errors.length > 0) throw new Error(\`Service adapter registry is invalid:\\n\${errors.join("\\n")}\`);
|
|
308
|
+
|
|
309
|
+
export const defaultServiceAdapters = ${JSON.stringify(selection.adapters, null, 2)} as const;
|
|
310
|
+
export type ServiceAdapterId = (typeof serviceAdapterRegistry)[number]["id"];
|
|
311
|
+
`;
|
|
312
|
+
await writeText(join(target, "src", "adapters", "registry.ts"), registrySource);
|
|
313
|
+
await writeText(join(target, "src", "adapters", "index.ts"), `export { defaultServiceAdapters, serviceAdapterRegistry } from "./registry";\nexport type { ServiceAdapterId } from "./registry";\n`);
|
|
314
|
+
}
|
|
315
|
+
async function rewriteServices(target, selection) {
|
|
316
|
+
let index = "";
|
|
317
|
+
for (const kind of SERVICE_KINDS) {
|
|
318
|
+
const id = selection.adapters[kind];
|
|
319
|
+
const path = join(target, "src", "services", `${kind}.ts`);
|
|
320
|
+
if (!id) {
|
|
321
|
+
await removePath(path);
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
const source = serviceSources[id];
|
|
325
|
+
if (!source)
|
|
326
|
+
throw new Error(`Missing service template: ${id}`);
|
|
327
|
+
await writeText(path, source);
|
|
328
|
+
index += serviceExportNames[kind];
|
|
329
|
+
}
|
|
330
|
+
await writeText(join(target, "src", "services", "index.ts"), index);
|
|
331
|
+
}
|
|
332
|
+
async function rewriteModuleRegistry(target, selection) {
|
|
333
|
+
const imports = selection.modules
|
|
334
|
+
.map((id) => `import { ${id.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())}Module } from "@/features/${id}/manifest";`)
|
|
335
|
+
.join("\n");
|
|
336
|
+
const entries = selection.modules
|
|
337
|
+
.map((id) => ` ${id.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())}Module,`)
|
|
338
|
+
.join("\n");
|
|
339
|
+
await writeText(join(target, "src", "modules", "registry.ts"), `import { createModuleRegistry } from "@/core/modules";\n${imports}\n\nexport const moduleRegistry = createModuleRegistry([\n${entries}\n]);\n`);
|
|
340
|
+
await writeText(join(target, "src", "modules", "presets.ts"), `import type { ModuleId } from "@/core/modules";\n\nexport const modulePresets = {\n generated: ${JSON.stringify(selection.modules)},\n} as const satisfies Record<string, readonly ModuleId[]>;\n\nexport type ModulePresetName = keyof typeof modulePresets;\n`);
|
|
341
|
+
}
|
|
342
|
+
async function rewriteSchema(target, selection, catalog) {
|
|
343
|
+
const groups = [
|
|
344
|
+
...new Set(selection.modules.flatMap((id) => catalog.modules[id]?.schema ?? [])),
|
|
345
|
+
];
|
|
346
|
+
await writeText(join(target, "src", "db", "schema", "index.ts"), `${groups.map((group) => `export * from "./${group}";`).join("\n")}\n`);
|
|
347
|
+
const selected = new Set(groups);
|
|
348
|
+
for (const file of await readdir(join(target, "src", "db", "schema"))) {
|
|
349
|
+
const group = file.replace(/\.ts$/, "");
|
|
350
|
+
if (group !== "index" && !selected.has(group)) {
|
|
351
|
+
await removePath(join(target, "src", "db", "schema", file));
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
async function rewriteTranslations(target, selection, catalog) {
|
|
356
|
+
const namespaces = new Set(selection.modules.flatMap((id) => catalog.modules[id]?.translations ?? []));
|
|
357
|
+
for (const locale of ["en", "zh"]) {
|
|
358
|
+
const path = join(target, "messages", `${locale}.json`);
|
|
359
|
+
const current = JSON.parse(await readFile(path, "utf8"));
|
|
360
|
+
await writeJson(path, Object.fromEntries(Object.entries(current).filter(([key]) => namespaces.has(key))));
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
function localeLayout(selection) {
|
|
364
|
+
const analyticsImport = hasModule(selection, "analytics")
|
|
365
|
+
? 'import { Analytics } from "@/features/analytics";\n'
|
|
366
|
+
: "";
|
|
367
|
+
const marketingImport = hasModule(selection, "marketing")
|
|
368
|
+
? 'import { CookieConsent } from "@/features/marketing";\n'
|
|
369
|
+
: "";
|
|
370
|
+
return `import type { Metadata } from "next";
|
|
371
|
+
import { notFound } from "next/navigation";
|
|
372
|
+
import { NextIntlClientProvider } from "next-intl";
|
|
373
|
+
import { getMessages } from "next-intl/server";
|
|
374
|
+
import { Toaster } from "sonner";
|
|
375
|
+
import { siteConfig } from "@/config";
|
|
376
|
+
${analyticsImport}${marketingImport}import { Providers } from "@/features/shared";
|
|
377
|
+
import { routing } from "@/i18n/routing";
|
|
378
|
+
|
|
379
|
+
export function generateStaticParams() {
|
|
380
|
+
return routing.locales.map((locale) => ({ locale }));
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }): Promise<Metadata> {
|
|
384
|
+
const { locale } = await params;
|
|
385
|
+
const baseUrl = siteConfig.url;
|
|
386
|
+
return { alternates: { canonical: \`\${baseUrl}/\${locale}\`, languages: { en: \`\${baseUrl}/en\`, zh: \`\${baseUrl}/zh\`, "x-default": \`\${baseUrl}/en\` } } };
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export default async function LocaleLayout({ children, params }: { children: React.ReactNode; params: Promise<{ locale: string }> }) {
|
|
390
|
+
const { locale } = await params;
|
|
391
|
+
if (!routing.locales.includes(locale as "en" | "zh")) notFound();
|
|
392
|
+
const messages = await getMessages();
|
|
393
|
+
return (
|
|
394
|
+
<NextIntlClientProvider messages={messages}>
|
|
395
|
+
<Providers locale={locale}>
|
|
396
|
+
{children}
|
|
397
|
+
${hasModule(selection, "marketing") ? "<CookieConsent />" : ""}
|
|
398
|
+
<Toaster richColors position="top-right" />
|
|
399
|
+
${hasModule(selection, "analytics") ? "<Analytics />" : ""}
|
|
400
|
+
</Providers>
|
|
401
|
+
</NextIntlClientProvider>
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
`;
|
|
405
|
+
}
|
|
406
|
+
function dashboardLayout(selection) {
|
|
407
|
+
const creditsImport = hasModule(selection, "credits")
|
|
408
|
+
? 'import { CreditBalanceBadge } from "@/features/credits";\n'
|
|
409
|
+
: "";
|
|
410
|
+
const subscriptionImport = hasModule(selection, "subscription")
|
|
411
|
+
? 'import { CurrentPlanBadge } from "@/features/subscription";\n'
|
|
412
|
+
: "";
|
|
413
|
+
const props = [
|
|
414
|
+
hasModule(selection, "credits")
|
|
415
|
+
? "compactAccountBadge={<CreditBalanceBadge />}"
|
|
416
|
+
: "",
|
|
417
|
+
hasModule(selection, "subscription")
|
|
418
|
+
? "accountPlanBadge={<CurrentPlanBadge />}"
|
|
419
|
+
: "",
|
|
420
|
+
]
|
|
421
|
+
.filter(Boolean)
|
|
422
|
+
.join(" ");
|
|
423
|
+
return `import { redirect } from "next/navigation";
|
|
424
|
+
${creditsImport}import { DashboardMainWrapper, DashboardSidebar, SidebarProvider } from "@/features/dashboard";
|
|
425
|
+
${subscriptionImport}import { getServerSession } from "@/lib/auth/server";
|
|
426
|
+
|
|
427
|
+
export default async function DashboardLayout({ children }: { children: React.ReactNode }) {
|
|
428
|
+
const session = await getServerSession();
|
|
429
|
+
if (!session?.user) redirect("/sign-in");
|
|
430
|
+
return (
|
|
431
|
+
<SidebarProvider>
|
|
432
|
+
<div className="min-h-screen bg-muted/40">
|
|
433
|
+
<DashboardSidebar ${props} />
|
|
434
|
+
<DashboardMainWrapper>{children}</DashboardMainWrapper>
|
|
435
|
+
</div>
|
|
436
|
+
</SidebarProvider>
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
`;
|
|
440
|
+
}
|
|
441
|
+
const BASIC_DASHBOARD = `import { getTranslations } from "next-intl/server";
|
|
442
|
+
|
|
443
|
+
export default async function DashboardPage({ params }: { params: Promise<{ locale: string }> }) {
|
|
444
|
+
const { locale } = await params;
|
|
445
|
+
const t = await getTranslations({ locale, namespace: "DashboardPages.dashboard" });
|
|
446
|
+
return (
|
|
447
|
+
<div className="space-y-2">
|
|
448
|
+
<h1 className="text-2xl font-bold tracking-tight">{t("title")}</h1>
|
|
449
|
+
<p className="text-muted-foreground">{t("subtitle")}</p>
|
|
450
|
+
</div>
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
`;
|
|
454
|
+
function sitemapSource(selection) {
|
|
455
|
+
const imports = [
|
|
456
|
+
'import type { MetadataRoute } from "next";',
|
|
457
|
+
'import { siteConfig } from "@/config";',
|
|
458
|
+
hasModule(selection, "pseo")
|
|
459
|
+
? 'import { getAllPseoParams } from "@/features/pseo";'
|
|
460
|
+
: "",
|
|
461
|
+
hasModule(selection, "blog") || hasModule(selection, "marketing")
|
|
462
|
+
? `import { ${[
|
|
463
|
+
hasModule(selection, "blog") ? "getAllBlogSlugs" : "",
|
|
464
|
+
hasModule(selection, "marketing") ? "getAllLegalSlugs" : "",
|
|
465
|
+
]
|
|
466
|
+
.filter(Boolean)
|
|
467
|
+
.join(", ")} } from "@/lib/source";`
|
|
468
|
+
: "",
|
|
469
|
+
]
|
|
470
|
+
.filter(Boolean)
|
|
471
|
+
.join("\n");
|
|
472
|
+
const staticPaths = [
|
|
473
|
+
hasModule(selection, "marketing") ? "" : undefined,
|
|
474
|
+
hasModule(selection, "blog") ? "/blog" : undefined,
|
|
475
|
+
hasModule(selection, "pseo") ? "/pseo" : undefined,
|
|
476
|
+
].filter((value) => value !== undefined);
|
|
477
|
+
const dynamic = [
|
|
478
|
+
hasModule(selection, "blog")
|
|
479
|
+
? ` for (const { locale, slug } of getAllBlogSlugs()) routes.push({ url: \`\${baseUrl}/\${locale}/blog/\${slug}\`, lastModified: now });`
|
|
480
|
+
: "",
|
|
481
|
+
hasModule(selection, "marketing")
|
|
482
|
+
? ` for (const { locale, slug } of getAllLegalSlugs()) routes.push({ url: \`\${baseUrl}/\${locale}/legal/\${slug}\`, lastModified: now });`
|
|
483
|
+
: "",
|
|
484
|
+
hasModule(selection, "pseo")
|
|
485
|
+
? ` for (const { locale, slug } of getAllPseoParams()) routes.push({ url: \`\${baseUrl}/\${locale}/pseo/\${slug}\`, lastModified: now });`
|
|
486
|
+
: "",
|
|
487
|
+
]
|
|
488
|
+
.filter(Boolean)
|
|
489
|
+
.join("\n");
|
|
490
|
+
return `${imports}
|
|
491
|
+
|
|
492
|
+
const locales = ["en", "zh"] as const;
|
|
493
|
+
|
|
494
|
+
export default function sitemap(): MetadataRoute.Sitemap {
|
|
495
|
+
const baseUrl = siteConfig.url;
|
|
496
|
+
const now = new Date();
|
|
497
|
+
const routes: MetadataRoute.Sitemap = [];
|
|
498
|
+
for (const locale of locales) {
|
|
499
|
+
for (const path of ${JSON.stringify(staticPaths)}) {
|
|
500
|
+
routes.push({ url: \`\${baseUrl}/\${locale}\${path}\`, lastModified: now });
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
${dynamic}
|
|
504
|
+
return routes;
|
|
505
|
+
}
|
|
506
|
+
`;
|
|
507
|
+
}
|
|
508
|
+
async function rewriteComposition(target, selection) {
|
|
509
|
+
await writeText(join(target, "src", "app", "[locale]", "layout.tsx"), localeLayout(selection));
|
|
510
|
+
await writeText(join(target, "src", "app", "[locale]", "(dashboard)", "layout.tsx"), dashboardLayout(selection));
|
|
511
|
+
if (!hasModule(selection, "credits")) {
|
|
512
|
+
await writeText(join(target, "src", "app", "[locale]", "(dashboard)", "dashboard", "page.tsx"), BASIC_DASHBOARD);
|
|
513
|
+
}
|
|
514
|
+
await writeText(join(target, "src", "app", "sitemap.ts"), sitemapSource(selection));
|
|
515
|
+
}
|
|
516
|
+
async function rewriteEnvironment(target, selection) {
|
|
517
|
+
const keys = [
|
|
518
|
+
"APP_VERSION",
|
|
519
|
+
"NEXT_PUBLIC_APP_URL",
|
|
520
|
+
"NEXT_PUBLIC_APP_NAME",
|
|
521
|
+
...selection.env,
|
|
522
|
+
...(selection.target === "docker"
|
|
523
|
+
? ["PORT", "POSTGRES_DB", "POSTGRES_PASSWORD", "POSTGRES_USER"]
|
|
524
|
+
: selection.target === "server"
|
|
525
|
+
? ["PORT"]
|
|
526
|
+
: []),
|
|
527
|
+
];
|
|
528
|
+
const unique = [...new Set(keys)]
|
|
529
|
+
.filter((key) => selection.target !== "cloudflare" ||
|
|
530
|
+
(key !== "NEXT_PUBLIC_SENTRY_DSN" && key !== "SENTRY_AUTH_TOKEN"))
|
|
531
|
+
.sort();
|
|
532
|
+
await writeText(join(target, ".env.example"), `# Generated for the ${selection.preset} preset (${selection.target}).\n${unique
|
|
533
|
+
.map((key) => `${key}=${ENV_DEFAULTS[key] ?? ""}`)
|
|
534
|
+
.join("\n")}\n`);
|
|
535
|
+
}
|
|
536
|
+
async function rewriteDeployment(target, selection) {
|
|
537
|
+
const removeDocker = async () => {
|
|
538
|
+
await removePath(join(target, ".dockerignore"));
|
|
539
|
+
await removePath(join(target, "compose.yaml"));
|
|
540
|
+
await removePath(join(target, "Dockerfile"));
|
|
541
|
+
await removePath(join(target, "deploy", "docker"));
|
|
542
|
+
};
|
|
543
|
+
const removeServer = async () => {
|
|
544
|
+
await removePath(join(target, "deploy", "server"));
|
|
545
|
+
};
|
|
546
|
+
const removeVercel = async () => {
|
|
547
|
+
await removePath(join(target, "deploy", "vercel"));
|
|
548
|
+
await removePath(join(target, "vercel.json"));
|
|
549
|
+
};
|
|
550
|
+
const removeCloudflare = async () => {
|
|
551
|
+
await removePath(join(target, "cloudflare"));
|
|
552
|
+
await removePath(join(target, "open-next.config.ts"));
|
|
553
|
+
await removePath(join(target, "src", "lib", "cloudflare"));
|
|
554
|
+
await removePath(join(target, "wrangler.jsonc"));
|
|
555
|
+
};
|
|
556
|
+
if (selection.target === "docker") {
|
|
557
|
+
await removeServer();
|
|
558
|
+
await removeVercel();
|
|
559
|
+
await removeCloudflare();
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
if (selection.target === "server") {
|
|
563
|
+
await removeDocker();
|
|
564
|
+
await removeVercel();
|
|
565
|
+
await removeCloudflare();
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
if (selection.target === "vercel") {
|
|
569
|
+
await removeDocker();
|
|
570
|
+
await removeServer();
|
|
571
|
+
await removeCloudflare();
|
|
572
|
+
const vercelConfig = {
|
|
573
|
+
$schema: "https://openapi.vercel.sh/vercel.json",
|
|
574
|
+
framework: "nextjs",
|
|
575
|
+
buildCommand: "pnpm build",
|
|
576
|
+
...(hasModule(selection, "credits")
|
|
577
|
+
? {
|
|
578
|
+
crons: [
|
|
579
|
+
{
|
|
580
|
+
path: "/api/jobs/credits/expire",
|
|
581
|
+
schedule: "0 0 * * *",
|
|
582
|
+
},
|
|
583
|
+
],
|
|
584
|
+
}
|
|
585
|
+
: {}),
|
|
586
|
+
};
|
|
587
|
+
await writeJson(join(target, "vercel.json"), vercelConfig);
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
await removePath(join(target, "deploy"));
|
|
591
|
+
await removeDocker();
|
|
592
|
+
await removeVercel();
|
|
593
|
+
await writeJson(join(target, "wrangler.jsonc"), cloudflareConfig(target, selection));
|
|
594
|
+
await writeText(join(target, "src", "db", "index.ts"), cloudflareDatabaseSource());
|
|
595
|
+
await writeText(join(target, "src", "lib", "monitoring", "index.ts"), cloudflareMonitoringSource());
|
|
596
|
+
await removePath(join(target, "src", "instrumentation.ts"));
|
|
597
|
+
await removePath(join(target, "sentry.client.config.ts"));
|
|
598
|
+
await removePath(join(target, "sentry.edge.config.ts"));
|
|
599
|
+
await removePath(join(target, "sentry.server.config.ts"));
|
|
600
|
+
await rewriteCloudflareRuntime(target, selection);
|
|
601
|
+
}
|
|
602
|
+
async function rewritePackage(target, selection, catalog) {
|
|
603
|
+
const path = join(target, "package.json");
|
|
604
|
+
const packageJson = JSON.parse(await readFile(path, "utf8"));
|
|
605
|
+
const allAdapterPackages = new Set(Object.values(catalog.adapters).flatMap((adapter) => adapter.packages));
|
|
606
|
+
const selectedPackages = new Set(selection.packages);
|
|
607
|
+
for (const dependency of allAdapterPackages) {
|
|
608
|
+
if (!selectedPackages.has(dependency))
|
|
609
|
+
delete packageJson.dependencies[dependency];
|
|
610
|
+
}
|
|
611
|
+
if (!hasModule(selection, "analytics")) {
|
|
612
|
+
delete packageJson.dependencies["@next/third-parties"];
|
|
613
|
+
}
|
|
614
|
+
if (!hasModule(selection, "marketing")) {
|
|
615
|
+
delete packageJson.dependencies["fumadocs-core"];
|
|
616
|
+
delete packageJson.dependencies["fumadocs-mdx"];
|
|
617
|
+
delete packageJson.scripts.postinstall;
|
|
618
|
+
}
|
|
619
|
+
else {
|
|
620
|
+
packageJson.scripts.postinstall = "fumadocs-mdx";
|
|
621
|
+
}
|
|
622
|
+
packageJson.name = projectName(target);
|
|
623
|
+
packageJson.private = true;
|
|
624
|
+
delete packageJson.scripts["verify:generated"];
|
|
625
|
+
delete packageJson.scripts["deploy:check"];
|
|
626
|
+
delete packageJson.scripts["deploy:server:build"];
|
|
627
|
+
delete packageJson.scripts["verify:health"];
|
|
628
|
+
packageJson.scripts["db:generate:init"] = "drizzle-kit generate --name init";
|
|
629
|
+
if (selection.target !== "cloudflare") {
|
|
630
|
+
packageJson.scripts["deploy:check"] = "node deploy/check-health.mjs";
|
|
631
|
+
packageJson.scripts["verify:health"] =
|
|
632
|
+
"node deploy/verify-production-health.mjs";
|
|
633
|
+
}
|
|
634
|
+
if (selection.target === "server") {
|
|
635
|
+
packageJson.scripts["deploy:server:build"] = "bash deploy/server/build.sh";
|
|
636
|
+
}
|
|
637
|
+
if (selection.target === "cloudflare") {
|
|
638
|
+
delete packageJson.dependencies["@react-email/components"];
|
|
639
|
+
delete packageJson.dependencies["@react-email/render"];
|
|
640
|
+
delete packageJson.dependencies["@react-email/tailwind"];
|
|
641
|
+
delete packageJson.dependencies["@sentry/nextjs"];
|
|
642
|
+
delete packageJson.dependencies.pino;
|
|
643
|
+
delete packageJson.dependencies.pg;
|
|
644
|
+
delete packageJson.dependencies.ws;
|
|
645
|
+
delete packageJson.devDependencies["@types/pg"];
|
|
646
|
+
delete packageJson.devDependencies["@types/ws"];
|
|
647
|
+
delete packageJson.devDependencies["pino-pretty"];
|
|
648
|
+
packageJson.scripts["cf:build"] = "opennextjs-cloudflare build";
|
|
649
|
+
packageJson.scripts["cf:deploy"] =
|
|
650
|
+
"opennextjs-cloudflare build && wrangler deploy --minify";
|
|
651
|
+
packageJson.scripts["cf:preview"] =
|
|
652
|
+
"opennextjs-cloudflare build && wrangler dev";
|
|
653
|
+
packageJson.scripts["cf:types"] = "wrangler types";
|
|
654
|
+
}
|
|
655
|
+
else {
|
|
656
|
+
delete packageJson.scripts["cf:build"];
|
|
657
|
+
delete packageJson.scripts["cf:deploy"];
|
|
658
|
+
delete packageJson.scripts["cf:preview"];
|
|
659
|
+
delete packageJson.scripts["cf:types"];
|
|
660
|
+
delete packageJson.devDependencies["@opennextjs/cloudflare"];
|
|
661
|
+
delete packageJson.devDependencies.wrangler;
|
|
662
|
+
}
|
|
663
|
+
await writeJson(path, packageJson);
|
|
664
|
+
}
|
|
665
|
+
function isRecord(value) {
|
|
666
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
667
|
+
}
|
|
668
|
+
async function rewriteLockfile(target) {
|
|
669
|
+
const packageJson = JSON.parse(await readFile(join(target, "package.json"), "utf8"));
|
|
670
|
+
const lockfilePath = join(target, "pnpm-lock.yaml");
|
|
671
|
+
const lockfile = parse(await readFile(lockfilePath, "utf8"));
|
|
672
|
+
if (!isRecord(lockfile) || !isRecord(lockfile.importers)) {
|
|
673
|
+
throw new Error("Generated pnpm lockfile has no importers");
|
|
674
|
+
}
|
|
675
|
+
const rootImporter = lockfile.importers["."];
|
|
676
|
+
if (!isRecord(rootImporter)) {
|
|
677
|
+
throw new Error("Generated pnpm lockfile has no root importer");
|
|
678
|
+
}
|
|
679
|
+
for (const group of [
|
|
680
|
+
"dependencies",
|
|
681
|
+
"devDependencies",
|
|
682
|
+
"optionalDependencies",
|
|
683
|
+
]) {
|
|
684
|
+
const selected = packageJson[group];
|
|
685
|
+
const locked = rootImporter[group];
|
|
686
|
+
if (selected === undefined) {
|
|
687
|
+
delete rootImporter[group];
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
if (!isRecord(selected) || !isRecord(locked)) {
|
|
691
|
+
throw new Error(`Generated pnpm lockfile is missing ${group}`);
|
|
692
|
+
}
|
|
693
|
+
rootImporter[group] = Object.fromEntries(Object.entries(selected).map(([name, specifier]) => {
|
|
694
|
+
const entry = locked[name];
|
|
695
|
+
if (!isRecord(entry) || typeof specifier !== "string") {
|
|
696
|
+
throw new Error(`Generated pnpm lockfile is missing ${group}.${name}`);
|
|
697
|
+
}
|
|
698
|
+
return [name, { ...entry, specifier }];
|
|
699
|
+
}));
|
|
700
|
+
}
|
|
701
|
+
await writeText(lockfilePath, stringify(lockfile, { lineWidth: 0 }));
|
|
702
|
+
}
|
|
703
|
+
async function writeSmokeTest(target, manifest) {
|
|
704
|
+
await removePath(join(target, "src", "test"));
|
|
705
|
+
await writeText(join(target, "src", "test", "generated.test.ts"), `import { describe, expect, it } from "vitest";\nimport manifest from "../../nextdevtpl.generated.json";\n\ndescribe("generated project", () => {\n it("records a stable selection", () => {\n expect(manifest.modules).toEqual(${JSON.stringify(manifest.modules)});\n expect(manifest.target).toBe(${JSON.stringify(manifest.target)});\n });\n});\n`);
|
|
706
|
+
await writeText(join(target, "vitest.config.ts"), `import { defineConfig } from "vitest/config";\n\nexport default defineConfig({ test: { environment: "node", include: ["src/test/**/*.test.ts"] } });\n`);
|
|
707
|
+
}
|
|
708
|
+
export async function generateProject(options) {
|
|
709
|
+
const discovered = await findGeneratorAssets();
|
|
710
|
+
const assets = {
|
|
711
|
+
catalogPath: options.catalogPath ?? discovered.catalogPath,
|
|
712
|
+
templateManifestPath: options.templateManifestPath ?? discovered.templateManifestPath,
|
|
713
|
+
templateRoot: options.templateRoot ?? discovered.templateRoot,
|
|
714
|
+
};
|
|
715
|
+
const catalog = await loadCatalog(assets.catalogPath);
|
|
716
|
+
const selection = createProjectSelection(catalog, options);
|
|
717
|
+
const target = await prepareTarget(options.targetDirectory);
|
|
718
|
+
try {
|
|
719
|
+
await copyTemplate(assets.templateRoot, assets.templateManifestPath, target.path);
|
|
720
|
+
await removePath(join(target.path, "drizzle"));
|
|
721
|
+
await pruneModules(target.path, selection, catalog);
|
|
722
|
+
await rewriteAdapters(target.path, selection, catalog);
|
|
723
|
+
await rewriteServices(target.path, selection);
|
|
724
|
+
await rewriteModuleRegistry(target.path, selection);
|
|
725
|
+
await rewriteSchema(target.path, selection, catalog);
|
|
726
|
+
await rewriteTranslations(target.path, selection, catalog);
|
|
727
|
+
await rewriteComposition(target.path, selection);
|
|
728
|
+
await rewriteEnvironment(target.path, selection);
|
|
729
|
+
await rewriteDeployment(target.path, selection);
|
|
730
|
+
await rewritePackage(target.path, selection, catalog);
|
|
731
|
+
await rewriteLockfile(target.path);
|
|
732
|
+
const manifest = {
|
|
733
|
+
catalogVersion: catalog.version,
|
|
734
|
+
preset: selection.preset,
|
|
735
|
+
target: selection.target,
|
|
736
|
+
modules: selection.modules,
|
|
737
|
+
adapters: selection.adapters,
|
|
738
|
+
bindings: selection.bindings,
|
|
739
|
+
};
|
|
740
|
+
await writeJson(join(target.path, "nextdevtpl.generated.json"), manifest);
|
|
741
|
+
await writeSmokeTest(target.path, manifest);
|
|
742
|
+
if (options.install !== false) {
|
|
743
|
+
const packageManager = options.packageManager ?? "pnpm";
|
|
744
|
+
await installDependencies(target.path, packageManager);
|
|
745
|
+
await runPackageScript(target.path, packageManager, "db:generate:init");
|
|
746
|
+
if (selection.target === "cloudflare") {
|
|
747
|
+
await runPackageScript(target.path, packageManager, "cf:types");
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
return { manifest, targetDirectory: target.path };
|
|
751
|
+
}
|
|
752
|
+
catch (error) {
|
|
753
|
+
if (target.created)
|
|
754
|
+
await rm(target.path, { recursive: true, force: true });
|
|
755
|
+
throw error;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
//# sourceMappingURL=generate.js.map
|