@usequeek/theme-kit 0.1.5 → 0.1.7
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/README.md +55 -5
- package/client.ts +2 -0
- package/components/default-metaobject-page.tsx +40 -0
- package/components/product-metafields.tsx +226 -0
- package/next-reference.d.ts +12 -0
- package/package.json +19 -3
- package/page-renderer.tsx +55 -1
- package/scripts/verify-package.ts +104 -0
- package/sdk/queek-client.ts +31 -2
- package/sdk/server-store-client.ts +13 -0
- package/shared-blocks/core-blocks.css +63 -0
- package/shared-blocks/index.ts +1 -0
- package/shared-blocks/manifest.ts +5 -0
- package/shared-blocks/metaobjects.tsx +50 -0
- package/tests/default-metaobject-page.test.tsx +56 -0
- package/tests/fixtures/external-theme.tsx +69 -0
- package/tests/manifest-parity.test.ts +54 -0
- package/tests/markdown-render.test.ts +47 -0
- package/tests/metaobjects-block.test.tsx +70 -0
- package/tests/product-metafields.test.tsx +154 -0
- package/tests/public-surface.test.ts +38 -0
- package/tests/storefront-client-scope.test.ts +43 -0
- package/types/block.ts +31 -2
- package/types/product.ts +31 -0
- package/types/theme.ts +27 -2
- package/utils/product-normalizer.ts +14 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { request } from '../sdk/client';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The storefront must ONLY ever call the `/v1/client/*` backend scope. That scope's
|
|
8
|
+
* ResolveClientContext middleware resolves the vendor AND forces the platform
|
|
9
|
+
* (storefront/instore_qr) — so prices are always the storefront's, never the
|
|
10
|
+
* marketplace commission-inclusive price. Legacy v1 routes (/products, /vendors/*,
|
|
11
|
+
* /categories, ...) bypass that middleware → wrong platform → wrong price.
|
|
12
|
+
*
|
|
13
|
+
* This guard prevents that whole class of bug from ever returning.
|
|
14
|
+
*/
|
|
15
|
+
describe('storefront API scope guard', () => {
|
|
16
|
+
afterEach(() => vi.unstubAllGlobals());
|
|
17
|
+
|
|
18
|
+
it('refuses any backend route outside /v1/client', async () => {
|
|
19
|
+
for (const badPath of ['/products/abc', '/vendors/foo/products', '/categories', '/vendors/foo/collections/x/products']) {
|
|
20
|
+
await expect(request('http://api/api/v1', badPath)).rejects.toThrow(/only call \/v1\/client/);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('allows /client/* routes through', async () => {
|
|
25
|
+
const fetchMock = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ data: [] }) });
|
|
26
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
27
|
+
|
|
28
|
+
await expect(request('http://api/api/v1', '/client/store/products')).resolves.toEqual({ data: [] });
|
|
29
|
+
expect(fetchMock).toHaveBeenCalledOnce();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('does not reintroduce a raw v1-route client (createServerClient)', () => {
|
|
33
|
+
const root = process.cwd();
|
|
34
|
+
const sdk = readFileSync(join(root, 'sdk/client.ts'), 'utf8');
|
|
35
|
+
expect(sdk).not.toContain('export function createServerClient');
|
|
36
|
+
|
|
37
|
+
const apiDir = join(root, 'api');
|
|
38
|
+
for (const file of readdirSync(apiDir).filter((name) => name.endsWith('.ts'))) {
|
|
39
|
+
const src = readFileSync(join(apiDir, file), 'utf8');
|
|
40
|
+
expect(src, `${file} must not use the removed createServerClient (v1 routes)`).not.toContain('createServerClient');
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
});
|
package/types/block.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { DesignTokens } from './vendor';
|
|
2
|
-
import type { Product } from './product';
|
|
2
|
+
import type { MetaobjectEntry, Product } from './product';
|
|
3
3
|
|
|
4
4
|
export type BlockType =
|
|
5
5
|
| 'content'
|
|
@@ -18,7 +18,8 @@ export type BlockType =
|
|
|
18
18
|
| 'reviews'
|
|
19
19
|
| 'faq'
|
|
20
20
|
| 'product_qa'
|
|
21
|
-
| 'blog'
|
|
21
|
+
| 'blog'
|
|
22
|
+
| 'metaobjects';
|
|
22
23
|
|
|
23
24
|
export interface ContentBlockData {
|
|
24
25
|
markdown: string;
|
|
@@ -245,6 +246,25 @@ export interface BlogBlockData {
|
|
|
245
246
|
limit?: number;
|
|
246
247
|
}
|
|
247
248
|
|
|
249
|
+
export interface MetaobjectsBlockData {
|
|
250
|
+
title?: string | null;
|
|
251
|
+
/** Metaobject definition handle (`designer`, `ingredient`, …). */
|
|
252
|
+
type: string;
|
|
253
|
+
limit?: number;
|
|
254
|
+
layout?: 'grid' | 'list';
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Server-seeded resolution for a `metaobjects` block — the entries of
|
|
259
|
+
* `data.type` plus the definition meta needed to decide whether each card
|
|
260
|
+
* links out (`definition.has_pages`). Mirrors `MetaobjectsResolved` shape
|
|
261
|
+
* used by `hydrateProductBlocks` in the storefront.
|
|
262
|
+
*/
|
|
263
|
+
export interface MetaobjectsResolved {
|
|
264
|
+
entries: MetaobjectEntry[];
|
|
265
|
+
definition: { type: string; name: string; has_pages: boolean };
|
|
266
|
+
}
|
|
267
|
+
|
|
248
268
|
export type BlockDataMap = {
|
|
249
269
|
content: ContentBlockData;
|
|
250
270
|
image: ImageBlockData;
|
|
@@ -263,6 +283,7 @@ export type BlockDataMap = {
|
|
|
263
283
|
faq: FaqBlockData;
|
|
264
284
|
product_qa: ProductQaBlockData;
|
|
265
285
|
blog: BlogBlockData;
|
|
286
|
+
metaobjects: MetaobjectsBlockData;
|
|
266
287
|
};
|
|
267
288
|
|
|
268
289
|
export type Block<T extends BlockType = BlockType> = {
|
|
@@ -289,4 +310,12 @@ export type Block<T extends BlockType = BlockType> = {
|
|
|
289
310
|
* block type and for any route that hasn't been wired to hydrate.
|
|
290
311
|
*/
|
|
291
312
|
productsPromise?: Promise<Product[]>;
|
|
313
|
+
/**
|
|
314
|
+
* Server-seeded entries + definition meta for `metaobjects` blocks — set
|
|
315
|
+
* by `hydrateProductBlocks` alongside `productsPromise`. Same contract:
|
|
316
|
+
* absent for every other block type and for any route that hasn't been
|
|
317
|
+
* wired to hydrate; the renderer falls back to rendering nothing (themes
|
|
318
|
+
* never fetch metaobjects client-side).
|
|
319
|
+
*/
|
|
320
|
+
metaobjectsPromise?: Promise<MetaobjectsResolved>;
|
|
292
321
|
};
|
package/types/product.ts
CHANGED
|
@@ -157,6 +157,32 @@ export interface ProductFlags {
|
|
|
157
157
|
is_new: boolean;
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
/** A scalar metafield value (single_line_text, boolean, date, url, …). */
|
|
161
|
+
export type MetafieldScalar = string | number | boolean;
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* One metaobject entry linked through a `metaobject_reference` /
|
|
165
|
+
* `list.metaobject_reference` metafield. Presented by the backend
|
|
166
|
+
* (MetafieldPresenter) as the entry resource minus timestamps — a `draft`
|
|
167
|
+
* entry, or one whose definition is not storefront-visible, is already
|
|
168
|
+
* omitted on client surfaces, so the storefront renders whatever arrives.
|
|
169
|
+
*/
|
|
170
|
+
export interface MetaobjectEntry {
|
|
171
|
+
p_id: number;
|
|
172
|
+
type: string;
|
|
173
|
+
handle: string;
|
|
174
|
+
display_name: string;
|
|
175
|
+
/** Flat entry values (definitions never nest references). */
|
|
176
|
+
fields: Record<string, MetafieldScalar | MetafieldScalar[]>;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** A presented product metafield value: scalar, scalar list, or linked entry/entries. */
|
|
180
|
+
export type MetafieldValue =
|
|
181
|
+
| MetafieldScalar
|
|
182
|
+
| MetafieldScalar[]
|
|
183
|
+
| MetaobjectEntry
|
|
184
|
+
| MetaobjectEntry[];
|
|
185
|
+
|
|
160
186
|
export interface Product {
|
|
161
187
|
shop_id: string;
|
|
162
188
|
id: string;
|
|
@@ -183,6 +209,9 @@ export interface Product {
|
|
|
183
209
|
variants: ProductVariant[];
|
|
184
210
|
addons: ProductAddon[];
|
|
185
211
|
reviews: ProductReview[];
|
|
212
|
+
/** Presented product metafields keyed by definition key (incl. linked
|
|
213
|
+
* metaobject entries). Absent when the backend sends none. */
|
|
214
|
+
metafields?: Record<string, MetafieldValue>;
|
|
186
215
|
created_at: string | null;
|
|
187
216
|
updated_at: string | null;
|
|
188
217
|
}
|
|
@@ -271,6 +300,8 @@ export interface LegacyProductLike {
|
|
|
271
300
|
>;
|
|
272
301
|
addons?: ProductAddon[];
|
|
273
302
|
reviews?: ProductReview[];
|
|
303
|
+
/** Raw presented metafields — passed through verbatim by the normalizer. */
|
|
304
|
+
metafields?: Record<string, unknown>;
|
|
274
305
|
review_summary?: Partial<ProductReviewSummary>;
|
|
275
306
|
created_at?: string | null;
|
|
276
307
|
updated_at?: string | null;
|
package/types/theme.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { CartItem as CartLineItem } from './cart';
|
|
|
4
4
|
import type { ImageVariants } from './media';
|
|
5
5
|
import type { MenuItem } from './menu';
|
|
6
6
|
import type { BlogCategory, Collection, Page, Pagination, Post } from './page';
|
|
7
|
-
import type { Product } from './product';
|
|
7
|
+
import type { MetaobjectEntry, Product } from './product';
|
|
8
8
|
import type { DesignTokenAxisPath, DesignTokens, StorefrontConfig, VendorProfile } from './vendor';
|
|
9
9
|
|
|
10
10
|
export interface HomePageProps {
|
|
@@ -39,6 +39,19 @@ export interface PoliciesPageProps {
|
|
|
39
39
|
policies: Page[];
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/** Field label/type metadata for a metaobject entry page — the same shape
|
|
43
|
+
* `GET store/metaobject-definitions` sends, scoped to one definition. */
|
|
44
|
+
export interface MetaobjectPageDefinition {
|
|
45
|
+
type: string;
|
|
46
|
+
name: string;
|
|
47
|
+
fields: Array<{ key: string; name: string; type: string }>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface MetaobjectPageProps {
|
|
51
|
+
entry: MetaobjectEntry;
|
|
52
|
+
definition: MetaobjectPageDefinition;
|
|
53
|
+
}
|
|
54
|
+
|
|
42
55
|
export interface ShopCategory {
|
|
43
56
|
id: string;
|
|
44
57
|
slug: string;
|
|
@@ -63,6 +76,13 @@ export interface CollectionPageProps {
|
|
|
63
76
|
|
|
64
77
|
export interface ProductPageProps {
|
|
65
78
|
product: Product;
|
|
79
|
+
/**
|
|
80
|
+
* Product metafield definitions keyed by metafield key — the labels (and
|
|
81
|
+
* types) the core-owned `<ProductMetafields />` section renders values
|
|
82
|
+
* under. OPTIONAL so external themes that destructure `{ product }` keep
|
|
83
|
+
* compiling; unknown keys fall back to a humanised key label.
|
|
84
|
+
*/
|
|
85
|
+
metafieldDefinitions?: Record<string, { name: string; type: string }>;
|
|
66
86
|
}
|
|
67
87
|
|
|
68
88
|
export interface GalleryPageProps {
|
|
@@ -336,7 +356,7 @@ export interface ThemeManifest {
|
|
|
336
356
|
* Block types that are framework-owned (React logic ships in core, themes
|
|
337
357
|
* only style via `.core-block-*` CSS). Themes do NOT implement these.
|
|
338
358
|
*/
|
|
339
|
-
export type FrameworkBlockType = 'divider' | 'embed' | 'video' | 'table' | 'button' | 'image' | 'reviews' | 'faq' | 'callout' | 'quote' | 'product_qa';
|
|
359
|
+
export type FrameworkBlockType = 'divider' | 'embed' | 'video' | 'table' | 'button' | 'image' | 'reviews' | 'faq' | 'callout' | 'quote' | 'product_qa' | 'metaobjects';
|
|
340
360
|
|
|
341
361
|
/**
|
|
342
362
|
* Block types the theme is responsible for implementing. Framework-owned
|
|
@@ -376,6 +396,11 @@ export interface ThemeModule {
|
|
|
376
396
|
Collection: FC<CollectionPageProps>;
|
|
377
397
|
Product: FC<ProductPageProps>;
|
|
378
398
|
Gallery: FC<GalleryPageProps>;
|
|
399
|
+
/** Optional override for a metaobject entry's own page (`/{type}/{handle}`,
|
|
400
|
+
* only routed when the definition has `has_pages`). When absent, core
|
|
401
|
+
* renders `DefaultMetaobjectPage` (themeable via `.core-metaobject-page-*`
|
|
402
|
+
* CSS), same fallback pattern as `Policies`. */
|
|
403
|
+
Metaobject?: FC<MetaobjectPageProps>;
|
|
379
404
|
};
|
|
380
405
|
shells?: {
|
|
381
406
|
CartShell?: FC<CartShellProps>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
LegacyProductLike,
|
|
3
|
+
MetafieldValue,
|
|
3
4
|
Product,
|
|
4
5
|
ProductCategory,
|
|
5
6
|
ProductOption,
|
|
@@ -196,6 +197,18 @@ function normalizeCategories(raw: LegacyProductLike): ProductCategory[] {
|
|
|
196
197
|
}, []);
|
|
197
198
|
}
|
|
198
199
|
|
|
200
|
+
function normalizeMetafields(raw: LegacyProductLike): Record<string, MetafieldValue> {
|
|
201
|
+
// Explicit whitelist like every other field above: without this the
|
|
202
|
+
// presented metafields (incl. linked metaobject entries) are dropped at
|
|
203
|
+
// runtime and <ProductMetafields /> renders nothing at every call site.
|
|
204
|
+
const source = raw.metafields;
|
|
205
|
+
if (!source || typeof source !== 'object' || Array.isArray(source)) {
|
|
206
|
+
return {};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return source as Record<string, MetafieldValue>;
|
|
210
|
+
}
|
|
211
|
+
|
|
199
212
|
export function normalizeProduct(raw: LegacyProductLike): Product {
|
|
200
213
|
const shopId = String(raw.shop_id ?? raw.vendor_id ?? raw.shop?.id ?? raw.vendor?.id ?? '');
|
|
201
214
|
|
|
@@ -292,6 +305,7 @@ export function normalizeProduct(raw: LegacyProductLike): Product {
|
|
|
292
305
|
: [],
|
|
293
306
|
addons: raw.addons ?? [],
|
|
294
307
|
reviews: raw.reviews ?? [],
|
|
308
|
+
metafields: normalizeMetafields(raw),
|
|
295
309
|
created_at: raw.created_at ?? null,
|
|
296
310
|
updated_at: raw.updated_at ?? null,
|
|
297
311
|
};
|