@wocart/storefront 2.2.0 → 3.1.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.
- package/CHANGELOG.md +138 -0
- package/USAGE.md +189 -95
- package/dist/answer-readers.js +16 -43
- package/dist/astro.d.ts +6 -2
- package/dist/astro.js +3 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/products.d.ts +2 -3
- package/dist/client/products.js +2 -3
- package/dist/endpoints/revalidate-endpoint.d.ts +37 -0
- package/dist/endpoints/revalidate-endpoint.js +32 -0
- package/dist/endpoints/storefront-routes.js +1 -1
- package/dist/index.d.ts +8 -4
- package/dist/index.js +4 -2
- package/dist/locale-routing.d.ts +11 -20
- package/dist/locale-routing.js +28 -8
- package/dist/middleware.d.ts +6 -26
- package/dist/middleware.js +0 -9
- package/dist/product-view.d.ts +0 -8
- package/dist/product-view.js +0 -17
- package/dist/revalidate.d.ts +6 -0
- package/dist/revalidate.js +6 -0
- package/dist/routes.d.ts +44 -27
- package/dist/routes.js +54 -13
- package/dist/schemas.d.ts +144 -222
- package/dist/schemas.js +77 -137
- package/dist/seo.js +2 -2
- package/dist/server.d.ts +1 -1
- package/dist/server.js +1 -1
- package/dist/store-request.d.ts +5 -4
- package/dist/store-request.js +1 -1
- package/dist/storefront-middleware.d.ts +88 -0
- package/dist/storefront-middleware.js +37 -0
- package/dist/thank-you.d.ts +32 -0
- package/dist/thank-you.js +33 -0
- package/dist/types.d.ts +76 -111
- package/dist/unavailable-store.d.ts +23 -0
- package/dist/unavailable-store.js +58 -0
- package/dist/variant-picker.d.ts +6 -2
- package/dist/variant-picker.js +6 -9
- package/dist/whatsapp.d.ts +59 -6
- package/dist/whatsapp.js +28 -10
- package/package.json +1 -1
package/dist/answer-readers.js
CHANGED
|
@@ -2,59 +2,33 @@ const isObject = (value) => typeof value === "object" && value !== null && !Arra
|
|
|
2
2
|
const isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
|
|
3
3
|
const isInteger = (value) => typeof value === "number" && Number.isSafeInteger(value);
|
|
4
4
|
const isNullableString = (value) => value === null || typeof value === "string";
|
|
5
|
-
/** An optional field the schema reads as `.nullish()`: absent stays absent, null and the type are kept, anything else refuses. */
|
|
6
|
-
function nullish(source, key, accepts) {
|
|
7
|
-
if (!(key in source) || source[key] === undefined)
|
|
8
|
-
return { ok: true, entry: {} };
|
|
9
|
-
const value = source[key];
|
|
10
|
-
return value === null || accepts(value) ? { ok: true, entry: { [key]: value } } : { ok: false, entry: {} };
|
|
11
|
-
}
|
|
12
|
-
const isString = (value) => typeof value === "string";
|
|
13
5
|
const BLOCKERS = ["missing", "unlisted", "out_of_stock"];
|
|
6
|
+
const FREE_BY = ["offer", "coupon", "threshold"];
|
|
14
7
|
function readQuoteLine(value) {
|
|
15
8
|
if (!isObject(value))
|
|
16
9
|
return null;
|
|
17
|
-
const { productId, variantId, variantLabel, productName, quantity, unitPrice, lineTotal, maxQuantity, blocker } = value;
|
|
10
|
+
const { productId, variantId, variantLabel, productName, quantity, unitPrice, lineTotal, offerId, offerName, discount, maxQuantity, blocker } = value;
|
|
18
11
|
if (typeof productId !== "string" || !isNullableString(variantId) || !isNullableString(variantLabel) || typeof productName !== "string")
|
|
19
12
|
return null;
|
|
20
13
|
if (!isInteger(quantity) || !isFiniteNumber(unitPrice) || !isFiniteNumber(lineTotal))
|
|
21
14
|
return null;
|
|
15
|
+
if (!isNullableString(offerId) || !isNullableString(offerName) || !isFiniteNumber(discount))
|
|
16
|
+
return null;
|
|
22
17
|
if (maxQuantity !== null && !isFiniteNumber(maxQuantity))
|
|
23
18
|
return null;
|
|
24
19
|
if (blocker !== null && !(typeof blocker === "string" && BLOCKERS.includes(blocker)))
|
|
25
20
|
return null;
|
|
26
|
-
|
|
27
|
-
const offerName = nullish(value, "offerName", isString);
|
|
28
|
-
const discount = nullish(value, "discount", isFiniteNumber);
|
|
29
|
-
if (!offerId.ok || !offerName.ok || !discount.ok)
|
|
30
|
-
return null;
|
|
31
|
-
return {
|
|
32
|
-
productId,
|
|
33
|
-
variantId,
|
|
34
|
-
variantLabel,
|
|
35
|
-
productName,
|
|
36
|
-
quantity,
|
|
37
|
-
unitPrice,
|
|
38
|
-
lineTotal,
|
|
39
|
-
...offerId.entry,
|
|
40
|
-
...offerName.entry,
|
|
41
|
-
...discount.entry,
|
|
42
|
-
maxQuantity,
|
|
43
|
-
blocker: blocker,
|
|
44
|
-
};
|
|
21
|
+
return { productId, variantId, variantLabel, productName, quantity, unitPrice, lineTotal, offerId, offerName, discount, maxQuantity, blocker: blocker };
|
|
45
22
|
}
|
|
46
23
|
function readReward(value) {
|
|
47
24
|
if (!isObject(value))
|
|
48
25
|
return null;
|
|
49
|
-
const { offerId, productId, productName, quantity } = value;
|
|
50
|
-
if (typeof offerId !== "string" || !isNullableString(
|
|
26
|
+
const { offerId, offerName, productId, productName, variantId, variantLabel, quantity } = value;
|
|
27
|
+
if (typeof offerId !== "string" || !isNullableString(offerName) || !isNullableString(productId) || !isNullableString(productName))
|
|
51
28
|
return null;
|
|
52
|
-
|
|
53
|
-
const variantId = nullish(value, "variantId", isString);
|
|
54
|
-
const variantLabel = nullish(value, "variantLabel", isString);
|
|
55
|
-
if (!offerName.ok || !variantId.ok || !variantLabel.ok)
|
|
29
|
+
if (!isNullableString(variantId) || !isNullableString(variantLabel) || !isInteger(quantity))
|
|
56
30
|
return null;
|
|
57
|
-
return { offerId,
|
|
31
|
+
return { offerId, offerName, productId, productName, variantId, variantLabel, quantity };
|
|
58
32
|
}
|
|
59
33
|
function readDelivery(value) {
|
|
60
34
|
if (!isObject(value))
|
|
@@ -93,25 +67,24 @@ export function readCartQuote(value) {
|
|
|
93
67
|
return null;
|
|
94
68
|
if (free.threshold !== null && !isFiniteNumber(free.threshold))
|
|
95
69
|
return null;
|
|
70
|
+
if (free.by !== null && !(typeof free.by === "string" && FREE_BY.includes(free.by)))
|
|
71
|
+
return null;
|
|
96
72
|
let delivery = null;
|
|
97
73
|
if ("delivery" in value && value.delivery !== undefined) {
|
|
98
74
|
delivery = readDelivery(value.delivery);
|
|
99
75
|
if (delivery === null)
|
|
100
76
|
return null;
|
|
101
77
|
}
|
|
102
|
-
|
|
103
|
-
if (
|
|
104
|
-
|
|
105
|
-
if (coupon === null && value.coupon !== null)
|
|
106
|
-
return null;
|
|
107
|
-
}
|
|
78
|
+
const coupon = value.coupon === null ? null : readCouponOutcome(value.coupon);
|
|
79
|
+
if (coupon === null && value.coupon !== null)
|
|
80
|
+
return null;
|
|
108
81
|
return {
|
|
109
82
|
lines,
|
|
110
83
|
subtotal: value.subtotal,
|
|
111
84
|
rewards,
|
|
112
|
-
freeDelivery: { fromOffer: free.fromOffer, threshold: free.threshold, qualified: free.qualified, remaining: free.remaining },
|
|
85
|
+
freeDelivery: { fromOffer: free.fromOffer, threshold: free.threshold, qualified: free.qualified, remaining: free.remaining, by: free.by },
|
|
113
86
|
...(delivery === null ? {} : { delivery }),
|
|
114
|
-
|
|
87
|
+
coupon,
|
|
115
88
|
};
|
|
116
89
|
}
|
|
117
90
|
/** The delivery rates of a value (`ShippingRates`), or null when it is not a rate table. */
|
package/dist/astro.d.ts
CHANGED
|
@@ -4,9 +4,13 @@ export { createStorefrontRoutes, REVIEWS_PAGE_DEFAULT, REVIEWS_PAGE_MAX, } from
|
|
|
4
4
|
export type { StorefrontApiRoute, StorefrontRoutes, StorefrontRoutesOptions, } from "./endpoints/storefront-routes.js";
|
|
5
5
|
export { placeOrderInput, executePlaceOrder, collectForwardedHeaders, } from "./actions.js";
|
|
6
6
|
export type { PlaceOrderInput, PlaceOrderResult } from "./actions.js";
|
|
7
|
-
export { runFormAction
|
|
8
|
-
export type {
|
|
7
|
+
export { runFormAction } from "./middleware.js";
|
|
8
|
+
export type { ActionMiddlewareDeps, ActionDescriptor } from "./middleware.js";
|
|
9
9
|
export { lookupOrder, orderLookupInput } from "./order-lookup.js";
|
|
10
10
|
export type { OrderLookup, OrderLookupInput } from "./order-lookup.js";
|
|
11
11
|
export { notFound, resolvePreviewStore, resolveStoreRequest } from "./store-request.js";
|
|
12
12
|
export type { NotFoundContext, ResolvedStore, StoreRequestOptions, StoreRequestResolution } from "./store-request.js";
|
|
13
|
+
export { createStorefrontMiddleware } from "./storefront-middleware.js";
|
|
14
|
+
export type { PreviewLocals, StorefrontActionContext, StorefrontMiddleware, StorefrontMiddlewareContext, StorefrontMiddlewareOptions, } from "./storefront-middleware.js";
|
|
15
|
+
export { revalidateEndpoint } from "./endpoints/revalidate-endpoint.js";
|
|
16
|
+
export type { RevalidateEndpointContext, RevalidateEndpointOptions } from "./endpoints/revalidate-endpoint.js";
|
package/dist/astro.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export { createProxyEndpoint } from "./endpoints/proxy.js";
|
|
2
2
|
export { createStorefrontRoutes, REVIEWS_PAGE_DEFAULT, REVIEWS_PAGE_MAX, } from "./endpoints/storefront-routes.js";
|
|
3
3
|
export { placeOrderInput, executePlaceOrder, collectForwardedHeaders, } from "./actions.js";
|
|
4
|
-
export { runFormAction
|
|
4
|
+
export { runFormAction } from "./middleware.js";
|
|
5
5
|
export { lookupOrder, orderLookupInput } from "./order-lookup.js";
|
|
6
6
|
export { notFound, resolvePreviewStore, resolveStoreRequest } from "./store-request.js";
|
|
7
|
+
export { createStorefrontMiddleware } from "./storefront-middleware.js";
|
|
8
|
+
export { revalidateEndpoint } from "./endpoints/revalidate-endpoint.js";
|
package/dist/client/index.d.ts
CHANGED
package/dist/client/index.js
CHANGED
|
@@ -41,9 +41,8 @@ export interface ProductsNamespace {
|
|
|
41
41
|
* The products a shopper's words find — every word in the product's name,
|
|
42
42
|
* in the client's language or the catalog's, or in its tags — as a page,
|
|
43
43
|
* with the listing filters, sort and paging. Without a `sort`, products
|
|
44
|
-
* whose name holds every word come first. Blank words find nothing
|
|
45
|
-
*
|
|
46
|
-
* `onError`), never the whole catalog.
|
|
44
|
+
* whose name holds every word come first. Blank words find nothing, never
|
|
45
|
+
* the whole catalog.
|
|
47
46
|
*/
|
|
48
47
|
search(text: string, filters?: ProductListFilters): Promise<ProductPage>;
|
|
49
48
|
/**
|
package/dist/client/products.js
CHANGED
|
@@ -29,7 +29,6 @@ export function createProductsNamespace(core) {
|
|
|
29
29
|
categoryId: filters?.categoryId || undefined,
|
|
30
30
|
collection: filters?.collection || undefined,
|
|
31
31
|
onSale: filters?.onSale ? "true" : undefined,
|
|
32
|
-
featured: filters?.featured ? "true" : undefined,
|
|
33
32
|
sort: filters?.sort || undefined,
|
|
34
33
|
limit: filters?.limit === undefined ? undefined : String(filters.limit),
|
|
35
34
|
offset: filters?.offset ? String(filters.offset) : undefined,
|
|
@@ -53,11 +52,11 @@ export function createProductsNamespace(core) {
|
|
|
53
52
|
operation,
|
|
54
53
|
});
|
|
55
54
|
const rows = envelope?.data ?? [];
|
|
56
|
-
return { items: rows.slice(0, size), hasNext: rows.length > size, total: envelope?.total ??
|
|
55
|
+
return { items: rows.slice(0, size), hasNext: rows.length > size, total: envelope?.total ?? 0 };
|
|
57
56
|
}
|
|
58
57
|
catch (error) {
|
|
59
58
|
reportDegradedRead(core.config, error, operation);
|
|
60
|
-
return { items: [], hasNext: false, total:
|
|
59
|
+
return { items: [], hasNext: false, total: 0 };
|
|
61
60
|
}
|
|
62
61
|
};
|
|
63
62
|
return {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The revalidation route a theme exports, ready-made:
|
|
3
|
+
*
|
|
4
|
+
* ```ts
|
|
5
|
+
* // src/pages/wocart-revalidate.ts
|
|
6
|
+
* import { revalidateEndpoint } from "@wocart/storefront/astro";
|
|
7
|
+
*
|
|
8
|
+
* export const prerender = false;
|
|
9
|
+
* export const { POST, ALL } = revalidateEndpoint({ publicKey: () => WOCART_PREVIEW_KEY, theme: THEME_KEY });
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* `POST` verifies the platform's token (`readRevalidateRequest`), drops every
|
|
13
|
+
* page cached under `PAGES_CACHE_TAG` and answers 204; a refused token
|
|
14
|
+
* answers its status and drops nothing. `ALL` answers any other method 405.
|
|
15
|
+
* Neither answer is ever cached.
|
|
16
|
+
*/
|
|
17
|
+
import { type RevalidateRequestOptions } from "../revalidate.js";
|
|
18
|
+
/** What the route uses of Astro's `APIContext`. */
|
|
19
|
+
export interface RevalidateEndpointContext {
|
|
20
|
+
request: Request;
|
|
21
|
+
cache: {
|
|
22
|
+
invalidate(options: {
|
|
23
|
+
tags: string[];
|
|
24
|
+
}): Promise<void>;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export interface RevalidateEndpointOptions {
|
|
28
|
+
/** The deployment's preview public key; a function reads it on each request. Absent: every request answers 404. */
|
|
29
|
+
publicKey: RevalidateRequestOptions["publicKey"] | (() => RevalidateRequestOptions["publicKey"]);
|
|
30
|
+
/** This theme's key. */
|
|
31
|
+
theme: string;
|
|
32
|
+
}
|
|
33
|
+
/** The theme's `/wocart-revalidate` route: `POST` drops the cached pages for a verified token, `ALL` answers 405. */
|
|
34
|
+
export declare function revalidateEndpoint(options: RevalidateEndpointOptions): {
|
|
35
|
+
POST: (context: RevalidateEndpointContext) => Promise<Response>;
|
|
36
|
+
ALL: () => Response;
|
|
37
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The revalidation route a theme exports, ready-made:
|
|
3
|
+
*
|
|
4
|
+
* ```ts
|
|
5
|
+
* // src/pages/wocart-revalidate.ts
|
|
6
|
+
* import { revalidateEndpoint } from "@wocart/storefront/astro";
|
|
7
|
+
*
|
|
8
|
+
* export const prerender = false;
|
|
9
|
+
* export const { POST, ALL } = revalidateEndpoint({ publicKey: () => WOCART_PREVIEW_KEY, theme: THEME_KEY });
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* `POST` verifies the platform's token (`readRevalidateRequest`), drops every
|
|
13
|
+
* page cached under `PAGES_CACHE_TAG` and answers 204; a refused token
|
|
14
|
+
* answers its status and drops nothing. `ALL` answers any other method 405.
|
|
15
|
+
* Neither answer is ever cached.
|
|
16
|
+
*/
|
|
17
|
+
import { PAGES_CACHE_TAG, readRevalidateRequest } from "../revalidate.js";
|
|
18
|
+
const noStore = (status, headers = {}) => new Response(null, { status, headers: { "Cache-Control": "no-store", ...headers } });
|
|
19
|
+
/** The theme's `/wocart-revalidate` route: `POST` drops the cached pages for a verified token, `ALL` answers 405. */
|
|
20
|
+
export function revalidateEndpoint(options) {
|
|
21
|
+
return {
|
|
22
|
+
POST: async (context) => {
|
|
23
|
+
const publicKey = typeof options.publicKey === "function" ? options.publicKey() : options.publicKey;
|
|
24
|
+
const revalidation = await readRevalidateRequest(context.request, { publicKey, theme: options.theme });
|
|
25
|
+
if (!revalidation.ok)
|
|
26
|
+
return noStore(revalidation.status);
|
|
27
|
+
await context.cache.invalidate({ tags: [PAGES_CACHE_TAG] });
|
|
28
|
+
return noStore(204);
|
|
29
|
+
},
|
|
30
|
+
ALL: () => noStore(405, { Allow: "POST" }),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -75,7 +75,7 @@ const reviewsListInput = z.object({
|
|
|
75
75
|
*/
|
|
76
76
|
const reviewSubmitInput = z.object({
|
|
77
77
|
orderNumber: z.string().min(1).max(64),
|
|
78
|
-
phone: z.string().min(1).max(32)
|
|
78
|
+
phone: z.string().min(1).max(32),
|
|
79
79
|
productId: z.string().min(1).max(200),
|
|
80
80
|
rating: z.number(),
|
|
81
81
|
title: z.string().max(500).optional(),
|
package/dist/index.d.ts
CHANGED
|
@@ -15,13 +15,15 @@ export { isMediaKey, mediaImageSrcset, mediaImageUrl, mediaUrl, pictureOf } from
|
|
|
15
15
|
export type { MediaDelivery, MediaDeliveryKind, MediaImage, MediaImageParams, MediaImageSrcsetOptions, PickedImage, Picture } from "./utils/image.js";
|
|
16
16
|
export { productImageSource } from "./utils/product-image.js";
|
|
17
17
|
export type { ProductImageSourceInput } from "./utils/product-image.js";
|
|
18
|
-
export { MAX_RESOLVED_PREFILL_LENGTH, PREFILL_TOKENS,
|
|
19
|
-
export type { PrefillToken,
|
|
20
|
-
export { STOREFRONT_PAGE_PATHS,
|
|
18
|
+
export { MAX_RESOLVED_PREFILL_LENGTH, PREFILL_TOKENS, whatsAppContact } from "./whatsapp.js";
|
|
19
|
+
export type { PrefillToken, WhatsAppContact, WhatsAppPage, WhatsAppWords } from "./whatsapp.js";
|
|
20
|
+
export { SINGLE_LANGUAGE_PAGE_TYPES, STOREFRONT_API_PATHS, STOREFRONT_PAGE_PATHS, isSingleLanguagePage, matchStorefrontPath, showsWidgetOn, storefrontApiPath, storefrontPath } from "./routes.js";
|
|
21
21
|
export { servedLanguage } from "./locales.js";
|
|
22
|
+
export { unavailableStoreConfig } from "./unavailable-store.js";
|
|
23
|
+
export type { UnavailableStoreOptions } from "./unavailable-store.js";
|
|
22
24
|
export { alternateLinks, localeRouting, localizeHref, resolveLocalePath } from "./locale-routing.js";
|
|
23
25
|
export type { LocalePath, LocaleRouting } from "./locale-routing.js";
|
|
24
|
-
export type { StorefrontPageMatch, StorefrontPageType, StorefrontPathParams
|
|
26
|
+
export type { StorefrontApiName, StorefrontApiParams, StorefrontPageMatch, StorefrontPageType, StorefrontPathParams } from "./routes.js";
|
|
25
27
|
export { addedLines, createBasketStore, createMemoryStorageAdapter, createLocalStorageAdapter, DEFAULT_BASKET_STORAGE_KEY, toOrderItems, } from "./basket.js";
|
|
26
28
|
export { MAX_BASKET_LINES, MAX_BASKET_LINE_QUANTITY, basketLineKey } from "./basket-line.js";
|
|
27
29
|
export type { BasketStore, BasketStorageAdapter, BasketStoreOptions, BasketListener, BasketSelectionLine, } from "./basket.js";
|
|
@@ -36,6 +38,8 @@ export { readCartQuote, readCouponOutcome, readShippingRates } from "./answer-re
|
|
|
36
38
|
export { createQuoter } from "./quote.js";
|
|
37
39
|
export type { Quoter, QuoterOptions, QuoteResult } from "./quote.js";
|
|
38
40
|
export { isOrderNumber, ORDER_NUMBER_PATTERN } from "./order-number.js";
|
|
41
|
+
export { readThankYou, thankYouHref } from "./thank-you.js";
|
|
42
|
+
export type { ThankYouOrder } from "./thank-you.js";
|
|
39
43
|
export { DEFAULT_COLLECTION_HANDLES, PRODUCT_SORTS, SHOPPER_ORDER_JOURNEY, SHOPPER_ORDER_STAGES } from "./catalog.js";
|
|
40
44
|
export { SEO_TEXT_LIMITS, canonicalUrl, jsonLd, productMeta, productStructuredData } from "./seo.js";
|
|
41
45
|
export type { CanonicalPage, PageMeta, ProductStructuredDataOptions } from "./seo.js";
|
package/dist/index.js
CHANGED
|
@@ -12,9 +12,10 @@
|
|
|
12
12
|
export { formatMoney } from "./utils/money.js";
|
|
13
13
|
export { isMediaKey, mediaImageSrcset, mediaImageUrl, mediaUrl, pictureOf } from "./utils/image.js";
|
|
14
14
|
export { productImageSource } from "./utils/product-image.js";
|
|
15
|
-
export { MAX_RESOLVED_PREFILL_LENGTH, PREFILL_TOKENS,
|
|
16
|
-
export { STOREFRONT_PAGE_PATHS,
|
|
15
|
+
export { MAX_RESOLVED_PREFILL_LENGTH, PREFILL_TOKENS, whatsAppContact } from "./whatsapp.js";
|
|
16
|
+
export { SINGLE_LANGUAGE_PAGE_TYPES, STOREFRONT_API_PATHS, STOREFRONT_PAGE_PATHS, isSingleLanguagePage, matchStorefrontPath, showsWidgetOn, storefrontApiPath, storefrontPath } from "./routes.js";
|
|
17
17
|
export { servedLanguage } from "./locales.js";
|
|
18
|
+
export { unavailableStoreConfig } from "./unavailable-store.js";
|
|
18
19
|
export { alternateLinks, localeRouting, localizeHref, resolveLocalePath } from "./locale-routing.js";
|
|
19
20
|
export { addedLines, createBasketStore, createMemoryStorageAdapter, createLocalStorageAdapter, DEFAULT_BASKET_STORAGE_KEY, toOrderItems, } from "./basket.js";
|
|
20
21
|
export { MAX_BASKET_LINES, MAX_BASKET_LINE_QUANTITY, basketLineKey } from "./basket-line.js";
|
|
@@ -26,6 +27,7 @@ export { isWilayaId } from "./wilaya-id.js";
|
|
|
26
27
|
export { readCartQuote, readCouponOutcome, readShippingRates } from "./answer-readers.js";
|
|
27
28
|
export { createQuoter } from "./quote.js";
|
|
28
29
|
export { isOrderNumber, ORDER_NUMBER_PATTERN } from "./order-number.js";
|
|
30
|
+
export { readThankYou, thankYouHref } from "./thank-you.js";
|
|
29
31
|
export { DEFAULT_COLLECTION_HANDLES, PRODUCT_SORTS, SHOPPER_ORDER_JOURNEY, SHOPPER_ORDER_STAGES } from "./catalog.js";
|
|
30
32
|
export { SEO_TEXT_LIMITS, canonicalUrl, jsonLd, productMeta, productStructuredData } from "./seo.js";
|
|
31
33
|
export { REVIEW_REFUSAL_CODES, REVIEW_TEXT_LIMITS } from "./reviews.js";
|
package/dist/locale-routing.d.ts
CHANGED
|
@@ -1,16 +1,3 @@
|
|
|
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
1
|
import type { StoreConfig } from "./types.js";
|
|
15
2
|
/** The store's languages as URLs need them. */
|
|
16
3
|
export interface LocaleRouting {
|
|
@@ -27,7 +14,7 @@ export type LocalePath =
|
|
|
27
14
|
locale: string;
|
|
28
15
|
pathname: string;
|
|
29
16
|
}
|
|
30
|
-
/** The primary under a prefix: send the shopper to its one address. */
|
|
17
|
+
/** The primary under a prefix, or a single-language page under any: send the shopper to its one address. */
|
|
31
18
|
| {
|
|
32
19
|
kind: "redirect";
|
|
33
20
|
location: string;
|
|
@@ -35,23 +22,27 @@ export type LocalePath =
|
|
|
35
22
|
/**
|
|
36
23
|
* Which language a request is for, and the path it renders. A first segment
|
|
37
24
|
* that is a published language other than the primary selects it; the
|
|
38
|
-
* primary's own prefix redirects
|
|
39
|
-
*
|
|
40
|
-
* or
|
|
25
|
+
* primary's own prefix redirects, and so does any prefix on a page that
|
|
26
|
+
* exists in the primary language only (`isSingleLanguagePage`: `/fr/lp/x` →
|
|
27
|
+
* `/lp/x`); anything else — an unpublished or unknown code included — is an
|
|
28
|
+
* ordinary path in the primary language, so it renders or 404s exactly as
|
|
29
|
+
* before.
|
|
41
30
|
*/
|
|
42
31
|
export declare function resolveLocalePath(pathname: string, routing: LocaleRouting): LocalePath;
|
|
43
32
|
/**
|
|
44
33
|
* An internal link in `locale`: `/products/x` → `/fr/products/x`, `/` → `/fr`.
|
|
45
34
|
* Left unchanged: the primary language, external and protocol-relative URLs,
|
|
46
|
-
* fragments, `mailto:`/`tel:`, endpoints and files,
|
|
47
|
-
*
|
|
35
|
+
* fragments, `mailto:`/`tel:`, endpoints and files, a page that exists in the
|
|
36
|
+
* primary language only (a landing page), and a link that already carries a
|
|
37
|
+
* language prefix. Query strings and fragments are kept.
|
|
48
38
|
*/
|
|
49
39
|
export declare function localizeHref(href: string, locale: string, routing: LocaleRouting): string;
|
|
50
40
|
/**
|
|
51
41
|
* Every published language's URL for one page, primary first, plus
|
|
52
42
|
* `x-default` (the primary) — the `<link rel="alternate" hreflang>` set.
|
|
53
43
|
* `pathname` is the page's unprefixed path; `origin` makes the URLs absolute,
|
|
54
|
-
* as search engines require.
|
|
44
|
+
* as search engines require. None for a page that exists in the primary
|
|
45
|
+
* language only.
|
|
55
46
|
*/
|
|
56
47
|
export declare function alternateLinks(pathname: string, routing: LocaleRouting, origin: string): {
|
|
57
48
|
hreflang: string;
|
package/dist/locale-routing.js
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
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 { isSingleLanguagePage } from "./routes.js";
|
|
1
15
|
export function localeRouting(config) {
|
|
2
16
|
const published = config.locales.map((language) => language.code);
|
|
3
17
|
return {
|
|
@@ -12,16 +26,18 @@ function isPagePath(pathname) {
|
|
|
12
26
|
/**
|
|
13
27
|
* Which language a request is for, and the path it renders. A first segment
|
|
14
28
|
* that is a published language other than the primary selects it; the
|
|
15
|
-
* primary's own prefix redirects
|
|
16
|
-
*
|
|
17
|
-
* or
|
|
29
|
+
* primary's own prefix redirects, and so does any prefix on a page that
|
|
30
|
+
* exists in the primary language only (`isSingleLanguagePage`: `/fr/lp/x` →
|
|
31
|
+
* `/lp/x`); anything else — an unpublished or unknown code included — is an
|
|
32
|
+
* ordinary path in the primary language, so it renders or 404s exactly as
|
|
33
|
+
* before.
|
|
18
34
|
*/
|
|
19
35
|
export function resolveLocalePath(pathname, routing) {
|
|
20
36
|
const match = /^\/([^/]+)(\/.*)?$/.exec(pathname);
|
|
21
37
|
const segment = match?.[1];
|
|
22
38
|
if (segment !== undefined && routing.published.includes(segment)) {
|
|
23
39
|
const rest = match?.[2] ?? "/";
|
|
24
|
-
if (segment === routing.primary)
|
|
40
|
+
if (segment === routing.primary || isSingleLanguagePage(rest))
|
|
25
41
|
return { kind: "redirect", location: rest };
|
|
26
42
|
return { kind: "page", locale: segment, pathname: rest };
|
|
27
43
|
}
|
|
@@ -30,8 +46,9 @@ export function resolveLocalePath(pathname, routing) {
|
|
|
30
46
|
/**
|
|
31
47
|
* An internal link in `locale`: `/products/x` → `/fr/products/x`, `/` → `/fr`.
|
|
32
48
|
* Left unchanged: the primary language, external and protocol-relative URLs,
|
|
33
|
-
* fragments, `mailto:`/`tel:`, endpoints and files,
|
|
34
|
-
*
|
|
49
|
+
* fragments, `mailto:`/`tel:`, endpoints and files, a page that exists in the
|
|
50
|
+
* primary language only (a landing page), and a link that already carries a
|
|
51
|
+
* language prefix. Query strings and fragments are kept.
|
|
35
52
|
*/
|
|
36
53
|
export function localizeHref(href, locale, routing) {
|
|
37
54
|
if (locale === routing.primary || !href.startsWith("/") || href.startsWith("//"))
|
|
@@ -39,7 +56,7 @@ export function localizeHref(href, locale, routing) {
|
|
|
39
56
|
const split = /^([^?#]*)(.*)$/.exec(href);
|
|
40
57
|
const pathname = split[1];
|
|
41
58
|
const rest = split[2];
|
|
42
|
-
if (!isPagePath(pathname))
|
|
59
|
+
if (!isPagePath(pathname) || isSingleLanguagePage(pathname))
|
|
43
60
|
return href;
|
|
44
61
|
const first = /^\/([^/]+)/.exec(pathname)?.[1];
|
|
45
62
|
if (first !== undefined && routing.published.includes(first))
|
|
@@ -50,9 +67,12 @@ export function localizeHref(href, locale, routing) {
|
|
|
50
67
|
* Every published language's URL for one page, primary first, plus
|
|
51
68
|
* `x-default` (the primary) — the `<link rel="alternate" hreflang>` set.
|
|
52
69
|
* `pathname` is the page's unprefixed path; `origin` makes the URLs absolute,
|
|
53
|
-
* as search engines require.
|
|
70
|
+
* as search engines require. None for a page that exists in the primary
|
|
71
|
+
* language only.
|
|
54
72
|
*/
|
|
55
73
|
export function alternateLinks(pathname, routing, origin) {
|
|
74
|
+
if (isSingleLanguagePage(pathname))
|
|
75
|
+
return [];
|
|
56
76
|
const base = origin.replace(/\/+$/, "");
|
|
57
77
|
const links = routing.published.map((locale) => ({
|
|
58
78
|
hreflang: locale,
|
package/dist/middleware.d.ts
CHANGED
|
@@ -53,10 +53,11 @@ export interface ActionDescriptor<TResult extends ActionHandlerResult = ActionHa
|
|
|
53
53
|
handler: () => Promise<TResult>;
|
|
54
54
|
}
|
|
55
55
|
/**
|
|
56
|
-
* `
|
|
57
|
-
* Astro 5 used a string,
|
|
58
|
-
*
|
|
59
|
-
* through, so no theme ever
|
|
56
|
+
* What `runFormAction` needs from `getActionContext()`. `TSerialized` is the
|
|
57
|
+
* framework's result-cookie token, opaque to this SDK: Astro 5 used a string,
|
|
58
|
+
* Astro 7 an object (`SerializedActionResult`). The generics thread whatever
|
|
59
|
+
* `getActionContext()` hands the theme straight through, so no theme ever
|
|
60
|
+
* casts.
|
|
60
61
|
*/
|
|
61
62
|
export interface ActionMiddlewareDeps<TResult extends ActionHandlerResult = ActionHandlerResult, TSerialized = string> {
|
|
62
63
|
action?: ActionDescriptor<TResult>;
|
|
@@ -64,31 +65,10 @@ export interface ActionMiddlewareDeps<TResult extends ActionHandlerResult = Acti
|
|
|
64
65
|
setActionResult: (name: string, serialized: TSerialized) => void;
|
|
65
66
|
/** Serializes an ActionResult into the cookie value (Astro's serializeActionResult). */
|
|
66
67
|
serializeActionResult: (result: TResult) => TSerialized;
|
|
67
|
-
/** @deprecated Unused since 1.2.0: nothing redirects. */
|
|
68
|
-
referer?: string | null;
|
|
69
|
-
/** @deprecated Unused since 1.2.0: nothing redirects. */
|
|
70
|
-
pathname?: string;
|
|
71
68
|
}
|
|
72
|
-
export type PrgDecision =
|
|
73
|
-
/** Never produced since 1.2.0 — kept so existing `decision.kind` checks compile. */
|
|
74
|
-
{
|
|
75
|
-
kind: "redirect";
|
|
76
|
-
location: string;
|
|
77
|
-
status: 303;
|
|
78
|
-
}
|
|
79
|
-
/** Render the page from this POST. */
|
|
80
|
-
| {
|
|
81
|
-
kind: "next";
|
|
82
|
-
};
|
|
83
69
|
/**
|
|
84
70
|
* Run an action called from an HTML form and store its result for the page
|
|
85
71
|
* this POST renders. Resolves to that result, or `undefined` when the request
|
|
86
72
|
* is not a form-called action (a JSON call or a plain request is Astro's).
|
|
87
73
|
*/
|
|
88
|
-
export declare function runFormAction<TResult extends ActionHandlerResult, TSerialized>(deps:
|
|
89
|
-
/**
|
|
90
|
-
* @deprecated Use `runFormAction` and call `next()`. Runs the action the same
|
|
91
|
-
* way and always answers `{ kind: "next" }`: an errored post renders in place,
|
|
92
|
-
* because a redirect drops the result (see the note at the top of this file).
|
|
93
|
-
*/
|
|
94
|
-
export declare function resolveActionRedirectDecision<TResult extends ActionHandlerResult, TSerialized>(deps: ActionMiddlewareDeps<TResult, TSerialized>): Promise<PrgDecision>;
|
|
74
|
+
export declare function runFormAction<TResult extends ActionHandlerResult, TSerialized>(deps: ActionMiddlewareDeps<TResult, TSerialized>): Promise<TResult | undefined>;
|
package/dist/middleware.js
CHANGED
|
@@ -51,12 +51,3 @@ export async function runFormAction(deps) {
|
|
|
51
51
|
setActionResult(action.name, serializeActionResult(result));
|
|
52
52
|
return result;
|
|
53
53
|
}
|
|
54
|
-
/**
|
|
55
|
-
* @deprecated Use `runFormAction` and call `next()`. Runs the action the same
|
|
56
|
-
* way and always answers `{ kind: "next" }`: an errored post renders in place,
|
|
57
|
-
* because a redirect drops the result (see the note at the top of this file).
|
|
58
|
-
*/
|
|
59
|
-
export async function resolveActionRedirectDecision(deps) {
|
|
60
|
-
await runFormAction(deps);
|
|
61
|
-
return { kind: "next" };
|
|
62
|
-
}
|
package/dist/product-view.d.ts
CHANGED
|
@@ -10,11 +10,3 @@ export interface ProductView {
|
|
|
10
10
|
}
|
|
11
11
|
export declare function getProductView(product: ProductDetail): ProductView;
|
|
12
12
|
export declare function getProductListView(product: ProductList): ProductView;
|
|
13
|
-
/**
|
|
14
|
-
* A product's tags as a list, whichever read it came from: a product page's
|
|
15
|
-
* `tags` is already a list, a listing's is the stored JSON text. Anything
|
|
16
|
-
* that is not a list of text reads as no tags.
|
|
17
|
-
*/
|
|
18
|
-
export declare function productTags(product: {
|
|
19
|
-
tags: string | null | readonly string[];
|
|
20
|
-
}): string[];
|
package/dist/product-view.js
CHANGED
|
@@ -44,20 +44,3 @@ export function getProductListView(product) {
|
|
|
44
44
|
...stockOf(product),
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
|
-
/**
|
|
48
|
-
* A product's tags as a list, whichever read it came from: a product page's
|
|
49
|
-
* `tags` is already a list, a listing's is the stored JSON text. Anything
|
|
50
|
-
* that is not a list of text reads as no tags.
|
|
51
|
-
*/
|
|
52
|
-
export function productTags(product) {
|
|
53
|
-
let tags = product.tags;
|
|
54
|
-
if (typeof tags === "string") {
|
|
55
|
-
try {
|
|
56
|
-
tags = JSON.parse(tags);
|
|
57
|
-
}
|
|
58
|
-
catch {
|
|
59
|
-
return [];
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
return Array.isArray(tags) ? tags.filter((tag) => typeof tag === "string") : [];
|
|
63
|
-
}
|
package/dist/revalidate.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
/** Where a theme that can drop its cached pages answers a revalidation. */
|
|
2
2
|
export declare const REVALIDATE_PATH = "/wocart-revalidate";
|
|
3
|
+
/**
|
|
4
|
+
* The cache tag every cached page carries, and the one a revalidation drops:
|
|
5
|
+
* a publish (pages, settings or text) can change any page, so a theme tags
|
|
6
|
+
* every cached route with it (`routeRules` in `astro.config.mjs`).
|
|
7
|
+
*/
|
|
8
|
+
export declare const PAGES_CACHE_TAG = "pages";
|
|
3
9
|
/** What a verified revalidation token says. */
|
|
4
10
|
export interface RevalidateClaims {
|
|
5
11
|
/** The storefront origin the token was made for. */
|
package/dist/revalidate.js
CHANGED
|
@@ -17,6 +17,12 @@
|
|
|
17
17
|
import { timingRefusal, verifiedPayload } from "./signed-token.js";
|
|
18
18
|
/** Where a theme that can drop its cached pages answers a revalidation. */
|
|
19
19
|
export const REVALIDATE_PATH = "/wocart-revalidate";
|
|
20
|
+
/**
|
|
21
|
+
* The cache tag every cached page carries, and the one a revalidation drops:
|
|
22
|
+
* a publish (pages, settings or text) can change any page, so a theme tags
|
|
23
|
+
* every cached route with it (`routeRules` in `astro.config.mjs`).
|
|
24
|
+
*/
|
|
25
|
+
export const PAGES_CACHE_TAG = "pages";
|
|
20
26
|
/** The longest a revalidation token may live. */
|
|
21
27
|
const MAX_LIFETIME_SECONDS = 5 * 60;
|
|
22
28
|
const refuse = (status, reason) => ({ ok: false, status, reason });
|