@wocart/theme 0.0.0-stage → 1.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 +71 -0
- package/LICENSE +202 -0
- package/NOTICE +2 -0
- package/README.md +479 -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 +422 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +18 -0
- package/dist/instances.d.ts +43 -0
- package/dist/instances.js +307 -0
- package/dist/locales.d.ts +12 -0
- package/dist/locales.js +14 -0
- package/dist/manifest.d.ts +178 -0
- package/dist/manifest.js +325 -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 +57 -0
- package/dist/schemas.js +108 -0
- package/dist/settings.d.ts +15 -0
- package/dist/settings.js +80 -0
- package/dist/types.d.ts +479 -0
- package/dist/types.js +1 -0
- package/package.json +80 -4
|
@@ -0,0 +1,57 @@
|
|
|
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
|
+
import type { ComponentInstance, JsonValue } from "./types.js";
|
|
7
|
+
/**
|
|
8
|
+
* A theme's key: lowercase kebab-case, at most 64 characters. The platform
|
|
9
|
+
* stores a merchant's text edits under it, so it is the theme's identity on the
|
|
10
|
+
* platform, and the server applies the same rule.
|
|
11
|
+
*/
|
|
12
|
+
export declare const THEME_KEY_PATTERN: RegExp;
|
|
13
|
+
/** Type keys are lowercase kebab-case. */
|
|
14
|
+
export declare const TYPE_KEY_PATTERN: RegExp;
|
|
15
|
+
/**
|
|
16
|
+
* The page-document **surface** key — the address of a composed page
|
|
17
|
+
* (`home` today; `page:<slug>` / `lp:<slug>` are reserved conventions).
|
|
18
|
+
*
|
|
19
|
+
* The server applies the same pattern. Module-level on purpose: it is part of
|
|
20
|
+
* the persisted-data contract,
|
|
21
|
+
* not of the package's curated public surface (`src/index.ts`).
|
|
22
|
+
*/
|
|
23
|
+
export declare const SURFACE_KEY_PATTERN: RegExp;
|
|
24
|
+
/** A `color` field's value: a hex color, `#rgb` or `#rrggbb`. */
|
|
25
|
+
export declare const COLOR_PATTERN: RegExp;
|
|
26
|
+
/**
|
|
27
|
+
* A `menu` field's value: the handle of one of the store's menus
|
|
28
|
+
* (`main-menu`, `footer`). The server applies the same pattern to menu
|
|
29
|
+
* handles; a menu that does not exist reads as no menu.
|
|
30
|
+
*/
|
|
31
|
+
export declare const MENU_HANDLE_PATTERN: RegExp;
|
|
32
|
+
export declare function isLocaleTag(value: string): boolean;
|
|
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 declare function isLocaleMap(value: unknown): value is Readonly<Record<string, string>>;
|
|
38
|
+
/**
|
|
39
|
+
* Whether a `url` field value is safe to render as an `href`: a site path,
|
|
40
|
+
* fragment or query, or an `http(s)`, `mailto` or `tel` URL. Rejects
|
|
41
|
+
* `javascript:`/`data:` and every other scheme, protocol-relative `//host`,
|
|
42
|
+
* backslashes (browsers read `/\host` as `//host`), whitespace and control
|
|
43
|
+
* characters, and anything over 2048 characters.
|
|
44
|
+
*/
|
|
45
|
+
export declare function isSafeLinkTarget(value: string): boolean;
|
|
46
|
+
export declare const JsonValueSchema: z.ZodType<JsonValue>;
|
|
47
|
+
export declare const ComponentInstanceSchema: z.ZodType<ComponentInstance>;
|
|
48
|
+
/**
|
|
49
|
+
* The envelope schema. **Loose on purpose**: unknown top-level keys
|
|
50
|
+
* survive `.parse`, matching `parsePageData` — a writer that records metadata
|
|
51
|
+
* must not have it stripped by whichever reader runs first.
|
|
52
|
+
*/
|
|
53
|
+
export declare const PageDataSchema: z.ZodObject<{
|
|
54
|
+
version: z.ZodLiteral<1>;
|
|
55
|
+
surface: z.ZodString;
|
|
56
|
+
sections: z.ZodArray<z.ZodType<ComponentInstance, unknown, z.core.$ZodTypeInternals<ComponentInstance, unknown>>>;
|
|
57
|
+
}, z.core.$loose>;
|
package/dist/schemas.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
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
|
+
/** A `color` field's value: a hex color, `#rgb` or `#rrggbb`. */
|
|
24
|
+
export const COLOR_PATTERN = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
|
|
25
|
+
/**
|
|
26
|
+
* A `menu` field's value: the handle of one of the store's menus
|
|
27
|
+
* (`main-menu`, `footer`). The server applies the same pattern to menu
|
|
28
|
+
* handles; a menu that does not exist reads as no menu.
|
|
29
|
+
*/
|
|
30
|
+
export const MENU_HANDLE_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
31
|
+
/**
|
|
32
|
+
* A BCP 47 language tag in canonical case: a 2–3 letter language, an optional
|
|
33
|
+
* 4-letter script, an optional 2-letter or 3-digit region (`ar`, `zgh`,
|
|
34
|
+
* `ar-DZ`, `zh-Hant-TW`, `es-419`). Canonical case keeps map keys unique:
|
|
35
|
+
* `fr` and `FR` can never both be present.
|
|
36
|
+
*/
|
|
37
|
+
const LOCALE_TAG_PATTERN = /^[a-z]{2,3}(?:-[A-Z][a-z]{3})?(?:-(?:[A-Z]{2}|\d{3}))?$/;
|
|
38
|
+
export function isLocaleTag(value) {
|
|
39
|
+
return LOCALE_TAG_PATTERN.test(value);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Whether `value` is a per-language map (`{ ar: "…", fr: "…" }`): a plain
|
|
43
|
+
* object with at least one entry, every key a locale tag, every value a string.
|
|
44
|
+
*/
|
|
45
|
+
export function isLocaleMap(value) {
|
|
46
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
47
|
+
return false;
|
|
48
|
+
const entries = Object.entries(value);
|
|
49
|
+
return (entries.length > 0 &&
|
|
50
|
+
entries.every(([key, text]) => isLocaleTag(key) && typeof text === "string"));
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Whether a `url` field value is safe to render as an `href`: a site path,
|
|
54
|
+
* fragment or query, or an `http(s)`, `mailto` or `tel` URL. Rejects
|
|
55
|
+
* `javascript:`/`data:` and every other scheme, protocol-relative `//host`,
|
|
56
|
+
* backslashes (browsers read `/\host` as `//host`), whitespace and control
|
|
57
|
+
* characters, and anything over 2048 characters.
|
|
58
|
+
*/
|
|
59
|
+
export function isSafeLinkTarget(value) {
|
|
60
|
+
if (value.length === 0 || value.length > 2048)
|
|
61
|
+
return false;
|
|
62
|
+
if (/[\s\\\u0000-\u001f\u007f]/.test(value))
|
|
63
|
+
return false;
|
|
64
|
+
if (value.startsWith("/"))
|
|
65
|
+
return !value.startsWith("//");
|
|
66
|
+
if (value.startsWith("#") || value.startsWith("?"))
|
|
67
|
+
return true;
|
|
68
|
+
const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(value)?.[1]?.toLowerCase();
|
|
69
|
+
if (scheme === "http" || scheme === "https") {
|
|
70
|
+
return /^https?:\/\/[a-z0-9.-]+(:\d{1,5})?([/?#]|$)/i.test(value);
|
|
71
|
+
}
|
|
72
|
+
if (scheme === "mailto" || scheme === "tel")
|
|
73
|
+
return value.length > scheme.length + 1;
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
export const JsonValueSchema = z.lazy(() => z.union([
|
|
77
|
+
z.string(),
|
|
78
|
+
z
|
|
79
|
+
.number()
|
|
80
|
+
.refine(Number.isFinite, { message: "numbers must be finite" })
|
|
81
|
+
// JSON has no -0: it serializes as 0, so keeping it would break round-trips.
|
|
82
|
+
.transform((value) => (Object.is(value, -0) ? 0 : value)),
|
|
83
|
+
z.boolean(),
|
|
84
|
+
z.null(),
|
|
85
|
+
z.array(JsonValueSchema),
|
|
86
|
+
z.record(z.string(), JsonValueSchema),
|
|
87
|
+
]));
|
|
88
|
+
export const ComponentInstanceSchema = z.lazy(() => z.object({
|
|
89
|
+
id: z.string().min(1),
|
|
90
|
+
type: z
|
|
91
|
+
.string()
|
|
92
|
+
.regex(TYPE_KEY_PATTERN, "type keys are lowercase kebab-case"),
|
|
93
|
+
props: z.exactOptional(z.record(z.string(), JsonValueSchema)),
|
|
94
|
+
children: z.exactOptional(z.array(ComponentInstanceSchema)),
|
|
95
|
+
slots: z.exactOptional(z.record(z.string(), z.array(ComponentInstanceSchema))),
|
|
96
|
+
}));
|
|
97
|
+
/**
|
|
98
|
+
* The envelope schema. **Loose on purpose**: unknown top-level keys
|
|
99
|
+
* survive `.parse`, matching `parsePageData` — a writer that records metadata
|
|
100
|
+
* must not have it stripped by whichever reader runs first.
|
|
101
|
+
*/
|
|
102
|
+
export const PageDataSchema = z.looseObject({
|
|
103
|
+
version: z.literal(1),
|
|
104
|
+
surface: z
|
|
105
|
+
.string()
|
|
106
|
+
.regex(SURFACE_KEY_PATTERN, "surface keys are lowercase, start alphanumeric, and use kebab/dot/colon/underscore"),
|
|
107
|
+
sections: z.array(ComponentInstanceSchema),
|
|
108
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ResolutionScope, ThemeDefinition, ThemeSettingsIssue, ThemeSettingsResult } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* The effective settings for rendering: every declared setting, from the
|
|
4
|
+
* store's value when the declaration accepts it, else from the theme's
|
|
5
|
+
* default. `stored` is the persisted object as it arrived (anything else —
|
|
6
|
+
* null, an array — reads as no values and is reported). Translated text uses
|
|
7
|
+
* `scope.locales`; text settings may default from `scope.content`.
|
|
8
|
+
*/
|
|
9
|
+
export declare function resolveThemeSettings(theme: ThemeDefinition, stored: unknown, scope?: ResolutionScope): ThemeSettingsResult;
|
|
10
|
+
/**
|
|
11
|
+
* What a writer must check before saving a store's settings for a theme:
|
|
12
|
+
* every key declared, every value accepted by its setting — in every language
|
|
13
|
+
* for a translated one. An empty list means the values may be saved.
|
|
14
|
+
*/
|
|
15
|
+
export declare function checkThemeSettings(theme: ThemeDefinition, values: unknown): ThemeSettingsIssue[];
|
package/dist/settings.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A store's theme settings: the merchant's values for the settings a theme
|
|
3
|
+
* declares (`ThemeDefinition.settings`), read against that declaration.
|
|
4
|
+
*
|
|
5
|
+
* Values are stored flat by setting key, for one theme (a value is only
|
|
6
|
+
* meaningful against the theme that declared it). Reading never fails: a
|
|
7
|
+
* value the declaration does not accept — a setting since removed, a color
|
|
8
|
+
* that is not a color, an option no longer offered — is reported and the
|
|
9
|
+
* setting falls back to the theme's default, exactly as a page document's
|
|
10
|
+
* field does. Precedence: the merchant's value → the theme's default (or its
|
|
11
|
+
* content pack, for text) → absent.
|
|
12
|
+
*/
|
|
13
|
+
import { checkStoredValue, ownValue, resolveField } from "./instances.js";
|
|
14
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
15
|
+
function declaredSettings(theme) {
|
|
16
|
+
const fields = new Map();
|
|
17
|
+
for (const group of theme.settings ?? [])
|
|
18
|
+
for (const field of group.fields)
|
|
19
|
+
if (!fields.has(field.key))
|
|
20
|
+
fields.set(field.key, field);
|
|
21
|
+
return fields;
|
|
22
|
+
}
|
|
23
|
+
const PAGE_TO_SETTINGS = {
|
|
24
|
+
"invalid-prop": "invalid-setting",
|
|
25
|
+
"missing-required-prop": "missing-required-setting",
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* The effective settings for rendering: every declared setting, from the
|
|
29
|
+
* store's value when the declaration accepts it, else from the theme's
|
|
30
|
+
* default. `stored` is the persisted object as it arrived (anything else —
|
|
31
|
+
* null, an array — reads as no values and is reported). Translated text uses
|
|
32
|
+
* `scope.locales`; text settings may default from `scope.content`.
|
|
33
|
+
*/
|
|
34
|
+
export function resolveThemeSettings(theme, stored, scope = {}) {
|
|
35
|
+
const issues = [];
|
|
36
|
+
const declared = declaredSettings(theme);
|
|
37
|
+
let values = {};
|
|
38
|
+
if (stored !== undefined && stored !== null && !isRecord(stored)) {
|
|
39
|
+
issues.push({ path: "settings", code: "invalid-settings", message: "stored settings must be an object; the theme's defaults apply" });
|
|
40
|
+
}
|
|
41
|
+
else if (isRecord(stored)) {
|
|
42
|
+
values = stored;
|
|
43
|
+
}
|
|
44
|
+
for (const key of Object.keys(values)) {
|
|
45
|
+
if (!declared.has(key))
|
|
46
|
+
issues.push({ path: `settings.${key}`, code: "unknown-setting", message: `"${key}" is not a setting of "${theme.key}"; ignored` });
|
|
47
|
+
}
|
|
48
|
+
const resolved = {};
|
|
49
|
+
const fieldIssues = [];
|
|
50
|
+
for (const [key, field] of declared) {
|
|
51
|
+
const value = resolveField(field, ownValue(values, key), scope.content ?? {}, scope.locales ?? [], fieldIssues, "settings");
|
|
52
|
+
if (value !== undefined)
|
|
53
|
+
resolved[key] = value;
|
|
54
|
+
}
|
|
55
|
+
for (const issue of fieldIssues)
|
|
56
|
+
issues.push({ path: issue.path, code: PAGE_TO_SETTINGS[issue.code] ?? "invalid-setting", message: issue.message });
|
|
57
|
+
return { values: resolved, issues };
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* What a writer must check before saving a store's settings for a theme:
|
|
61
|
+
* every key declared, every value accepted by its setting — in every language
|
|
62
|
+
* for a translated one. An empty list means the values may be saved.
|
|
63
|
+
*/
|
|
64
|
+
export function checkThemeSettings(theme, values) {
|
|
65
|
+
if (!isRecord(values))
|
|
66
|
+
return [{ path: "settings", code: "invalid-settings", message: "settings must be an object" }];
|
|
67
|
+
const declared = declaredSettings(theme);
|
|
68
|
+
const issues = [];
|
|
69
|
+
for (const [key, value] of Object.entries(values)) {
|
|
70
|
+
const field = declared.get(key);
|
|
71
|
+
if (field === undefined) {
|
|
72
|
+
issues.push({ path: `settings.${key}`, code: "unknown-setting", message: `"${key}" is not a setting of "${theme.key}"` });
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const checked = checkStoredValue(field, value);
|
|
76
|
+
if (!checked.ok)
|
|
77
|
+
issues.push({ path: `settings.${key}`, code: "invalid-setting", message: `"${key}" ${checked.message}` });
|
|
78
|
+
}
|
|
79
|
+
return issues;
|
|
80
|
+
}
|