@vitrine-kit/vitrine 0.3.0 → 0.4.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.
Files changed (122) hide show
  1. package/README.md +18 -18
  2. package/dist/index.js +304 -285
  3. package/kit/registry/README.md +9 -0
  4. package/kit/registry/_index.json +74 -0
  5. package/kit/registry/cart/docs/cart.md +13 -0
  6. package/kit/registry/cart/feature.json +51 -0
  7. package/kit/registry/cart/files/app/(frontend)/cart/page.tsx +21 -0
  8. package/kit/registry/cart/files/app/api/cart/route.ts +41 -0
  9. package/kit/registry/cart/files/components/cart/AddToCart.tsx +40 -0
  10. package/kit/registry/cart/files/components/cart/CartIndicator.tsx +11 -0
  11. package/kit/registry/cart/files/components/cart/CartLineItem.tsx +27 -0
  12. package/kit/registry/cart/files/components/cart/CartSummary.tsx +31 -0
  13. package/kit/registry/cart/files/components/cart/CartView.tsx +31 -0
  14. package/kit/registry/cart/files/lib/cart/data.ts +12 -0
  15. package/kit/registry/cart/files/lib/cart/register.ts +10 -0
  16. package/kit/registry/catalog/docs/catalog.md +18 -0
  17. package/kit/registry/catalog/feature.json +43 -0
  18. package/kit/registry/catalog/files/components/catalog/CategoryNav.tsx +26 -0
  19. package/kit/registry/catalog/files/components/catalog/ProductCard.tsx +42 -0
  20. package/kit/registry/catalog/files/components/catalog/ProductGrid.tsx +25 -0
  21. package/kit/registry/catalog/files/lib/catalog/data.ts +24 -0
  22. package/kit/registry/catalog/files/lib/catalog/register.ts +8 -0
  23. package/kit/registry/checkout/docs/checkout.md +17 -0
  24. package/kit/registry/checkout/feature.json +47 -0
  25. package/kit/registry/checkout/files/app/api/checkout/route.ts +14 -0
  26. package/kit/registry/checkout/files/components/checkout/CheckoutButton.tsx +31 -0
  27. package/kit/registry/checkout/files/lib/checkout/fulfill.ts +72 -0
  28. package/kit/registry/checkout/files/lib/checkout/register.ts +8 -0
  29. package/kit/registry/checkout-paddle/docs/checkout-paddle.md +22 -0
  30. package/kit/registry/checkout-paddle/feature.json +62 -0
  31. package/kit/registry/checkout-paddle/files/app/api/webhooks/paddle/route.ts +22 -0
  32. package/kit/registry/checkout-paddle/files/lib/checkout-paddle/provider.ts +59 -0
  33. package/kit/registry/checkout-paddle/files/lib/checkout-paddle/register.ts +7 -0
  34. package/kit/registry/checkout-stripe/docs/checkout-stripe.md +17 -0
  35. package/kit/registry/checkout-stripe/feature.json +54 -0
  36. package/kit/registry/checkout-stripe/files/app/api/webhooks/stripe/route.ts +23 -0
  37. package/kit/registry/checkout-stripe/files/lib/checkout-stripe/provider.ts +73 -0
  38. package/kit/registry/checkout-stripe/files/lib/checkout-stripe/register.ts +7 -0
  39. package/kit/registry/checkout-yookassa/docs/checkout-yookassa.md +20 -0
  40. package/kit/registry/checkout-yookassa/feature.json +52 -0
  41. package/kit/registry/checkout-yookassa/files/app/api/webhooks/yookassa/route.ts +22 -0
  42. package/kit/registry/checkout-yookassa/files/lib/checkout-yookassa/provider.ts +82 -0
  43. package/kit/registry/checkout-yookassa/files/lib/checkout-yookassa/register.ts +7 -0
  44. package/kit/registry/product-page/docs/product-page.md +16 -0
  45. package/kit/registry/product-page/feature.json +39 -0
  46. package/kit/registry/product-page/files/components/product/ProductGallery.tsx +37 -0
  47. package/kit/registry/product-page/files/components/product/ProductView.tsx +42 -0
  48. package/kit/registry/product-page/files/lib/product/data.ts +10 -0
  49. package/kit/registry/seo/docs/seo.md +14 -0
  50. package/kit/registry/seo/feature.json +35 -0
  51. package/kit/registry/seo/files/components/seo/JsonLd.tsx +15 -0
  52. package/kit/registry/seo/files/lib/seo/jsonld.ts +29 -0
  53. package/kit/registry/seo/files/lib/seo/metadata.ts +35 -0
  54. package/kit/registry/stubs/route-glue.d.ts +45 -0
  55. package/kit/registry/tsconfig.json +21 -0
  56. package/kit/registry/tsconfig.routes.json +9 -0
  57. package/kit/templates/README.md +27 -0
  58. package/kit/templates/backend-payload/files/.dockerignore +8 -0
  59. package/kit/templates/backend-payload/files/Dockerfile +29 -0
  60. package/kit/templates/backend-payload/files/app/(payload)/admin/[[...segments]]/not-found.tsx +18 -0
  61. package/kit/templates/backend-payload/files/app/(payload)/admin/[[...segments]]/page.tsx +18 -0
  62. package/kit/templates/backend-payload/files/app/(payload)/admin/importMap.js +3 -0
  63. package/kit/templates/backend-payload/files/app/(payload)/api/[...slug]/route.ts +18 -0
  64. package/kit/templates/backend-payload/files/app/(payload)/api/graphql/route.ts +6 -0
  65. package/kit/templates/backend-payload/files/app/(payload)/api/graphql-playground/route.ts +5 -0
  66. package/kit/templates/backend-payload/files/app/(payload)/custom.scss +1 -0
  67. package/kit/templates/backend-payload/files/app/(payload)/layout.tsx +23 -0
  68. package/kit/templates/backend-payload/files/docker-compose.yml +35 -0
  69. package/kit/templates/backend-payload/files/lib/adapter/db-decision.ts +43 -0
  70. package/kit/templates/backend-payload/files/lib/adapter/db-ping.ts +29 -0
  71. package/kit/templates/backend-payload/files/lib/adapter/db.ts +27 -0
  72. package/kit/templates/backend-payload/files/lib/adapter/index.ts +31 -0
  73. package/kit/templates/backend-payload/files/lib/adapter/map.ts +112 -0
  74. package/kit/templates/backend-payload/files/lib/adapter/payload-catalog.ts +79 -0
  75. package/kit/templates/backend-payload/files/lib/adapter/payload-commerce.ts +154 -0
  76. package/kit/templates/backend-payload/files/lib/adapter/payload-types.ts +50 -0
  77. package/kit/templates/backend-payload/files/lib/seed/admin.ts +23 -0
  78. package/kit/templates/backend-payload/files/lib/seed/demo.ts +79 -0
  79. package/kit/templates/backend-payload/files/lib/seed/guards.ts +5 -0
  80. package/kit/templates/backend-payload/files/lib/seed/richtext.ts +26 -0
  81. package/kit/templates/backend-payload/files/lib/seed/run.ts +51 -0
  82. package/kit/templates/backend-payload/files/next.config.mjs +10 -0
  83. package/kit/templates/backend-payload/files/payload.config.ts +32 -0
  84. package/kit/templates/backend-payload/files/seed-assets/placeholder-1.svg +5 -0
  85. package/kit/templates/backend-payload/files/seed-assets/placeholder-2.svg +5 -0
  86. package/kit/templates/backend-payload/files/seed-assets/placeholder-3.svg +5 -0
  87. package/kit/templates/backend-payload/files/seed-assets/placeholder-4.svg +5 -0
  88. package/kit/templates/backend-payload/files/seed-assets/placeholder-5.svg +5 -0
  89. package/kit/templates/backend-vendure/files/.dockerignore +8 -0
  90. package/kit/templates/backend-vendure/files/Dockerfile +24 -0
  91. package/kit/templates/backend-vendure/files/docker-compose.yml +50 -0
  92. package/kit/templates/backend-vendure/files/lib/adapter/graphql.ts +27 -0
  93. package/kit/templates/backend-vendure/files/lib/adapter/index.ts +20 -0
  94. package/kit/templates/backend-vendure/files/lib/adapter/map.ts +107 -0
  95. package/kit/templates/backend-vendure/files/lib/adapter/vendure-catalog.ts +62 -0
  96. package/kit/templates/backend-vendure/files/lib/adapter/vendure-commerce.ts +76 -0
  97. package/kit/templates/backend-vendure/files/lib/adapter/vendure-types.ts +61 -0
  98. package/kit/templates/backend-vendure/files/lib/seed/populate.ts +22 -0
  99. package/kit/templates/backend-vendure/files/src/index.ts +15 -0
  100. package/kit/templates/backend-vendure/files/vendure-config.ts +56 -0
  101. package/kit/templates/base/files/.claude/commands/add-feature.md +18 -0
  102. package/kit/templates/base/files/.claude/commands/design.md +14 -0
  103. package/kit/templates/base/files/.claude/commands/doctor.md +15 -0
  104. package/kit/templates/base/files/.claude/commands/setup.md +15 -0
  105. package/kit/templates/base/files/.claude/commands/update.md +16 -0
  106. package/kit/templates/base/files/AGENTS.md +20 -0
  107. package/kit/templates/base/files/app/(frontend)/categories/[slug]/page.tsx +28 -0
  108. package/kit/templates/base/files/app/(frontend)/layout.tsx +32 -0
  109. package/kit/templates/base/files/app/(frontend)/page.tsx +27 -0
  110. package/kit/templates/base/files/app/(frontend)/products/[slug]/page.tsx +43 -0
  111. package/kit/templates/base/files/app/globals.css +6 -0
  112. package/kit/templates/base/files/components/layout/SiteFooter.tsx +15 -0
  113. package/kit/templates/base/files/components/layout/SiteHeader.tsx +23 -0
  114. package/kit/templates/base/files/design/README.md +9 -0
  115. package/kit/templates/base/files/lib/site.ts +10 -0
  116. package/kit/templates/base/files/next.config.mjs +7 -0
  117. package/kit/templates/base/files/postcss.config.mjs +7 -0
  118. package/kit/templates/base/files/public/.gitkeep +0 -0
  119. package/kit/templates/base/files/tailwind.config.ts +13 -0
  120. package/kit/templates/base/files/tsconfig.json +25 -0
  121. package/kit/templates/tsconfig.json +18 -0
  122. package/package.json +7 -6
@@ -0,0 +1,32 @@
1
+ // The client's Payload 3 config. Collections are assembled by the blueprint (base +
2
+ // additive feature extensions, lib/blueprint.ts — generated by the CLI). The DB is
3
+ // chosen zero-config (db.ts → §18.1). onInit runs the demo seed and dev admin (dev only).
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { buildConfig } from 'payload';
7
+ import type { CollectionConfig } from 'payload';
8
+ import { lexicalEditor } from '@payloadcms/richtext-lexical';
9
+ import sharp from 'sharp';
10
+ import { collections } from './lib/blueprint.js';
11
+ import { resolveDbAdapter } from './lib/adapter/db.js';
12
+ import { seedDemo } from './lib/seed/run.js';
13
+ import { ensureDevAdmin } from './lib/seed/admin.js';
14
+
15
+ const dirname = path.dirname(fileURLToPath(import.meta.url));
16
+
17
+ export default buildConfig({
18
+ admin: {
19
+ user: 'users',
20
+ importMap: { baseDir: path.resolve(dirname) },
21
+ },
22
+ collections: collections as unknown as CollectionConfig[],
23
+ editor: lexicalEditor(),
24
+ secret: process.env.PAYLOAD_SECRET ?? 'dev-secret-change-me',
25
+ db: await resolveDbAdapter(),
26
+ sharp,
27
+ typescript: { outputFile: path.resolve(dirname, 'payload-types.ts') },
28
+ async onInit(payload) {
29
+ await seedDemo(payload);
30
+ await ensureDevAdmin(payload);
31
+ },
32
+ });
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="600" height="600" viewBox="0 0 600 600" role="img" aria-label="Classic T-Shirt">
2
+ <rect width="600" height="600" fill="#e7e5e4"/>
3
+ <rect x="40" y="40" width="520" height="520" rx="16" fill="#d6d3d1"/>
4
+ <text x="300" y="312" font-family="system-ui, sans-serif" font-size="40" fill="#57534e" text-anchor="middle">T-Shirt</text>
5
+ </svg>
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="600" height="600" viewBox="0 0 600 600" role="img" aria-label="Zip Hoodie">
2
+ <rect width="600" height="600" fill="#e0e7ff"/>
3
+ <rect x="40" y="40" width="520" height="520" rx="16" fill="#c7d2fe"/>
4
+ <text x="300" y="312" font-family="system-ui, sans-serif" font-size="48" fill="#4338ca" text-anchor="middle">Hoodie</text>
5
+ </svg>
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="600" height="600" viewBox="0 0 600 600" role="img" aria-label="Logo Cap">
2
+ <rect width="600" height="600" fill="#dcfce7"/>
3
+ <rect x="40" y="40" width="520" height="520" rx="16" fill="#bbf7d0"/>
4
+ <text x="300" y="312" font-family="system-ui, sans-serif" font-size="48" fill="#15803d" text-anchor="middle">Cap</text>
5
+ </svg>
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="600" height="600" viewBox="0 0 600 600" role="img" aria-label="Tote Bag">
2
+ <rect width="600" height="600" fill="#fef3c7"/>
3
+ <rect x="40" y="40" width="520" height="520" rx="16" fill="#fde68a"/>
4
+ <text x="300" y="312" font-family="system-ui, sans-serif" font-size="44" fill="#b45309" text-anchor="middle">Tote</text>
5
+ </svg>
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="600" height="600" viewBox="0 0 600 600" role="img" aria-label="Socks, 3 Pairs">
2
+ <rect width="600" height="600" fill="#fae8ff"/>
3
+ <rect x="40" y="40" width="520" height="520" rx="16" fill="#f5d0fe"/>
4
+ <text x="300" y="312" font-family="system-ui, sans-serif" font-size="48" fill="#a21caf" text-anchor="middle">Socks</text>
5
+ </svg>
@@ -0,0 +1,8 @@
1
+ node_modules
2
+ .next
3
+ .git
4
+ .vitrine
5
+ .env
6
+ .env.local
7
+ static/assets
8
+ **/*.tsbuildinfo
@@ -0,0 +1,24 @@
1
+ # syntax=docker/dockerfile:1
2
+ # Vitrine client image on Vendure. One image, two commands (compose): the Vendure server
3
+ # (`node dist/index.js`) and the Next storefront (`next start`). Target — VPS.
4
+ FROM node:20-alpine AS base
5
+ RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
6
+ WORKDIR /app
7
+
8
+ FROM base AS deps
9
+ COPY package.json pnpm-lock.yaml* ./
10
+ RUN pnpm install --frozen-lockfile || pnpm install
11
+
12
+ FROM base AS build
13
+ ENV NEXT_TELEMETRY_DISABLED=1
14
+ COPY --from=deps /app/node_modules ./node_modules
15
+ COPY . .
16
+ RUN pnpm build
17
+
18
+ FROM base AS runtime
19
+ ENV NODE_ENV=production
20
+ WORKDIR /app
21
+ COPY --from=build /app ./
22
+ EXPOSE 3000 3001
23
+ # The command is overridden in docker-compose (web / server).
24
+ CMD ["pnpm", "start"]
@@ -0,0 +1,50 @@
1
+ # Reference Vitrine deployment on Vendure: db (Postgres) + server (Vendure) + web (Next).
2
+ # Run: VENDURE_SUPERADMIN_PASSWORD=... docker compose up --build
3
+ services:
4
+ db:
5
+ image: postgres:16-alpine
6
+ restart: unless-stopped
7
+ environment:
8
+ POSTGRES_USER: vitrine
9
+ POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-vitrine}
10
+ POSTGRES_DB: vitrine
11
+ volumes:
12
+ - pgdata:/var/lib/postgresql/data
13
+ healthcheck:
14
+ test: ['CMD-SHELL', 'pg_isready -U vitrine']
15
+ interval: 5s
16
+ timeout: 5s
17
+ retries: 10
18
+
19
+ server:
20
+ build:
21
+ context: .
22
+ restart: unless-stopped
23
+ command: ['node', 'dist/index.js']
24
+ environment:
25
+ NODE_ENV: production
26
+ DATABASE_URL: postgres://vitrine:${POSTGRES_PASSWORD:-vitrine}@db:5432/vitrine
27
+ VENDURE_SUPERADMIN_USERNAME: ${VENDURE_SUPERADMIN_USERNAME:-superadmin}
28
+ VENDURE_SUPERADMIN_PASSWORD: ${VENDURE_SUPERADMIN_PASSWORD:?set VENDURE_SUPERADMIN_PASSWORD}
29
+ ports:
30
+ - '3001:3001'
31
+ depends_on:
32
+ db:
33
+ condition: service_healthy
34
+
35
+ web:
36
+ build:
37
+ context: .
38
+ restart: unless-stopped
39
+ command: ['pnpm', 'start']
40
+ environment:
41
+ NODE_ENV: production
42
+ VENDURE_SHOP_API_URL: http://server:3001/shop-api
43
+ NEXT_PUBLIC_SITE_URL: ${NEXT_PUBLIC_SITE_URL:-http://localhost:3000}
44
+ ports:
45
+ - '3000:3000'
46
+ depends_on:
47
+ - server
48
+
49
+ volumes:
50
+ pgdata:
@@ -0,0 +1,27 @@
1
+ // Thin Vendure Shop API client (GraphQL over fetch, no dependencies). The active
2
+ // order's session uses a bearer token (vendure-auth-token). Next glue.
3
+ const SHOP_API = process.env.VENDURE_SHOP_API_URL ?? 'http://localhost:3001/shop-api';
4
+
5
+ export interface ShopResult<T> {
6
+ data: T;
7
+ token?: string;
8
+ }
9
+
10
+ export async function shopQuery<T>(
11
+ query: string,
12
+ variables: Record<string, unknown> = {},
13
+ token?: string,
14
+ ): Promise<ShopResult<T>> {
15
+ const res = await fetch(SHOP_API, {
16
+ method: 'POST',
17
+ headers: {
18
+ 'content-type': 'application/json',
19
+ ...(token ? { authorization: `Bearer ${token}` } : {}),
20
+ },
21
+ body: JSON.stringify({ query, variables }),
22
+ });
23
+ const nextToken = res.headers.get('vendure-auth-token') ?? token ?? undefined;
24
+ const json = (await res.json()) as { data: T; errors?: unknown[] };
25
+ if (json.errors) throw new Error(`[vitrine] Vendure GraphQL: ${JSON.stringify(json.errors)}`);
26
+ return { data: json.data, token: nextToken };
27
+ }
@@ -0,0 +1,20 @@
1
+ // Resolver for the active backend (Vendure). The same interface as backend-payload —
2
+ // so the storefront (app/(frontend)) and the catalog/cart features don't change.
3
+ import type { CatalogSource, CommerceBackend } from '@vitrine-kit/contracts';
4
+ import { VendureCatalogSource } from './vendure-catalog.js';
5
+ import { VendureCommerceBackend } from './vendure-commerce.js';
6
+
7
+ let catalog: CatalogSource | null = null;
8
+ let commerce: CommerceBackend | null = null;
9
+
10
+ export async function getCatalogSource(): Promise<CatalogSource> {
11
+ if (!catalog) catalog = new VendureCatalogSource();
12
+ return catalog;
13
+ }
14
+
15
+ export async function getCommerceBackend(): Promise<CommerceBackend> {
16
+ if (!commerce) {
17
+ commerce = new VendureCommerceBackend(process.env.NEXT_PUBLIC_SITE_URL ?? 'http://localhost:3000');
18
+ }
19
+ return commerce;
20
+ }
@@ -0,0 +1,107 @@
1
+ // Pure mappers from Vendure Shop API → contract types. They depend only on
2
+ // @vitrine-kit/contracts and local structural types. This is the proof of
3
+ // portability: the same Product/Category/Cart/Order types the Payload adapter returns
4
+ // — meaning the catalog/cart features work on Vendure WITHOUT changes.
5
+ import type { Cart, Category, Money, Order, OrderStatus, Product, Variant } from '@vitrine-kit/contracts';
6
+ import type { VAsset, VCollection, VOrder, VProduct, VVariant } from './vendure-types.js';
7
+
8
+ const asId = (v: string | number): string => String(v);
9
+
10
+ export function mapVendureCollection(c: VCollection): Category {
11
+ return {
12
+ id: asId(c.id),
13
+ slug: c.slug,
14
+ title: c.name,
15
+ description: c.description ?? undefined,
16
+ parentId: c.parent ? asId(c.parent.id) : null,
17
+ };
18
+ }
19
+
20
+ function mapVariant(v: VVariant): Variant {
21
+ return {
22
+ id: asId(v.id),
23
+ sku: v.sku,
24
+ title: v.name ?? undefined,
25
+ price: v.priceWithTax as Money,
26
+ currency: v.currencyCode,
27
+ stock: null, // the Shop API returns stockLevel as a string — we don't track numeric stock here
28
+ };
29
+ }
30
+
31
+ export function mapVendureProduct(p: VProduct): Product {
32
+ const variants = p.variants.map(mapVariant);
33
+ const assets = [p.featuredAsset, ...(p.assets ?? [])].filter((a): a is VAsset => a != null);
34
+ const images = assets.map((a) => ({ url: a.preview, width: a.width ?? undefined, height: a.height ?? undefined }));
35
+ const prices = variants.map((v) => v.price);
36
+ const currency = variants[0]?.currency ?? 'USD';
37
+
38
+ return {
39
+ id: asId(p.id),
40
+ slug: p.slug,
41
+ title: p.name,
42
+ description: p.description ?? undefined,
43
+ categoryIds: (p.collections ?? []).map((c) => asId(c.id)),
44
+ images,
45
+ variants,
46
+ priceRange: prices.length > 0 ? { min: Math.min(...prices), max: Math.max(...prices), currency } : undefined,
47
+ };
48
+ }
49
+
50
+ /** Vendure order state → contract OrderStatus. */
51
+ export function mapOrderState(state: string): OrderStatus {
52
+ switch (state) {
53
+ case 'PaymentSettled':
54
+ case 'PaymentAuthorized':
55
+ return 'paid';
56
+ case 'Shipped':
57
+ case 'Delivered':
58
+ case 'Fulfilled':
59
+ return 'fulfilled';
60
+ case 'Cancelled':
61
+ return 'cancelled';
62
+ default:
63
+ return 'pending';
64
+ }
65
+ }
66
+
67
+ /** Active Vendure order → Cart (the same the cart feature gets on Payload). */
68
+ export function mapVendureOrderToCart(o: VOrder): Cart {
69
+ return {
70
+ id: o.code,
71
+ currency: o.currencyCode,
72
+ subtotal: o.subTotalWithTax,
73
+ total: o.totalWithTax,
74
+ lines: o.lines.map((l) => ({
75
+ id: asId(l.id),
76
+ variantId: asId(l.productVariant.id),
77
+ productId: l.productVariant.product ? asId(l.productVariant.product.id) : '',
78
+ title: l.productVariant.name,
79
+ quantity: l.quantity,
80
+ unitPrice: l.unitPriceWithTax,
81
+ lineTotal: l.linePriceWithTax,
82
+ image: l.featuredAsset?.preview,
83
+ })),
84
+ };
85
+ }
86
+
87
+ export function mapVendureOrder(o: VOrder): Order {
88
+ const cart = mapVendureOrderToCart(o);
89
+ return {
90
+ id: asId(o.id),
91
+ number: o.code,
92
+ status: mapOrderState(o.state),
93
+ lines: cart.lines.map((l) => ({
94
+ variantId: l.variantId,
95
+ productId: l.productId,
96
+ title: l.title,
97
+ quantity: l.quantity,
98
+ unitPrice: l.unitPrice,
99
+ lineTotal: l.lineTotal,
100
+ })),
101
+ currency: o.currencyCode,
102
+ subtotal: o.subTotalWithTax,
103
+ total: o.totalWithTax,
104
+ email: o.customer?.emailAddress ?? undefined,
105
+ createdAt: o.createdAt ?? new Date().toISOString(),
106
+ };
107
+ }
@@ -0,0 +1,62 @@
1
+ // CatalogSource contract implementation on top of the Vendure Shop API. Normalization
2
+ // lives in the pure mappers (map.ts). The same contract as the Payload adapter → the
3
+ // catalog features are portable without changes.
4
+ import type { CatalogSource, Category, Product, ProductQuery } from '@vitrine-kit/contracts';
5
+ import { shopQuery } from './graphql.js';
6
+ import { mapVendureCollection, mapVendureProduct } from './map.js';
7
+ import type { VCollection, VProduct } from './vendure-types.js';
8
+
9
+ const PRODUCT_FIELDS = `
10
+ id name slug description
11
+ featuredAsset { preview width height }
12
+ assets { preview }
13
+ collections { id slug name }
14
+ variants { id sku name priceWithTax currencyCode stockLevel }
15
+ `;
16
+
17
+ export class VendureCatalogSource implements CatalogSource {
18
+ async listProducts(query: ProductQuery = {}): Promise<Product[]> {
19
+ const take = query.perPage ?? 24;
20
+ const skip = ((query.page ?? 1) - 1) * take;
21
+
22
+ if (query.category) {
23
+ const { data } = await shopQuery<{ collection: { productVariants: { items: { product: VProduct }[] } } | null }>(
24
+ `query ($slug: String!) { collection(slug: $slug) { productVariants { items { product { ${PRODUCT_FIELDS} } } } } }`,
25
+ { slug: query.category },
26
+ );
27
+ const byId = new Map<string, VProduct>();
28
+ for (const it of data.collection?.productVariants.items ?? []) byId.set(String(it.product.id), it.product);
29
+ return [...byId.values()].map(mapVendureProduct);
30
+ }
31
+
32
+ const { data } = await shopQuery<{ products: { items: VProduct[] } }>(
33
+ `query ($take: Int, $skip: Int) { products(options: { take: $take, skip: $skip }) { items { ${PRODUCT_FIELDS} } } }`,
34
+ { take, skip },
35
+ );
36
+ return data.products.items.map(mapVendureProduct);
37
+ }
38
+
39
+ async getProduct(slug: string): Promise<Product | null> {
40
+ const { data } = await shopQuery<{ product: VProduct | null }>(
41
+ `query ($slug: String!) { product(slug: $slug) { ${PRODUCT_FIELDS} } }`,
42
+ { slug },
43
+ );
44
+ return data.product ? mapVendureProduct(data.product) : null;
45
+ }
46
+
47
+ async listCategories(): Promise<Category[]> {
48
+ const { data } = await shopQuery<{ collections: { items: VCollection[] } }>(
49
+ `{ collections { items { id slug name description parent { id } } } }`,
50
+ );
51
+ return data.collections.items.map(mapVendureCollection);
52
+ }
53
+
54
+ async search(term: string): Promise<Product[]> {
55
+ const { data } = await shopQuery<{ search: { items: { slug: string }[] } }>(
56
+ `query ($term: String!) { search(input: { term: $term, groupByProduct: true }) { items { slug } } }`,
57
+ { term },
58
+ );
59
+ const products = await Promise.all(data.search.items.map((i) => this.getProduct(i.slug)));
60
+ return products.filter((p): p is Product => p !== null);
61
+ }
62
+ }
@@ -0,0 +1,76 @@
1
+ // CommerceBackend contract implementation on top of Vendure (active order + session
2
+ // token). cartId == the Vendure session auth-token. Money totals come from Vendure;
3
+ // normalization lives in map.ts. Payment — Vendure's Stripe plugin (startCheckout moves
4
+ // the order to ArrangingPayment). Next glue, validated against a running Vendure.
5
+ import type { Cart, CommerceBackend, Order } from '@vitrine-kit/contracts';
6
+ import { shopQuery } from './graphql.js';
7
+ import { mapVendureOrder, mapVendureOrderToCart } from './map.js';
8
+ import type { VOrder } from './vendure-types.js';
9
+
10
+ const ORDER_FIELDS = `
11
+ id code state currencyCode subTotalWithTax totalWithTax createdAt
12
+ customer { emailAddress }
13
+ lines {
14
+ id quantity unitPriceWithTax linePriceWithTax
15
+ featuredAsset { preview }
16
+ productVariant { id sku name product { id slug } }
17
+ }
18
+ `;
19
+
20
+ const emptyCart = (id: string): Cart => ({ id, lines: [], currency: 'USD', subtotal: 0, total: 0 });
21
+
22
+ export class VendureCommerceBackend implements CommerceBackend {
23
+ constructor(private readonly baseUrl: string) {}
24
+
25
+ async createCart(): Promise<Cart> {
26
+ // Vendure issues a session token on any request; the active order is created on addItem.
27
+ const { token } = await shopQuery(`{ activeChannel { id } }`);
28
+ return emptyCart(token ?? '');
29
+ }
30
+
31
+ async getCart(cartId: string): Promise<Cart | null> {
32
+ const { data } = await shopQuery<{ activeOrder: VOrder | null }>(`{ activeOrder { ${ORDER_FIELDS} } }`, {}, cartId);
33
+ return data.activeOrder ? mapVendureOrderToCart(data.activeOrder) : emptyCart(cartId);
34
+ }
35
+
36
+ async addItem(cartId: string, variantId: string, qty: number): Promise<Cart> {
37
+ const { data } = await shopQuery<{ addItemToOrder: VOrder }>(
38
+ `mutation ($id: ID!, $q: Int!) { addItemToOrder(productVariantId: $id, quantity: $q) { ...on Order { ${ORDER_FIELDS} } } }`,
39
+ { id: variantId, q: qty },
40
+ cartId,
41
+ );
42
+ return mapVendureOrderToCart(data.addItemToOrder);
43
+ }
44
+
45
+ async updateItem(cartId: string, lineId: string, qty: number): Promise<Cart> {
46
+ const { data } = await shopQuery<{ adjustOrderLine: VOrder }>(
47
+ `mutation ($id: ID!, $q: Int!) { adjustOrderLine(orderLineId: $id, quantity: $q) { ...on Order { ${ORDER_FIELDS} } } }`,
48
+ { id: lineId, q: qty },
49
+ cartId,
50
+ );
51
+ return mapVendureOrderToCart(data.adjustOrderLine);
52
+ }
53
+
54
+ async removeItem(cartId: string, lineId: string): Promise<Cart> {
55
+ const { data } = await shopQuery<{ removeOrderLine: VOrder }>(
56
+ `mutation ($id: ID!) { removeOrderLine(orderLineId: $id) { ...on Order { ${ORDER_FIELDS} } } }`,
57
+ { id: lineId },
58
+ cartId,
59
+ );
60
+ return mapVendureOrderToCart(data.removeOrderLine);
61
+ }
62
+
63
+ async startCheckout(cartId: string): Promise<{ redirectUrl: string }> {
64
+ // Transition to payment; Vendure's Stripe plugin creates a PaymentIntent on the payment page.
65
+ await shopQuery(`mutation { transitionOrderToState(state: "ArrangingPayment") { ...on Order { id } } }`, {}, cartId);
66
+ return { redirectUrl: `${this.baseUrl}/checkout/payment` };
67
+ }
68
+
69
+ async getOrder(id: string): Promise<Order | null> {
70
+ const { data } = await shopQuery<{ orderByCode: VOrder | null }>(
71
+ `query ($code: String!) { orderByCode(code: $code) { ${ORDER_FIELDS} } }`,
72
+ { code: id },
73
+ );
74
+ return data.orderByCode ? mapVendureOrder(data.orderByCode) : null;
75
+ }
76
+ }
@@ -0,0 +1,61 @@
1
+ // Structural shapes of Vendure Shop API responses (GraphQL). Intentionally loose and
2
+ // do NOT import the generated Vendure types — so that map.ts stays pure
3
+ // (contracts only) and testable without a running Vendure.
4
+
5
+ export type VId = string | number;
6
+
7
+ export interface VAsset {
8
+ preview: string;
9
+ width?: number | null;
10
+ height?: number | null;
11
+ }
12
+
13
+ export interface VCollection {
14
+ id: VId;
15
+ slug: string;
16
+ name: string;
17
+ description?: string | null;
18
+ parent?: { id: VId } | null;
19
+ }
20
+
21
+ export interface VVariant {
22
+ id: VId;
23
+ sku: string;
24
+ name?: string | null;
25
+ /** Vendure stores the price as an integer in minor units (like Money). */
26
+ priceWithTax: number;
27
+ currencyCode: string;
28
+ stockLevel?: string | null; // 'IN_STOCK' | 'OUT_OF_STOCK' | ... (not a number in the Shop API)
29
+ }
30
+
31
+ export interface VProduct {
32
+ id: VId;
33
+ name: string;
34
+ slug: string;
35
+ description?: string | null;
36
+ featuredAsset?: VAsset | null;
37
+ assets?: VAsset[] | null;
38
+ variants: VVariant[];
39
+ collections?: VCollection[] | null;
40
+ }
41
+
42
+ export interface VOrderLine {
43
+ id: VId;
44
+ quantity: number;
45
+ unitPriceWithTax: number;
46
+ linePriceWithTax: number;
47
+ featuredAsset?: VAsset | null;
48
+ productVariant: { id: VId; sku: string; name: string; product?: { id: VId; slug?: string } | null };
49
+ }
50
+
51
+ export interface VOrder {
52
+ id: VId;
53
+ code: string;
54
+ state: string;
55
+ currencyCode: string;
56
+ subTotalWithTax: number;
57
+ totalWithTax: number;
58
+ createdAt?: string;
59
+ customer?: { emailAddress?: string | null } | null;
60
+ lines: VOrderLine[];
61
+ }
@@ -0,0 +1,22 @@
1
+ // Vendure demo catalog (§18 equivalent): dev ONLY and when the DB is empty
2
+ // (idempotent). Full population — Vendure populate(app, initialData,
3
+ // productsCsv) or admin import; here — a guard + an integration point. The superadmin
4
+ // is created by Vendure from configuration (vendure-config.ts, env).
5
+ import type { INestApplicationContext } from '@nestjs/common';
6
+ import { Logger, ProductService, RequestContextService } from '@vendure/core';
7
+
8
+ export async function populateDemo(app: INestApplicationContext): Promise<void> {
9
+ if (process.env.NODE_ENV === 'production') return; // never in prod
10
+
11
+ const ctx = await app.get(RequestContextService).create({ apiType: 'admin' });
12
+ const { totalItems } = await app.get(ProductService).findAll(ctx, { take: 1 });
13
+ if (totalItems > 0) return; // idempotent: a non-empty DB — leave it alone
14
+
15
+ Logger.info(
16
+ '[vitrine] Vendure: empty DB in dev — populate the demo catalog via ' +
17
+ 'populate(app, ./seed/initial-data, ./seed/products.csv) or admin import.',
18
+ 'Vitrine',
19
+ );
20
+ // This is where Vendure populate() wires up with local initial-data + products.csv
21
+ // (offline, assets in static/). Left as an integration point (requires a running Vendure).
22
+ }
@@ -0,0 +1,15 @@
1
+ // Vendure server bootstrap (separate process: `pnpm vendure`). The Next storefront
2
+ // (`pnpm dev`) talks to the Shop API via lib/adapter. In dev with an empty DB —
3
+ // demo populate (guarded inside populateDemo).
4
+ import { bootstrap } from '@vendure/core';
5
+ import { config } from '../vendure-config.js';
6
+ import { populateDemo } from '../lib/seed/populate.js';
7
+
8
+ bootstrap(config)
9
+ .then(async (app) => {
10
+ await populateDemo(app);
11
+ })
12
+ .catch((err: unknown) => {
13
+ console.error('[vitrine] Vendure bootstrap failed:', err);
14
+ process.exit(1);
15
+ });
@@ -0,0 +1,56 @@
1
+ // Vendure server config. DB: Postgres (DATABASE_URL) or the embedded better-sqlite3
2
+ // in dev (zero-config, §18 equivalent). Superadmin — from env (change it for prod).
3
+ // The Stripe plugin is wired in at tier=full with payments.
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { DefaultJobQueuePlugin, DefaultSearchPlugin, type VendureConfig } from '@vendure/core';
7
+ import { AssetServerPlugin } from '@vendure/asset-server-plugin';
8
+
9
+ const dirname = path.dirname(fileURLToPath(import.meta.url));
10
+ const IS_DEV = process.env.NODE_ENV !== 'production';
11
+
12
+ function dbConnectionOptions(): VendureConfig['dbConnectionOptions'] {
13
+ const url = process.env.DATABASE_URL;
14
+ if (url) {
15
+ return { type: 'postgres', url, synchronize: IS_DEV } as VendureConfig['dbConnectionOptions'];
16
+ }
17
+ if (!IS_DEV) {
18
+ // In production there's no silent fallback to SQLite (§18).
19
+ throw new Error('[vitrine] DATABASE_URL is required in production');
20
+ }
21
+ // dev zero-config: embedded SQLite (no DB server).
22
+ return {
23
+ type: 'better-sqlite3',
24
+ database: path.join(dirname, '.vitrine', 'vendure.sqlite'),
25
+ synchronize: true,
26
+ } as VendureConfig['dbConnectionOptions'];
27
+ }
28
+
29
+ export const config: VendureConfig = {
30
+ apiOptions: {
31
+ port: Number(process.env.VENDURE_PORT ?? 3001),
32
+ adminApiPath: 'admin-api',
33
+ shopApiPath: 'shop-api',
34
+ },
35
+ authOptions: {
36
+ tokenMethod: ['bearer', 'cookie'],
37
+ superadminCredentials: {
38
+ identifier: process.env.VENDURE_SUPERADMIN_USERNAME ?? 'superadmin',
39
+ // dev default; for prod you must set VENDURE_SUPERADMIN_PASSWORD.
40
+ password: process.env.VENDURE_SUPERADMIN_PASSWORD ?? 'superadmin',
41
+ },
42
+ cookieOptions: { secret: process.env.VENDURE_COOKIE_SECRET ?? 'dev-cookie-secret' },
43
+ },
44
+ dbConnectionOptions: dbConnectionOptions(),
45
+ paymentOptions: {
46
+ paymentMethodHandlers: [
47
+ // + StripePlugin handler (env STRIPE_SECRET_KEY / STRIPE_WEBHOOK_SECRET) for tier=full with payments.
48
+ ],
49
+ },
50
+ plugins: [
51
+ DefaultJobQueuePlugin.init({}),
52
+ DefaultSearchPlugin.init({ bufferUpdates: false }),
53
+ AssetServerPlugin.init({ route: 'assets', assetUploadDir: path.join(dirname, 'static', 'assets') }),
54
+ // StripePlugin.init({ storeCustomersInStripe: true }),
55
+ ],
56
+ };
@@ -0,0 +1,18 @@
1
+ ---
2
+ description: Add a feature from the Vitrine registry and style it
3
+ argument-hint: <feature>
4
+ ---
5
+
6
+ Add the `$ARGUMENTS` feature to the project:
7
+
8
+ 1. `vitrine list` — check that the feature is available and not yet installed (show the list).
9
+ 2. `vitrine add $ARGUMENTS` — copies files, registers slots/blueprint, merges in
10
+ env and dependencies, updates `site.config.ts`, `vitrine.json`, and the table in `CLAUDE.md`.
11
+ The feature's dependencies are pulled in automatically.
12
+ 3. If the feature added keys to `.env.example` — suggest filling them in `.env`.
13
+ 4. `vitrine design apply` — style the new feature for the current brand (only if `/design`
14
+ has an export; otherwise skip).
15
+ 5. `pnpm typecheck` (or `pnpm build`) — make sure the build is green.
16
+ 6. Don't edit generated files by hand. Remind the user to commit the changes.
17
+
18
+ To remove a feature — `vitrine remove <feature>` (only for `removable` ones).
@@ -0,0 +1,14 @@
1
+ ---
2
+ description: Apply the design from /design to the tokens (vitrine design apply)
3
+ ---
4
+
5
+ Apply the client design:
6
+
7
+ 1. Check that the `/design` folder has a brand export (something besides `README.md`: tokens,
8
+ screenshots, assets). If it's empty — ask the user to add the export and stop.
9
+ 2. `vitrine design apply` — a wrapper over Claude Code: it sets ONLY the CSS variable values
10
+ in `theme/client.css`, without touching logic/data/routing/structure. The token set is closed.
11
+ 3. Show the `theme/client.css` diff and briefly describe the palette/typography/spacing changes.
12
+ 4. The step is idempotent — re-running converges. Run it again if needed.
13
+
14
+ If Claude Code isn't found in PATH — point to it via `--bin` or `VITRINE_CLAUDE_BIN`.
@@ -0,0 +1,15 @@
1
+ ---
2
+ description: Check project consistency and fix discrepancies
3
+ ---
4
+
5
+ Check the project's health:
6
+
7
+ 1. `vitrine doctor` — reconciles `vitrine.json` ↔ files ↔ packages (`package.json`) ↔ env (`.env.example`).
8
+ 2. Work through the output. Each issue has a suggested fix:
9
+ - missing file/slot/provider/dependency → `vitrine add <feature>` (reinstalls and regenerates);
10
+ - version in the repo ≠ registry → `vitrine update <feature>`;
11
+ - feature not found in the registry → `vitrine kit update`;
12
+ - missing env key → add it to `.env.example`/`.env`;
13
+ - no design-instruction block in `CLAUDE.md` → refresh it (`kit update` brings it).
14
+ 3. Apply the fixes (prefer CLI commands over hand edits), then re-run `vitrine doctor`
15
+ until it's green. Report anything left that needs a user decision (e.g. secrets).