@wocart/theme 1.4.0 → 1.6.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 +71 -0
- package/README.md +299 -17
- package/dist/astro/InstanceView.astro +21 -5
- package/dist/astro/index.d.ts +15 -14
- package/dist/astro/index.js +44 -0
- package/dist/composition.d.ts +17 -0
- package/dist/composition.js +60 -0
- package/dist/definitions.js +305 -6
- package/dist/index.d.ts +18 -5
- package/dist/index.js +12 -4
- package/dist/instances.d.ts +7 -1
- package/dist/instances.js +251 -13
- package/dist/manifest-diff.d.ts +14 -0
- package/dist/manifest-diff.js +160 -0
- package/dist/manifest.d.ts +192 -19
- package/dist/manifest.js +77 -16
- package/dist/operations.d.ts +71 -0
- package/dist/operations.js +377 -0
- package/dist/page-data.js +29 -2
- package/dist/preview-bridge.d.ts +28 -0
- package/dist/preview-bridge.js +475 -0
- package/dist/preview-protocol.d.ts +147 -0
- package/dist/preview-protocol.js +51 -0
- package/dist/rich-text.d.ts +31 -0
- package/dist/rich-text.js +233 -0
- package/dist/schemas.d.ts +21 -0
- package/dist/schemas.js +35 -2
- package/dist/types.d.ts +259 -15
- package/dist/version.d.ts +7 -0
- package/dist/version.js +7 -0
- package/package.json +7 -2
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composition rules a writer applies when it adds a component to a document:
|
|
3
|
+
* where a component may go, and how a preset becomes instances. Pure: no
|
|
4
|
+
* I/O, no randomness — ids come from the caller.
|
|
5
|
+
*/
|
|
6
|
+
import { createInstance } from "./instances.js";
|
|
7
|
+
/** Whether a surface key is `entry` itself or one of its alternates (`product.compact` for `product`). */
|
|
8
|
+
export function surfaceCovers(entry, surface) {
|
|
9
|
+
return surface === entry || surface.startsWith(`${entry}.`);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Whether an editor may insert this component on `surface`: it is not marked
|
|
13
|
+
* `insertable: false`, and its `capabilities.surfaces` (absent = any surface)
|
|
14
|
+
* cover the surface. Accepts a definition or its manifest spec.
|
|
15
|
+
*/
|
|
16
|
+
export function isInsertableOn(component, surface) {
|
|
17
|
+
const capabilities = component.capabilities;
|
|
18
|
+
if (capabilities?.insertable === false)
|
|
19
|
+
return false;
|
|
20
|
+
const surfaces = capabilities?.surfaces;
|
|
21
|
+
return surfaces === undefined || surfaces.some((entry) => surfaceCovers(entry, surface));
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The instances a preset describes: the component with the preset's props
|
|
25
|
+
* over its field defaults (`createInstance`), and every child component the
|
|
26
|
+
* same way, each with an id from `newId`. Throws on what is a programming
|
|
27
|
+
* error — an unknown component or preset, or props its fields refuse — as
|
|
28
|
+
* `createInstance` does; `validateThemeDefinition` reports these at assembly.
|
|
29
|
+
*/
|
|
30
|
+
export function instantiatePreset(theme, type, presetKey, newId) {
|
|
31
|
+
const definition = Object.hasOwn(theme.components, type) ? theme.components[type] : undefined;
|
|
32
|
+
if (definition === undefined)
|
|
33
|
+
throw new TypeError(`instantiatePreset: "${type}" is not in the theme catalog`);
|
|
34
|
+
const preset = (definition.presets ?? []).find((candidate) => candidate.key === presetKey);
|
|
35
|
+
if (preset === undefined)
|
|
36
|
+
throw new TypeError(`instantiatePreset: "${type}" has no preset "${presetKey}"`);
|
|
37
|
+
return build(theme, { ...presetBody(preset), type }, newId);
|
|
38
|
+
}
|
|
39
|
+
function presetBody(preset) {
|
|
40
|
+
const body = {};
|
|
41
|
+
if (preset.props !== undefined)
|
|
42
|
+
body.props = preset.props;
|
|
43
|
+
if (preset.children !== undefined)
|
|
44
|
+
body.children = preset.children;
|
|
45
|
+
if (preset.slots !== undefined)
|
|
46
|
+
body.slots = preset.slots;
|
|
47
|
+
return body;
|
|
48
|
+
}
|
|
49
|
+
function build(theme, node, newId) {
|
|
50
|
+
const definition = Object.hasOwn(theme.components, node.type) ? theme.components[node.type] : undefined;
|
|
51
|
+
if (definition === undefined)
|
|
52
|
+
throw new TypeError(`instantiatePreset: "${node.type}" is not in the theme catalog`);
|
|
53
|
+
const instance = createInstance(definition, newId(), node.props ?? {});
|
|
54
|
+
if (node.children !== undefined)
|
|
55
|
+
instance.children = node.children.map((child) => build(theme, child, newId));
|
|
56
|
+
if (node.slots !== undefined) {
|
|
57
|
+
instance.slots = Object.fromEntries(Object.entries(node.slots).map(([name, list]) => [name, list.map((child) => build(theme, child, newId))]));
|
|
58
|
+
}
|
|
59
|
+
return instance;
|
|
60
|
+
}
|
package/dist/definitions.js
CHANGED
|
@@ -4,7 +4,11 @@
|
|
|
4
4
|
* data — the validator exists so a theme's test suite fails loudly on catalog
|
|
5
5
|
* bugs when the catalog is assembled.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import { instantiatePreset } from "./composition.js";
|
|
8
|
+
import { checkStoredValue, onStepGrid } from "./instances.js";
|
|
9
|
+
import { validatePageData } from "./page-data.js";
|
|
10
|
+
import { RICH_TEXT_FORMATS, isRichTextDocumentShape, richTextProblem } from "./rich-text.js";
|
|
11
|
+
import { COLOR_PATTERN, MENU_HANDLE_PATTERN, COLLECTION_HANDLE_PATTERN, PAGE_TYPES, RESOURCE_HANDLE_PATTERN, SURFACE_KEY_PATTERN, THEME_KEY_PATTERN, TYPE_KEY_PATTERN, isLocaleMap, isLocaleMapOf, isLocaleTag, isSafeLinkTarget, } from "./schemas.js";
|
|
8
12
|
export function defineComponent(definition) {
|
|
9
13
|
return definition;
|
|
10
14
|
}
|
|
@@ -61,7 +65,6 @@ function inspectLocales(locales, issues) {
|
|
|
61
65
|
/** Check the whole catalog: key/type identity, field rules, slot rules. */
|
|
62
66
|
export function validateThemeDefinition(theme, options = {}) {
|
|
63
67
|
const issues = [];
|
|
64
|
-
const contentKeys = options.contentKeys === undefined ? undefined : new Set(options.contentKeys);
|
|
65
68
|
if (!isRecord(theme)) {
|
|
66
69
|
addIssue(issues, "", "invalid-theme", "theme must be an object");
|
|
67
70
|
return issues;
|
|
@@ -77,6 +80,12 @@ export function validateThemeDefinition(theme, options = {}) {
|
|
|
77
80
|
return issues;
|
|
78
81
|
}
|
|
79
82
|
const locales = inspectLocales(theme.locales, issues);
|
|
83
|
+
if (theme.preview !== undefined && typeof theme.preview !== "boolean") {
|
|
84
|
+
addIssue(issues, "preview", "invalid-theme", "`preview` must be a boolean");
|
|
85
|
+
}
|
|
86
|
+
const packKeys = theme.content === undefined ? undefined : inspectContent(theme.content, issues, locales, theme.locales !== undefined);
|
|
87
|
+
// An explicit key set wins; declared packs supply one otherwise.
|
|
88
|
+
const contentKeys = options.contentKeys !== undefined ? new Set(options.contentKeys) : packKeys;
|
|
80
89
|
// Surfaces are optional theme-level metadata for the serializable manifest
|
|
81
90
|
// (`themeManifest`). Absent means "declares none"; present entries are
|
|
82
91
|
// checked so a typo fails loudly at assembly time. Never consulted by page
|
|
@@ -93,6 +102,9 @@ export function validateThemeDefinition(theme, options = {}) {
|
|
|
93
102
|
contentKeys,
|
|
94
103
|
catalogKeys: new Set(Object.keys(theme.components)),
|
|
95
104
|
locales,
|
|
105
|
+
surfaceKeys: new Set((Array.isArray(theme.surfaces) ? theme.surfaces : [])
|
|
106
|
+
.map((surface) => (isRecord(surface) ? surface.key : undefined))
|
|
107
|
+
.filter((key) => typeof key === "string")),
|
|
96
108
|
};
|
|
97
109
|
if (theme.settings !== undefined)
|
|
98
110
|
inspectSettings(theme.settings, issues, rules);
|
|
@@ -108,8 +120,91 @@ export function validateThemeDefinition(theme, options = {}) {
|
|
|
108
120
|
addIssue(issues, `${path}.type`, "type-mismatch", `definition type "${definition.type}" must equal its catalog key "${key}"`);
|
|
109
121
|
}
|
|
110
122
|
}
|
|
123
|
+
// Documents and presets are read through the catalog, so they are checked
|
|
124
|
+
// once the catalog itself is sound.
|
|
125
|
+
if (issues.length === 0)
|
|
126
|
+
inspectComposition(theme, issues);
|
|
111
127
|
return issues;
|
|
112
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* The theme's own packs: one per language, each a map of keys to text.
|
|
131
|
+
* Returns the keys every pack has — what a `contentKey` may name.
|
|
132
|
+
*/
|
|
133
|
+
function inspectContent(content, issues, locales, localesDeclared) {
|
|
134
|
+
if (!isRecord(content)) {
|
|
135
|
+
addIssue(issues, "content", "invalid-content", "`content` must map language tags to content packs");
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
// An empty map declares no packs, as an absent one does (the manifest normalizes to it).
|
|
139
|
+
if (Object.keys(content).length === 0)
|
|
140
|
+
return undefined;
|
|
141
|
+
let common;
|
|
142
|
+
for (const [locale, pack] of Object.entries(content)) {
|
|
143
|
+
const path = `content.${locale}`;
|
|
144
|
+
if (!isLocaleTag(locale)) {
|
|
145
|
+
addIssue(issues, path, "invalid-content", `${JSON.stringify(locale)} is not a BCP 47 language tag in canonical case`);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (localesDeclared && !locales.includes(locale)) {
|
|
149
|
+
addIssue(issues, path, "invalid-content", `"${locale}" is not one of the theme's languages`);
|
|
150
|
+
}
|
|
151
|
+
if (!isRecord(pack)) {
|
|
152
|
+
addIssue(issues, path, "invalid-content", "a content pack must map keys to text");
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
for (const [key, text] of Object.entries(pack)) {
|
|
156
|
+
if (key.length === 0 || key === "__proto__" || typeof text !== "string") {
|
|
157
|
+
addIssue(issues, `${path}.${key}`, "invalid-content", "a content pack maps non-empty keys to strings");
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const keys = new Set(Object.keys(pack));
|
|
161
|
+
common = common === undefined ? keys : new Set([...common].filter((key) => keys.has(key)));
|
|
162
|
+
}
|
|
163
|
+
for (const locale of locales) {
|
|
164
|
+
if (!Object.hasOwn(content, locale)) {
|
|
165
|
+
addIssue(issues, `content.${locale}`, "invalid-content", `the theme declares "${locale}" but ships no content pack for it`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return common;
|
|
169
|
+
}
|
|
170
|
+
/** Default documents and presets, read through the (sound) catalog as a writer would. */
|
|
171
|
+
function inspectComposition(theme, issues) {
|
|
172
|
+
(theme.surfaces ?? []).forEach((surface, index) => {
|
|
173
|
+
if (surface.document === undefined)
|
|
174
|
+
return;
|
|
175
|
+
const path = `surfaces[${index}].document`;
|
|
176
|
+
const checked = validatePageData(theme, surface.document);
|
|
177
|
+
if (checked.page !== null && checked.page.surface !== surface.key) {
|
|
178
|
+
addIssue(issues, `${path}.surface`, "invalid-document", `the default document is for "${checked.page.surface}", not "${surface.key}"`);
|
|
179
|
+
}
|
|
180
|
+
for (const issue of checked.issues) {
|
|
181
|
+
// Content-keyed text is filled from the store's pack at render.
|
|
182
|
+
if (issue.code === "missing-required-prop")
|
|
183
|
+
continue;
|
|
184
|
+
addIssue(issues, issue.path === "" ? path : `${path}.${issue.path}`, "invalid-document", issue.message);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
for (const [type, definition] of Object.entries(theme.components)) {
|
|
188
|
+
(definition.presets ?? []).forEach((preset, index) => {
|
|
189
|
+
const path = `components.${type}.presets[${index}]`;
|
|
190
|
+
let counter = 0;
|
|
191
|
+
let instance;
|
|
192
|
+
try {
|
|
193
|
+
instance = instantiatePreset(theme, type, preset.key, () => `preset-${(counter += 1)}`);
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
addIssue(issues, path, "invalid-preset", error instanceof Error ? error.message : String(error));
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const checked = validatePageData(theme, { version: 1, surface: "preset", sections: [instance] });
|
|
200
|
+
for (const issue of checked.issues) {
|
|
201
|
+
if (issue.code === "missing-required-prop")
|
|
202
|
+
continue;
|
|
203
|
+
addIssue(issues, `${path}.${issue.path.replace(/^sections\[0\]\.?/, "")}`.replace(/\.$/, ""), "invalid-preset", issue.message);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
113
208
|
/**
|
|
114
209
|
* The theme's settings groups: an array of `{ key, label, fields }`, group keys
|
|
115
210
|
* unique and kebab-case, fields checked like a component's, and every setting
|
|
@@ -177,6 +272,22 @@ function inspectSurfaces(surfaces, issues, locales) {
|
|
|
177
272
|
if (labelProblem !== null) {
|
|
178
273
|
addIssue(issues, `${surfacePath}.label`, "invalid-theme", `\`label\` ${labelProblem}`);
|
|
179
274
|
}
|
|
275
|
+
if (surface.pageType !== undefined && !PAGE_TYPES.includes(surface.pageType)) {
|
|
276
|
+
addIssue(issues, `${surfacePath}.pageType`, "invalid-theme", `\`pageType\` must be one of ${PAGE_TYPES.join(", ")}`);
|
|
277
|
+
}
|
|
278
|
+
if (surface.document !== undefined && !isRecord(surface.document)) {
|
|
279
|
+
addIssue(issues, `${surfacePath}.document`, "invalid-document", "`document` must be a page document");
|
|
280
|
+
}
|
|
281
|
+
if (typeof surface.key === "string" && surface.key.includes(".")) {
|
|
282
|
+
const baseKey = surface.key.slice(0, surface.key.indexOf("."));
|
|
283
|
+
const base = surfaces.find((candidate) => isRecord(candidate) && candidate.key === baseKey);
|
|
284
|
+
if (!isRecord(base)) {
|
|
285
|
+
addIssue(issues, `${surfacePath}.key`, "invalid-theme", `"${surface.key}" is an alternate of "${baseKey}", which the theme does not declare`);
|
|
286
|
+
}
|
|
287
|
+
else if (base.pageType !== surface.pageType) {
|
|
288
|
+
addIssue(issues, `${surfacePath}.pageType`, "invalid-theme", `an alternate renders its base's page type (${JSON.stringify(base.pageType ?? null)})`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
180
291
|
});
|
|
181
292
|
}
|
|
182
293
|
function inspectDefinition(definition, path, issues, rules) {
|
|
@@ -217,11 +328,31 @@ function inspectDefinition(definition, path, issues, rules) {
|
|
|
217
328
|
if (!isRecord(definition.capabilities)) {
|
|
218
329
|
addIssue(issues, `${path}.capabilities`, "invalid-capability", "`capabilities` must be an object");
|
|
219
330
|
}
|
|
220
|
-
else
|
|
221
|
-
|
|
222
|
-
|
|
331
|
+
else {
|
|
332
|
+
if (definition.capabilities.insertable !== undefined &&
|
|
333
|
+
typeof definition.capabilities.insertable !== "boolean") {
|
|
334
|
+
addIssue(issues, `${path}.capabilities.insertable`, "invalid-capability", "`insertable` must be a boolean");
|
|
335
|
+
}
|
|
336
|
+
const surfaces = definition.capabilities.surfaces;
|
|
337
|
+
if (surfaces !== undefined) {
|
|
338
|
+
if (!Array.isArray(surfaces) || surfaces.length === 0) {
|
|
339
|
+
addIssue(issues, `${path}.capabilities.surfaces`, "invalid-capability", "`surfaces` must be a non-empty array of surface keys (omit it for any surface)");
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
surfaces.forEach((surface, index) => {
|
|
343
|
+
if (typeof surface !== "string" || !rules.surfaceKeys.has(surface)) {
|
|
344
|
+
addIssue(issues, `${path}.capabilities.surfaces[${index}]`, "invalid-capability", `${JSON.stringify(surface)} is not a surface this theme declares`);
|
|
345
|
+
}
|
|
346
|
+
else if (surfaces.indexOf(surface) !== index) {
|
|
347
|
+
addIssue(issues, `${path}.capabilities.surfaces[${index}]`, "invalid-capability", `duplicate surface "${surface}"`);
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
}
|
|
223
352
|
}
|
|
224
353
|
}
|
|
354
|
+
if (definition.presets !== undefined)
|
|
355
|
+
inspectPresets(definition.presets, path, issues, rules);
|
|
225
356
|
if (definition.fields !== undefined) {
|
|
226
357
|
if (!Array.isArray(definition.fields)) {
|
|
227
358
|
addIssue(issues, `${path}.fields`, "invalid-field", "`fields` must be an array");
|
|
@@ -239,7 +370,7 @@ function inspectDefinition(definition, path, issues, rules) {
|
|
|
239
370
|
}
|
|
240
371
|
}
|
|
241
372
|
}
|
|
242
|
-
function inspectFields(fields, path, issues, rules) {
|
|
373
|
+
function inspectFields(fields, path, issues, rules, { inList = false } = {}) {
|
|
243
374
|
const { contentKeys, locales } = rules;
|
|
244
375
|
const seenKeys = new Set();
|
|
245
376
|
fields.forEach((field, index) => {
|
|
@@ -248,6 +379,17 @@ function inspectFields(fields, path, issues, rules) {
|
|
|
248
379
|
addIssue(issues, fieldPath, "invalid-field", "field must be an object");
|
|
249
380
|
return;
|
|
250
381
|
}
|
|
382
|
+
if (inList) {
|
|
383
|
+
if (field.kind === "list") {
|
|
384
|
+
addIssue(issues, `${fieldPath}.kind`, "invalid-field", "a list item cannot hold another list");
|
|
385
|
+
}
|
|
386
|
+
if (field.key === "id") {
|
|
387
|
+
addIssue(issues, `${fieldPath}.key`, "invalid-field", "`id` cannot be a list item field: it is the item's own identity");
|
|
388
|
+
}
|
|
389
|
+
if (field.contentKey !== undefined) {
|
|
390
|
+
addIssue(issues, `${fieldPath}.contentKey`, "invalid-field", "a list item field cannot default from the content pack: every item would read the same key");
|
|
391
|
+
}
|
|
392
|
+
}
|
|
251
393
|
if (typeof field.key !== "string" || field.key.length === 0) {
|
|
252
394
|
addIssue(issues, `${fieldPath}.key`, "invalid-field", "`key` must be a non-empty string");
|
|
253
395
|
}
|
|
@@ -311,6 +453,9 @@ function inspectFields(fields, path, issues, rules) {
|
|
|
311
453
|
field.min > field.max) {
|
|
312
454
|
addIssue(issues, fieldPath, "invalid-field", "`min` must be <= `max`");
|
|
313
455
|
}
|
|
456
|
+
if (field.step !== undefined && !(typeof field.step === "number" && Number.isFinite(field.step) && field.step > 0)) {
|
|
457
|
+
addIssue(issues, `${fieldPath}.step`, "invalid-field", "`step` must be a finite number greater than 0");
|
|
458
|
+
}
|
|
314
459
|
// A default outside the declared bounds would sail through assembly
|
|
315
460
|
// and then behave incoherently at runtime (`resolveInstance` trusts
|
|
316
461
|
// authored defaults while rejecting the same out-of-range instance
|
|
@@ -323,6 +468,10 @@ function inspectFields(fields, path, issues, rules) {
|
|
|
323
468
|
if (typeof field.max === "number" && field.default > field.max) {
|
|
324
469
|
addIssue(issues, `${fieldPath}.default`, "invalid-field", `\`default\` must be <= ${field.max}`);
|
|
325
470
|
}
|
|
471
|
+
const base = typeof field.min === "number" ? field.min : 0;
|
|
472
|
+
if (typeof field.step === "number" && field.step > 0 && !onStepGrid(field.default, base, field.step)) {
|
|
473
|
+
addIssue(issues, `${fieldPath}.default`, "invalid-field", `\`default\` must be a multiple of ${field.step} from ${base}`);
|
|
474
|
+
}
|
|
326
475
|
}
|
|
327
476
|
break;
|
|
328
477
|
}
|
|
@@ -343,6 +492,22 @@ function inspectFields(fields, path, issues, rules) {
|
|
|
343
492
|
(typeof field.default !== "string" || !options.includes(field.default))) {
|
|
344
493
|
addIssue(issues, `${fieldPath}.default`, "invalid-field", "`default` must be one of `options`");
|
|
345
494
|
}
|
|
495
|
+
if (field.optionLabels !== undefined) {
|
|
496
|
+
if (!isRecord(field.optionLabels)) {
|
|
497
|
+
addIssue(issues, `${fieldPath}.optionLabels`, "invalid-field", "`optionLabels` must map options to labels");
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
for (const [option, label] of Object.entries(field.optionLabels)) {
|
|
501
|
+
if (!Array.isArray(options) || !options.includes(option)) {
|
|
502
|
+
addIssue(issues, `${fieldPath}.optionLabels.${option}`, "invalid-field", `"${option}" is not one of \`options\``);
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
const problem = localizedTextProblem(label, locales, { allowEmpty: false });
|
|
506
|
+
if (problem !== null)
|
|
507
|
+
addIssue(issues, `${fieldPath}.optionLabels.${option}`, "invalid-field", `the label ${problem}`);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
346
511
|
break;
|
|
347
512
|
}
|
|
348
513
|
case "color": {
|
|
@@ -363,6 +528,28 @@ function inspectFields(fields, path, issues, rules) {
|
|
|
363
528
|
}
|
|
364
529
|
break;
|
|
365
530
|
}
|
|
531
|
+
case "product":
|
|
532
|
+
case "category": {
|
|
533
|
+
if (field.default !== undefined && (typeof field.default !== "string" || !RESOURCE_HANDLE_PATTERN.test(field.default))) {
|
|
534
|
+
addIssue(issues, `${fieldPath}.default`, "invalid-field", `\`default\` must be a ${field.kind === "product" ? "product handle" : "category slug"}: no spaces, slashes, \`?\`, \`#\` or \`%\``);
|
|
535
|
+
}
|
|
536
|
+
break;
|
|
537
|
+
}
|
|
538
|
+
case "image": {
|
|
539
|
+
if (field.default !== undefined) {
|
|
540
|
+
addIssue(issues, `${fieldPath}.default`, "invalid-field", "an image field has no default: a theme cannot know a store's media");
|
|
541
|
+
}
|
|
542
|
+
break;
|
|
543
|
+
}
|
|
544
|
+
case "richtext": {
|
|
545
|
+
inspectRichTextField(field, fieldPath, issues, locales);
|
|
546
|
+
break;
|
|
547
|
+
}
|
|
548
|
+
case "list": {
|
|
549
|
+
if (!inList)
|
|
550
|
+
inspectListField(field, fieldPath, issues, rules);
|
|
551
|
+
break;
|
|
552
|
+
}
|
|
366
553
|
case "url": {
|
|
367
554
|
if (field.default !== undefined &&
|
|
368
555
|
(typeof field.default !== "string" || !isSafeLinkTarget(field.default))) {
|
|
@@ -375,6 +562,118 @@ function inspectFields(fields, path, issues, rules) {
|
|
|
375
562
|
}
|
|
376
563
|
});
|
|
377
564
|
}
|
|
565
|
+
/** Preset shapes; what they build is checked once the catalog is sound (`inspectComposition`). */
|
|
566
|
+
function inspectPresets(presets, path, issues, rules) {
|
|
567
|
+
if (!Array.isArray(presets)) {
|
|
568
|
+
addIssue(issues, `${path}.presets`, "invalid-preset", "`presets` must be an array");
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
const keys = new Set();
|
|
572
|
+
presets.forEach((preset, index) => {
|
|
573
|
+
const presetPath = `${path}.presets[${index}]`;
|
|
574
|
+
if (!isRecord(preset)) {
|
|
575
|
+
addIssue(issues, presetPath, "invalid-preset", "a preset must be an object");
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
if (typeof preset.key !== "string" || !TYPE_KEY_PATTERN.test(preset.key)) {
|
|
579
|
+
addIssue(issues, `${presetPath}.key`, "invalid-preset", "`key` must be lowercase kebab-case");
|
|
580
|
+
}
|
|
581
|
+
else if (keys.has(preset.key)) {
|
|
582
|
+
addIssue(issues, `${presetPath}.key`, "invalid-preset", `duplicate preset "${preset.key}"`);
|
|
583
|
+
}
|
|
584
|
+
else {
|
|
585
|
+
keys.add(preset.key);
|
|
586
|
+
}
|
|
587
|
+
const labelProblem = localizedTextProblem(preset.label, rules.locales, { allowEmpty: false });
|
|
588
|
+
if (labelProblem !== null)
|
|
589
|
+
addIssue(issues, `${presetPath}.label`, "invalid-preset", `\`label\` ${labelProblem}`);
|
|
590
|
+
if (preset.description !== undefined) {
|
|
591
|
+
const problem = localizedTextProblem(preset.description, rules.locales, { allowEmpty: false });
|
|
592
|
+
if (problem !== null)
|
|
593
|
+
addIssue(issues, `${presetPath}.description`, "invalid-preset", `\`description\` ${problem}`);
|
|
594
|
+
}
|
|
595
|
+
if (preset.props !== undefined && !isRecord(preset.props)) {
|
|
596
|
+
addIssue(issues, `${presetPath}.props`, "invalid-preset", "`props` must be an object");
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
function inspectRichTextField(field, fieldPath, issues, locales) {
|
|
601
|
+
if (field.contentKey !== undefined) {
|
|
602
|
+
addIssue(issues, `${fieldPath}.contentKey`, "invalid-field", "a rich-text field cannot default from the content pack, which holds plain text");
|
|
603
|
+
}
|
|
604
|
+
let formats;
|
|
605
|
+
if (field.formats !== undefined) {
|
|
606
|
+
const declared = field.formats;
|
|
607
|
+
if (!Array.isArray(declared) || declared.length === 0) {
|
|
608
|
+
addIssue(issues, `${fieldPath}.formats`, "invalid-field", "`formats` must be a non-empty array (omit it to allow every format)");
|
|
609
|
+
}
|
|
610
|
+
else {
|
|
611
|
+
const seen = new Set();
|
|
612
|
+
declared.forEach((format, index) => {
|
|
613
|
+
if (!RICH_TEXT_FORMATS.includes(format)) {
|
|
614
|
+
addIssue(issues, `${fieldPath}.formats[${index}]`, "invalid-field", `unknown rich-text format ${JSON.stringify(format)}; known: ${RICH_TEXT_FORMATS.join(", ")}`);
|
|
615
|
+
}
|
|
616
|
+
else if (seen.has(format)) {
|
|
617
|
+
addIssue(issues, `${fieldPath}.formats[${index}]`, "invalid-field", `duplicate format "${String(format)}"`);
|
|
618
|
+
}
|
|
619
|
+
seen.add(format);
|
|
620
|
+
});
|
|
621
|
+
formats = declared.filter((format) => RICH_TEXT_FORMATS.includes(format));
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
if (field.default === undefined)
|
|
625
|
+
return;
|
|
626
|
+
const value = field.default;
|
|
627
|
+
if (isRichTextDocumentShape(value)) {
|
|
628
|
+
const problem = richTextProblem(value, formats);
|
|
629
|
+
if (problem !== null)
|
|
630
|
+
addIssue(issues, `${fieldPath}.default`, "invalid-field", `\`default\` ${problem}`);
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
if (!isLocaleMapOf(value, isRecord)) {
|
|
634
|
+
addIssue(issues, `${fieldPath}.default`, "invalid-field", "`default` must be a rich-text document or a map from language tags to documents");
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
for (const [locale, document] of Object.entries(value)) {
|
|
638
|
+
const problem = richTextProblem(document, formats);
|
|
639
|
+
if (problem !== null)
|
|
640
|
+
addIssue(issues, `${fieldPath}.default.${locale}`, "invalid-field", `\`default\` (${locale}) ${problem}`);
|
|
641
|
+
}
|
|
642
|
+
const missing = locales.filter((locale) => !Object.hasOwn(value, locale));
|
|
643
|
+
if (missing.length > 0) {
|
|
644
|
+
addIssue(issues, `${fieldPath}.default`, "invalid-field", `\`default\` is missing the theme's languages: ${missing.join(", ")}`);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
function inspectListField(field, fieldPath, issues, rules) {
|
|
648
|
+
const itemFields = field.fields;
|
|
649
|
+
if (!Array.isArray(itemFields) || itemFields.length === 0) {
|
|
650
|
+
addIssue(issues, `${fieldPath}.fields`, "invalid-field", "`fields` must be a non-empty array of item fields");
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
const before = issues.length;
|
|
654
|
+
inspectFields(itemFields, fieldPath, issues, rules, { inList: true });
|
|
655
|
+
const itemFieldsValid = issues.length === before;
|
|
656
|
+
for (const bound of ["min", "max"]) {
|
|
657
|
+
const value = field[bound];
|
|
658
|
+
if (value !== undefined && !(typeof value === "number" && Number.isInteger(value) && value >= (bound === "max" ? 1 : 0))) {
|
|
659
|
+
addIssue(issues, `${fieldPath}.${bound}`, "invalid-field", `\`${bound}\` must be a whole number${bound === "max" ? " of at least 1" : " of at least 0"}`);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
if (typeof field.min === "number" && typeof field.max === "number" && field.min > field.max) {
|
|
663
|
+
addIssue(issues, fieldPath, "invalid-field", "`min` must be <= `max`");
|
|
664
|
+
}
|
|
665
|
+
if (field.itemLabel !== undefined) {
|
|
666
|
+
const labelField = itemFields.find((itemField) => isRecord(itemField) && itemField.key === field.itemLabel);
|
|
667
|
+
if (!isRecord(labelField) || (labelField.kind !== "text" && labelField.kind !== "textarea")) {
|
|
668
|
+
addIssue(issues, `${fieldPath}.itemLabel`, "invalid-field", "`itemLabel` must name one of the item's `text` or `textarea` fields");
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
if (field.default !== undefined && itemFieldsValid) {
|
|
672
|
+
const checked = checkStoredValue(field, field.default);
|
|
673
|
+
if (!checked.ok)
|
|
674
|
+
addIssue(issues, `${fieldPath}.default`, "invalid-field", `\`default\` ${checked.message}`);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
378
677
|
function inspectSlots(slots, path, issues, catalogKeys) {
|
|
379
678
|
const seenNames = new Set();
|
|
380
679
|
slots.forEach((slot, index) => {
|
package/dist/index.d.ts
CHANGED
|
@@ -3,18 +3,31 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Code-owned: `defineComponent` / `defineTheme` / `validateThemeDefinition`.
|
|
5
5
|
* Data-owned: `parsePageData` / `validatePageData` / `resolveInstance` /
|
|
6
|
-
* `createInstance`
|
|
7
|
-
*
|
|
6
|
+
* `createInstance` / `checkFieldValue`, and the document operations a writer
|
|
7
|
+
* applies (`insertInstance`, …, `checkPageData`).
|
|
8
|
+
* Serializable: `themeManifest` / `validateThemeManifest` /
|
|
9
|
+
* `manifestCatalog` / `diffThemeManifests`.
|
|
8
10
|
*
|
|
9
11
|
* This entry is framework-neutral. The reference Astro renderer lives in the
|
|
10
12
|
* `./astro` subpath.
|
|
11
13
|
*/
|
|
12
14
|
export { defineComponent, defineTheme, validateThemeDefinition } from "./definitions.js";
|
|
13
15
|
export { parsePageData, validatePageData } from "./page-data.js";
|
|
14
|
-
export { resolveInstance, createInstance } from "./instances.js";
|
|
16
|
+
export { resolveInstance, createInstance, checkStoredValue as checkFieldValue } from "./instances.js";
|
|
17
|
+
export type { CheckedValue as FieldValueCheck } from "./instances.js";
|
|
18
|
+
export { RICH_TEXT_FORMATS, richTextToHtml, richTextToPlainText } from "./rich-text.js";
|
|
19
|
+
export { instantiatePreset, isInsertableOn } from "./composition.js";
|
|
20
|
+
export { checkPageData, duplicateInstance, findInstance, insertInstance, moveInstance, newInstanceId, removeInstance, setInstanceHidden, setInstanceProps, } from "./operations.js";
|
|
21
|
+
export type { DocumentEdit, DocumentIssue, DocumentIssueCode, InstanceLocation, InstancePlacement, } from "./operations.js";
|
|
22
|
+
export { THEME_SDK_VERSION } from "./version.js";
|
|
23
|
+
export type { RichTextHtmlOptions } from "./rich-text.js";
|
|
15
24
|
export { resolveThemeSettings, checkThemeSettings } from "./settings.js";
|
|
16
25
|
export { localize } from "./locales.js";
|
|
17
26
|
export { PLURAL_CATEGORIES, formatMessage, messageForms, pluralFormIssues } from "./messages.js";
|
|
18
27
|
export type { MessageOptions, MessageValues, PluralCategory } from "./messages.js";
|
|
19
|
-
export { THEME_MANIFEST_CONTRACT_VERSION, themeManifest, validateThemeManifest, } from "./manifest.js";
|
|
20
|
-
export
|
|
28
|
+
export { THEME_MANIFEST_CONTRACT_VERSION, THEME_MANIFEST_PATH, manifestCatalog, themeManifest, validateThemeManifest, } from "./manifest.js";
|
|
29
|
+
export { diffThemeManifests } from "./manifest-diff.js";
|
|
30
|
+
export { PREVIEW_PROTOCOL_VERSION, THEME_PREVIEW_PATH, parsePreviewMarker, previewEndMarker, previewStartMarker, } from "./preview-protocol.js";
|
|
31
|
+
export type { PreviewEditorMessage, PreviewInstance, PreviewMode, PreviewPageMessage, PreviewPatch, PreviewRect, } from "./preview-protocol.js";
|
|
32
|
+
export type { ManifestChange, ManifestDiff } from "./manifest-diff.js";
|
|
33
|
+
export type { JsonValue, JsonObject, LocaleCode, LocalizedText, ResolutionScope, FieldKind, FieldDefinition, FieldValue, InferProps, FieldValues, SlotDefinition, ComponentCapabilities, ComponentPreset, PresetInstance, SurfacePageType, ComponentDefinition, ComponentSpec, SurfaceSpec, ThemeDefinition, SettingsGroup, InferSettings, ThemeSettingsIssue, ThemeSettingsIssueCode, ThemeSettingsResult, NumberFieldDefinition, ColorFieldDefinition, MenuFieldDefinition, CollectionFieldDefinition, ProductFieldDefinition, CategoryFieldDefinition, ImageFieldDefinition, RichTextFieldDefinition, RichTextFormat, ListFieldDefinition, ListItemFieldDefinition, ListItem, MediaReference, ResolvedImage, RichTextValue, RichTextDocument, RichTextBlock, RichTextParagraph, RichTextHeading, RichTextList, RichTextListItem, RichTextInline, RichTextText, RichTextBreak, RichTextMark, ThemeManifest, ThemeManifestIssue, ThemeManifestIssueCode, ThemeManifestResult, ComponentInstance, PageData, ThemeRenderContext, LoadContext, LoadFn, ReadCache, PageDataIssue, PageDataIssueCode, PageDataResult, ResolvedInstance, ThemeDefinitionIssue, ThemeDefinitionIssueCode, ThemeValidationOptions, } from "./types.js";
|
package/dist/index.js
CHANGED
|
@@ -3,16 +3,24 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Code-owned: `defineComponent` / `defineTheme` / `validateThemeDefinition`.
|
|
5
5
|
* Data-owned: `parsePageData` / `validatePageData` / `resolveInstance` /
|
|
6
|
-
* `createInstance`
|
|
7
|
-
*
|
|
6
|
+
* `createInstance` / `checkFieldValue`, and the document operations a writer
|
|
7
|
+
* applies (`insertInstance`, …, `checkPageData`).
|
|
8
|
+
* Serializable: `themeManifest` / `validateThemeManifest` /
|
|
9
|
+
* `manifestCatalog` / `diffThemeManifests`.
|
|
8
10
|
*
|
|
9
11
|
* This entry is framework-neutral. The reference Astro renderer lives in the
|
|
10
12
|
* `./astro` subpath.
|
|
11
13
|
*/
|
|
12
14
|
export { defineComponent, defineTheme, validateThemeDefinition } from "./definitions.js";
|
|
13
15
|
export { parsePageData, validatePageData } from "./page-data.js";
|
|
14
|
-
export { resolveInstance, createInstance } from "./instances.js";
|
|
16
|
+
export { resolveInstance, createInstance, checkStoredValue as checkFieldValue } from "./instances.js";
|
|
17
|
+
export { RICH_TEXT_FORMATS, richTextToHtml, richTextToPlainText } from "./rich-text.js";
|
|
18
|
+
export { instantiatePreset, isInsertableOn } from "./composition.js";
|
|
19
|
+
export { checkPageData, duplicateInstance, findInstance, insertInstance, moveInstance, newInstanceId, removeInstance, setInstanceHidden, setInstanceProps, } from "./operations.js";
|
|
20
|
+
export { THEME_SDK_VERSION } from "./version.js";
|
|
15
21
|
export { resolveThemeSettings, checkThemeSettings } from "./settings.js";
|
|
16
22
|
export { localize } from "./locales.js";
|
|
17
23
|
export { PLURAL_CATEGORIES, formatMessage, messageForms, pluralFormIssues } from "./messages.js";
|
|
18
|
-
export { THEME_MANIFEST_CONTRACT_VERSION, themeManifest, validateThemeManifest, } from "./manifest.js";
|
|
24
|
+
export { THEME_MANIFEST_CONTRACT_VERSION, THEME_MANIFEST_PATH, manifestCatalog, themeManifest, validateThemeManifest, } from "./manifest.js";
|
|
25
|
+
export { diffThemeManifests } from "./manifest-diff.js";
|
|
26
|
+
export { PREVIEW_PROTOCOL_VERSION, THEME_PREVIEW_PATH, parsePreviewMarker, previewEndMarker, previewStartMarker, } from "./preview-protocol.js";
|
package/dist/instances.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ export type CheckedValue = {
|
|
|
12
12
|
ok: false;
|
|
13
13
|
message: string;
|
|
14
14
|
};
|
|
15
|
+
/** Whether a field shows something with no stored value: a default, or content-pack text. */
|
|
16
|
+
export declare function hasFallback(field: FieldDefinition): boolean;
|
|
15
17
|
/** An own property only: `constructor` or `toString` must never read the prototype. */
|
|
16
18
|
export declare function ownValue<T>(record: Record<string, T>, key: string): T | undefined;
|
|
17
19
|
/**
|
|
@@ -23,9 +25,13 @@ export declare function resolveField(field: FieldDefinition, raw: unknown, conte
|
|
|
23
25
|
/**
|
|
24
26
|
* What a writer may store: a value that passes the field's rules, or — for a
|
|
25
27
|
* translatable field — a per-language map whose every entry passes them, so no
|
|
26
|
-
* language can hold an unsafe link or a wrong type.
|
|
28
|
+
* language can hold an unsafe link or a wrong type. Stricter than reading: an
|
|
29
|
+
* image reference or list item with keys the contract does not define is
|
|
30
|
+
* refused, never stripped.
|
|
27
31
|
*/
|
|
28
32
|
export declare function checkStoredValue(field: FieldDefinition, value: unknown): CheckedValue;
|
|
33
|
+
/** On a grid of `step` from `base`, allowing for binary floating point. */
|
|
34
|
+
export declare function onStepGrid(value: number, base: number, step: number): boolean;
|
|
29
35
|
/**
|
|
30
36
|
* Create a new instance with insertion-time defaults:
|
|
31
37
|
* materialises `field.default` values only — content-keyed fields stay unset
|