@wocart/storefront 0.0.0-stage → 1.0.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 (82) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +2 -0
  4. package/README.md +143 -2
  5. package/USAGE.md +1465 -0
  6. package/dist/abandoned-checkout.d.ts +22 -0
  7. package/dist/abandoned-checkout.js +89 -0
  8. package/dist/actions.d.ts +117 -0
  9. package/dist/actions.js +196 -0
  10. package/dist/astro.d.ts +6 -0
  11. package/dist/astro.js +3 -0
  12. package/dist/basket.d.ts +87 -0
  13. package/dist/basket.js +258 -0
  14. package/dist/client/cart.d.ts +30 -0
  15. package/dist/client/cart.js +26 -0
  16. package/dist/client/categories.d.ts +25 -0
  17. package/dist/client/categories.js +37 -0
  18. package/dist/client/config.d.ts +29 -0
  19. package/dist/client/config.js +41 -0
  20. package/dist/client/core.d.ts +86 -0
  21. package/dist/client/core.js +582 -0
  22. package/dist/client/errors.d.ts +19 -0
  23. package/dist/client/errors.js +31 -0
  24. package/dist/client/geo.d.ts +31 -0
  25. package/dist/client/geo.js +39 -0
  26. package/dist/client/index.d.ts +39 -0
  27. package/dist/client/index.js +80 -0
  28. package/dist/client/orders.d.ts +68 -0
  29. package/dist/client/orders.js +76 -0
  30. package/dist/client/page-data.d.ts +31 -0
  31. package/dist/client/page-data.js +37 -0
  32. package/dist/client/pages.d.ts +33 -0
  33. package/dist/client/pages.js +44 -0
  34. package/dist/client/paths.d.ts +8 -0
  35. package/dist/client/paths.js +10 -0
  36. package/dist/client/products.d.ts +56 -0
  37. package/dist/client/products.js +78 -0
  38. package/dist/client/shipping.d.ts +37 -0
  39. package/dist/client/shipping.js +48 -0
  40. package/dist/data/communes.d.ts +25 -0
  41. package/dist/data/communes.js +1690 -0
  42. package/dist/data/wilayas.d.ts +6 -0
  43. package/dist/data/wilayas.js +64 -0
  44. package/dist/domain.d.ts +332 -0
  45. package/dist/domain.js +214 -0
  46. package/dist/endpoints/proxy.d.ts +111 -0
  47. package/dist/endpoints/proxy.js +161 -0
  48. package/dist/index.d.ts +42 -0
  49. package/dist/index.js +30 -0
  50. package/dist/locale-routing.d.ts +59 -0
  51. package/dist/locale-routing.js +62 -0
  52. package/dist/locales.d.ts +15 -0
  53. package/dist/locales.js +8 -0
  54. package/dist/meta-tracking.d.ts +34 -0
  55. package/dist/meta-tracking.js +72 -0
  56. package/dist/middleware.d.ts +100 -0
  57. package/dist/middleware.js +72 -0
  58. package/dist/offer-view.d.ts +10 -0
  59. package/dist/offer-view.js +13 -0
  60. package/dist/otp.d.ts +80 -0
  61. package/dist/otp.js +295 -0
  62. package/dist/phone.d.ts +2 -0
  63. package/dist/phone.js +34 -0
  64. package/dist/product-view.d.ts +12 -0
  65. package/dist/product-view.js +42 -0
  66. package/dist/routes.d.ts +45 -0
  67. package/dist/routes.js +59 -0
  68. package/dist/schemas.d.ts +753 -0
  69. package/dist/schemas.js +377 -0
  70. package/dist/server.d.ts +4 -0
  71. package/dist/server.js +2 -0
  72. package/dist/types.d.ts +418 -0
  73. package/dist/types.js +1 -0
  74. package/dist/utils/image.d.ts +140 -0
  75. package/dist/utils/image.js +131 -0
  76. package/dist/utils/money.d.ts +39 -0
  77. package/dist/utils/money.js +43 -0
  78. package/dist/utils/product-image.d.ts +37 -0
  79. package/dist/utils/product-image.js +38 -0
  80. package/dist/whatsapp.d.ts +55 -0
  81. package/dist/whatsapp.js +89 -0
  82. package/package.json +76 -4
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Deep Proxy Endpoint Factory
3
+ *
4
+ * Consolidates 8 separate endpoint files into ONE factory function.
5
+ * Hides: input parsing, validation, CSRF protection, header forwarding,
6
+ * error normalization, and response formatting.
7
+ *
8
+ * Deep module principle: Complex proxy logic concentrated in one place,
9
+ * giving callers maximum leverage with minimal interface.
10
+ */
11
+ import type { z } from "zod";
12
+ /**
13
+ * Astro APIRoute handler type (avoiding direct Astro dependency in types)
14
+ */
15
+ export type AstroAPIContext = {
16
+ request: Request;
17
+ params: Record<string, string | undefined>;
18
+ };
19
+ export type AstroAPIRoute = (context: AstroAPIContext) => Response | Promise<Response>;
20
+ /**
21
+ * Client function that makes the actual API call to the server.
22
+ *
23
+ * On failure, every extra field besides `success` and `error` (e.g.
24
+ * `code`, `windowSeconds`, `attemptsRemaining`, `terminal`) is forwarded
25
+ * into the error response body — the storefront UI reads these to render
26
+ * rate-limit countdowns and attempt counters.
27
+ */
28
+ export type ProxyClientFn<TInput, TOutput> = (data: TInput, headers?: Record<string, string>) => Promise<{
29
+ success: true;
30
+ data: TOutput;
31
+ [key: string]: unknown;
32
+ } | {
33
+ success: false;
34
+ error: string;
35
+ [key: string]: unknown;
36
+ }>;
37
+ /**
38
+ * Configuration for creating a proxy endpoint
39
+ */
40
+ export interface ProxyEndpointConfig<TInput, TOutput> {
41
+ /** HTTP method: 'GET' or 'POST' */
42
+ method: "GET" | "POST";
43
+ /** Client function that calls the server — usually a `StoreClient` method. */
44
+ client: ProxyClientFn<TInput, TOutput>;
45
+ /** Input validation schema (optional for GET without query validation) */
46
+ inputSchema?: z.Schema<TInput>;
47
+ /**
48
+ * Query parameter names for GET requests
49
+ * Example: ['wilayaId'] will extract { wilayaId: '42' } from ?wilayaId=42
50
+ */
51
+ queryParams?: string[];
52
+ /**
53
+ * Path parameter substitution for dynamic URLs
54
+ * Example: { wilayaId: 'number' } will parse params.wilayaId as number
55
+ */
56
+ pathParams?: Record<string, "string" | "number">;
57
+ /**
58
+ * Enable CSRF protection (cross-origin check)
59
+ * Default: true for POST, false for GET
60
+ */
61
+ csrfProtection?: boolean;
62
+ /**
63
+ * Forward attribution headers (User-Agent, X-Forwarded-For)
64
+ * Default: false
65
+ */
66
+ forwardAttribution?: boolean;
67
+ /**
68
+ * Success status code (default: 200)
69
+ */
70
+ successStatus?: number;
71
+ /**
72
+ * Status code for upstream failures (default: 502).
73
+ * Typical choices: 502 for cart/abandoned/otp-send, 422 for otp-verify,
74
+ * 400 for submit-review.
75
+ */
76
+ errorStatus?: number;
77
+ /**
78
+ * Shape the success response body (default: `(data) => ({ data })`).
79
+ * Use when the browser contract carries more than `{data}` — e.g.
80
+ * `(d) => ({ success: true, data: d })` for cart validation, or
81
+ * `({ rows, total }) => ({ data: rows, total })` for review pagination.
82
+ */
83
+ successBody?: (data: TOutput) => unknown;
84
+ /**
85
+ * Return 204 No Content on success instead of JSON
86
+ * Default: false
87
+ */
88
+ noContent?: boolean;
89
+ }
90
+ /**
91
+ * Create a proxy endpoint handler
92
+ *
93
+ * @example
94
+ * ```ts
95
+ * // POST endpoint with validation
96
+ * export const POST = createProxyEndpoint({
97
+ * method: 'POST',
98
+ * client: validateCart,
99
+ * inputSchema: cartSchema,
100
+ * csrfProtection: true,
101
+ * });
102
+ *
103
+ * // GET endpoint with path params
104
+ * export const GET = createProxyEndpoint({
105
+ * method: 'GET',
106
+ * client: fetchCommunes,
107
+ * pathParams: { wilayaId: 'number' },
108
+ * });
109
+ * ```
110
+ */
111
+ export declare function createProxyEndpoint<TInput, TOutput>(config: ProxyEndpointConfig<TInput, TOutput>): AstroAPIRoute;
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Deep Proxy Endpoint Factory
3
+ *
4
+ * Consolidates 8 separate endpoint files into ONE factory function.
5
+ * Hides: input parsing, validation, CSRF protection, header forwarding,
6
+ * error normalization, and response formatting.
7
+ *
8
+ * Deep module principle: Complex proxy logic concentrated in one place,
9
+ * giving callers maximum leverage with minimal interface.
10
+ */
11
+ import { collectForwardedHeaders } from "../actions.js";
12
+ /**
13
+ * Create a proxy endpoint handler
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * // POST endpoint with validation
18
+ * export const POST = createProxyEndpoint({
19
+ * method: 'POST',
20
+ * client: validateCart,
21
+ * inputSchema: cartSchema,
22
+ * csrfProtection: true,
23
+ * });
24
+ *
25
+ * // GET endpoint with path params
26
+ * export const GET = createProxyEndpoint({
27
+ * method: 'GET',
28
+ * client: fetchCommunes,
29
+ * pathParams: { wilayaId: 'number' },
30
+ * });
31
+ * ```
32
+ */
33
+ export function createProxyEndpoint(config) {
34
+ const { method, client, inputSchema, queryParams = [], pathParams = {}, csrfProtection = method === "POST", forwardAttribution = false, successStatus = 200, errorStatus = 502, successBody, noContent = false, } = config;
35
+ return async (context) => {
36
+ const { request, params } = context;
37
+ // CSRF Protection: reject cross-origin requests
38
+ if (csrfProtection && isCrossOrigin(request)) {
39
+ return jsonResponse({ error: "Cross-origin not allowed" }, 403);
40
+ }
41
+ // Parse input based on method
42
+ let input;
43
+ try {
44
+ if (method === "GET") {
45
+ input = parseGetInput(request, params, queryParams, pathParams);
46
+ }
47
+ else {
48
+ input = await parsePostInput(request);
49
+ }
50
+ }
51
+ catch (error) {
52
+ return jsonResponse({ error: error instanceof Error ? error.message : "Invalid input" }, 400);
53
+ }
54
+ // Validate input with Zod schema
55
+ if (inputSchema) {
56
+ const parsed = inputSchema.safeParse(input);
57
+ if (!parsed.success) {
58
+ return jsonResponse({ error: "Invalid payload" }, 400);
59
+ }
60
+ input = parsed.data;
61
+ }
62
+ // Prepare attribution headers
63
+ const headers = forwardAttribution
64
+ ? collectForwardedHeaders(request)
65
+ : undefined;
66
+ // Call client function
67
+ let result;
68
+ try {
69
+ result = await client(input, headers);
70
+ }
71
+ catch (error) {
72
+ return jsonResponse({ error: error instanceof Error ? error.message : "Upstream request failed" }, errorStatus);
73
+ }
74
+ // Handle client errors — forward every diagnostic field the client
75
+ // attached (code, windowSeconds, attemptsRemaining, terminal, ...)
76
+ // so the storefront UI can render countdowns and attempt counters.
77
+ if (!result.success) {
78
+ const extra = { ...result };
79
+ delete extra.success;
80
+ delete extra.error;
81
+ return jsonResponse({ error: result.error, ...extra }, errorStatus);
82
+ }
83
+ // Return success response
84
+ if (noContent) {
85
+ return new Response(null, { status: 204 });
86
+ }
87
+ const body = successBody ? successBody(result.data) : { data: result.data };
88
+ return jsonResponse(body, successStatus);
89
+ };
90
+ }
91
+ /**
92
+ * Parse GET request input from query params and path params
93
+ */
94
+ function parseGetInput(request, params, queryParams, pathParams) {
95
+ const url = new URL(request.url);
96
+ const input = {};
97
+ // Extract query parameters
98
+ for (const key of queryParams) {
99
+ const value = url.searchParams.get(key);
100
+ if (value !== null) {
101
+ input[key] = value;
102
+ }
103
+ }
104
+ // Extract and parse path parameters
105
+ for (const [key, type] of Object.entries(pathParams)) {
106
+ const value = params[key];
107
+ if (!value) {
108
+ throw new Error(`Missing path parameter: ${key}`);
109
+ }
110
+ if (type === "number") {
111
+ const parsed = Number(value);
112
+ if (!Number.isFinite(parsed) || !Number.isInteger(parsed)) {
113
+ throw new Error(`Invalid number for path parameter: ${key}`);
114
+ }
115
+ input[key] = parsed;
116
+ }
117
+ else {
118
+ input[key] = value;
119
+ }
120
+ }
121
+ return input;
122
+ }
123
+ /**
124
+ * Parse POST request JSON body
125
+ */
126
+ async function parsePostInput(request) {
127
+ try {
128
+ return await request.json();
129
+ }
130
+ catch {
131
+ throw new Error("Invalid JSON body");
132
+ }
133
+ }
134
+ /**
135
+ * Check if request is cross-origin
136
+ *
137
+ * Mirrors Astro Actions' built-in CSRF check:
138
+ * - Requests without Origin header are allowed (older browsers, curl, server-to-server)
139
+ * - Requests with matching origin are allowed
140
+ * - Requests with different origin are rejected
141
+ */
142
+ function isCrossOrigin(request) {
143
+ const origin = request.headers.get("Origin");
144
+ if (!origin)
145
+ return false;
146
+ try {
147
+ return new URL(origin).origin !== new URL(request.url).origin;
148
+ }
149
+ catch {
150
+ return true;
151
+ }
152
+ }
153
+ /**
154
+ * Create JSON response
155
+ */
156
+ function jsonResponse(body, status) {
157
+ return new Response(JSON.stringify(body), {
158
+ status,
159
+ headers: { "Content-Type": "application/json" },
160
+ });
161
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Storefront SDK — public entry point.
3
+ *
4
+ * Browser-safe storefront contracts and presentation utilities.
5
+ *
6
+ * Server code imports `@wocart/storefront/server` for the API-key client.
7
+ * Astro server code imports `@wocart/storefront/astro` for proxy and action
8
+ * adapters. Neither server-only entry point is part of this root surface.
9
+ *
10
+ * Internal modules stay reachable only through their intentional entry points.
11
+ */
12
+ export { formatMoney } from "./utils/money.js";
13
+ export type { MoneyFormatOptions } from "./utils/money.js";
14
+ export { optimizeImage, generateSrcSet, IMAGE_SIZES, getCardImage, getGalleryImage, getThumbnailImage, getHeroImage, getCategoryImage, getLogoImage, createImageHelpers, } from "./utils/image.js";
15
+ export type { ImageHelpers, ImageResizeOptions } from "./utils/image.js";
16
+ export { productImageSource } from "./utils/product-image.js";
17
+ export type { ProductImageSourceInput } from "./utils/product-image.js";
18
+ export { MAX_RESOLVED_PREFILL_LENGTH, PREFILL_TOKENS, stripTrackingParams, truncateAtWordBoundary, resolvePrefillText, buildWhatsAppHref, } from "./whatsapp.js";
19
+ export type { PrefillToken, ResolvePrefillParams } from "./whatsapp.js";
20
+ export { STOREFRONT_ROUTES, showsWidgetOn } from "./routes.js";
21
+ export { servedLanguage } from "./locales.js";
22
+ export { alternateLinks, localeRouting, localizeHref, resolveLocalePath } from "./locale-routing.js";
23
+ export type { LocalePath, LocaleRouting } from "./locale-routing.js";
24
+ export type { StorefrontRoute } from "./routes.js";
25
+ export { createBasketStore, createMemoryStorageAdapter, createLocalStorageAdapter, DEFAULT_BASKET_STORAGE_KEY, toOrderItems, } from "./basket.js";
26
+ export type { BasketStore, BasketStorageAdapter, BasketStoreOptions, BasketListener, } from "./basket.js";
27
+ export { createOtpMachine, createOtpTransport, mapOtpResponse, otpTransportFailure, } from "./otp.js";
28
+ export type { OtpMachine, OtpMachineOptions, OtpFailureContext, OtpResponseOptions, OtpState, OtpStatus, OtpTransport, OtpTransportResult, OtpStateListener, } from "./otp.js";
29
+ export { WILAYAS } from "./data/wilayas.js";
30
+ export { ALGERIA_COMMUNES, communesForWilaya } from "./data/communes.js";
31
+ export type { CommuneTuple } from "./data/communes.js";
32
+ export { normalizeAlgerianPhone, toLocalAlgerianMobile } from "./phone.js";
33
+ export * from "./types.js";
34
+ export * from "./schemas.js";
35
+ export * from "./domain.js";
36
+ export * from "./product-view.js";
37
+ export { getOfferTierView } from "./offer-view.js";
38
+ export type { OfferTierInput, OfferTierView } from "./offer-view.js";
39
+ export { createAbandonedCheckoutTracker } from "./abandoned-checkout.js";
40
+ export type { AbandonedCheckoutPorts, AbandonedCheckoutSnapshot, AbandonedCheckoutStorage, AbandonedCheckoutTracker, } from "./abandoned-checkout.js";
41
+ export { META_CURRENCY, buildMetaAddToCartPayload, buildMetaInitiateCheckoutPayload, buildMetaProductPayload, buildMetaViewContentPayload, createMetaPixelTracker, } from "./meta-tracking.js";
42
+ export type { MetaCartPayload, MetaCatalogPayload, MetaContent, MetaEventName, MetaPixelFunction, MetaPixelOptions, MetaPixelTracker, } from "./meta-tracking.js";
package/dist/index.js ADDED
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Storefront SDK — public entry point.
3
+ *
4
+ * Browser-safe storefront contracts and presentation utilities.
5
+ *
6
+ * Server code imports `@wocart/storefront/server` for the API-key client.
7
+ * Astro server code imports `@wocart/storefront/astro` for proxy and action
8
+ * adapters. Neither server-only entry point is part of this root surface.
9
+ *
10
+ * Internal modules stay reachable only through their intentional entry points.
11
+ */
12
+ export { formatMoney } from "./utils/money.js";
13
+ export { optimizeImage, generateSrcSet, IMAGE_SIZES, getCardImage, getGalleryImage, getThumbnailImage, getHeroImage, getCategoryImage, getLogoImage, createImageHelpers, } from "./utils/image.js";
14
+ export { productImageSource } from "./utils/product-image.js";
15
+ export { MAX_RESOLVED_PREFILL_LENGTH, PREFILL_TOKENS, stripTrackingParams, truncateAtWordBoundary, resolvePrefillText, buildWhatsAppHref, } from "./whatsapp.js";
16
+ export { STOREFRONT_ROUTES, showsWidgetOn } from "./routes.js";
17
+ export { servedLanguage } from "./locales.js";
18
+ export { alternateLinks, localeRouting, localizeHref, resolveLocalePath } from "./locale-routing.js";
19
+ export { createBasketStore, createMemoryStorageAdapter, createLocalStorageAdapter, DEFAULT_BASKET_STORAGE_KEY, toOrderItems, } from "./basket.js";
20
+ export { createOtpMachine, createOtpTransport, mapOtpResponse, otpTransportFailure, } from "./otp.js";
21
+ export { WILAYAS } from "./data/wilayas.js";
22
+ export { ALGERIA_COMMUNES, communesForWilaya } from "./data/communes.js";
23
+ export { normalizeAlgerianPhone, toLocalAlgerianMobile } from "./phone.js";
24
+ export * from "./types.js";
25
+ export * from "./schemas.js";
26
+ export * from "./domain.js";
27
+ export * from "./product-view.js";
28
+ export { getOfferTierView } from "./offer-view.js";
29
+ export { createAbandonedCheckoutTracker } from "./abandoned-checkout.js";
30
+ export { META_CURRENCY, buildMetaAddToCartPayload, buildMetaInitiateCheckoutPayload, buildMetaProductPayload, buildMetaViewContentPayload, createMetaPixelTracker, } from "./meta-tracking.js";
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Language-prefixed storefront URLs — Shopify's
3
+ * subfolders: the primary language keeps today's URLs (`/products/x`), every
4
+ * other published language lives under its code (`/fr/products/x`), and the
5
+ * primary has exactly one address (`/ar/products/x` redirects to
6
+ * `/products/x`).
7
+ *
8
+ * Pure functions over the store's own languages (`StoreConfig.lang` and
9
+ * `StoreConfig.locales`): a theme reads the request's language with
10
+ * `resolveLocalePath`, builds every internal link with `localizeHref`, and
11
+ * lists a page's language versions with `alternateLinks` — the counterpart of
12
+ * Shopify's `request.locale.root_url` and `routes`.
13
+ */
14
+ import type { StoreConfig } from "./types.js";
15
+ /** The store's languages as URLs need them. */
16
+ export interface LocaleRouting {
17
+ /** The primary language: the one without a prefix. */
18
+ readonly primary: string;
19
+ /** Every published language, primary first. */
20
+ readonly published: readonly string[];
21
+ }
22
+ export declare function localeRouting(config: Pick<StoreConfig, "lang" | "locales">): LocaleRouting;
23
+ export type LocalePath =
24
+ /** Serve `pathname` in `locale`. */
25
+ {
26
+ kind: "page";
27
+ locale: string;
28
+ pathname: string;
29
+ }
30
+ /** The primary under a prefix: send the shopper to its one address. */
31
+ | {
32
+ kind: "redirect";
33
+ location: string;
34
+ };
35
+ /**
36
+ * Which language a request is for, and the path it renders. A first segment
37
+ * that is a published language other than the primary selects it; the
38
+ * primary's own prefix redirects; anything else — an unpublished or unknown
39
+ * code included — is an ordinary path in the primary language, so it renders
40
+ * or 404s exactly as before.
41
+ */
42
+ export declare function resolveLocalePath(pathname: string, routing: LocaleRouting): LocalePath;
43
+ /**
44
+ * An internal link in `locale`: `/products/x` → `/fr/products/x`, `/` → `/fr`.
45
+ * Left unchanged: the primary language, external and protocol-relative URLs,
46
+ * fragments, `mailto:`/`tel:`, endpoints and files, and a link that already
47
+ * carries a language prefix. Query strings and fragments are kept.
48
+ */
49
+ export declare function localizeHref(href: string, locale: string, routing: LocaleRouting): string;
50
+ /**
51
+ * Every published language's URL for one page, primary first, plus
52
+ * `x-default` (the primary) — the `<link rel="alternate" hreflang>` set.
53
+ * `pathname` is the page's unprefixed path; `origin` makes the URLs absolute,
54
+ * as search engines require.
55
+ */
56
+ export declare function alternateLinks(pathname: string, routing: LocaleRouting, origin: string): {
57
+ hreflang: string;
58
+ href: string;
59
+ }[];
@@ -0,0 +1,62 @@
1
+ export function localeRouting(config) {
2
+ const published = config.locales.map((language) => language.code);
3
+ return {
4
+ primary: config.lang,
5
+ published: published.includes(config.lang) ? published : [config.lang, ...published],
6
+ };
7
+ }
8
+ /** Paths that are never pages: endpoints, framework internals, files. */
9
+ function isPagePath(pathname) {
10
+ return !/^\/(?:api|_[^/]*)(?:\/|$)/.test(pathname) && !/\/[^/]*\.[a-z0-9]+$/i.test(pathname);
11
+ }
12
+ /**
13
+ * Which language a request is for, and the path it renders. A first segment
14
+ * that is a published language other than the primary selects it; the
15
+ * primary's own prefix redirects; anything else — an unpublished or unknown
16
+ * code included — is an ordinary path in the primary language, so it renders
17
+ * or 404s exactly as before.
18
+ */
19
+ export function resolveLocalePath(pathname, routing) {
20
+ const match = /^\/([^/]+)(\/.*)?$/.exec(pathname);
21
+ const segment = match?.[1];
22
+ if (segment !== undefined && routing.published.includes(segment)) {
23
+ const rest = match?.[2] ?? "/";
24
+ if (segment === routing.primary)
25
+ return { kind: "redirect", location: rest };
26
+ return { kind: "page", locale: segment, pathname: rest };
27
+ }
28
+ return { kind: "page", locale: routing.primary, pathname };
29
+ }
30
+ /**
31
+ * An internal link in `locale`: `/products/x` → `/fr/products/x`, `/` → `/fr`.
32
+ * Left unchanged: the primary language, external and protocol-relative URLs,
33
+ * fragments, `mailto:`/`tel:`, endpoints and files, and a link that already
34
+ * carries a language prefix. Query strings and fragments are kept.
35
+ */
36
+ export function localizeHref(href, locale, routing) {
37
+ if (locale === routing.primary || !href.startsWith("/") || href.startsWith("//"))
38
+ return href;
39
+ const split = /^([^?#]*)(.*)$/.exec(href);
40
+ const pathname = split[1];
41
+ const rest = split[2];
42
+ if (!isPagePath(pathname))
43
+ return href;
44
+ const first = /^\/([^/]+)/.exec(pathname)?.[1];
45
+ if (first !== undefined && routing.published.includes(first))
46
+ return href;
47
+ return `/${locale}${pathname === "/" ? "" : pathname}${rest}`;
48
+ }
49
+ /**
50
+ * Every published language's URL for one page, primary first, plus
51
+ * `x-default` (the primary) — the `<link rel="alternate" hreflang>` set.
52
+ * `pathname` is the page's unprefixed path; `origin` makes the URLs absolute,
53
+ * as search engines require.
54
+ */
55
+ export function alternateLinks(pathname, routing, origin) {
56
+ const base = origin.replace(/\/+$/, "");
57
+ const links = routing.published.map((locale) => ({
58
+ hreflang: locale,
59
+ href: `${base}${localizeHref(pathname, locale, routing)}`,
60
+ }));
61
+ return [...links, { hreflang: "x-default", href: `${base}${pathname}` }];
62
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Store languages as a storefront receives them.
3
+ *
4
+ * The platform's list of supported languages lives on the server, never in
5
+ * this package: `/store/config` sends each served language's code, name and
6
+ * text direction, so a language the platform adds reaches every theme without
7
+ * an SDK release — as Shopify themes read `localization.available_languages`.
8
+ */
9
+ import type { StoreConfig, StorefrontLanguage } from "./types.js";
10
+ /**
11
+ * The served language with `code` (the store's primary by default), or `null`
12
+ * when the store does not serve it — the shape a theme reads `<html dir>` and
13
+ * a language switcher's labels from.
14
+ */
15
+ export declare function servedLanguage(config: Pick<StoreConfig, "lang" | "locales">, code?: string): StorefrontLanguage | null;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * The served language with `code` (the store's primary by default), or `null`
3
+ * when the store does not serve it — the shape a theme reads `<html dir>` and
4
+ * a language switcher's labels from.
5
+ */
6
+ export function servedLanguage(config, code = config.lang) {
7
+ return config.locales.find((language) => language.code === code) ?? null;
8
+ }
@@ -0,0 +1,34 @@
1
+ import type { BasketLine } from "./domain.js";
2
+ export declare const META_CURRENCY: "DZD";
3
+ export type MetaEventName = "ViewContent" | "AddToCart" | "InitiateCheckout" | "Lead" | "Purchase";
4
+ export interface MetaContent {
5
+ readonly id: string;
6
+ readonly quantity: number;
7
+ readonly item_price: number;
8
+ }
9
+ export interface MetaCatalogPayload {
10
+ readonly [key: string]: unknown;
11
+ readonly content_ids: readonly string[];
12
+ readonly content_type: "product";
13
+ readonly value: number;
14
+ readonly currency: typeof META_CURRENCY;
15
+ }
16
+ export interface MetaCartPayload extends MetaCatalogPayload {
17
+ readonly contents: readonly MetaContent[];
18
+ readonly num_items: number;
19
+ }
20
+ export interface MetaPixelOptions {
21
+ readonly eventID?: string;
22
+ }
23
+ export type MetaPixelFunction = (...args: unknown[]) => void;
24
+ export interface MetaPixelTracker {
25
+ track(eventName: MetaEventName, params: Record<string, unknown>): void;
26
+ trackSingle(pixelId: string, eventName: MetaEventName | string, params: Record<string, unknown>, options?: MetaPixelOptions): void;
27
+ }
28
+ export declare function createMetaPixelTracker(options: {
29
+ readonly getPixel: () => MetaPixelFunction | null;
30
+ }): MetaPixelTracker;
31
+ export declare function buildMetaProductPayload(productId: string, value: number): MetaCatalogPayload;
32
+ export declare function buildMetaViewContentPayload(productId: string, value: number): MetaCatalogPayload;
33
+ export declare function buildMetaAddToCartPayload(lines: readonly BasketLine[]): MetaCartPayload | null;
34
+ export declare function buildMetaInitiateCheckoutPayload(lines: readonly BasketLine[], value: number): MetaCartPayload | null;
@@ -0,0 +1,72 @@
1
+ export const META_CURRENCY = "DZD";
2
+ export function createMetaPixelTracker(options) {
3
+ return {
4
+ track(eventName, params) {
5
+ options.getPixel()?.("track", eventName, params);
6
+ },
7
+ trackSingle(pixelId, eventName, params, eventOptions) {
8
+ if (!pixelId)
9
+ return;
10
+ const pixel = options.getPixel();
11
+ if (!pixel)
12
+ return;
13
+ if (eventOptions) {
14
+ pixel("trackSingle", pixelId, eventName, params, eventOptions);
15
+ }
16
+ else {
17
+ pixel("trackSingle", pixelId, eventName, params);
18
+ }
19
+ },
20
+ };
21
+ }
22
+ function contentsOf(lines) {
23
+ return lines.map((line) => ({
24
+ id: line.productId,
25
+ quantity: line.quantity,
26
+ item_price: line.unitPrice,
27
+ }));
28
+ }
29
+ function idsOf(lines) {
30
+ return [...new Set(lines.map((line) => line.productId))];
31
+ }
32
+ function countOf(lines) {
33
+ return lines.reduce((total, line) => total + line.quantity, 0);
34
+ }
35
+ function subtotalOf(lines) {
36
+ return lines.reduce((total, line) => total + line.unitPrice * line.quantity, 0);
37
+ }
38
+ export function buildMetaProductPayload(productId, value) {
39
+ return {
40
+ content_ids: [productId],
41
+ content_type: "product",
42
+ value,
43
+ currency: META_CURRENCY,
44
+ };
45
+ }
46
+ export function buildMetaViewContentPayload(productId, value) {
47
+ return buildMetaProductPayload(productId, value);
48
+ }
49
+ export function buildMetaAddToCartPayload(lines) {
50
+ if (lines.length === 0)
51
+ return null;
52
+ return {
53
+ content_ids: idsOf(lines),
54
+ content_type: "product",
55
+ contents: contentsOf(lines),
56
+ num_items: countOf(lines),
57
+ value: subtotalOf(lines),
58
+ currency: META_CURRENCY,
59
+ };
60
+ }
61
+ export function buildMetaInitiateCheckoutPayload(lines, value) {
62
+ if (lines.length === 0)
63
+ return null;
64
+ return {
65
+ content_ids: idsOf(lines),
66
+ content_type: "product",
67
+ contents: contentsOf(lines),
68
+ num_items: countOf(lines),
69
+ value,
70
+ currency: META_CURRENCY,
71
+ };
72
+ }