@salifbiaye/create-boutique 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/cli/dist/commands.js +382 -0
- package/cli/dist/create.js +194 -0
- package/cli/dist/docker.js +19 -0
- package/cli/dist/generate.js +322 -0
- package/cli/dist/guide.js +72 -0
- package/cli/dist/index.js +156 -0
- package/cli/dist/paths.js +29 -0
- package/cli/dist/ports.js +54 -0
- package/cli/dist/project.js +119 -0
- package/cli/dist/themes.js +24 -0
- package/docs/fiche-client.en.html +137 -0
- package/docs/fiche-client.html +137 -0
- package/docs/guide-complet.en.html +259 -0
- package/docs/guide-complet.fr.html +259 -0
- package/package.json +51 -0
- package/templates/backend/.dockerignore +9 -0
- package/templates/backend/Dockerfile +30 -0
- package/templates/backend/README.md +78 -0
- package/templates/backend/TEMPLATE_VERSION +1 -0
- package/templates/backend/docker/entrypoint.sh +28 -0
- package/templates/backend/instrumentation.ts +24 -0
- package/templates/backend/medusa-config.ts +114 -0
- package/templates/backend/package-lock.json +16222 -0
- package/templates/backend/package.json +39 -0
- package/templates/backend/src/admin/README.md +33 -0
- package/templates/backend/src/admin/components/image-field.tsx +61 -0
- package/templates/backend/src/admin/i18n/README.md +58 -0
- package/templates/backend/src/admin/i18n/index.ts +8 -0
- package/templates/backend/src/admin/i18n/json/en.json +66 -0
- package/templates/backend/src/admin/i18n/json/fr.json +66 -0
- package/templates/backend/src/admin/lib/client.ts +9 -0
- package/templates/backend/src/admin/routes/boutique-campaigns/campaign-drawer.tsx +286 -0
- package/templates/backend/src/admin/routes/boutique-campaigns/page.tsx +113 -0
- package/templates/backend/src/admin/tsconfig.json +24 -0
- package/templates/backend/src/admin/vite-env.d.ts +1 -0
- package/templates/backend/src/admin/widgets/category-image.tsx +56 -0
- package/templates/backend/src/api/README.md +135 -0
- package/templates/backend/src/api/admin/boutique-campaigns/[salesChannelId]/route.ts +30 -0
- package/templates/backend/src/api/admin/boutique-campaigns/route.ts +24 -0
- package/templates/backend/src/api/middlewares.ts +28 -0
- package/templates/backend/src/api/rate-limit.ts +30 -0
- package/templates/backend/src/api/route.ts +6 -0
- package/templates/backend/src/api/store/campaign/route.ts +25 -0
- package/templates/backend/src/api/store/order-lookup/route.ts +74 -0
- package/templates/backend/src/emails/en.json +64 -0
- package/templates/backend/src/emails/fr.json +64 -0
- package/templates/backend/src/jobs/README.md +38 -0
- package/templates/backend/src/lib/emails.ts +94 -0
- package/templates/backend/src/lib/notify.ts +148 -0
- package/templates/backend/src/lib/project.ts +146 -0
- package/templates/backend/src/links/README.md +26 -0
- package/templates/backend/src/links/campaign-sales-channel.ts +9 -0
- package/templates/backend/src/modules/README.md +117 -0
- package/templates/backend/src/modules/campaign/index.ts +9 -0
- package/templates/backend/src/modules/campaign/migrations/.snapshot-boutique-campaign.json +272 -0
- package/templates/backend/src/modules/campaign/migrations/Migration20260928141440.ts +16 -0
- package/templates/backend/src/modules/campaign/migrations/Migration20260928145243.ts +13 -0
- package/templates/backend/src/modules/campaign/models/campaign.ts +28 -0
- package/templates/backend/src/modules/campaign/service.ts +6 -0
- package/templates/backend/src/modules/campaign/validators.ts +94 -0
- package/templates/backend/src/modules/resend/index.ts +6 -0
- package/templates/backend/src/modules/resend/service.ts +63 -0
- package/templates/backend/src/scripts/README.md +63 -0
- package/templates/backend/src/scripts/campaign-export.ts +15 -0
- package/templates/backend/src/scripts/campaign-import.ts +38 -0
- package/templates/backend/src/scripts/price-report.ts +34 -0
- package/templates/backend/src/scripts/seed.ts +418 -0
- package/templates/backend/src/scripts/storage.ts +63 -0
- package/templates/backend/src/subscribers/README.md +61 -0
- package/templates/backend/src/subscribers/account-emails.ts +27 -0
- package/templates/backend/src/subscribers/fulfillment-emails.ts +42 -0
- package/templates/backend/src/subscribers/order-emails.ts +12 -0
- package/templates/backend/src/subscribers/return-refund-emails.ts +50 -0
- package/templates/backend/src/workflows/README.md +81 -0
- package/templates/backend/src/workflows/steps/upsert-campaign.ts +53 -0
- package/templates/backend/src/workflows/upsert-campaign.ts +30 -0
- package/templates/backend/tsconfig.json +36 -0
- package/templates/storefront/.dockerignore +7 -0
- package/templates/storefront/.github/ISSUE_TEMPLATE/bug-report.yml +71 -0
- package/templates/storefront/.prettierrc +8 -0
- package/templates/storefront/Dockerfile +37 -0
- package/templates/storefront/LICENSE +21 -0
- package/templates/storefront/README.md +127 -0
- package/templates/storefront/locales/en.json +269 -0
- package/templates/storefront/locales/fr.json +269 -0
- package/templates/storefront/next-env.d.ts +7 -0
- package/templates/storefront/next-sitemap.js +19 -0
- package/templates/storefront/next.config.js +29 -0
- package/templates/storefront/package-lock.json +9031 -0
- package/templates/storefront/package.json +49 -0
- package/templates/storefront/postcss.config.js +6 -0
- package/templates/storefront/public/favicon.ico +0 -0
- package/templates/storefront/scripts/sync-design.mjs +30 -0
- package/templates/storefront/src/app/(checkout)/checkout/page.tsx +31 -0
- package/templates/storefront/src/app/(checkout)/layout.tsx +29 -0
- package/templates/storefront/src/app/(checkout)/not-found.tsx +20 -0
- package/templates/storefront/src/app/(main)/account/@dashboard/addresses/page.tsx +34 -0
- package/templates/storefront/src/app/(main)/account/@dashboard/loading.tsx +9 -0
- package/templates/storefront/src/app/(main)/account/@dashboard/orders/details/[id]/page.tsx +33 -0
- package/templates/storefront/src/app/(main)/account/@dashboard/orders/page.tsx +37 -0
- package/templates/storefront/src/app/(main)/account/@dashboard/page.tsx +22 -0
- package/templates/storefront/src/app/(main)/account/@dashboard/profile/page.tsx +53 -0
- package/templates/storefront/src/app/(main)/account/@login/page.tsx +12 -0
- package/templates/storefront/src/app/(main)/account/layout.tsx +20 -0
- package/templates/storefront/src/app/(main)/account/loading.tsx +9 -0
- package/templates/storefront/src/app/(main)/cart/loading.tsx +5 -0
- package/templates/storefront/src/app/(main)/cart/not-found.tsx +21 -0
- package/templates/storefront/src/app/(main)/cart/page.tsx +21 -0
- package/templates/storefront/src/app/(main)/categories/[...category]/page.tsx +55 -0
- package/templates/storefront/src/app/(main)/collections/[handle]/page.tsx +55 -0
- package/templates/storefront/src/app/(main)/layout.tsx +43 -0
- package/templates/storefront/src/app/(main)/legal/[page]/page.tsx +42 -0
- package/templates/storefront/src/app/(main)/not-found.tsx +21 -0
- package/templates/storefront/src/app/(main)/order/[id]/confirmed/loading.tsx +5 -0
- package/templates/storefront/src/app/(main)/order/[id]/confirmed/page.tsx +23 -0
- package/templates/storefront/src/app/(main)/order/[id]/transfer/[token]/accept/page.tsx +42 -0
- package/templates/storefront/src/app/(main)/order/[id]/transfer/[token]/decline/page.tsx +42 -0
- package/templates/storefront/src/app/(main)/order/[id]/transfer/[token]/page.tsx +35 -0
- package/templates/storefront/src/app/(main)/order/track/page.tsx +32 -0
- package/templates/storefront/src/app/(main)/page.tsx +68 -0
- package/templates/storefront/src/app/(main)/products/[handle]/page.tsx +86 -0
- package/templates/storefront/src/app/(main)/store/page.tsx +28 -0
- package/templates/storefront/src/app/api/health/route.ts +13 -0
- package/templates/storefront/src/app/layout.tsx +42 -0
- package/templates/storefront/src/app/not-found.tsx +21 -0
- package/templates/storefront/src/app/opengraph-image.jpg +0 -0
- package/templates/storefront/src/app/twitter-image.jpg +0 -0
- package/templates/storefront/src/instrumentation-node.ts +18 -0
- package/templates/storefront/src/instrumentation.ts +9 -0
- package/templates/storefront/src/lib/boutique/campaign.ts +116 -0
- package/templates/storefront/src/lib/boutique/config.ts +152 -0
- package/templates/storefront/src/lib/boutique/fonts.ts +29 -0
- package/templates/storefront/src/lib/boutique/i18n-client.tsx +34 -0
- package/templates/storefront/src/lib/boutique/i18n.ts +43 -0
- package/templates/storefront/src/lib/boutique/schema.ts +131 -0
- package/templates/storefront/src/lib/config.ts +39 -0
- package/templates/storefront/src/lib/constants.tsx +75 -0
- package/templates/storefront/src/lib/context/modal-context.tsx +34 -0
- package/templates/storefront/src/lib/data/cart.ts +434 -0
- package/templates/storefront/src/lib/data/categories.ts +49 -0
- package/templates/storefront/src/lib/data/collections.ts +59 -0
- package/templates/storefront/src/lib/data/cookies.ts +103 -0
- package/templates/storefront/src/lib/data/customer.ts +261 -0
- package/templates/storefront/src/lib/data/fulfillment.ts +68 -0
- package/templates/storefront/src/lib/data/onboarding.ts +9 -0
- package/templates/storefront/src/lib/data/order-lookup.ts +34 -0
- package/templates/storefront/src/lib/data/orders.ts +112 -0
- package/templates/storefront/src/lib/data/payment.ts +35 -0
- package/templates/storefront/src/lib/data/products.ts +127 -0
- package/templates/storefront/src/lib/data/regions.ts +70 -0
- package/templates/storefront/src/lib/data/variants.ts +38 -0
- package/templates/storefront/src/lib/hooks/use-in-view.tsx +29 -0
- package/templates/storefront/src/lib/hooks/use-toggle-state.tsx +46 -0
- package/templates/storefront/src/lib/util/compare-addresses.ts +28 -0
- package/templates/storefront/src/lib/util/get-percentage-diff.ts +6 -0
- package/templates/storefront/src/lib/util/get-product-price.ts +79 -0
- package/templates/storefront/src/lib/util/isEmpty.ts +11 -0
- package/templates/storefront/src/lib/util/medusa-error.ts +22 -0
- package/templates/storefront/src/lib/util/money.ts +35 -0
- package/templates/storefront/src/lib/util/product.ts +5 -0
- package/templates/storefront/src/lib/util/repeat.ts +5 -0
- package/templates/storefront/src/lib/util/sort-products.ts +50 -0
- package/templates/storefront/src/modules/account/components/account-info/index.tsx +140 -0
- package/templates/storefront/src/modules/account/components/account-nav/index.tsx +198 -0
- package/templates/storefront/src/modules/account/components/address-book/index.tsx +28 -0
- package/templates/storefront/src/modules/account/components/address-card/add-address.tsx +169 -0
- package/templates/storefront/src/modules/account/components/address-card/edit-address-modal.tsx +241 -0
- package/templates/storefront/src/modules/account/components/login/index.tsx +65 -0
- package/templates/storefront/src/modules/account/components/order-card/index.tsx +88 -0
- package/templates/storefront/src/modules/account/components/order-overview/index.tsx +47 -0
- package/templates/storefront/src/modules/account/components/overview/index.tsx +169 -0
- package/templates/storefront/src/modules/account/components/profile-billing-address/index.tsx +193 -0
- package/templates/storefront/src/modules/account/components/profile-email/index.tsx +75 -0
- package/templates/storefront/src/modules/account/components/profile-name/index.tsx +79 -0
- package/templates/storefront/src/modules/account/components/profile-password/index.tsx +72 -0
- package/templates/storefront/src/modules/account/components/profile-phone/index.tsx +74 -0
- package/templates/storefront/src/modules/account/components/register/index.tsx +107 -0
- package/templates/storefront/src/modules/account/components/transfer-request-form/index.tsx +83 -0
- package/templates/storefront/src/modules/account/templates/account-layout.tsx +43 -0
- package/templates/storefront/src/modules/account/templates/login-template.tsx +27 -0
- package/templates/storefront/src/modules/cart/components/cart-item-select/index.tsx +75 -0
- package/templates/storefront/src/modules/cart/components/empty-cart-message/index.tsx +25 -0
- package/templates/storefront/src/modules/cart/components/item/index.tsx +144 -0
- package/templates/storefront/src/modules/cart/components/sign-in-prompt/index.tsx +27 -0
- package/templates/storefront/src/modules/cart/templates/index.tsx +51 -0
- package/templates/storefront/src/modules/cart/templates/items.tsx +58 -0
- package/templates/storefront/src/modules/cart/templates/preview.tsx +51 -0
- package/templates/storefront/src/modules/cart/templates/summary.tsx +50 -0
- package/templates/storefront/src/modules/categories/templates/index.tsx +94 -0
- package/templates/storefront/src/modules/checkout/components/address-select/index.tsx +117 -0
- package/templates/storefront/src/modules/checkout/components/addresses/index.tsx +186 -0
- package/templates/storefront/src/modules/checkout/components/billing_address/index.tsx +114 -0
- package/templates/storefront/src/modules/checkout/components/country-select/index.tsx +50 -0
- package/templates/storefront/src/modules/checkout/components/discount-code/index.tsx +181 -0
- package/templates/storefront/src/modules/checkout/components/error-message/index.tsx +13 -0
- package/templates/storefront/src/modules/checkout/components/payment/index.tsx +261 -0
- package/templates/storefront/src/modules/checkout/components/payment-button/index.tsx +195 -0
- package/templates/storefront/src/modules/checkout/components/payment-container/index.tsx +130 -0
- package/templates/storefront/src/modules/checkout/components/payment-test/index.tsx +12 -0
- package/templates/storefront/src/modules/checkout/components/payment-wrapper/index.tsx +42 -0
- package/templates/storefront/src/modules/checkout/components/payment-wrapper/stripe-wrapper.tsx +54 -0
- package/templates/storefront/src/modules/checkout/components/review/index.tsx +54 -0
- package/templates/storefront/src/modules/checkout/components/shipping/index.tsx +417 -0
- package/templates/storefront/src/modules/checkout/components/shipping-address/index.tsx +219 -0
- package/templates/storefront/src/modules/checkout/components/submit-button/index.tsx +32 -0
- package/templates/storefront/src/modules/checkout/templates/checkout-form/index.tsx +38 -0
- package/templates/storefront/src/modules/checkout/templates/checkout-summary/index.tsx +31 -0
- package/templates/storefront/src/modules/collections/templates/index.tsx +44 -0
- package/templates/storefront/src/modules/common/components/cart-totals/index.tsx +84 -0
- package/templates/storefront/src/modules/common/components/checkbox/index.tsx +44 -0
- package/templates/storefront/src/modules/common/components/delete-button/index.tsx +42 -0
- package/templates/storefront/src/modules/common/components/divider/index.tsx +9 -0
- package/templates/storefront/src/modules/common/components/filter-radio-group/index.tsx +61 -0
- package/templates/storefront/src/modules/common/components/input/index.tsx +79 -0
- package/templates/storefront/src/modules/common/components/interactive-link/index.tsx +33 -0
- package/templates/storefront/src/modules/common/components/line-item-options/index.tsx +26 -0
- package/templates/storefront/src/modules/common/components/line-item-price/index.tsx +63 -0
- package/templates/storefront/src/modules/common/components/line-item-unit-price/index.tsx +62 -0
- package/templates/storefront/src/modules/common/components/localized-client-link/index.tsx +29 -0
- package/templates/storefront/src/modules/common/components/modal/index.tsx +118 -0
- package/templates/storefront/src/modules/common/components/native-select/index.tsx +76 -0
- package/templates/storefront/src/modules/common/components/radio/index.tsx +27 -0
- package/templates/storefront/src/modules/common/icons/back.tsx +37 -0
- package/templates/storefront/src/modules/common/icons/bancontact.tsx +26 -0
- package/templates/storefront/src/modules/common/icons/chevron-down.tsx +30 -0
- package/templates/storefront/src/modules/common/icons/eye-off.tsx +37 -0
- package/templates/storefront/src/modules/common/icons/eye.tsx +37 -0
- package/templates/storefront/src/modules/common/icons/fast-delivery.tsx +65 -0
- package/templates/storefront/src/modules/common/icons/ideal.tsx +26 -0
- package/templates/storefront/src/modules/common/icons/map-pin.tsx +37 -0
- package/templates/storefront/src/modules/common/icons/package.tsx +44 -0
- package/templates/storefront/src/modules/common/icons/paypal.tsx +30 -0
- package/templates/storefront/src/modules/common/icons/placeholder-image.tsx +44 -0
- package/templates/storefront/src/modules/common/icons/refresh.tsx +51 -0
- package/templates/storefront/src/modules/common/icons/spinner.tsx +37 -0
- package/templates/storefront/src/modules/common/icons/trash.tsx +51 -0
- package/templates/storefront/src/modules/common/icons/user.tsx +37 -0
- package/templates/storefront/src/modules/common/icons/x.tsx +37 -0
- package/templates/storefront/src/modules/layout/components/cart-mismatch-banner/index.tsx +59 -0
- package/templates/storefront/src/modules/order/components/help/index.tsx +26 -0
- package/templates/storefront/src/modules/order/components/item/index.tsx +57 -0
- package/templates/storefront/src/modules/order/components/items/index.tsx +44 -0
- package/templates/storefront/src/modules/order/components/onboarding-cta/index.tsx +28 -0
- package/templates/storefront/src/modules/order/components/order-details/index.tsx +64 -0
- package/templates/storefront/src/modules/order/components/order-summary/index.tsx +61 -0
- package/templates/storefront/src/modules/order/components/payment-details/index.tsx +64 -0
- package/templates/storefront/src/modules/order/components/shipping-details/index.tsx +74 -0
- package/templates/storefront/src/modules/order/components/transfer-actions/index.tsx +83 -0
- package/templates/storefront/src/modules/order/components/transfer-image/index.tsx +275 -0
- package/templates/storefront/src/modules/order/templates/order-completed-template.tsx +53 -0
- package/templates/storefront/src/modules/order/templates/order-details-template.tsx +46 -0
- package/templates/storefront/src/modules/products/components/thumbnail/index.tsx +70 -0
- package/templates/storefront/src/modules/products/templates/index.tsx +118 -0
- package/templates/storefront/src/modules/shipping/components/free-shipping-price-nudge/index.tsx +285 -0
- package/templates/storefront/src/modules/skeletons/components/skeleton-button/index.tsx +5 -0
- package/templates/storefront/src/modules/skeletons/components/skeleton-card-details/index.tsx +10 -0
- package/templates/storefront/src/modules/skeletons/components/skeleton-cart-item/index.tsx +35 -0
- package/templates/storefront/src/modules/skeletons/components/skeleton-cart-totals/index.tsx +30 -0
- package/templates/storefront/src/modules/skeletons/components/skeleton-code-form/index.tsx +13 -0
- package/templates/storefront/src/modules/skeletons/components/skeleton-line-item/index.tsx +35 -0
- package/templates/storefront/src/modules/skeletons/components/skeleton-order-confirmed-header/index.tsx +14 -0
- package/templates/storefront/src/modules/skeletons/components/skeleton-order-information/index.tsx +36 -0
- package/templates/storefront/src/modules/skeletons/components/skeleton-order-items/index.tsx +43 -0
- package/templates/storefront/src/modules/skeletons/components/skeleton-order-summary/index.tsx +15 -0
- package/templates/storefront/src/modules/skeletons/components/skeleton-product-preview/index.tsx +15 -0
- package/templates/storefront/src/modules/skeletons/templates/skeleton-cart-page/index.tsx +65 -0
- package/templates/storefront/src/modules/skeletons/templates/skeleton-order-confirmed/index.tsx +21 -0
- package/templates/storefront/src/modules/skeletons/templates/skeleton-product-grid/index.tsx +23 -0
- package/templates/storefront/src/modules/store/components/pagination/index.tsx +114 -0
- package/templates/storefront/src/modules/store/components/refinement-list/index.tsx +41 -0
- package/templates/storefront/src/modules/store/components/refinement-list/sort-products/index.tsx +48 -0
- package/templates/storefront/src/modules/store/templates/index.tsx +42 -0
- package/templates/storefront/src/modules/store/templates/paginated-products.tsx +80 -0
- package/templates/storefront/src/modules/ub/button.tsx +41 -0
- package/templates/storefront/src/modules/ub/campaign-blocks.tsx +197 -0
- package/templates/storefront/src/modules/ub/campaign-ended.tsx +28 -0
- package/templates/storefront/src/modules/ub/category-tiles.tsx +41 -0
- package/templates/storefront/src/modules/ub/countdown.tsx +46 -0
- package/templates/storefront/src/modules/ub/footer.tsx +56 -0
- package/templates/storefront/src/modules/ub/icons.tsx +42 -0
- package/templates/storefront/src/modules/ub/nav.tsx +90 -0
- package/templates/storefront/src/modules/ub/pdp/buy-box.tsx +140 -0
- package/templates/storefront/src/modules/ub/pdp/gallery.tsx +40 -0
- package/templates/storefront/src/modules/ub/product-card.tsx +66 -0
- package/templates/storefront/src/modules/ub/top-bar.tsx +20 -0
- package/templates/storefront/src/modules/ub/track-form.tsx +92 -0
- package/templates/storefront/src/proxy.ts +89 -0
- package/templates/storefront/src/styles/globals.css +98 -0
- package/templates/storefront/src/styles/ub/bundle.css +372 -0
- package/templates/storefront/src/styles/ub/fonts.css +17 -0
- package/templates/storefront/src/styles/ub/medusa-ui.css +90 -0
- package/templates/storefront/src/styles/ub/overrides.css +12 -0
- package/templates/storefront/src/styles/ub/tokens.css +9 -0
- package/templates/storefront/src/types/global.ts +24 -0
- package/templates/storefront/src/types/icon.ts +4 -0
- package/templates/storefront/tailwind.config.js +164 -0
- package/templates/storefront/themes/black-friday/campaign.en.json +31 -0
- package/templates/storefront/themes/black-friday/campaign.fr.json +31 -0
- package/templates/storefront/themes/black-friday/theme.json +9 -0
- package/templates/storefront/themes/cyber-monday/campaign.en.json +31 -0
- package/templates/storefront/themes/cyber-monday/campaign.fr.json +31 -0
- package/templates/storefront/themes/cyber-monday/theme.json +9 -0
- package/templates/storefront/themes/halloween/campaign.en.json +31 -0
- package/templates/storefront/themes/halloween/campaign.fr.json +31 -0
- package/templates/storefront/themes/halloween/theme.json +9 -0
- package/templates/storefront/themes/noel/campaign.en.json +31 -0
- package/templates/storefront/themes/noel/campaign.fr.json +31 -0
- package/templates/storefront/themes/noel/theme.json +9 -0
- package/templates/storefront/themes/nouvel-an/campaign.en.json +31 -0
- package/templates/storefront/themes/nouvel-an/campaign.fr.json +31 -0
- package/templates/storefront/themes/nouvel-an/theme.json +9 -0
- package/templates/storefront/themes/thanksgiving/campaign.en.json +31 -0
- package/templates/storefront/themes/thanksgiving/campaign.fr.json +31 -0
- package/templates/storefront/themes/thanksgiving/theme.json +9 -0
- package/templates/storefront/tsconfig.json +53 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { currencies, effective } from "./project.js";
|
|
5
|
+
const secret = () => crypto.randomBytes(32).toString("base64url");
|
|
6
|
+
// ─────────────────────────── docker-compose.yml ───────────────────────────
|
|
7
|
+
export function composeYaml(p) {
|
|
8
|
+
const services = p.boutiques
|
|
9
|
+
.map((b) => ` storefront-${b.slug}:
|
|
10
|
+
<<: *storefront
|
|
11
|
+
ports: ["${b.port}:8000"]
|
|
12
|
+
environment: { <<: *storefront-env, BOUTIQUE: ${b.slug} }
|
|
13
|
+
`)
|
|
14
|
+
.join("\n");
|
|
15
|
+
return `# GÉNÉRÉ par create-boutique à partir de config/project.json — ne pas modifier à la main.
|
|
16
|
+
# Pour changer quelque chose : éditer config/project.json puis \`create-boutique apply .\`
|
|
17
|
+
name: ${p.project}
|
|
18
|
+
|
|
19
|
+
x-storefront: &storefront
|
|
20
|
+
build: ./storefront
|
|
21
|
+
image: ${p.project}-storefront
|
|
22
|
+
restart: unless-stopped
|
|
23
|
+
depends_on:
|
|
24
|
+
backend: { condition: service_healthy }
|
|
25
|
+
environment: &storefront-env
|
|
26
|
+
MEDUSA_BACKEND_URL: http://backend:9000
|
|
27
|
+
S3_FILE_URL: \${S3_FILE_URL}
|
|
28
|
+
STRIPE_PUBLISHABLE_KEY: \${STRIPE_PUBLISHABLE_KEY:-}
|
|
29
|
+
volumes:
|
|
30
|
+
- ./config:/app/config:ro # project.json + campagnes (lecture seule)
|
|
31
|
+
- keys:/app/keys:ro # publishable keys écrites par le backend
|
|
32
|
+
|
|
33
|
+
services:
|
|
34
|
+
# ───────── Boutiques : 1 instance par boutique, même image ─────────
|
|
35
|
+
${services}
|
|
36
|
+
# ───────── Backend Medusa + admin : 1 seul, partagé par les boutiques ─────────
|
|
37
|
+
backend:
|
|
38
|
+
build: ./backend
|
|
39
|
+
image: ${p.project}-backend
|
|
40
|
+
restart: unless-stopped
|
|
41
|
+
env_file: .env
|
|
42
|
+
environment:
|
|
43
|
+
# Postgres interne au réseau Docker, sans SSL (Medusa active le SSL par défaut hors localhost)
|
|
44
|
+
DATABASE_URL: postgres://medusa:\${POSTGRES_PASSWORD}@postgres:5432/medusa?sslmode=disable
|
|
45
|
+
REDIS_URL: redis://redis:6379
|
|
46
|
+
S3_ENDPOINT: http://storage:9000
|
|
47
|
+
ports: ["${p.apiPort}:9000"] # API + admin (/app)
|
|
48
|
+
volumes:
|
|
49
|
+
- ./config:/app/config:ro # boutiques, devises, pays → régions, sales channels, clés
|
|
50
|
+
- ./seed:/app/seed:ro # products.csv + images à importer
|
|
51
|
+
- keys:/app/keys # y écrit la publishable key de chaque boutique
|
|
52
|
+
depends_on:
|
|
53
|
+
postgres: { condition: service_healthy }
|
|
54
|
+
redis: { condition: service_healthy }
|
|
55
|
+
storage: { condition: service_healthy }
|
|
56
|
+
|
|
57
|
+
# ───────── Services internes ─────────
|
|
58
|
+
postgres:
|
|
59
|
+
image: postgres:16-alpine
|
|
60
|
+
restart: unless-stopped
|
|
61
|
+
environment:
|
|
62
|
+
POSTGRES_USER: medusa
|
|
63
|
+
POSTGRES_PASSWORD: \${POSTGRES_PASSWORD}
|
|
64
|
+
POSTGRES_DB: medusa
|
|
65
|
+
volumes: [pg_data:/var/lib/postgresql/data]
|
|
66
|
+
healthcheck:
|
|
67
|
+
test: ["CMD-SHELL", "pg_isready -U medusa"]
|
|
68
|
+
interval: 5s
|
|
69
|
+
retries: 20
|
|
70
|
+
|
|
71
|
+
redis:
|
|
72
|
+
image: redis:7-alpine
|
|
73
|
+
restart: unless-stopped
|
|
74
|
+
volumes: [redis_data:/data]
|
|
75
|
+
healthcheck:
|
|
76
|
+
test: ["CMD", "redis-cli", "ping"]
|
|
77
|
+
interval: 5s
|
|
78
|
+
retries: 20
|
|
79
|
+
|
|
80
|
+
storage: # RustFS : stockage S3 des images
|
|
81
|
+
image: rustfs/rustfs:latest
|
|
82
|
+
restart: unless-stopped
|
|
83
|
+
environment:
|
|
84
|
+
RUSTFS_ACCESS_KEY: \${S3_ACCESS_KEY_ID}
|
|
85
|
+
RUSTFS_SECRET_KEY: \${S3_SECRET_ACCESS_KEY}
|
|
86
|
+
# Seule exception au « pas de port public » : les navigateurs chargent les images ici.
|
|
87
|
+
# Le backend rend le bucket lisible (lecture seule) ; l'écriture exige les identifiants.
|
|
88
|
+
ports: ["${p.storagePort}:9000"]
|
|
89
|
+
volumes: [storage_data:/data]
|
|
90
|
+
healthcheck:
|
|
91
|
+
test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:9000/health >/dev/null || exit 1"]
|
|
92
|
+
interval: 5s
|
|
93
|
+
retries: 20
|
|
94
|
+
|
|
95
|
+
volumes:
|
|
96
|
+
pg_data:
|
|
97
|
+
redis_data:
|
|
98
|
+
storage_data:
|
|
99
|
+
keys:
|
|
100
|
+
`;
|
|
101
|
+
}
|
|
102
|
+
/** Variables qui dépendent de project.json (réécrites à chaque apply). Les secrets ne sont jamais régénérés. */
|
|
103
|
+
function derivedEnv(p) {
|
|
104
|
+
const origins = p.boutiques.map((b) => b.url ?? `http://localhost:${b.port}`);
|
|
105
|
+
const admin = `http://localhost:${p.apiPort}`;
|
|
106
|
+
return {
|
|
107
|
+
PROJECT: p.project,
|
|
108
|
+
STORE_CORS: origins.join(","),
|
|
109
|
+
ADMIN_CORS: admin,
|
|
110
|
+
AUTH_CORS: admin,
|
|
111
|
+
S3_FILE_URL: `http://localhost:${p.storagePort}/medusa`,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
export function writeEnv(dir, p, s) {
|
|
115
|
+
const storageKey = `${p.project}-storage`;
|
|
116
|
+
const lines = [
|
|
117
|
+
"# GÉNÉRÉ par create-boutique — secrets du projet. Ne jamais committer ni publier ce fichier.",
|
|
118
|
+
"",
|
|
119
|
+
"# Base de données",
|
|
120
|
+
`POSTGRES_PASSWORD=${secret()}`,
|
|
121
|
+
"",
|
|
122
|
+
"# Medusa",
|
|
123
|
+
`JWT_SECRET=${secret()}`,
|
|
124
|
+
`COOKIE_SECRET=${secret()}`,
|
|
125
|
+
`ADMIN_EMAIL=${s.adminEmail}`,
|
|
126
|
+
`ADMIN_PASSWORD=${secret()}`,
|
|
127
|
+
"",
|
|
128
|
+
"# Images (RustFS, compatible S3)",
|
|
129
|
+
"S3_BUCKET=medusa",
|
|
130
|
+
"S3_REGION=us-east-1",
|
|
131
|
+
`S3_ACCESS_KEY_ID=${storageKey}`,
|
|
132
|
+
`S3_SECRET_ACCESS_KEY=${secret()}`,
|
|
133
|
+
"",
|
|
134
|
+
"# Stripe (vide = paiement manuel seulement). Après modification : docker compose restart backend",
|
|
135
|
+
`STRIPE_API_KEY=${s.stripeSk ?? ""}`,
|
|
136
|
+
`STRIPE_WEBHOOK_SECRET=${s.stripeWhsec ?? ""}`,
|
|
137
|
+
`STRIPE_PUBLISHABLE_KEY=${s.stripePk ?? ""}`,
|
|
138
|
+
"",
|
|
139
|
+
"# E-mails (Resend ; vide = aucun e-mail envoyé)",
|
|
140
|
+
`RESEND_API_KEY=${s.resendKey ?? ""}`,
|
|
141
|
+
`EMAIL_FROM=${s.emailFrom ?? ""}`,
|
|
142
|
+
"",
|
|
143
|
+
"# Dérivé de config/project.json (réécrit par `create-boutique apply`)",
|
|
144
|
+
...Object.entries(derivedEnv(p)).map(([k, v]) => `${k}=${v}`),
|
|
145
|
+
"",
|
|
146
|
+
];
|
|
147
|
+
fs.writeFileSync(path.join(dir, ".env"), lines.join("\n"), { mode: 0o600 });
|
|
148
|
+
}
|
|
149
|
+
/** Met à jour les variables dérivées de project.json sans toucher aux secrets. */
|
|
150
|
+
export function refreshEnv(dir, p) {
|
|
151
|
+
const file = path.join(dir, ".env");
|
|
152
|
+
let text = fs.readFileSync(file, "utf8");
|
|
153
|
+
for (const [k, v] of Object.entries(derivedEnv(p))) {
|
|
154
|
+
const re = new RegExp(`^${k}=.*$`, "m");
|
|
155
|
+
text = re.test(text) ? text.replace(re, `${k}=${v}`) : `${text.trimEnd()}\n${k}=${v}\n`;
|
|
156
|
+
}
|
|
157
|
+
fs.writeFileSync(file, text);
|
|
158
|
+
}
|
|
159
|
+
export function readEnv(dir) {
|
|
160
|
+
const file = path.join(dir, ".env");
|
|
161
|
+
if (!fs.existsSync(file))
|
|
162
|
+
return {};
|
|
163
|
+
return Object.fromEntries(fs
|
|
164
|
+
.readFileSync(file, "utf8")
|
|
165
|
+
.split(/\r?\n/)
|
|
166
|
+
.filter((l) => /^[A-Z0-9_]+=/.test(l))
|
|
167
|
+
.map((l) => [l.slice(0, l.indexOf("=")), l.slice(l.indexOf("=") + 1)]));
|
|
168
|
+
}
|
|
169
|
+
// ─────────────────────────── Campagnes ───────────────────────────
|
|
170
|
+
const THEME_ORDER = ["halloween", "thanksgiving", "black-friday", "cyber-monday", "noel", "nouvel-an"];
|
|
171
|
+
const DISCOUNT = {
|
|
172
|
+
halloween: "40%",
|
|
173
|
+
thanksgiving: "30%",
|
|
174
|
+
"black-friday": "50%",
|
|
175
|
+
"cyber-monday": "50%",
|
|
176
|
+
noel: "30%",
|
|
177
|
+
"nouvel-an": "35%",
|
|
178
|
+
};
|
|
179
|
+
const FREE_SHIPPING = { usd: 50, eur: 50, gbp: 50, cad: 75, xof: 25000, xaf: 25000 };
|
|
180
|
+
/** 4e jeudi de novembre (Thanksgiving US). */
|
|
181
|
+
function thanksgiving(year) {
|
|
182
|
+
const d = new Date(Date.UTC(year, 10, 1));
|
|
183
|
+
const firstThursday = 1 + ((4 - d.getUTCDay() + 7) % 7);
|
|
184
|
+
return new Date(Date.UTC(year, 10, firstThursday + 21));
|
|
185
|
+
}
|
|
186
|
+
const day = (d, add = 0, end = false) => {
|
|
187
|
+
const x = new Date(d.getTime() + add * 86400000);
|
|
188
|
+
x.setUTCHours(end ? 23 : 0, end ? 59 : 0, end ? 59 : 0, 0);
|
|
189
|
+
return x;
|
|
190
|
+
};
|
|
191
|
+
/** Dates de la prochaine occurrence de la campagne (si la saison est passée, l'année suivante). */
|
|
192
|
+
export function campaignDates(theme, now = new Date()) {
|
|
193
|
+
const build = (y) => {
|
|
194
|
+
const tg = thanksgiving(y);
|
|
195
|
+
switch (theme) {
|
|
196
|
+
case "halloween":
|
|
197
|
+
return [new Date(Date.UTC(y, 9, 20)), day(new Date(Date.UTC(y, 9, 31)), 0, true)];
|
|
198
|
+
case "thanksgiving":
|
|
199
|
+
return [day(tg, -6), day(tg, 0, true)];
|
|
200
|
+
case "black-friday":
|
|
201
|
+
return [day(tg, 1), day(tg, 3, true)];
|
|
202
|
+
case "cyber-monday":
|
|
203
|
+
return [day(tg, 4), day(tg, 4, true)];
|
|
204
|
+
case "noel":
|
|
205
|
+
return [new Date(Date.UTC(y, 11, 1)), day(new Date(Date.UTC(y, 11, 24)), 0, true)];
|
|
206
|
+
case "nouvel-an":
|
|
207
|
+
return [new Date(Date.UTC(y, 11, 26)), day(new Date(Date.UTC(y + 1, 0, 2)), 0, true)];
|
|
208
|
+
default:
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
let range = build(now.getUTCFullYear());
|
|
213
|
+
if (range && range[1] < now)
|
|
214
|
+
range = build(now.getUTCFullYear() + 1);
|
|
215
|
+
if (!range)
|
|
216
|
+
range = [day(now), day(now, 30, true)];
|
|
217
|
+
return { startsAt: range[0].toISOString(), endsAt: range[1].toISOString() };
|
|
218
|
+
}
|
|
219
|
+
/** Fichier de campagne initial (dates + variables) ; les textes viennent du thème. Jamais écrasé. */
|
|
220
|
+
export function writeCampaignFiles(dir, p, language) {
|
|
221
|
+
const campaignsDir = path.join(dir, "config", "campaigns");
|
|
222
|
+
fs.mkdirSync(campaignsDir, { recursive: true });
|
|
223
|
+
const locale = `${language}-${p.defaults.country.toUpperCase()}`;
|
|
224
|
+
// Campagne suivante : prochaine boutique dans l'ordre des saisons.
|
|
225
|
+
const ordered = [...p.boutiques].sort((a, b) => THEME_ORDER.indexOf(a.theme) - THEME_ORDER.indexOf(b.theme));
|
|
226
|
+
for (const b of p.boutiques) {
|
|
227
|
+
const file = path.join(campaignsDir, `${b.slug}.json`);
|
|
228
|
+
if (fs.existsSync(file))
|
|
229
|
+
continue;
|
|
230
|
+
const e = effective(p, b);
|
|
231
|
+
const i = ordered.findIndex((x) => x.slug === b.slug);
|
|
232
|
+
const next = ordered[i + 1]?.slug;
|
|
233
|
+
const threshold = FREE_SHIPPING[e.currency] ?? 50;
|
|
234
|
+
const freeShipping = new Intl.NumberFormat(locale, { style: "currency", currency: e.currency, maximumFractionDigits: 0 }).format(threshold);
|
|
235
|
+
const campaign = {
|
|
236
|
+
...campaignDates(b.theme),
|
|
237
|
+
...(next ? { nextCampaign: next } : {}),
|
|
238
|
+
vars: { discount: DISCOUNT[b.theme] ?? "20%", freeShipping },
|
|
239
|
+
};
|
|
240
|
+
fs.writeFileSync(file, JSON.stringify(campaign, null, 2) + "\n");
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// ─────────────────────────── README ───────────────────────────
|
|
244
|
+
export function writeReadme(dir, p) {
|
|
245
|
+
const rows = p.boutiques
|
|
246
|
+
.map((b) => {
|
|
247
|
+
const e = effective(p, b);
|
|
248
|
+
return `| ${b.slug} | ${b.theme} | ${e.currency.toUpperCase()} | http://localhost:${b.port} |`;
|
|
249
|
+
})
|
|
250
|
+
.join("\n");
|
|
251
|
+
const text = `# ${p.defaults.storeName} — projet \`${p.project}\`
|
|
252
|
+
|
|
253
|
+
Généré par **create-boutique**. Un backend Medusa (admin) partagé par ${p.boutiques.length} boutique(s).
|
|
254
|
+
|
|
255
|
+
## Lancer
|
|
256
|
+
|
|
257
|
+
\`\`\`bash
|
|
258
|
+
docker compose up -d
|
|
259
|
+
\`\`\`
|
|
260
|
+
|
|
261
|
+
Le premier démarrage prend quelques minutes (construction des images, base de données, import du catalogue).
|
|
262
|
+
Suivre l'avancement : \`docker compose logs -f backend\`.
|
|
263
|
+
|
|
264
|
+
| Boutique | Thème | Devise | Adresse locale |
|
|
265
|
+
| --- | --- | --- | --- |
|
|
266
|
+
${rows}
|
|
267
|
+
|
|
268
|
+
- **Admin** : http://localhost:${p.apiPort}/app — e-mail \`ADMIN_EMAIL\` du fichier \`.env\`, mot de passe \`ADMIN_PASSWORD\` (transmis à part, à changer à la première connexion).
|
|
269
|
+
- **Images** : http://localhost:${p.storagePort}/medusa/…
|
|
270
|
+
- Mémoire : environ 80 Mo par boutique, plus le backend (~500 Mo) et les services.
|
|
271
|
+
|
|
272
|
+
## Modifier le site
|
|
273
|
+
|
|
274
|
+
| Je veux changer… | Où | Ensuite |
|
|
275
|
+
| --- | --- | --- |
|
|
276
|
+
| Produits, prix, stock, promos, commandes | Admin → Produits / Commandes | rien |
|
|
277
|
+
| Textes, dates, visuel, logo, image promo, compte à rebours | Admin → **Campagnes** | visible en ~1 min |
|
|
278
|
+
| Image d'une catégorie | Admin → Produits → Catégories → (catégorie) | visible en ~1 min |
|
|
279
|
+
| Nom, devise, pays, pays livrés, thème, ajout de boutique | \`config/project.json\` | \`create-boutique apply .\` |
|
|
280
|
+
| Clés Stripe, Resend, adresse d'envoi | \`.env\` | \`docker compose restart backend\` puis \`docker compose up -d\` |
|
|
281
|
+
| Mise en page, styles | ne pas modifier ici : template, puis \`create-boutique update .\` | — |
|
|
282
|
+
|
|
283
|
+
Commandes utiles :
|
|
284
|
+
|
|
285
|
+
\`\`\`bash
|
|
286
|
+
create-boutique add . --boutique ${p.project}-soldes:black-friday # ajouter une boutique
|
|
287
|
+
create-boutique set . --boutique ${p.boutiques[0].slug} --name "Nouveau nom"
|
|
288
|
+
create-boutique apply . # après une modification de config/project.json
|
|
289
|
+
create-boutique update . # mettre à jour le code (config, .env et données conservés)
|
|
290
|
+
create-boutique backup . # sauvegarde base + images dans backups/<date>/
|
|
291
|
+
\`\`\`
|
|
292
|
+
|
|
293
|
+
## Paiement Stripe
|
|
294
|
+
|
|
295
|
+
1. Renseigner \`STRIPE_API_KEY\` (sk_…) et \`STRIPE_PUBLISHABLE_KEY\` (pk_…) dans \`.env\`.
|
|
296
|
+
2. Une fois le backend en ligne en HTTPS (le webhook ne peut pas viser \`localhost\`) :
|
|
297
|
+
\`create-boutique stripe-webhook . --url https://<adresse du backend>\`
|
|
298
|
+
crée l'endpoint \`/hooks/payment/stripe_stripe\` dans Stripe, écrit \`STRIPE_WEBHOOK_SECRET\` dans \`.env\` et relance le backend.
|
|
299
|
+
3. À la main si besoin : Stripe → Développeurs → Webhooks, endpoint \`<adresse du backend>/hooks/payment/stripe_stripe\`, événements
|
|
300
|
+
\`payment_intent.succeeded\`, \`payment_intent.payment_failed\`, \`payment_intent.amount_capturable_updated\`, \`payment_intent.partially_funded\`,
|
|
301
|
+
puis copier le secret \`whsec_…\` dans \`STRIPE_WEBHOOK_SECRET\` et \`docker compose restart backend\`.
|
|
302
|
+
4. Apple Pay : déclarer les domaines des boutiques dans Stripe → Paramètres → Moyens de paiement.
|
|
303
|
+
|
|
304
|
+
## Avant de livrer : checklist
|
|
305
|
+
|
|
306
|
+
- [ ] Chaque boutique s'ouvre à son adresse, avec son thème et sa devise.
|
|
307
|
+
- [ ] Chaque produit a un prix dans la devise de chaque boutique où il est vendu.
|
|
308
|
+
- [ ] Logo, textes et dates de chaque campagne vérifiés (Admin → Campagnes).
|
|
309
|
+
- [ ] Stripe configuré (clés + webhook) ou paiement manuel assumé.
|
|
310
|
+
- [ ] E-mails : \`RESEND_API_KEY\` renseignée, une commande test reçue par le client et le marchand.
|
|
311
|
+
- [ ] Une commande test passée dans chaque boutique (\`create-boutique check .\`).
|
|
312
|
+
- [ ] Sauvegarde programmée (\`create-boutique backup .\`).
|
|
313
|
+
|
|
314
|
+
## À ne pas faire
|
|
315
|
+
|
|
316
|
+
- Ne pas modifier \`docker-compose.yml\` à la main (régénéré par \`apply\`).
|
|
317
|
+
- Ne pas renommer le \`slug\` d'une boutique (il relie la boutique à ses ventes, sa clé et sa campagne).
|
|
318
|
+
- Ne jamais publier le fichier \`.env\`.
|
|
319
|
+
`;
|
|
320
|
+
fs.writeFileSync(path.join(dir, "README.md"), text);
|
|
321
|
+
}
|
|
322
|
+
export const projectCurrencies = currencies;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
import { PACKAGE_ROOT } from "./paths.js";
|
|
6
|
+
import { currencies, effective } from "./project.js";
|
|
7
|
+
const escapeHtml = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
8
|
+
/** Chrome / Edge / Chromium installé (CHROME_PATH prioritaire). */
|
|
9
|
+
function findBrowser() {
|
|
10
|
+
const candidates = [
|
|
11
|
+
process.env.CHROME_PATH,
|
|
12
|
+
"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
|
|
13
|
+
"C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
|
|
14
|
+
"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe",
|
|
15
|
+
"C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe",
|
|
16
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
17
|
+
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
|
18
|
+
"/usr/bin/google-chrome",
|
|
19
|
+
"/usr/bin/chromium",
|
|
20
|
+
"/usr/bin/chromium-browser",
|
|
21
|
+
"/usr/bin/microsoft-edge",
|
|
22
|
+
];
|
|
23
|
+
return candidates.find((c) => c && fs.existsSync(c));
|
|
24
|
+
}
|
|
25
|
+
/** Remplit un modèle HTML (valeurs échappées ; les clés *_HTML sont du HTML construit ici) puis l'imprime en PDF. */
|
|
26
|
+
function render(source, out, dir, vars) {
|
|
27
|
+
let html = fs.readFileSync(source, "utf8");
|
|
28
|
+
for (const [k, v] of Object.entries(vars))
|
|
29
|
+
html = html.split(`{{${k}}}`).join(k.endsWith("_HTML") ? v : escapeHtml(v) || " ");
|
|
30
|
+
const htmlFile = path.join(dir, `${out}.html`);
|
|
31
|
+
const pdfFile = path.join(dir, `${out}.pdf`);
|
|
32
|
+
fs.writeFileSync(htmlFile, html);
|
|
33
|
+
const browser = findBrowser();
|
|
34
|
+
if (!browser)
|
|
35
|
+
return { pdf: false, file: htmlFile };
|
|
36
|
+
const r = spawnSync(browser, ["--headless", "--disable-gpu", "--no-pdf-header-footer", `--print-to-pdf=${pdfFile}`, pathToFileURL(htmlFile).href], { stdio: "ignore", timeout: 90000 });
|
|
37
|
+
if (r.status === 0 && fs.existsSync(pdfFile)) {
|
|
38
|
+
fs.rmSync(htmlFile);
|
|
39
|
+
return { pdf: true, file: pdfFile };
|
|
40
|
+
}
|
|
41
|
+
return { pdf: false, file: htmlFile };
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* GUIDE-CLIENT.pdf (1 page, docs/fiche-client[.en].html) et GUIDE-COMPLET.pdf (docs/guide-complet.<lang>.html),
|
|
45
|
+
* remplis avec l'adresse de l'admin, l'e-mail admin et les boutiques du projet.
|
|
46
|
+
* Le mot de passe n'est JAMAIS imprimé (champ laissé vide, transmis à part).
|
|
47
|
+
* Langue = langue du projet (fr, sinon en). Sans Chrome/Edge : fichiers HTML (à imprimer en PDF).
|
|
48
|
+
*/
|
|
49
|
+
export function writeGuides(dir, p, adminEmail, support = "") {
|
|
50
|
+
const fr = p.defaults.language === "fr";
|
|
51
|
+
const adminUrl = `http://localhost:${p.apiPort}/app`;
|
|
52
|
+
const stores = p.boutiques
|
|
53
|
+
.map((b) => {
|
|
54
|
+
const e = effective(p, b);
|
|
55
|
+
return `<tr><td>${escapeHtml(e.storeName)}</td><td><code>${escapeHtml(b.slug)}</code></td><td>${escapeHtml(b.theme)}</td><td>${e.currency.toUpperCase()}</td><td>${escapeHtml(b.url ?? `http://localhost:${b.port}`)}</td></tr>`;
|
|
56
|
+
})
|
|
57
|
+
.join("");
|
|
58
|
+
const vars = {
|
|
59
|
+
ADMIN_URL: adminUrl,
|
|
60
|
+
ADMIN_EMAIL: adminEmail,
|
|
61
|
+
SUPPORT: support,
|
|
62
|
+
PROJECT: p.project,
|
|
63
|
+
STORE_NAME: p.defaults.storeName,
|
|
64
|
+
CURRENCIES: currencies(p).map((c) => c.toUpperCase()).join(", "),
|
|
65
|
+
STORES_HTML: stores,
|
|
66
|
+
};
|
|
67
|
+
const docs = path.join(PACKAGE_ROOT, "docs");
|
|
68
|
+
return [
|
|
69
|
+
render(path.join(docs, fr ? "fiche-client.html" : "fiche-client.en.html"), "GUIDE-CLIENT", dir, vars),
|
|
70
|
+
render(path.join(docs, fr ? "guide-complet.fr.html" : "guide-complet.en.html"), "GUIDE-COMPLET", dir, vars),
|
|
71
|
+
];
|
|
72
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import { parseArgs } from "node:util";
|
|
4
|
+
import pc from "picocolors";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { add, admin, apply, backup, campaignExport, campaignImport, check, guide, remove, set, stripeWebhook, themes, update } from "./commands.js";
|
|
7
|
+
import { answersSchema, create } from "./create.js";
|
|
8
|
+
import { ProjectError } from "./project.js";
|
|
9
|
+
const HELP = `create-boutique — Usine à Boutiques
|
|
10
|
+
|
|
11
|
+
create-boutique <dossier> [options] crée un projet (questions si des options manquent)
|
|
12
|
+
--name "<nom>" nom affiché
|
|
13
|
+
--email <email> e-mail admin (et du marchand)
|
|
14
|
+
--currency usd (défaut usd) --country us (défaut us) --language en (défaut en)
|
|
15
|
+
--themes a,b,c | all une boutique par thème, slug = <dossier>-<thème>
|
|
16
|
+
--boutique <slug>:<thème> ajoute une boutique au nom choisi (répétable)
|
|
17
|
+
--stripe-pk / --stripe-sk / --stripe-whsec clés Stripe (optionnelles)
|
|
18
|
+
--resend-key / --email-from e-mails (optionnels)
|
|
19
|
+
--products <csv> --images <dossier> catalogue (optionnel)
|
|
20
|
+
--store-port 8001 --api-port 9000 --storage-port 9100 premiers ports essayés
|
|
21
|
+
--config <fichier.json> toutes les options dans un fichier (mêmes clés, en camelCase)
|
|
22
|
+
--yes aucune question, valeurs par défaut
|
|
23
|
+
|
|
24
|
+
create-boutique apply <dossier> [--no-up] applique config/project.json
|
|
25
|
+
create-boutique add <dossier> --boutique <slug>:<thème>
|
|
26
|
+
create-boutique remove <dossier> --boutique <slug>
|
|
27
|
+
create-boutique set <dossier> [--boutique <slug>] [--name …] [--theme …] [--currency …] [--country …] [--language …]
|
|
28
|
+
create-boutique themes liste les thèmes
|
|
29
|
+
create-boutique update <dossier> [--no-up] met à jour le code (config, .env, données conservés)
|
|
30
|
+
create-boutique check <dossier> commande test dans chaque boutique
|
|
31
|
+
create-boutique backup <dossier> base + images dans backups/<date>/
|
|
32
|
+
create-boutique admin <dossier> adresse, e-mail et mot de passe de l'admin
|
|
33
|
+
create-boutique guide <dossier> régénère GUIDE-CLIENT.pdf et GUIDE-COMPLET.pdf
|
|
34
|
+
create-boutique stripe-webhook <dossier> --url https://api.marque.com crée le webhook Stripe, écrit STRIPE_WEBHOOK_SECRET
|
|
35
|
+
create-boutique campaign export <dossier> campagnes en base → config/campaigns/*.json
|
|
36
|
+
create-boutique campaign import <dossier> [--boutique <slug>] [--yes] JSON → base (écrase)
|
|
37
|
+
`;
|
|
38
|
+
const COMMANDS = new Set(["apply", "add", "remove", "set", "themes", "update", "check", "backup", "campaign", "admin", "guide", "stripe-webhook", "help"]);
|
|
39
|
+
async function main() {
|
|
40
|
+
const { values, positionals } = parseArgs({
|
|
41
|
+
allowPositionals: true,
|
|
42
|
+
options: {
|
|
43
|
+
name: { type: "string" },
|
|
44
|
+
email: { type: "string" },
|
|
45
|
+
currency: { type: "string" },
|
|
46
|
+
country: { type: "string" },
|
|
47
|
+
language: { type: "string" },
|
|
48
|
+
themes: { type: "string" },
|
|
49
|
+
boutique: { type: "string", multiple: true },
|
|
50
|
+
theme: { type: "string" },
|
|
51
|
+
"stripe-pk": { type: "string" },
|
|
52
|
+
"stripe-sk": { type: "string" },
|
|
53
|
+
"stripe-whsec": { type: "string" },
|
|
54
|
+
"resend-key": { type: "string" },
|
|
55
|
+
"email-from": { type: "string" },
|
|
56
|
+
products: { type: "string" },
|
|
57
|
+
images: { type: "string" },
|
|
58
|
+
"store-port": { type: "string" },
|
|
59
|
+
"api-port": { type: "string" },
|
|
60
|
+
"storage-port": { type: "string" },
|
|
61
|
+
config: { type: "string" },
|
|
62
|
+
url: { type: "string" },
|
|
63
|
+
yes: { type: "boolean", short: "y" },
|
|
64
|
+
"no-up": { type: "boolean" },
|
|
65
|
+
help: { type: "boolean", short: "h" },
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
const [first, ...rest] = positionals;
|
|
69
|
+
if (values.help || !first || first === "help")
|
|
70
|
+
return console.log(HELP);
|
|
71
|
+
const up = { up: !values["no-up"] };
|
|
72
|
+
const dirArg = (i = 0) => {
|
|
73
|
+
const d = rest[i];
|
|
74
|
+
if (!d)
|
|
75
|
+
throw new ProjectError("dossier du projet manquant (ex. create-boutique apply ./boutique-yout)");
|
|
76
|
+
return d;
|
|
77
|
+
};
|
|
78
|
+
const lower = (v) => v?.toLowerCase();
|
|
79
|
+
if (!COMMANDS.has(first)) {
|
|
80
|
+
// create-boutique <dossier> [options]
|
|
81
|
+
const fromFile = values.config ? answersSchema.parse(JSON.parse(fs.readFileSync(values.config, "utf8"))) : {};
|
|
82
|
+
const port = (v) => (v ? Number(v) : undefined);
|
|
83
|
+
const fromFlags = answersSchema.parse(Object.fromEntries(Object.entries({
|
|
84
|
+
name: values.name,
|
|
85
|
+
email: values.email,
|
|
86
|
+
currency: lower(values.currency),
|
|
87
|
+
country: lower(values.country),
|
|
88
|
+
language: lower(values.language),
|
|
89
|
+
themes: values.themes === "all" ? "all" : values.themes?.split(",").map((s) => s.trim()).filter(Boolean),
|
|
90
|
+
boutiques: values.boutique,
|
|
91
|
+
stripePk: values["stripe-pk"],
|
|
92
|
+
stripeSk: values["stripe-sk"],
|
|
93
|
+
stripeWhsec: values["stripe-whsec"],
|
|
94
|
+
resendKey: values["resend-key"],
|
|
95
|
+
emailFrom: values["email-from"],
|
|
96
|
+
products: values.products,
|
|
97
|
+
images: values.images,
|
|
98
|
+
storePort: port(values["store-port"]),
|
|
99
|
+
apiPort: port(values["api-port"]),
|
|
100
|
+
storagePort: port(values["storage-port"]),
|
|
101
|
+
}).filter(([, v]) => v !== undefined)));
|
|
102
|
+
return create(first, { ...fromFile, ...fromFlags }, { yes: !!values.yes });
|
|
103
|
+
}
|
|
104
|
+
switch (first) {
|
|
105
|
+
case "apply":
|
|
106
|
+
return apply(dirArg(), up);
|
|
107
|
+
case "add":
|
|
108
|
+
return add(dirArg(), values.boutique ?? [], up);
|
|
109
|
+
case "remove":
|
|
110
|
+
if (!values.boutique?.length)
|
|
111
|
+
throw new ProjectError("--boutique <slug> obligatoire");
|
|
112
|
+
return remove(dirArg(), values.boutique, up);
|
|
113
|
+
case "set":
|
|
114
|
+
return set(dirArg(), {
|
|
115
|
+
boutique: values.boutique?.[0],
|
|
116
|
+
name: values.name,
|
|
117
|
+
theme: values.theme,
|
|
118
|
+
currency: lower(values.currency),
|
|
119
|
+
country: lower(values.country),
|
|
120
|
+
language: lower(values.language),
|
|
121
|
+
}, up);
|
|
122
|
+
case "themes":
|
|
123
|
+
return themes();
|
|
124
|
+
case "update":
|
|
125
|
+
return update(dirArg(), up);
|
|
126
|
+
case "check":
|
|
127
|
+
return check(dirArg());
|
|
128
|
+
case "backup":
|
|
129
|
+
return backup(dirArg());
|
|
130
|
+
case "admin":
|
|
131
|
+
return admin(dirArg());
|
|
132
|
+
case "guide":
|
|
133
|
+
return guide(dirArg());
|
|
134
|
+
case "stripe-webhook":
|
|
135
|
+
return stripeWebhook(dirArg(), values.url, up);
|
|
136
|
+
case "campaign": {
|
|
137
|
+
const [action, dir] = rest;
|
|
138
|
+
if (!dir)
|
|
139
|
+
throw new ProjectError("usage : create-boutique campaign export|import <dossier>");
|
|
140
|
+
if (action === "export")
|
|
141
|
+
return campaignExport(dir);
|
|
142
|
+
if (action === "import")
|
|
143
|
+
return campaignImport(dir, values.boutique ?? [], !!values.yes);
|
|
144
|
+
throw new ProjectError("usage : create-boutique campaign export|import <dossier>");
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
main().catch((e) => {
|
|
149
|
+
if (e instanceof ProjectError)
|
|
150
|
+
console.error(pc.red("✖ config invalide, rien n'a été modifié :\n") + e.message);
|
|
151
|
+
else if (e instanceof z.ZodError)
|
|
152
|
+
console.error(pc.red("✖ options invalides :\n") + z.prettifyError(e));
|
|
153
|
+
else
|
|
154
|
+
console.error(pc.red(`✖ ${e instanceof Error ? e.message : String(e)}`));
|
|
155
|
+
process.exit(1);
|
|
156
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
/** Racine du package create-boutique (contient templates/). */
|
|
5
|
+
export const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
6
|
+
export const TEMPLATES = path.join(PACKAGE_ROOT, "templates");
|
|
7
|
+
export const THEMES_DIR = path.join(TEMPLATES, "storefront", "themes");
|
|
8
|
+
export function templateVersion() {
|
|
9
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, "package.json"), "utf8"));
|
|
10
|
+
return pkg.version;
|
|
11
|
+
}
|
|
12
|
+
/** Fichiers et dossiers jamais copiés dans un projet (dépendances, builds, secrets). */
|
|
13
|
+
const IGNORED = new Set(["node_modules", ".next", ".medusa", "static", "uploads", ".git", ".turbo", "dist"]);
|
|
14
|
+
export function shouldCopy(name) {
|
|
15
|
+
return !IGNORED.has(name) && !name.startsWith(".env") && !name.endsWith(".log") && name !== "tsconfig.tsbuildinfo";
|
|
16
|
+
}
|
|
17
|
+
export function copyDir(from, to) {
|
|
18
|
+
fs.mkdirSync(to, { recursive: true });
|
|
19
|
+
for (const entry of fs.readdirSync(from, { withFileTypes: true })) {
|
|
20
|
+
if (!shouldCopy(entry.name))
|
|
21
|
+
continue;
|
|
22
|
+
const src = path.join(from, entry.name);
|
|
23
|
+
const dst = path.join(to, entry.name);
|
|
24
|
+
if (entry.isDirectory())
|
|
25
|
+
copyDir(src, dst);
|
|
26
|
+
else
|
|
27
|
+
fs.copyFileSync(src, dst);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import net from "node:net";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
/**
|
|
6
|
+
* Registre local des ports réservés par les projets générés sur cette machine
|
|
7
|
+
* (~/.create-boutique/ports.json), pour que deux projets ne prennent pas les mêmes.
|
|
8
|
+
*/
|
|
9
|
+
const REGISTRY = path.join(os.homedir(), ".create-boutique", "ports.json");
|
|
10
|
+
function readRegistry() {
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(fs.readFileSync(REGISTRY, "utf8"));
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return {};
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function reservePorts(projectDir, ports) {
|
|
19
|
+
const reg = readRegistry();
|
|
20
|
+
reg[path.resolve(projectDir)] = ports;
|
|
21
|
+
fs.mkdirSync(path.dirname(REGISTRY), { recursive: true });
|
|
22
|
+
fs.writeFileSync(REGISTRY, JSON.stringify(reg, null, 2) + "\n");
|
|
23
|
+
}
|
|
24
|
+
export function releasePorts(projectDir) {
|
|
25
|
+
const reg = readRegistry();
|
|
26
|
+
delete reg[path.resolve(projectDir)];
|
|
27
|
+
fs.mkdirSync(path.dirname(REGISTRY), { recursive: true });
|
|
28
|
+
fs.writeFileSync(REGISTRY, JSON.stringify(reg, null, 2) + "\n");
|
|
29
|
+
}
|
|
30
|
+
/** Ports réservés par les AUTRES projets (un projet existant n'est pas en conflit avec lui-même). */
|
|
31
|
+
export function reservedByOthers(projectDir) {
|
|
32
|
+
const self = path.resolve(projectDir);
|
|
33
|
+
const reg = readRegistry();
|
|
34
|
+
// On oublie les projets dont le dossier a été supprimé.
|
|
35
|
+
return new Set(Object.entries(reg).filter(([dir]) => dir !== self && fs.existsSync(dir)).flatMap(([, p]) => p));
|
|
36
|
+
}
|
|
37
|
+
export function isPortFree(port) {
|
|
38
|
+
return new Promise((resolve) => {
|
|
39
|
+
const srv = net.createServer();
|
|
40
|
+
srv.once("error", () => resolve(false));
|
|
41
|
+
srv.once("listening", () => srv.close(() => resolve(true)));
|
|
42
|
+
srv.listen(port, "0.0.0.0");
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/** Premier port libre à partir de `from`, en évitant `taken` (ports déjà choisis ou réservés). */
|
|
46
|
+
export async function nextFreePort(from, taken) {
|
|
47
|
+
for (let p = from; p < 65535; p++) {
|
|
48
|
+
if (taken.has(p))
|
|
49
|
+
continue;
|
|
50
|
+
if (await isPortFree(p))
|
|
51
|
+
return p;
|
|
52
|
+
}
|
|
53
|
+
throw new Error(`aucun port libre à partir de ${from}`);
|
|
54
|
+
}
|