Refresh now imports keys from the storefront and deletes unused slots plus their files. Co-authored-by: Cursor <cursoragent@cursor.com>
100 lines
3.1 KiB
TypeScript
100 lines
3.1 KiB
TypeScript
import { SPECIAL_GROUP_KEY_PATTERN } from '../common/special-group-key';
|
|
|
|
export const WEBSITE_STATIC_IMAGE_CATALOG_PATH = '/meshkee/static-image-slots';
|
|
|
|
export type WebsiteStaticImageCatalogSlot = {
|
|
key: string;
|
|
label: string;
|
|
kind: 'single' | 'list';
|
|
pageKey: string;
|
|
aspectRatio: string | null;
|
|
recommendedWidth: number | null;
|
|
itemCount: number | null;
|
|
};
|
|
|
|
const ASPECT_RATIO_PATTERN = /^[0-9]{1,3}:[0-9]{1,3}$/;
|
|
|
|
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
return value && typeof value === 'object' && !Array.isArray(value)
|
|
? (value as Record<string, unknown>)
|
|
: null;
|
|
}
|
|
|
|
function readString(value: unknown): string {
|
|
return typeof value === 'string' ? value.trim() : '';
|
|
}
|
|
|
|
function readOptionalInt(value: unknown, min: number, max: number): number | null {
|
|
if (value == null || value === '') return null;
|
|
const parsed = typeof value === 'number' ? value : Number(value);
|
|
if (!Number.isInteger(parsed) || parsed < min || parsed > max) return null;
|
|
return parsed;
|
|
}
|
|
|
|
export function parseWebsiteStaticImageCatalog(
|
|
payload: unknown,
|
|
): WebsiteStaticImageCatalogSlot[] {
|
|
const root = asRecord(payload);
|
|
const rawSlots = Array.isArray(root?.slots) ? root.slots : Array.isArray(payload) ? payload : null;
|
|
if (!rawSlots) {
|
|
throw new Error('Slot catalog must be { slots: [...] }');
|
|
}
|
|
|
|
const slots: WebsiteStaticImageCatalogSlot[] = [];
|
|
const seen = new Set<string>();
|
|
|
|
for (const entry of rawSlots) {
|
|
const row = asRecord(entry);
|
|
if (!row) continue;
|
|
|
|
const key = readString(row.key);
|
|
if (!key || !SPECIAL_GROUP_KEY_PATTERN.test(key) || seen.has(key)) continue;
|
|
|
|
const kind = readString(row.kind) === 'single' ? 'single' : 'list';
|
|
const label = readString(row.label) || key;
|
|
const pageKey = readString(row.pageKey) || 'home';
|
|
if (!SPECIAL_GROUP_KEY_PATTERN.test(pageKey)) continue;
|
|
|
|
const aspectRatioRaw = readString(row.aspectRatio);
|
|
const aspectRatio =
|
|
aspectRatioRaw && ASPECT_RATIO_PATTERN.test(aspectRatioRaw) ? aspectRatioRaw : null;
|
|
|
|
const itemCount =
|
|
kind === 'single' ? 1 : readOptionalInt(row.itemCount, 1, 24);
|
|
|
|
seen.add(key);
|
|
slots.push({
|
|
key,
|
|
label: label.slice(0, 255),
|
|
kind,
|
|
pageKey,
|
|
aspectRatio,
|
|
recommendedWidth: readOptionalInt(row.recommendedWidth, 1, 8000),
|
|
itemCount,
|
|
});
|
|
}
|
|
|
|
if (slots.length === 0) {
|
|
throw new Error('Slot catalog did not contain any valid slots');
|
|
}
|
|
|
|
return slots;
|
|
}
|
|
|
|
export function websiteCatalogUrls(hosts: string[]): string[] {
|
|
const uniqueHosts = [...new Set(hosts.map((host) => host.toLowerCase().trim()).filter(Boolean))];
|
|
const urls: string[] = [];
|
|
|
|
for (const host of uniqueHosts) {
|
|
const bare = host.replace(/^www\./, '');
|
|
const variants = [bare, `www.${bare}`];
|
|
for (const candidate of variants) {
|
|
urls.push(`https://${candidate}${WEBSITE_STATIC_IMAGE_CATALOG_PATH}`);
|
|
urls.push(`http://${candidate}${WEBSITE_STATIC_IMAGE_CATALOG_PATH}`);
|
|
urls.push(`https://${candidate}${WEBSITE_STATIC_IMAGE_CATALOG_PATH}.json`);
|
|
}
|
|
}
|
|
|
|
return [...new Set(urls)];
|
|
}
|