@wocart/theme 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.md +47 -0
- package/dist/definitions.d.ts +23 -3
- package/dist/definitions.js +0 -1
- package/dist/image-service.d.ts +39 -0
- package/dist/image-service.js +73 -0
- package/dist/index.d.ts +1 -1
- package/dist/types.d.ts +16 -4
- package/package.json +6 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,32 @@ All notable changes to `@wocart/theme` are recorded here. The package follows
|
|
|
5
5
|
major version, and an export is marked `@deprecated` for at least one minor
|
|
6
6
|
release before a major removes it.
|
|
7
7
|
|
|
8
|
+
## 1.4.0 — 2026-10-18
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `@wocart/theme/image-service`: `mediaImageService(() => mediaDomain)`, an
|
|
13
|
+
Astro image service that resizes media-domain images with Cloudflare Image
|
|
14
|
+
Resizing on the media domain itself, so a storefront on Cloudflare and one
|
|
15
|
+
on Vercel get the same URLs and edge cache. Other images are served as they
|
|
16
|
+
are; the domain is read per image, so one build serves every store.
|
|
17
|
+
`mediaImageParams` maps an Astro transform to the resizing options (Astro's
|
|
18
|
+
`fit` in Cloudflare's words).
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- The `@wocart/storefront` peer range is now `^1.14.0` (`mediaImageUrl`).
|
|
23
|
+
|
|
24
|
+
## 1.3.0 — 2026-10-17
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- `load` is typed from the component's own fields: `defineComponent` hands it
|
|
29
|
+
`props` as `FieldValues<fields>` (a field with a default is always there,
|
|
30
|
+
any other may be absent), so a definition reads its settings — which
|
|
31
|
+
collection, how many products — without a cast. What `load` returns still
|
|
32
|
+
types the component's props (`InferProps`). Type `FieldValues`.
|
|
33
|
+
|
|
8
34
|
## 1.2.0 — 2026-10-17
|
|
9
35
|
|
|
10
36
|
### Added
|
package/README.md
CHANGED
|
@@ -37,6 +37,7 @@ Node 22.12 or newer.
|
|
|
37
37
|
| `@wocart/theme` | definitions, validation, page documents, resolution, the manifest, languages and messages — framework-neutral |
|
|
38
38
|
| `@wocart/theme/astro` | `createReadCache` and the `RendererIssue` type |
|
|
39
39
|
| `@wocart/theme/astro/InstanceView.astro` | the reference renderer component |
|
|
40
|
+
| `@wocart/theme/image-service` | `mediaImageService` — an Astro image service that resizes media-domain images on the media domain (any storefront host) |
|
|
40
41
|
|
|
41
42
|
## Use
|
|
42
43
|
|
|
@@ -89,6 +90,24 @@ props and undeclared children/slots, is reported through `onIssue` (default:
|
|
|
89
90
|
`console.warn`). The issue contains a stable `code`, `message`, `instanceId`,
|
|
90
91
|
and, for page-data resolution issues, `path`.
|
|
91
92
|
|
|
93
|
+
`load` receives the instance's resolved props typed from the component's own
|
|
94
|
+
fields, so a section reads its settings without a cast:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
defineComponent({
|
|
98
|
+
type: "best-sellers",
|
|
99
|
+
component: BestSellers,
|
|
100
|
+
label: { ar: "الأكثر مبيعاً", fr: "Meilleures ventes" },
|
|
101
|
+
fields: [
|
|
102
|
+
{ key: "collection", kind: "collection", label: { ar: "المجموعة", fr: "Collection" }, default: "best-sellers" },
|
|
103
|
+
{ key: "limit", kind: "number", label: { ar: "العدد", fr: "Nombre" }, default: 8, min: 1, max: 24 },
|
|
104
|
+
],
|
|
105
|
+
load: async ({ storefront, props }) => ({
|
|
106
|
+
products: await storefront.products.list({ collection: props.collection, limit: props.limit }),
|
|
107
|
+
}),
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
92
111
|
## Field kinds
|
|
93
112
|
|
|
94
113
|
| Kind | Value | Rules |
|
|
@@ -426,6 +445,34 @@ migration engine, by design; what follows is the whole policy:
|
|
|
426
445
|
stick to semver so the manifest validator stays quiet and future tooling can
|
|
427
446
|
reason about upgrades.
|
|
428
447
|
|
|
448
|
+
## Images
|
|
449
|
+
|
|
450
|
+
`@wocart/theme/image-service` resizes every `<Image>` of a media-domain image
|
|
451
|
+
with Cloudflare Image Resizing, on the media domain itself
|
|
452
|
+
(`https://<media>/cdn-cgi/image/…`). The media domain is a Cloudflare zone
|
|
453
|
+
wherever the storefront runs, so a storefront on Cloudflare and one on Vercel
|
|
454
|
+
share the same URLs and edge cache. Any other image is served as it is; with no
|
|
455
|
+
media domain, every image passes through.
|
|
456
|
+
|
|
457
|
+
The domain is read on each image, not at build — one build serves every store.
|
|
458
|
+
Bind it once:
|
|
459
|
+
|
|
460
|
+
```ts
|
|
461
|
+
// src/image-service.ts
|
|
462
|
+
import { MEDIA_DOMAIN } from "astro:env/server";
|
|
463
|
+
import { mediaImageService } from "@wocart/theme/image-service";
|
|
464
|
+
export default mediaImageService(() => MEDIA_DOMAIN);
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
```js
|
|
468
|
+
// astro.config.mjs
|
|
469
|
+
image: { service: { entrypoint: "./src/image-service.ts" } },
|
|
470
|
+
// @astrojs/cloudflare: imageService: "custom" · @astrojs/vercel: leave imageService off
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
The media domain's zone needs Image Transformations on (Images →
|
|
474
|
+
Transformations; included in the Free plan).
|
|
475
|
+
|
|
429
476
|
## Testing your theme
|
|
430
477
|
|
|
431
478
|
A theme proves it honors the contract with four checks in its own test suite:
|
package/dist/definitions.d.ts
CHANGED
|
@@ -1,7 +1,27 @@
|
|
|
1
|
-
import type { ComponentDefinition, ThemeDefinition, ThemeDefinitionIssue, ThemeValidationOptions } from "./types.js";
|
|
2
|
-
/**
|
|
3
|
-
|
|
1
|
+
import type { ComponentDefinition, FieldDefinition, FieldValues, LoadContext, ThemeDefinition, ThemeDefinitionIssue, ThemeValidationOptions } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Identity helper: returns the definition unchanged so literal types survive.
|
|
4
|
+
* `load` receives the component's props typed from its own `fields`, so a
|
|
5
|
+
* definition reads its settings without a cast, and what it returns types the
|
|
6
|
+
* component's props (`InferProps`).
|
|
7
|
+
*/
|
|
8
|
+
export declare function defineComponent<const T extends ComponentBase, Loaded extends Record<string, unknown>, const Fields extends readonly FieldDefinition[] = readonly []>(definition: T & {
|
|
9
|
+
fields?: Fields;
|
|
10
|
+
load: (context: LoadContext<FieldValues<Fields>>) => Promise<Loaded>;
|
|
11
|
+
}): T & FieldsPart<Fields> & {
|
|
12
|
+
load: (context: LoadContext) => Promise<Loaded>;
|
|
13
|
+
};
|
|
14
|
+
export declare function defineComponent<const T extends ComponentBase, const Fields extends readonly FieldDefinition[] = readonly []>(definition: T & {
|
|
15
|
+
fields?: Fields;
|
|
16
|
+
load?: undefined;
|
|
17
|
+
}): T & FieldsPart<Fields>;
|
|
18
|
+
type ComponentBase = Omit<ComponentDefinition, "fields" | "load">;
|
|
19
|
+
/** The field literals a definition declared, when it declared any. */
|
|
20
|
+
type FieldsPart<Fields extends readonly FieldDefinition[]> = [Fields] extends [readonly []] ? {} : {
|
|
21
|
+
fields: Fields;
|
|
22
|
+
};
|
|
4
23
|
/** Identity helper for the catalog. */
|
|
5
24
|
export declare function defineTheme<const T extends ThemeDefinition>(definition: T): T;
|
|
6
25
|
/** Check the whole catalog: key/type identity, field rules, slot rules. */
|
|
7
26
|
export declare function validateThemeDefinition(theme: ThemeDefinition, options?: ThemeValidationOptions): ThemeDefinitionIssue[];
|
|
27
|
+
export {};
|
package/dist/definitions.js
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
* bugs when the catalog is assembled.
|
|
6
6
|
*/
|
|
7
7
|
import { COLOR_PATTERN, MENU_HANDLE_PATTERN, COLLECTION_HANDLE_PATTERN, SURFACE_KEY_PATTERN, THEME_KEY_PATTERN, TYPE_KEY_PATTERN, isLocaleMap, isLocaleTag, isSafeLinkTarget, } from "./schemas.js";
|
|
8
|
-
/** Identity helper: returns the definition unchanged so literal types survive. */
|
|
9
8
|
export function defineComponent(definition) {
|
|
10
9
|
return definition;
|
|
11
10
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An Astro image service for Wocart storefronts: every `<Image>` and
|
|
3
|
+
* `getImage()` of a media-domain image is resized by Cloudflare Image Resizing
|
|
4
|
+
* on the media domain itself (`https://<media>/cdn-cgi/image/…/<path>`).
|
|
5
|
+
*
|
|
6
|
+
* The media domain is always a Cloudflare zone, wherever the storefront runs,
|
|
7
|
+
* so a storefront on Cloudflare and one on Vercel get the same URLs, the same
|
|
8
|
+
* edge cache and no second image bill. Any other image — another host, a file
|
|
9
|
+
* imported into the theme — is served as it is. Without a media domain (local
|
|
10
|
+
* development, a store without one) every image passes through unchanged.
|
|
11
|
+
*
|
|
12
|
+
* The domain is read per call, not at build: one theme build serves every
|
|
13
|
+
* store. Bind it once, in the theme, to wherever the store's media domain
|
|
14
|
+
* lives:
|
|
15
|
+
*
|
|
16
|
+
* ```ts
|
|
17
|
+
* // src/image-service.ts
|
|
18
|
+
* import { MEDIA_DOMAIN } from "astro:env/server";
|
|
19
|
+
* import { mediaImageService } from "@wocart/theme/image-service";
|
|
20
|
+
* export default mediaImageService(() => MEDIA_DOMAIN);
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* ```js
|
|
24
|
+
* // astro.config.mjs — and the adapter keeps it:
|
|
25
|
+
* // @astrojs/cloudflare: imageService: "custom"; @astrojs/vercel: no imageService
|
|
26
|
+
* image: { service: { entrypoint: "./src/image-service.ts" } },
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
import type { ExternalImageService, ImageTransform } from "astro";
|
|
30
|
+
import { type MediaImageParams } from "@wocart/storefront";
|
|
31
|
+
/** The Cloudflare resizing options an Astro image transform asks for. */
|
|
32
|
+
export declare function mediaImageParams(options: ImageTransform): MediaImageParams;
|
|
33
|
+
/**
|
|
34
|
+
* The image service, bound to the store's media domain.
|
|
35
|
+
*
|
|
36
|
+
* @param mediaDomain - Read on every image (e.g. `() => MEDIA_DOMAIN`): the
|
|
37
|
+
* store's media domain, or `undefined` to pass every image through.
|
|
38
|
+
*/
|
|
39
|
+
export declare function mediaImageService(mediaDomain: () => string | undefined): ExternalImageService;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An Astro image service for Wocart storefronts: every `<Image>` and
|
|
3
|
+
* `getImage()` of a media-domain image is resized by Cloudflare Image Resizing
|
|
4
|
+
* on the media domain itself (`https://<media>/cdn-cgi/image/…/<path>`).
|
|
5
|
+
*
|
|
6
|
+
* The media domain is always a Cloudflare zone, wherever the storefront runs,
|
|
7
|
+
* so a storefront on Cloudflare and one on Vercel get the same URLs, the same
|
|
8
|
+
* edge cache and no second image bill. Any other image — another host, a file
|
|
9
|
+
* imported into the theme — is served as it is. Without a media domain (local
|
|
10
|
+
* development, a store without one) every image passes through unchanged.
|
|
11
|
+
*
|
|
12
|
+
* The domain is read per call, not at build: one theme build serves every
|
|
13
|
+
* store. Bind it once, in the theme, to wherever the store's media domain
|
|
14
|
+
* lives:
|
|
15
|
+
*
|
|
16
|
+
* ```ts
|
|
17
|
+
* // src/image-service.ts
|
|
18
|
+
* import { MEDIA_DOMAIN } from "astro:env/server";
|
|
19
|
+
* import { mediaImageService } from "@wocart/theme/image-service";
|
|
20
|
+
* export default mediaImageService(() => MEDIA_DOMAIN);
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* ```js
|
|
24
|
+
* // astro.config.mjs — and the adapter keeps it:
|
|
25
|
+
* // @astrojs/cloudflare: imageService: "custom"; @astrojs/vercel: no imageService
|
|
26
|
+
* image: { service: { entrypoint: "./src/image-service.ts" } },
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
import { baseService } from "astro/assets";
|
|
30
|
+
import { mediaImageUrl } from "@wocart/storefront";
|
|
31
|
+
/** Astro's `fit` (CSS object-fit) in Cloudflare's words; a value Cloudflare also knows passes as it is. */
|
|
32
|
+
const CLOUDFLARE_FIT = {
|
|
33
|
+
cover: "cover",
|
|
34
|
+
contain: "contain",
|
|
35
|
+
fill: "squeeze",
|
|
36
|
+
"scale-down": "scale-down",
|
|
37
|
+
crop: "crop",
|
|
38
|
+
pad: "pad",
|
|
39
|
+
squeeze: "squeeze",
|
|
40
|
+
};
|
|
41
|
+
/** The Cloudflare resizing options an Astro image transform asks for. */
|
|
42
|
+
export function mediaImageParams(options) {
|
|
43
|
+
const params = {};
|
|
44
|
+
if (options.width)
|
|
45
|
+
params.width = options.width;
|
|
46
|
+
if (options.height)
|
|
47
|
+
params.height = options.height;
|
|
48
|
+
if (options.format)
|
|
49
|
+
params.format = options.format;
|
|
50
|
+
if (typeof options.quality === "number")
|
|
51
|
+
params.quality = options.quality;
|
|
52
|
+
const fit = options.fit ? CLOUDFLARE_FIT[options.fit] : undefined;
|
|
53
|
+
if (fit)
|
|
54
|
+
params.fit = fit;
|
|
55
|
+
return params;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The image service, bound to the store's media domain.
|
|
59
|
+
*
|
|
60
|
+
* @param mediaDomain - Read on every image (e.g. `() => MEDIA_DOMAIN`): the
|
|
61
|
+
* store's media domain, or `undefined` to pass every image through.
|
|
62
|
+
*/
|
|
63
|
+
export function mediaImageService(mediaDomain) {
|
|
64
|
+
return {
|
|
65
|
+
...baseService,
|
|
66
|
+
getURL(options) {
|
|
67
|
+
const src = options.src;
|
|
68
|
+
if (typeof src !== "string")
|
|
69
|
+
return src.src;
|
|
70
|
+
return mediaImageUrl(src, mediaImageParams(options), mediaDomain()) ?? src;
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -17,4 +17,4 @@ export { localize } from "./locales.js";
|
|
|
17
17
|
export { PLURAL_CATEGORIES, formatMessage, messageForms, pluralFormIssues } from "./messages.js";
|
|
18
18
|
export type { MessageOptions, MessageValues, PluralCategory } from "./messages.js";
|
|
19
19
|
export { THEME_MANIFEST_CONTRACT_VERSION, themeManifest, validateThemeManifest, } from "./manifest.js";
|
|
20
|
-
export type { JsonValue, JsonObject, LocaleCode, LocalizedText, ResolutionScope, FieldKind, FieldDefinition, FieldValue, InferProps, SlotDefinition, ComponentCapabilities, ComponentDefinition, ComponentSpec, SurfaceSpec, ThemeDefinition, SettingsGroup, InferSettings, ThemeSettingsIssue, ThemeSettingsIssueCode, ThemeSettingsResult, ColorFieldDefinition, MenuFieldDefinition, CollectionFieldDefinition, ThemeManifest, ThemeManifestIssue, ThemeManifestIssueCode, ThemeManifestResult, ComponentInstance, PageData, ThemeRenderContext, LoadContext, LoadFn, ReadCache, PageDataIssue, PageDataIssueCode, PageDataResult, ResolvedInstance, ThemeDefinitionIssue, ThemeDefinitionIssueCode, ThemeValidationOptions, } from "./types.js";
|
|
20
|
+
export type { JsonValue, JsonObject, LocaleCode, LocalizedText, ResolutionScope, FieldKind, FieldDefinition, FieldValue, InferProps, FieldValues, SlotDefinition, ComponentCapabilities, ComponentDefinition, ComponentSpec, SurfaceSpec, ThemeDefinition, SettingsGroup, InferSettings, ThemeSettingsIssue, ThemeSettingsIssueCode, ThemeSettingsResult, ColorFieldDefinition, MenuFieldDefinition, CollectionFieldDefinition, ThemeManifest, ThemeManifestIssue, ThemeManifestIssueCode, ThemeManifestResult, ComponentInstance, PageData, ThemeRenderContext, LoadContext, LoadFn, ReadCache, PageDataIssue, PageDataIssueCode, PageDataResult, ResolvedInstance, ThemeDefinitionIssue, ThemeDefinitionIssueCode, ThemeValidationOptions, } from "./types.js";
|
package/dist/types.d.ts
CHANGED
|
@@ -204,7 +204,7 @@ export interface ReadCache {
|
|
|
204
204
|
get<T>(key: string, load: () => T | Promise<T>): Promise<T>;
|
|
205
205
|
}
|
|
206
206
|
/** What a `load` hook receives. Server-only; reads go through the storefront SDK. */
|
|
207
|
-
export interface LoadContext {
|
|
207
|
+
export interface LoadContext<P = Record<string, unknown>> {
|
|
208
208
|
storefront: StoreClient;
|
|
209
209
|
context: ThemeRenderContext;
|
|
210
210
|
/**
|
|
@@ -215,11 +215,16 @@ export interface LoadContext {
|
|
|
215
215
|
* the same name. A definition whose component reads a prop from `load` should
|
|
216
216
|
* keep that name out of `fields`.
|
|
217
217
|
*/
|
|
218
|
-
props:
|
|
218
|
+
props: P;
|
|
219
219
|
reads: ReadCache;
|
|
220
220
|
}
|
|
221
|
-
/**
|
|
222
|
-
|
|
221
|
+
/**
|
|
222
|
+
* Server-side data fetch for one component definition. Through
|
|
223
|
+
* `defineComponent`, `props` is typed from the definition's own fields
|
|
224
|
+
* (`FieldValues`): a field with a default is always there, any other may be
|
|
225
|
+
* absent.
|
|
226
|
+
*/
|
|
227
|
+
export type LoadFn<P = Record<string, unknown>> = (context: LoadContext<P>) => Promise<Record<string, unknown>>;
|
|
223
228
|
/**
|
|
224
229
|
* Code-owned description of one reusable component. `component` is the real
|
|
225
230
|
* component reference — opaque here so the core stays framework-neutral; the
|
|
@@ -249,6 +254,13 @@ export type FieldValue<F extends FieldDefinition> = F extends {
|
|
|
249
254
|
type FieldsOf<D> = D extends {
|
|
250
255
|
fields: readonly (infer F extends FieldDefinition)[];
|
|
251
256
|
} ? F : never;
|
|
257
|
+
/**
|
|
258
|
+
* The resolved values of a list of fields, as `load` and the component receive
|
|
259
|
+
* them: a field with a `default` is always present, any other may be absent.
|
|
260
|
+
*/
|
|
261
|
+
export type FieldValues<Fields extends readonly FieldDefinition[]> = FieldProps<{
|
|
262
|
+
fields: Fields;
|
|
263
|
+
}>;
|
|
252
264
|
type FieldProps<D> = {
|
|
253
265
|
[F in FieldsOf<D> as F extends {
|
|
254
266
|
default: unknown;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wocart/theme",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "The Wocart theme contract: code-owned component definitions, JSON page documents, validation and resolution, and a reference Astro renderer.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"wocart",
|
|
@@ -38,6 +38,10 @@
|
|
|
38
38
|
"default": "./dist/astro/index.js"
|
|
39
39
|
},
|
|
40
40
|
"./astro/InstanceView.astro": "./dist/astro/InstanceView.astro",
|
|
41
|
+
"./image-service": {
|
|
42
|
+
"types": "./dist/image-service.d.ts",
|
|
43
|
+
"default": "./dist/image-service.js"
|
|
44
|
+
},
|
|
41
45
|
"./package.json": "./package.json"
|
|
42
46
|
},
|
|
43
47
|
"files": [
|
|
@@ -63,7 +67,7 @@
|
|
|
63
67
|
"zod": "^4.4.3"
|
|
64
68
|
},
|
|
65
69
|
"peerDependencies": {
|
|
66
|
-
"@wocart/storefront": "^1.
|
|
70
|
+
"@wocart/storefront": "^1.14.0",
|
|
67
71
|
"astro": "^7.0.0"
|
|
68
72
|
},
|
|
69
73
|
"peerDependenciesMeta": {
|