@wocart/theme 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.
- package/CHANGELOG.md +54 -0
- package/LICENSE +202 -0
- package/NOTICE +2 -0
- package/README.md +434 -2
- package/dist/astro/InstanceView.astro +167 -0
- package/dist/astro/index.d.ts +34 -0
- package/dist/astro/index.js +21 -0
- package/dist/definitions.d.ts +7 -0
- package/dist/definitions.js +363 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +17 -0
- package/dist/instances.d.ts +22 -0
- package/dist/instances.js +295 -0
- package/dist/locales.d.ts +12 -0
- package/dist/locales.js +14 -0
- package/dist/manifest.d.ts +98 -0
- package/dist/manifest.js +308 -0
- package/dist/messages.d.ts +60 -0
- package/dist/messages.js +100 -0
- package/dist/page-data.d.ts +9 -0
- package/dist/page-data.js +154 -0
- package/dist/schemas.d.ts +49 -0
- package/dist/schemas.js +100 -0
- package/dist/types.d.ts +412 -0
- package/dist/types.js +1 -0
- package/package.json +80 -4
package/dist/schemas.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zod schemas for the persistence boundary — the only place runtime input is
|
|
3
|
+
* validated. Definitions are typechecked code; page data is parsed data.
|
|
4
|
+
*/
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
/**
|
|
7
|
+
* A theme's key: lowercase kebab-case, at most 64 characters. The platform
|
|
8
|
+
* stores a merchant's text edits under it, so it is the theme's identity on the
|
|
9
|
+
* platform, and the server applies the same rule.
|
|
10
|
+
*/
|
|
11
|
+
export const THEME_KEY_PATTERN = /^(?=.{1,64}$)[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
12
|
+
/** Type keys are lowercase kebab-case. */
|
|
13
|
+
export const TYPE_KEY_PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
14
|
+
/**
|
|
15
|
+
* The page-document **surface** key — the address of a composed page
|
|
16
|
+
* (`home` today; `page:<slug>` / `lp:<slug>` are reserved conventions).
|
|
17
|
+
*
|
|
18
|
+
* The server applies the same pattern. Module-level on purpose: it is part of
|
|
19
|
+
* the persisted-data contract,
|
|
20
|
+
* not of the package's curated public surface (`src/index.ts`).
|
|
21
|
+
*/
|
|
22
|
+
export const SURFACE_KEY_PATTERN = /^[a-z0-9][a-z0-9._:-]{0,63}$/;
|
|
23
|
+
/**
|
|
24
|
+
* A BCP 47 language tag in canonical case: a 2–3 letter language, an optional
|
|
25
|
+
* 4-letter script, an optional 2-letter or 3-digit region (`ar`, `zgh`,
|
|
26
|
+
* `ar-DZ`, `zh-Hant-TW`, `es-419`). Canonical case keeps map keys unique:
|
|
27
|
+
* `fr` and `FR` can never both be present.
|
|
28
|
+
*/
|
|
29
|
+
const LOCALE_TAG_PATTERN = /^[a-z]{2,3}(?:-[A-Z][a-z]{3})?(?:-(?:[A-Z]{2}|\d{3}))?$/;
|
|
30
|
+
export function isLocaleTag(value) {
|
|
31
|
+
return LOCALE_TAG_PATTERN.test(value);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Whether `value` is a per-language map (`{ ar: "…", fr: "…" }`): a plain
|
|
35
|
+
* object with at least one entry, every key a locale tag, every value a string.
|
|
36
|
+
*/
|
|
37
|
+
export function isLocaleMap(value) {
|
|
38
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
39
|
+
return false;
|
|
40
|
+
const entries = Object.entries(value);
|
|
41
|
+
return (entries.length > 0 &&
|
|
42
|
+
entries.every(([key, text]) => isLocaleTag(key) && typeof text === "string"));
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Whether a `url` field value is safe to render as an `href`: a site path,
|
|
46
|
+
* fragment or query, or an `http(s)`, `mailto` or `tel` URL. Rejects
|
|
47
|
+
* `javascript:`/`data:` and every other scheme, protocol-relative `//host`,
|
|
48
|
+
* backslashes (browsers read `/\host` as `//host`), whitespace and control
|
|
49
|
+
* characters, and anything over 2048 characters.
|
|
50
|
+
*/
|
|
51
|
+
export function isSafeLinkTarget(value) {
|
|
52
|
+
if (value.length === 0 || value.length > 2048)
|
|
53
|
+
return false;
|
|
54
|
+
if (/[\s\\\u0000-\u001f\u007f]/.test(value))
|
|
55
|
+
return false;
|
|
56
|
+
if (value.startsWith("/"))
|
|
57
|
+
return !value.startsWith("//");
|
|
58
|
+
if (value.startsWith("#") || value.startsWith("?"))
|
|
59
|
+
return true;
|
|
60
|
+
const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(value)?.[1]?.toLowerCase();
|
|
61
|
+
if (scheme === "http" || scheme === "https") {
|
|
62
|
+
return /^https?:\/\/[a-z0-9.-]+(:\d{1,5})?([/?#]|$)/i.test(value);
|
|
63
|
+
}
|
|
64
|
+
if (scheme === "mailto" || scheme === "tel")
|
|
65
|
+
return value.length > scheme.length + 1;
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
export const JsonValueSchema = z.lazy(() => z.union([
|
|
69
|
+
z.string(),
|
|
70
|
+
z
|
|
71
|
+
.number()
|
|
72
|
+
.refine(Number.isFinite, { message: "numbers must be finite" })
|
|
73
|
+
// JSON has no -0: it serializes as 0, so keeping it would break round-trips.
|
|
74
|
+
.transform((value) => (Object.is(value, -0) ? 0 : value)),
|
|
75
|
+
z.boolean(),
|
|
76
|
+
z.null(),
|
|
77
|
+
z.array(JsonValueSchema),
|
|
78
|
+
z.record(z.string(), JsonValueSchema),
|
|
79
|
+
]));
|
|
80
|
+
export const ComponentInstanceSchema = z.lazy(() => z.object({
|
|
81
|
+
id: z.string().min(1),
|
|
82
|
+
type: z
|
|
83
|
+
.string()
|
|
84
|
+
.regex(TYPE_KEY_PATTERN, "type keys are lowercase kebab-case"),
|
|
85
|
+
props: z.exactOptional(z.record(z.string(), JsonValueSchema)),
|
|
86
|
+
children: z.exactOptional(z.array(ComponentInstanceSchema)),
|
|
87
|
+
slots: z.exactOptional(z.record(z.string(), z.array(ComponentInstanceSchema))),
|
|
88
|
+
}));
|
|
89
|
+
/**
|
|
90
|
+
* The envelope schema. **Loose on purpose**: unknown top-level keys
|
|
91
|
+
* survive `.parse`, matching `parsePageData` — a writer that records metadata
|
|
92
|
+
* must not have it stripped by whichever reader runs first.
|
|
93
|
+
*/
|
|
94
|
+
export const PageDataSchema = z.looseObject({
|
|
95
|
+
version: z.literal(1),
|
|
96
|
+
surface: z
|
|
97
|
+
.string()
|
|
98
|
+
.regex(SURFACE_KEY_PATTERN, "surface keys are lowercase, start alphanumeric, and use kebab/dot/colon/underscore"),
|
|
99
|
+
sections: z.array(ComponentInstanceSchema),
|
|
100
|
+
});
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `@wocart/theme` contract — every type in one place.
|
|
3
|
+
*
|
|
4
|
+
* Two sides, one seam:
|
|
5
|
+
* - code-owned: `ThemeDefinition` / `ComponentDefinition` (never serialized);
|
|
6
|
+
* - data-owned: `ComponentInstance` / `PageData` (always JSON).
|
|
7
|
+
*/
|
|
8
|
+
import type { StoreClient } from "@wocart/storefront/server";
|
|
9
|
+
import type { StoreConfig } from "@wocart/storefront";
|
|
10
|
+
/** Every value page data may hold. */
|
|
11
|
+
export type JsonValue = string | number | boolean | null | JsonValue[] | {
|
|
12
|
+
[key: string]: JsonValue;
|
|
13
|
+
};
|
|
14
|
+
/** A JSON object, as stored in `ComponentInstance.props`. */
|
|
15
|
+
export type JsonObject = {
|
|
16
|
+
[key: string]: JsonValue;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* A BCP 47 language tag in canonical case: `ar`, `fr`, `en`, `ar-DZ`, `zgh`.
|
|
20
|
+
* A plain string type on purpose: the set of languages is platform data, never
|
|
21
|
+
* a closed enum baked into the contract.
|
|
22
|
+
*/
|
|
23
|
+
export type LocaleCode = string;
|
|
24
|
+
/**
|
|
25
|
+
* Text that is the same in every language (a plain string), or one value per
|
|
26
|
+
* language. Used for translatable field values, translatable text defaults, and
|
|
27
|
+
* editor-facing labels.
|
|
28
|
+
*/
|
|
29
|
+
export type LocalizedText = string | Readonly<Record<LocaleCode, string>>;
|
|
30
|
+
/**
|
|
31
|
+
* What resolution needs besides the instance: the theme's content pack for
|
|
32
|
+
* the language being rendered, and the language preference — the requested
|
|
33
|
+
* language first, then the store's fallback chain.
|
|
34
|
+
*/
|
|
35
|
+
export interface ResolutionScope {
|
|
36
|
+
content?: Record<string, string>;
|
|
37
|
+
locales?: readonly LocaleCode[];
|
|
38
|
+
}
|
|
39
|
+
/** The V1 field kinds. */
|
|
40
|
+
export type FieldKind = "text" | "textarea" | "number" | "boolean" | "select" | "url";
|
|
41
|
+
interface FieldDefinitionBase {
|
|
42
|
+
/** The exact prop name the component expects. */
|
|
43
|
+
key: string;
|
|
44
|
+
/** Editor-facing name, the same in every language or translated. */
|
|
45
|
+
label: LocalizedText;
|
|
46
|
+
/** Editor-facing help text. */
|
|
47
|
+
description?: LocalizedText;
|
|
48
|
+
required?: boolean;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* `text` / `textarea`: a string that defaults from the content pack
|
|
52
|
+
* (`contentKey`) or from a literal (`default`) — mutually exclusive.
|
|
53
|
+
*/
|
|
54
|
+
export interface TextFieldDefinition extends FieldDefinitionBase {
|
|
55
|
+
kind: "text" | "textarea";
|
|
56
|
+
/**
|
|
57
|
+
* The key in the **theme's own** content pack that supplies this field's
|
|
58
|
+
* creation-time value. The SDK never models the pack: it reads
|
|
59
|
+
* `ThemeRenderContext.content` — an opaque flat string map — and falls
|
|
60
|
+
* through to `default` → omit when the key is absent. Pass the pack's key set
|
|
61
|
+
* to `validateThemeDefinition` to have typos reported.
|
|
62
|
+
*/
|
|
63
|
+
contentKey?: string;
|
|
64
|
+
/**
|
|
65
|
+
* Literal creation-time default (mutually exclusive with `contentKey`), the
|
|
66
|
+
* same in every language or translated. Prefer `contentKey` for theme copy:
|
|
67
|
+
* the pack is already per language.
|
|
68
|
+
*/
|
|
69
|
+
default?: LocalizedText;
|
|
70
|
+
}
|
|
71
|
+
/** `number`: a finite number, optionally bounded. */
|
|
72
|
+
export interface NumberFieldDefinition extends FieldDefinitionBase {
|
|
73
|
+
kind: "number";
|
|
74
|
+
default?: number;
|
|
75
|
+
min?: number;
|
|
76
|
+
max?: number;
|
|
77
|
+
}
|
|
78
|
+
/** `boolean`. */
|
|
79
|
+
export interface BooleanFieldDefinition extends FieldDefinitionBase {
|
|
80
|
+
kind: "boolean";
|
|
81
|
+
default?: boolean;
|
|
82
|
+
}
|
|
83
|
+
/** `select`: one of a closed option list. */
|
|
84
|
+
export interface SelectFieldDefinition extends FieldDefinitionBase {
|
|
85
|
+
kind: "select";
|
|
86
|
+
options: readonly string[];
|
|
87
|
+
default?: string;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* `url`: a link target an editor may set, safe to render as an `href`. Only a
|
|
91
|
+
* site path (`/products`), a fragment (`#best-sellers`), a query (`?page=2`),
|
|
92
|
+
* `http(s)://`, `mailto:` or `tel:` is accepted. Anything else, including
|
|
93
|
+
* `javascript:`, `data:`, protocol-relative `//host` and backslash tricks, is
|
|
94
|
+
* rejected at assembly and reported as `invalid-prop` on read.
|
|
95
|
+
*/
|
|
96
|
+
export interface UrlFieldDefinition extends FieldDefinitionBase {
|
|
97
|
+
kind: "url";
|
|
98
|
+
default?: string;
|
|
99
|
+
}
|
|
100
|
+
export type FieldDefinition = TextFieldDefinition | NumberFieldDefinition | BooleanFieldDefinition | SelectFieldDefinition | UrlFieldDefinition;
|
|
101
|
+
/**
|
|
102
|
+
* One composition point a component declares (`"default"` = children).
|
|
103
|
+
*
|
|
104
|
+
* **Renderer ceiling (V1):** the reference renderer (`@wocart/theme/astro`)
|
|
105
|
+
* fills **at most one named slot** per instance. The Astro compiler lifts the
|
|
106
|
+
* first slot-attribute expression of a dynamic child expression into the
|
|
107
|
+
* slots-object key, evaluated at component scope, so per-item slot names inside
|
|
108
|
+
* a `.map()` are not expressible. Any further named slot **with content** is
|
|
109
|
+
* reported as `multiple-slots-unsupported` — never silently dropped.
|
|
110
|
+
*
|
|
111
|
+
* Declaring several named slots is legal; it is a capability declaration, not a
|
|
112
|
+
* promise that all of them can be filled at once. A component that needs more
|
|
113
|
+
* than one filled can transfer Astro's own slots inside itself
|
|
114
|
+
* (`<slot name="x" slot="x" />`) — a component-level decision, deliberately not
|
|
115
|
+
* part of this contract.
|
|
116
|
+
*/
|
|
117
|
+
export interface SlotDefinition {
|
|
118
|
+
/** `"default"` or a lowercase kebab-case name. */
|
|
119
|
+
name: string;
|
|
120
|
+
/** False = at most one child instance. Defaults to true. */
|
|
121
|
+
multiple?: boolean;
|
|
122
|
+
/**
|
|
123
|
+
* The component types this slot accepts (catalog keys). Absent = any type.
|
|
124
|
+
* Assembly rejects a type that is not in the catalog; the reader drops a
|
|
125
|
+
* child of any other type with a `disallowed-child` issue. Narrowing a list
|
|
126
|
+
* after documents are saved hides their children, so treat it like removing
|
|
127
|
+
* a slot.
|
|
128
|
+
*/
|
|
129
|
+
allow?: readonly string[];
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* What a component permits.
|
|
133
|
+
*
|
|
134
|
+
* V1 carries one flag, and it is **advisory**: `insertable` is metadata for an
|
|
135
|
+
* editor/CLI's insertion palette. Neither `parsePageData` nor `resolveInstance`
|
|
136
|
+
* enforces it — a saved document containing a non-insertable component still
|
|
137
|
+
* renders, and the renderer reports only what the document tells it. The render
|
|
138
|
+
* path never second-guesses a document that was already saved. Enforcement, when
|
|
139
|
+
* it exists, belongs at *insertion* time in the writer, not at render.
|
|
140
|
+
*/
|
|
141
|
+
export interface ComponentCapabilities {
|
|
142
|
+
insertable?: boolean;
|
|
143
|
+
}
|
|
144
|
+
/** The request facts available while rendering (formalises `Astro.locals.store` + URL). */
|
|
145
|
+
export interface ThemeRenderContext {
|
|
146
|
+
store: StoreConfig;
|
|
147
|
+
/**
|
|
148
|
+
* The theme's resolved content pack: an opaque, flat string map with store
|
|
149
|
+
* overrides already merged. The SDK reads it (through a field's `contentKey`)
|
|
150
|
+
* and never shapes it — the taxonomy belongs to the theme, and the theme owns
|
|
151
|
+
* how its components consume the pack.
|
|
152
|
+
*/
|
|
153
|
+
content: Record<string, string>;
|
|
154
|
+
/**
|
|
155
|
+
* The language being rendered (BCP 47). Translatable field values resolve
|
|
156
|
+
* to it first; `content` is the pack for it.
|
|
157
|
+
*/
|
|
158
|
+
locale: LocaleCode;
|
|
159
|
+
/**
|
|
160
|
+
* The store's fallback chain after `locale`, in order (e.g. the store's
|
|
161
|
+
* primary language, then the platform default). A translatable value with
|
|
162
|
+
* no entry for `locale` tries these before the pack and the default.
|
|
163
|
+
*/
|
|
164
|
+
fallbackLocales?: readonly LocaleCode[];
|
|
165
|
+
isRTL: boolean;
|
|
166
|
+
/** Request pathname. */
|
|
167
|
+
url: string;
|
|
168
|
+
/**
|
|
169
|
+
* The route's dynamic segments (`Astro.params`), e.g. `{ slug: "chair" }` on
|
|
170
|
+
* `/products/[slug]`. A section on a templated surface reads its route
|
|
171
|
+
* resource through these, with a `reads` key that includes them, instead of
|
|
172
|
+
* parsing `url`.
|
|
173
|
+
*/
|
|
174
|
+
params?: Readonly<Record<string, string | undefined>>;
|
|
175
|
+
}
|
|
176
|
+
/** Per-render read sharing; the renderer provides the implementation. */
|
|
177
|
+
export interface ReadCache {
|
|
178
|
+
get<T>(key: string, load: () => T | Promise<T>): Promise<T>;
|
|
179
|
+
}
|
|
180
|
+
/** What a `load` hook receives. Server-only; reads go through the storefront SDK. */
|
|
181
|
+
export interface LoadContext {
|
|
182
|
+
storefront: StoreClient;
|
|
183
|
+
context: ThemeRenderContext;
|
|
184
|
+
/**
|
|
185
|
+
* The resolved field values for this instance.
|
|
186
|
+
*
|
|
187
|
+
* The renderer merges `load`'s return value **over** these
|
|
188
|
+
* (`{ ...props, ...loaded }`), so a load-returned key wins over a field of
|
|
189
|
+
* the same name. A definition whose component reads a prop from `load` should
|
|
190
|
+
* keep that name out of `fields`.
|
|
191
|
+
*/
|
|
192
|
+
props: Record<string, unknown>;
|
|
193
|
+
reads: ReadCache;
|
|
194
|
+
}
|
|
195
|
+
/** Server-side data fetch for one component definition. */
|
|
196
|
+
export type LoadFn = (context: LoadContext) => Promise<Record<string, unknown>>;
|
|
197
|
+
/**
|
|
198
|
+
* Code-owned description of one reusable component. `component` is the real
|
|
199
|
+
* component reference — opaque here so the core stays framework-neutral; the
|
|
200
|
+
* `./astro` renderer narrows it.
|
|
201
|
+
*/
|
|
202
|
+
export interface ComponentDefinition {
|
|
203
|
+
/** Stable type identity: lowercase kebab-case, unique in the catalog. */
|
|
204
|
+
type: string;
|
|
205
|
+
component: unknown;
|
|
206
|
+
label: LocalizedText;
|
|
207
|
+
/** The editor palette heading this component is listed under. */
|
|
208
|
+
group?: LocalizedText;
|
|
209
|
+
fields?: readonly FieldDefinition[];
|
|
210
|
+
slots?: readonly SlotDefinition[];
|
|
211
|
+
load?: LoadFn;
|
|
212
|
+
capabilities?: ComponentCapabilities;
|
|
213
|
+
}
|
|
214
|
+
/** The value type one field resolves to. A select narrows to its option literals. */
|
|
215
|
+
export type FieldValue<F extends FieldDefinition> = F extends {
|
|
216
|
+
kind: "number";
|
|
217
|
+
} ? number : F extends {
|
|
218
|
+
kind: "boolean";
|
|
219
|
+
} ? boolean : F extends {
|
|
220
|
+
kind: "select";
|
|
221
|
+
options: readonly (infer Option)[];
|
|
222
|
+
} ? Option : string;
|
|
223
|
+
type FieldsOf<D> = D extends {
|
|
224
|
+
fields: readonly (infer F extends FieldDefinition)[];
|
|
225
|
+
} ? F : never;
|
|
226
|
+
type FieldProps<D> = {
|
|
227
|
+
[F in FieldsOf<D> as F extends {
|
|
228
|
+
default: unknown;
|
|
229
|
+
} ? F["key"] : never]: FieldValue<F>;
|
|
230
|
+
} & {
|
|
231
|
+
[F in FieldsOf<D> as F extends {
|
|
232
|
+
default: unknown;
|
|
233
|
+
} ? never : F["key"]]?: FieldValue<F>;
|
|
234
|
+
};
|
|
235
|
+
type LoadedProps<D> = D extends {
|
|
236
|
+
load: (context: LoadContext) => Promise<infer L>;
|
|
237
|
+
} ? L : {};
|
|
238
|
+
/**
|
|
239
|
+
* The props `InstanceView` passes to a definition's component, derived from
|
|
240
|
+
* the definition so the component and its fields cannot drift apart:
|
|
241
|
+
*
|
|
242
|
+
* ```ts
|
|
243
|
+
* type Props = InferProps<typeof heroDefinition>;
|
|
244
|
+
* ```
|
|
245
|
+
*
|
|
246
|
+
* A field with a `default` is always present; any other field may be absent
|
|
247
|
+
* (no value, no content, or reported as invalid). Whatever `load` returns is
|
|
248
|
+
* merged over the fields, as the renderer does at runtime. Use `defineComponent`
|
|
249
|
+
* so field keys, kinds and select options keep their literal types.
|
|
250
|
+
*/
|
|
251
|
+
export type InferProps<D extends ComponentDefinition> = Omit<FieldProps<D>, keyof LoadedProps<D>> & LoadedProps<D>;
|
|
252
|
+
/** The catalog a theme ships — the unit the renderer and validator consume. */
|
|
253
|
+
export interface ThemeDefinition {
|
|
254
|
+
key: string;
|
|
255
|
+
version: string;
|
|
256
|
+
/**
|
|
257
|
+
* The surfaces this theme can render, declared as data. Optional and
|
|
258
|
+
* additive: a theme that declares none is treated as declaring none, and no
|
|
259
|
+
* existing page-document behavior changes — `parsePageData` /
|
|
260
|
+
* `validatePageData` never consult this list. Surface *enforcement* (rejecting
|
|
261
|
+
* a document whose surface the theme does not serve) is a later, separately
|
|
262
|
+
* tested concern; the manifest carries the declaration now so an editor can
|
|
263
|
+
* offer only valid surfaces.
|
|
264
|
+
*/
|
|
265
|
+
surfaces?: readonly SurfaceSpec[];
|
|
266
|
+
/**
|
|
267
|
+
* The languages this theme's content packs cover (BCP 47), e.g.
|
|
268
|
+
* `["ar", "fr", "en"]`. Declared, the validator requires every translated
|
|
269
|
+
* label to cover all of them, and the manifest tells an editor which
|
|
270
|
+
* languages the theme can serve. Absent means "not declared".
|
|
271
|
+
*/
|
|
272
|
+
locales?: readonly LocaleCode[];
|
|
273
|
+
components: Record<string, ComponentDefinition>;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* One surface a theme declares it can render.
|
|
277
|
+
*
|
|
278
|
+
* `key` uses the same surface-key pattern as the page-document envelope
|
|
279
|
+
* (`SURFACE_KEY_PATTERN` in `schemas.ts`); `label` is the editor-facing name.
|
|
280
|
+
*/
|
|
281
|
+
export interface SurfaceSpec {
|
|
282
|
+
key: string;
|
|
283
|
+
label: LocalizedText;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* One component as data: a `ComponentDefinition` minus its runtime-only
|
|
287
|
+
* implementation. `component` (the live component reference) and `load` (the
|
|
288
|
+
* server-only data hook) are intentionally absent — neither is
|
|
289
|
+
* JSON-serializable and neither belongs in a tooling contract.
|
|
290
|
+
*
|
|
291
|
+
* `fields` / `slots` are always present (normalized from absent to `[]` by the
|
|
292
|
+
* generator) so consumers never branch on "missing vs empty".
|
|
293
|
+
*/
|
|
294
|
+
export interface ComponentSpec {
|
|
295
|
+
/** Stable type identity: lowercase kebab-case, equal to the catalog key. */
|
|
296
|
+
type: string;
|
|
297
|
+
/** Editor-facing name. */
|
|
298
|
+
label: LocalizedText;
|
|
299
|
+
/** The editor palette heading this component is listed under. */
|
|
300
|
+
group?: LocalizedText;
|
|
301
|
+
fields: readonly FieldDefinition[];
|
|
302
|
+
slots: readonly SlotDefinition[];
|
|
303
|
+
capabilities?: ComponentCapabilities;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* The serializable theme contract.
|
|
307
|
+
*
|
|
308
|
+
* - `contractVersion` identifies this manifest's schema and interpretation
|
|
309
|
+
* contract (currently `1`). It versions the *format*, not the theme.
|
|
310
|
+
* - `key` / `version` identify the *theme* (same values as `ThemeDefinition`).
|
|
311
|
+
* - `surfaces` declares what the theme can render (possibly empty).
|
|
312
|
+
* - `components` is the data-only catalogue, keyed by component type.
|
|
313
|
+
*
|
|
314
|
+
* Three versioned contracts coexist and must not be confused:
|
|
315
|
+
* `contractVersion` (this manifest), `ThemeDefinition.version` (the theme's
|
|
316
|
+
* semver), and `PageData.version` (the page-document envelope, currently `1`).
|
|
317
|
+
*/
|
|
318
|
+
export interface ThemeManifest {
|
|
319
|
+
contractVersion: 1;
|
|
320
|
+
key: string;
|
|
321
|
+
version: string;
|
|
322
|
+
surfaces: readonly SurfaceSpec[];
|
|
323
|
+
/** The languages the theme's packs cover (possibly none declared). */
|
|
324
|
+
locales: readonly LocaleCode[];
|
|
325
|
+
components: Record<string, ComponentSpec>;
|
|
326
|
+
}
|
|
327
|
+
export type ThemeManifestIssueCode = "invalid-manifest" | "unsupported-contract" | "invalid-identity" | "invalid-version" | "invalid-surface" | "invalid-component" | "type-mismatch" | "non-serializable";
|
|
328
|
+
/** One reported manifest problem; `path` points into the manifest (e.g. `components.hero.fields[0].kind`). */
|
|
329
|
+
export interface ThemeManifestIssue {
|
|
330
|
+
path: string;
|
|
331
|
+
code: ThemeManifestIssueCode;
|
|
332
|
+
message: string;
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Result of the manifest boundary. `manifest` is non-null only when the input
|
|
336
|
+
* is structurally sound: structural failures (wrong shape, unsupported
|
|
337
|
+
* contract, malformed components, non-serializable values) yield `null` plus
|
|
338
|
+
* issues. The one advisory check — a non-semver `version` — returns the
|
|
339
|
+
* manifest *with* an `invalid-version` issue, so a version-format concern is
|
|
340
|
+
* never confused with a fatal structural failure.
|
|
341
|
+
*/
|
|
342
|
+
export interface ThemeManifestResult {
|
|
343
|
+
manifest: ThemeManifest | null;
|
|
344
|
+
issues: ThemeManifestIssue[];
|
|
345
|
+
}
|
|
346
|
+
/** One occurrence of a component on one page (JSON only). */
|
|
347
|
+
export interface ComponentInstance {
|
|
348
|
+
id: string;
|
|
349
|
+
type: string;
|
|
350
|
+
props?: JsonObject;
|
|
351
|
+
children?: ComponentInstance[];
|
|
352
|
+
slots?: Record<string, ComponentInstance[]>;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* One composed surface's persisted document.
|
|
356
|
+
*
|
|
357
|
+
* At runtime the envelope is **forward-compatible**: `parsePageData` and
|
|
358
|
+
* `PageDataSchema` carry unknown *top-level* keys through verbatim, so a later
|
|
359
|
+
* writer can record metadata (catalog provenance is the intended first use)
|
|
360
|
+
* without a `PageData.version` bump. Those keys are never read by the renderer,
|
|
361
|
+
* which only walks `sections`, and they are deliberately **not declared here**:
|
|
362
|
+
* an index signature would turn a `page.sectons` typo into `unknown` instead of
|
|
363
|
+
* a compile error. The field is added when a real reader exists.
|
|
364
|
+
*
|
|
365
|
+
* Unknown keys *inside* an instance are still stripped — the envelope is
|
|
366
|
+
* extensible, the instance is not.
|
|
367
|
+
*/
|
|
368
|
+
export interface PageData {
|
|
369
|
+
version: 1;
|
|
370
|
+
surface: string;
|
|
371
|
+
sections: ComponentInstance[];
|
|
372
|
+
}
|
|
373
|
+
export type PageDataIssueCode = "envelope-invalid" | "instance-dropped" | "duplicate-id" | "unknown-type" | "unknown-prop" | "invalid-prop" | "missing-required-prop" | "undeclared-children" | "unknown-slot" | "slot-not-multiple" | "disallowed-child";
|
|
374
|
+
/** One reported degradation; `path` points into the document (e.g. `sections[0].props.limit`). */
|
|
375
|
+
export interface PageDataIssue {
|
|
376
|
+
path: string;
|
|
377
|
+
code: PageDataIssueCode;
|
|
378
|
+
message: string;
|
|
379
|
+
}
|
|
380
|
+
/** Result of a read boundary: `page` is null only when the envelope itself failed. */
|
|
381
|
+
export interface PageDataResult {
|
|
382
|
+
page: PageData | null;
|
|
383
|
+
issues: PageDataIssue[];
|
|
384
|
+
}
|
|
385
|
+
/** What the renderer passes to the real component, after gating. */
|
|
386
|
+
export interface ResolvedInstance {
|
|
387
|
+
props: Record<string, unknown>;
|
|
388
|
+
children: ComponentInstance[];
|
|
389
|
+
slots: Record<string, ComponentInstance[]>;
|
|
390
|
+
issues: PageDataIssue[];
|
|
391
|
+
}
|
|
392
|
+
export type ThemeDefinitionIssueCode = "invalid-theme" | "invalid-definition" | "invalid-type" | "type-mismatch" | "invalid-field" | "duplicate-field-key" | "unknown-content-key" | "invalid-slot" | "duplicate-slot" | "invalid-capability" | "invalid-load";
|
|
393
|
+
export interface ThemeDefinitionIssue {
|
|
394
|
+
path: string;
|
|
395
|
+
code: ThemeDefinitionIssueCode;
|
|
396
|
+
message: string;
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Optional inputs to `validateThemeDefinition`. Every field is additive: with
|
|
400
|
+
* no options, the validator behaves exactly as it always has.
|
|
401
|
+
*
|
|
402
|
+
* `contentKeys` is the one cross-boundary check the Theme SDK can make and a
|
|
403
|
+
* theme cannot — it defines `contentKey` *and* reads the resolved content
|
|
404
|
+
* record, so a typo is invisible everywhere else. The SDK deliberately does not
|
|
405
|
+
* model content packs; it only compares a field's `contentKey` against the key
|
|
406
|
+
* set the theme supplies. Passing an **empty** array is meaningful: it enables
|
|
407
|
+
* the check and reports every declared key.
|
|
408
|
+
*/
|
|
409
|
+
export interface ThemeValidationOptions {
|
|
410
|
+
contentKeys?: readonly string[];
|
|
411
|
+
}
|
|
412
|
+
export {};
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,82 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wocart/theme",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "The Wocart theme contract: code-owned component definitions, JSON page documents, validation and resolution, and a reference Astro renderer.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"wocart",
|
|
7
|
+
"theme",
|
|
8
|
+
"storefront",
|
|
9
|
+
"ecommerce",
|
|
10
|
+
"page-builder",
|
|
11
|
+
"astro",
|
|
12
|
+
"astro-component",
|
|
13
|
+
"withastro",
|
|
14
|
+
"typescript"
|
|
15
|
+
],
|
|
16
|
+
"license": "Apache-2.0",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/bighadj22/codflow.git",
|
|
20
|
+
"directory": "wocart-sdk/theme"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://github.com/bighadj22/codflow/tree/main/wocart-sdk/theme#readme",
|
|
23
|
+
"bugs": "https://github.com/bighadj22/codflow/issues",
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"type": "module",
|
|
28
|
+
"sideEffects": false,
|
|
29
|
+
"main": "./dist/index.js",
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"types": "./dist/index.d.ts",
|
|
34
|
+
"default": "./dist/index.js"
|
|
35
|
+
},
|
|
36
|
+
"./astro": {
|
|
37
|
+
"types": "./dist/astro/index.d.ts",
|
|
38
|
+
"default": "./dist/astro/index.js"
|
|
39
|
+
},
|
|
40
|
+
"./astro/InstanceView.astro": "./dist/astro/InstanceView.astro",
|
|
41
|
+
"./package.json": "./package.json"
|
|
42
|
+
},
|
|
43
|
+
"files": [
|
|
44
|
+
"dist",
|
|
45
|
+
"README.md",
|
|
46
|
+
"CHANGELOG.md",
|
|
47
|
+
"LICENSE",
|
|
48
|
+
"NOTICE"
|
|
49
|
+
],
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "node --eval \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc && node scripts/copy-astro.mjs",
|
|
52
|
+
"typecheck": "tsc --noEmit && tsc --project tsconfig.test.json && astro check",
|
|
53
|
+
"test": "npm run build && vitest run",
|
|
54
|
+
"test:watch": "vitest",
|
|
55
|
+
"verify:package": "npm run build && node scripts/check-package.mjs && node scripts/verify-package.mjs",
|
|
56
|
+
"prepack": "npm run build",
|
|
57
|
+
"prepublishOnly": "node scripts/check-package.mjs"
|
|
58
|
+
},
|
|
59
|
+
"engines": {
|
|
60
|
+
"node": ">=22.12.0"
|
|
61
|
+
},
|
|
62
|
+
"dependencies": {
|
|
63
|
+
"zod": "^4.4.3"
|
|
64
|
+
},
|
|
65
|
+
"peerDependencies": {
|
|
66
|
+
"@wocart/storefront": "^1.0.0",
|
|
67
|
+
"astro": "^7.0.0"
|
|
68
|
+
},
|
|
69
|
+
"peerDependenciesMeta": {
|
|
70
|
+
"astro": {
|
|
71
|
+
"optional": true
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
"devDependencies": {
|
|
75
|
+
"@wocart/storefront": "^1.0.0",
|
|
76
|
+
"@types/node": "^26.2.0",
|
|
77
|
+
"astro": "^7.3.2",
|
|
78
|
+
"fast-check": "^4.6.0",
|
|
79
|
+
"typescript": "^7.0.2",
|
|
80
|
+
"vitest": "^5.0.0"
|
|
81
|
+
}
|
|
82
|
+
}
|