@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 +20 -3
- package/dist/index.d.ts +34 -34
- package/dist/index.js +10 -10
- package/dist/react.d.ts +4 -4
- package/dist/react.js +8 -1
- package/dist/{registry-B4Ak1hIR.d.ts → registry-CfORxf9H.d.ts} +2 -2
- package/package.json +9 -5
package/README.md
CHANGED
|
@@ -1,7 +1,24 @@
|
|
|
1
1
|
# @vitrine-kit/core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Vitrine's critical logic: the slot and adapter runtime, the order pipeline, and provider-neutral payment webhook dispatch.
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
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-
|
|
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
|
-
*
|
|
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
|
-
/**
|
|
37
|
+
/** Cart status (the Payload `carts` map): 'converted' = already paid. */
|
|
36
38
|
cartStatus?: string | null;
|
|
37
|
-
/**
|
|
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
|
-
*
|
|
44
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
54
|
+
/** The storefront base URL; success/cancel are built relative to it. */
|
|
53
55
|
baseUrl: string;
|
|
54
|
-
/**
|
|
56
|
+
/** Defaults to '/order/success'. */
|
|
55
57
|
successPath?: string;
|
|
56
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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]
|
|
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
|
|
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}"
|
|
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
|
|
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]
|
|
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]
|
|
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]
|
|
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-
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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.
|
|
4
|
-
"description": "Vitrine core — runtime
|
|
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
|
|
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
|
}
|