@vitrine-kit/core 0.2.0 → 0.2.2

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 CHANGED
@@ -1,7 +1,24 @@
1
1
  # @vitrine-kit/core
2
2
 
3
- Критическая логика Vitrine: runtime слотов и адаптера, order pipeline, обработчик Stripe-вебхука.
3
+ Vitrine's critical logic: the slot and adapter runtime, the order pipeline, and provider-neutral payment webhook dispatch.
4
4
 
5
- Здесь то, где «баг = инцидент у всех клиентов сразу» (§4 спеки). Поэтому это **версионируемый пакет**, а не copy-in: критфикс прилетает всем бампом версии.
5
+ This is where "a bug = an incident for every client at once" (spec §4). That's why it's a **versioned package** rather than copy-in: a critical fix reaches everyone via a version bump.
6
6
 
7
- Скелет (M0). Наполняется в M2 (runtime) и M8 (коммерция).
7
+ ## Global registries and isolation
8
+
9
+ `slotRegistry`, `adapters`, and `payments` are **module-level singletons** — state is shared by
10
+ everything importing this package in one process. That is the right default for a Vitrine client
11
+ (1 client = 1 repository = 1 store; `lib/slots.ts` / `lib/payments.ts` register once at module
12
+ load, which is idempotent-safe because generated registration modules only run once per process).
13
+
14
+ Do NOT reach for the globals when you need isolation:
15
+
16
+ - **Tests** — create your own: `createSlotRegistry()`, `createAdapterRegistry()`,
17
+ `createPaymentRegistry()`; `<Slot registry={...}>` accepts one. Or `clear()` the global in a
18
+ `finally` block.
19
+ - **Multi-tenant / multi-store servers** — one process serving several stores must NOT share the
20
+ globals (registrations from one tenant would leak into another). Keep a registry instance per
21
+ tenant and pass it explicitly.
22
+
23
+ Registering the same mount twice (e.g. calling a `register<Feature>Slots()` again after HMR)
24
+ duplicates it — re-registration should go through `clear()` first.
package/dist/index.d.ts CHANGED
@@ -1,10 +1,12 @@
1
- export { S as SlotRegistry, c as createSlotRegistry, g as getSlotMounts, r as registerSlot, s as slotRegistry } from './registry-B4Ak1hIR.js';
1
+ export { S as SlotRegistry, c as createSlotRegistry, g as getSlotMounts, r as registerSlot, s as slotRegistry } from './registry-CfORxf9H.js';
2
2
  import { Backend, SiteConfig, CatalogSource, CommerceBackend, Cart, Order, Money, CurrencyCode, OrderStatus } from '@vitrine-kit/contracts';
3
3
 
4
+ declare const CORE_VERSION: "0.2.2";
5
+
4
6
  interface AdapterFactory {
5
7
  backend: Backend;
6
8
  createCatalog(config: SiteConfig): CatalogSource;
7
- /** Отсутствует для чисто-каталожных бэкендов. */
9
+ /** Absent for catalog-only backends. */
8
10
  createCommerce?(config: SiteConfig): CommerceBackend;
9
11
  }
10
12
  interface AdapterRegistry {
@@ -14,82 +16,82 @@ interface AdapterRegistry {
14
16
  clear(): void;
15
17
  }
16
18
  declare function createAdapterRegistry(): AdapterRegistry;
17
- /** Глобальный реестр адаптеров по умолчанию. */
19
+ /** The default global adapter registry. */
18
20
  declare const adapters: AdapterRegistry;
19
21
 
20
22
  interface OrderPipelineContext {
21
23
  cart: Cart;
22
- /** Заполняется шагами пайплайна (создание заказа). */
24
+ /** Filled by pipeline stages (order creation). */
23
25
  order?: Order;
24
- /** Произвольные данные шагов (платёж, резерв склада, доставка). */
26
+ /** Arbitrary stage data (payment, stock reservation, shipping). */
25
27
  meta: Record<string, unknown>;
26
28
  }
27
29
  type OrderStage<T = OrderPipelineContext> = (ctx: T) => T | Promise<T>;
28
30
  /**
29
- * Последовательно прогоняет контекст через шаги. Любая ошибка шага прерывает
30
- * пайплайн (откат/идемпотентность ответственность конкретных шагов в M8).
31
+ * Runs the context through the stages sequentially. Any stage error aborts the
32
+ * pipeline (rollback/idempotency are the responsibility of specific stages).
31
33
  */
32
34
  declare function runPipeline<T>(ctx: T, stages: ReadonlyArray<OrderStage<T>>): Promise<T>;
33
35
 
34
36
  interface OrderCreationGuard {
35
- /** Статус корзины (карты Payload `carts`): 'converted' = уже оплачена. */
37
+ /** Cart status (the Payload `carts` map): 'converted' = already paid. */
36
38
  cartStatus?: string | null;
37
- /** Референс платежа текущего события (session/transaction/payment id). */
39
+ /** Payment reference of the current event (session/transaction/payment id). */
38
40
  providerRef?: string;
39
- /** paymentRef уже существующих заказов (обычно результат точечного поиска). */
41
+ /** paymentRef of existing orders (usually the result of a targeted lookup). */
40
42
  existingOrderRefs?: ReadonlyArray<string | undefined>;
41
43
  }
42
44
  /**
43
- * Создавать ли заказ для этого события. false, если корзина уже converted ИЛИ
44
- * для этого референса платежа заказ уже есть (повторная доставка/ретрай webhook).
45
+ * Whether to create an order for this event. false if the cart is already converted OR
46
+ * an order already exists for this payment reference (redelivery/retry of the webhook).
45
47
  */
46
48
  declare function shouldCreateOrder(guard: OrderCreationGuard): boolean;
47
49
 
48
- /** Совпадает с integrations.payments в site.config (контракт 4). */
50
+ /** Matches integrations.payments in site.config (contract 4). */
49
51
  type PaymentProviderName = 'stripe' | 'paddle' | 'yookassa';
50
52
  interface CreateCheckoutArgs {
51
53
  cart: Cart;
52
- /** Базовый URL витрины; success/cancel строятся относительно него. */
54
+ /** The storefront base URL; success/cancel are built relative to it. */
53
55
  baseUrl: string;
54
- /** По умолчанию '/order/success'. */
56
+ /** Defaults to '/order/success'. */
55
57
  successPath?: string;
56
- /** По умолчанию '/cart'. */
58
+ /** Defaults to '/cart'. */
57
59
  cancelPath?: string;
58
60
  }
59
61
  interface PaymentWebhookRequest {
60
62
  rawBody: string;
61
63
  headers: Record<string, string | null>;
62
64
  }
63
- /** Нормализованное событие провайдераобщий язык для роутов вебхуков. */
65
+ /** A normalized provider event the common language for webhook routes. */
64
66
  interface NormalizedPaymentEvent {
65
67
  kind: 'checkout_completed' | 'payment_failed' | 'unknown';
66
- /** Из metadata/custom_data/label провайдера. */
68
+ /** From the provider's metadata/custom_data/label. */
67
69
  cartId?: string;
68
- /** Уникальный референс платежа провайдераключ идемпотентности. */
70
+ /** The provider's unique payment reference the idempotency key. */
69
71
  providerRef?: string;
70
72
  email?: string;
71
- /** Исходный объект провайдера (на случай, если роуту нужны доп. поля). */
73
+ /** The raw provider object (in case the route needs extra fields). */
72
74
  raw: unknown;
73
75
  }
74
76
  interface PaymentProvider {
75
77
  name: PaymentProviderName;
76
- /** Создаёт hosted-checkout у провайдера → URL для редиректа. */
78
+ /** Creates a hosted checkout at the provider redirect URL. */
77
79
  createCheckout(args: CreateCheckoutArgs): Promise<{
78
80
  redirectUrl: string;
79
81
  }>;
80
- /** Верифицирует подпись/подлинность и нормализует событие. Бросает при невалидном. */
82
+ /** Verifies the signature/authenticity and normalizes the event. Throws if invalid. */
81
83
  verifyWebhook(req: PaymentWebhookRequest): Promise<NormalizedPaymentEvent>;
82
84
  }
83
85
 
84
86
  interface PaymentRegistry {
85
87
  register(provider: PaymentProvider): void;
86
- /** Активный провайдер по site.config.integrations.payments. */
88
+ /** The active provider by site.config.integrations.payments. */
87
89
  resolve(config: SiteConfig): PaymentProvider;
88
90
  get(name: PaymentProviderName): PaymentProvider | undefined;
89
91
  clear(): void;
90
92
  }
91
93
  declare function createPaymentRegistry(): PaymentRegistry;
92
- /** Глобальный реестр провайдеров по умолчанию. */
94
+ /** The default global provider registry. */
93
95
  declare const payments: PaymentRegistry;
94
96
 
95
97
  interface PaymentWebhookHandlers {
@@ -99,7 +101,7 @@ interface PaymentWebhookHandlers {
99
101
  interface PaymentWebhookResult {
100
102
  received: true;
101
103
  kind: NormalizedPaymentEvent['kind'];
102
- /** Был ли зарегистрирован обработчик для этого вида события. */
104
+ /** Whether a handler was registered for this event kind. */
103
105
  handled: boolean;
104
106
  }
105
107
  declare function handlePaymentWebhook(args: {
@@ -109,9 +111,9 @@ declare function handlePaymentWebhook(args: {
109
111
  }): Promise<PaymentWebhookResult>;
110
112
 
111
113
  declare function computeLineTotal(unitPrice: Money, quantity: number): Money;
112
- /** Пустая корзина. */
114
+ /** An empty cart. */
113
115
  declare function emptyCart(id: string, currency: CurrencyCode): Cart;
114
- /** Пересчитывает lineTotal каждой строки и итоги (subtotal/total с учётом скидки). */
116
+ /** Recomputes each line's lineTotal and the totals (subtotal/total incl. discount). */
115
117
  declare function recalcCart(cart: Cart): Cart;
116
118
  interface NewLineInput {
117
119
  id: string;
@@ -122,12 +124,12 @@ interface NewLineInput {
122
124
  quantity: number;
123
125
  image?: string;
124
126
  }
125
- /** Добавляет строку; если вариант уже в корзине увеличивает количество. */
127
+ /** Adds a line; if the variant is already in the cart, increases its quantity. */
126
128
  declare function addCartLine(cart: Cart, input: NewLineInput): Cart;
127
- /** Меняет количество строки; quantity ≤ 0 удаляет строку. */
129
+ /** Changes a line's quantity; quantity ≤ 0 removes the line. */
128
130
  declare function setCartLineQty(cart: Cart, lineId: string, quantity: number): Cart;
129
131
  declare function removeCartLine(cart: Cart, lineId: string): Cart;
130
- /** Суммарное число единиц в корзине (для индикатора в шапке). */
132
+ /** Total number of units in the cart (for the header indicator). */
131
133
  declare function cartItemCount(cart: Cart): number;
132
134
 
133
135
  interface BuildOrderOptions {
@@ -135,12 +137,10 @@ interface BuildOrderOptions {
135
137
  status?: OrderStatus;
136
138
  email?: string;
137
139
  number?: string;
138
- /** ISO-8601; по умолчанию сейчас. */
140
+ /** ISO-8601; defaults to now. */
139
141
  createdAt?: string;
140
142
  }
141
- /** Снимок корзины в заказ (после оплаты). Итоги берём из корзиныне пересчитываем. */
143
+ /** Snapshots the cart into an order (after payment). Totals come from the cart not recomputed. */
142
144
  declare function buildOrderFromCart(cart: Cart, opts: BuildOrderOptions): Order;
143
145
 
144
- declare const CORE_VERSION: "0.1.0";
145
-
146
146
  export { type AdapterFactory, type AdapterRegistry, type BuildOrderOptions, CORE_VERSION, type CreateCheckoutArgs, type NewLineInput, type NormalizedPaymentEvent, type OrderCreationGuard, type OrderPipelineContext, type OrderStage, type PaymentProvider, type PaymentProviderName, type PaymentRegistry, type PaymentWebhookHandlers, type PaymentWebhookRequest, type PaymentWebhookResult, adapters, addCartLine, buildOrderFromCart, cartItemCount, computeLineTotal, createAdapterRegistry, createPaymentRegistry, emptyCart, handlePaymentWebhook, payments, recalcCart, removeCartLine, runPipeline, setCartLineQty, shouldCreateOrder };
package/dist/index.js CHANGED
@@ -5,6 +5,9 @@ import {
5
5
  slotRegistry
6
6
  } from "./chunk-BQVNYHVU.js";
7
7
 
8
+ // src/version.generated.ts
9
+ var CORE_VERSION = "0.2.2";
10
+
8
11
  // src/adapter/resolver.ts
9
12
  function createAdapterRegistry() {
10
13
  const factories = /* @__PURE__ */ new Map();
@@ -14,7 +17,7 @@ function createAdapterRegistry() {
14
17
  function factoryFor(config) {
15
18
  const f = factories.get(config.backend);
16
19
  if (!f) {
17
- throw new Error(`[vitrine] \u043D\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043E\u0432\u0430\u043D \u0430\u0434\u0430\u043F\u0442\u0435\u0440 \u0434\u043B\u044F backend "${config.backend}"`);
20
+ throw new Error(`[vitrine] no adapter registered for backend "${config.backend}"`);
18
21
  }
19
22
  return f;
20
23
  }
@@ -23,11 +26,11 @@ function createAdapterRegistry() {
23
26
  }
24
27
  function resolveCommerce(config) {
25
28
  if (config.tier === "catalog") {
26
- throw new Error('[vitrine] CommerceBackend \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D \u043D\u0430 \u0443\u0440\u043E\u0432\u043D\u0435 tier "catalog"');
29
+ throw new Error('[vitrine] CommerceBackend is unavailable at tier "catalog"');
27
30
  }
28
31
  const f = factoryFor(config);
29
32
  if (!f.createCommerce) {
30
- throw new Error(`[vitrine] backend "${config.backend}" \u043D\u0435 \u0440\u0435\u0430\u043B\u0438\u0437\u0443\u0435\u0442 CommerceBackend`);
33
+ throw new Error(`[vitrine] backend "${config.backend}" does not implement CommerceBackend`);
31
34
  }
32
35
  return f.createCommerce(config);
33
36
  }
@@ -66,12 +69,12 @@ function createPaymentRegistry() {
66
69
  const name = config.integrations.payments;
67
70
  if (!name) {
68
71
  throw new Error(
69
- "[vitrine] integrations.payments \u043D\u0435 \u0437\u0430\u0434\u0430\u043D \u0432 site.config \u2014 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u0435 \u0444\u0438\u0447\u0443 checkout-<provider>"
72
+ "[vitrine] integrations.payments is not set in site.config \u2014 install a checkout-<provider> feature"
70
73
  );
71
74
  }
72
75
  const provider = providers.get(name);
73
76
  if (!provider) {
74
- throw new Error(`[vitrine] \u043F\u043B\u0430\u0442\u0451\u0436\u043D\u044B\u0439 \u043F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440 "${name}" \u043D\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043E\u0432\u0430\u043D`);
77
+ throw new Error(`[vitrine] payment provider "${name}" is not registered`);
75
78
  }
76
79
  return provider;
77
80
  }
@@ -123,7 +126,7 @@ function recalcCart(cart) {
123
126
  }
124
127
  function addCartLine(cart, input) {
125
128
  if (!Number.isInteger(input.quantity) || input.quantity <= 0) {
126
- throw new Error(`[vitrine] \u043D\u0435\u0434\u043E\u043F\u0443\u0441\u0442\u0438\u043C\u043E\u0435 \u043A\u043E\u043B\u0438\u0447\u0435\u0441\u0442\u0432\u043E \u0441\u0442\u0440\u043E\u043A\u0438: ${input.quantity} (\u043D\u0443\u0436\u043D\u043E \u0446\u0435\u043B\u043E\u0435 > 0)`);
129
+ throw new Error(`[vitrine] invalid line quantity: ${input.quantity} (must be an integer > 0)`);
127
130
  }
128
131
  const exists = cart.lines.some((l) => l.variantId === input.variantId);
129
132
  const lines = exists ? cart.lines.map(
@@ -145,7 +148,7 @@ function addCartLine(cart, input) {
145
148
  }
146
149
  function setCartLineQty(cart, lineId, quantity) {
147
150
  if (!Number.isInteger(quantity)) {
148
- throw new Error(`[vitrine] \u043D\u0435\u0434\u043E\u043F\u0443\u0441\u0442\u0438\u043C\u043E\u0435 \u043A\u043E\u043B\u0438\u0447\u0435\u0441\u0442\u0432\u043E: ${quantity} (\u043D\u0443\u0436\u043D\u043E \u0446\u0435\u043B\u043E\u0435)`);
151
+ throw new Error(`[vitrine] invalid quantity: ${quantity} (must be an integer)`);
149
152
  }
150
153
  if (quantity <= 0) return removeCartLine(cart, lineId);
151
154
  return recalcCart({
@@ -183,9 +186,6 @@ function buildOrderFromCart(cart, opts) {
183
186
  createdAt: opts.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
184
187
  };
185
188
  }
186
-
187
- // src/index.ts
188
- var CORE_VERSION = "0.1.0";
189
189
  export {
190
190
  CORE_VERSION,
191
191
  adapters,
package/dist/react.d.ts CHANGED
@@ -1,15 +1,15 @@
1
1
  import { ComponentType, ReactNode } from 'react';
2
2
  import { SlotId } from '@vitrine-kit/contracts';
3
- import { S as SlotRegistry } from './registry-B4Ak1hIR.js';
3
+ import { S as SlotRegistry } from './registry-CfORxf9H.js';
4
4
 
5
5
  type SlotComponentRegistry = SlotRegistry<ComponentType<Record<string, unknown>>>;
6
6
  interface SlotProps {
7
7
  name: SlotId;
8
- /** Реестр (по умолчанию — глобальный). Для тестов/изоляции можно передать свой. */
8
+ /** The registry (global by default). Pass your own for tests/isolation. */
9
9
  registry?: SlotComponentRegistry;
10
- /** Что рендерить, если слот пуст. */
10
+ /** What to render if the slot is empty. */
11
11
  fallback?: ReactNode;
12
- /** Остальные пропсы прокидываются в каждый смонтированный компонент. */
12
+ /** The remaining props are forwarded to each mounted component. */
13
13
  [prop: string]: unknown;
14
14
  }
15
15
  declare function Slot(props: SlotProps): ReactNode;
package/dist/react.js CHANGED
@@ -12,10 +12,17 @@ function Slot(props) {
12
12
  const reg = registry ?? slotRegistry;
13
13
  const mounts = reg.get(name);
14
14
  if (mounts.length === 0) return fallback;
15
+ const seen = /* @__PURE__ */ new Map();
15
16
  return createElement(
16
17
  Fragment,
17
18
  null,
18
- ...mounts.map((m, i) => createElement(m.component, { key: i, ...rest }))
19
+ ...mounts.map((m) => {
20
+ const c = m.component;
21
+ const base = c.displayName ?? (c.name || "mount");
22
+ const n = seen.get(base) ?? 0;
23
+ seen.set(base, n + 1);
24
+ return createElement(m.component, { key: n === 0 ? base : `${base}:${n}`, ...rest });
25
+ })
19
26
  );
20
27
  }
21
28
  export {
@@ -3,12 +3,12 @@ import { SlotMount, SlotId } from '@vitrine-kit/contracts';
3
3
  interface SlotRegistry<C = unknown> {
4
4
  register(mount: SlotMount<C>): void;
5
5
  registerMany(mounts: ReadonlyArray<SlotMount<C>>): void;
6
- /** Привязки слота, отсортированные по order (стабильно по порядку регистрации). */
6
+ /** A slot's bindings, sorted by order (stable by registration order). */
7
7
  get(slot: SlotId): SlotMount<C>[];
8
8
  clear(): void;
9
9
  }
10
10
  declare function createSlotRegistry<C = unknown>(): SlotRegistry<C>;
11
- /** Глобальный реестр по умолчанию (клиент регистрирует слоты в lib/slots.ts). */
11
+ /** The default global registry (the client registers slots in lib/slots.ts). */
12
12
  declare const slotRegistry: SlotRegistry;
13
13
  declare function registerSlot<C = unknown>(mount: SlotMount<C>): void;
14
14
  declare function getSlotMounts<C = unknown>(slot: SlotId): SlotMount<C>[];
package/package.json CHANGED
@@ -1,9 +1,13 @@
1
1
  {
2
2
  "name": "@vitrine-kit/core",
3
- "version": "0.2.0",
4
- "description": "Vitrine core — runtime слотов/адаптера, order pipeline, Stripe webhook. Критическая логика (баг = инцидент у всех).",
3
+ "version": "0.2.2",
4
+ "description": "Vitrine core — slot/adapter runtime, order pipeline, provider-neutral payment webhook dispatch. Critical logic (a bug = an incident for everyone).",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
+ "sideEffects": false,
8
+ "engines": {
9
+ "node": ">=20"
10
+ },
7
11
  "types": "./dist/index.d.ts",
8
12
  "main": "./dist/index.js",
9
13
  "module": "./dist/index.js",
@@ -29,7 +33,7 @@
29
33
  "directory": "packages/core"
30
34
  },
31
35
  "dependencies": {
32
- "@vitrine-kit/contracts": "1.1.0"
36
+ "@vitrine-kit/contracts": "1.2.1"
33
37
  },
34
38
  "peerDependencies": {
35
39
  "react": ">=18.0.0"
@@ -44,8 +48,8 @@
44
48
  "react": "^18.3.1"
45
49
  },
46
50
  "scripts": {
47
- "build": "tsup src/index.ts src/react.ts --format esm --dts --clean",
48
- "typecheck": "tsc --noEmit",
51
+ "build": "node scripts/generate-version.mjs && tsup src/index.ts src/react.ts --format esm --dts --clean",
52
+ "typecheck": "node scripts/generate-version.mjs && tsc --noEmit",
49
53
  "test": "vitest run --passWithNoTests"
50
54
  }
51
55
  }