@zohofinance/commerce 2.0.0 → 2.2.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.
Files changed (50) hide show
  1. package/VALIDATIONS.md +122 -0
  2. package/package.json +2 -2
  3. package/public/dev-error.liquid +49 -0
  4. package/ts-dist/commands/build-theme.d.ts.map +1 -1
  5. package/ts-dist/commands/build-theme.js +38 -11
  6. package/ts-dist/commands/serve-theme.d.ts.map +1 -1
  7. package/ts-dist/commands/serve-theme.js +414 -70
  8. package/ts-dist/index.js +0 -0
  9. package/ts-dist/mocks/global-objects.d.ts.map +1 -1
  10. package/ts-dist/mocks/global-objects.js +1 -0
  11. package/ts-dist/mocks/init-theme/i18n/en.schema.json +2 -1
  12. package/ts-dist/mocks/init-theme/sections/footer.liquid +3 -1
  13. package/ts-dist/mocks/init-theme/sections/header-topbar.liquid +3 -1
  14. package/ts-dist/mocks/init-theme/sections/product-collection.liquid +3 -1
  15. package/ts-dist/mocks/init-theme/sections/promo-banner.liquid +5 -1
  16. package/ts-dist/mocks/reviews.d.ts +6 -0
  17. package/ts-dist/mocks/reviews.d.ts.map +1 -0
  18. package/ts-dist/mocks/reviews.js +52 -0
  19. package/ts-dist/routes/storefront-routes.js +1 -1
  20. package/ts-dist/utils/api-handler.d.ts +6 -1
  21. package/ts-dist/utils/api-handler.d.ts.map +1 -1
  22. package/ts-dist/utils/api-handler.js +91 -35
  23. package/ts-dist/utils/editor-attributes.d.ts.map +1 -1
  24. package/ts-dist/utils/editor-attributes.js +2 -4
  25. package/ts-dist/utils/error-report.d.ts +1 -1
  26. package/ts-dist/utils/error-report.d.ts.map +1 -1
  27. package/ts-dist/utils/error-report.js +58 -18
  28. package/ts-dist/utils/load-theme-globals.d.ts +1 -1
  29. package/ts-dist/utils/load-theme-globals.d.ts.map +1 -1
  30. package/ts-dist/utils/load-theme-globals.js +42 -2
  31. package/ts-dist/utils/resolve-schema-settings.d.ts.map +1 -1
  32. package/ts-dist/utils/resolve-schema-settings.js +0 -1
  33. package/ts-dist/utils/resolve-schema-translations.d.ts +1 -0
  34. package/ts-dist/utils/resolve-schema-translations.d.ts.map +1 -1
  35. package/ts-dist/utils/resolve-schema-translations.js +3 -0
  36. package/ts-dist/utils/schema-validation.d.ts +34 -0
  37. package/ts-dist/utils/schema-validation.d.ts.map +1 -0
  38. package/ts-dist/utils/schema-validation.js +544 -0
  39. package/ts-dist/utils/serve-theme-utils.d.ts +1 -1
  40. package/ts-dist/utils/serve-theme-utils.d.ts.map +1 -1
  41. package/ts-dist/utils/serve-theme-utils.js +1 -1
  42. package/ts-dist/utils/template-reference-validation.d.ts +11 -0
  43. package/ts-dist/utils/template-reference-validation.d.ts.map +1 -0
  44. package/ts-dist/utils/template-reference-validation.js +466 -0
  45. package/ts-dist/utils/theme-block-schema.d.ts +5 -0
  46. package/ts-dist/utils/theme-block-schema.d.ts.map +1 -0
  47. package/ts-dist/utils/theme-block-schema.js +35 -0
  48. package/ts-dist/utils/theme-validation.d.ts +8 -1
  49. package/ts-dist/utils/theme-validation.d.ts.map +1 -1
  50. package/ts-dist/utils/theme-validation.js +84 -143
package/ts-dist/index.js CHANGED
File without changes
@@ -1 +1 @@
1
- {"version":3,"file":"global-objects.d.ts","sourceRoot":"","sources":["../../src/mocks/global-objects.ts"],"names":[],"mappings":"AAAA,KAAK,aAAa,GAAG;IACnB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB,CAAC;AAEF,QAAA,MAAM,aAAa,EAAE,aAw0fpB,CAAC;AAEF,eAAe,aAAa,CAAC"}
1
+ {"version":3,"file":"global-objects.d.ts","sourceRoot":"","sources":["../../src/mocks/global-objects.ts"],"names":[],"mappings":"AAAA,KAAK,aAAa,GAAG;IACnB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB,CAAC;AAEF,QAAA,MAAM,aAAa,EAAE,aAy0fpB,CAAC;AAEF,eAAe,aAAa,CAAC"}
@@ -182,6 +182,7 @@ const globalObjects = {
182
182
  show_zoho_powered_by: false,
183
183
  is_product_filters_enabled: false,
184
184
  is_sorting_enabled: false,
185
+ is_product_reviews_enabled: true,
185
186
  search_box_enabled: false,
186
187
  is_footer_enabled: false,
187
188
  product_specifications_enabled: false,
@@ -43,7 +43,8 @@
43
43
  "default": "Shop Now"
44
44
  },
45
45
  "cta_url": {
46
- "label": "Button URL"
46
+ "label": "Button URL",
47
+ "default": "Shop"
47
48
  }
48
49
  }
49
50
  },
@@ -6,6 +6,8 @@
6
6
 
7
7
  {% schema %}
8
8
  {
9
- "name": "t:sections.footer.name"
9
+ "name": "t:sections.footer.name",
10
+ "available_in": { "templates": ["footer-group"] },
11
+ "presets": [{ "name": "t:sections.footer.name" }]
10
12
  }
11
13
  {% endschema %}
@@ -23,6 +23,7 @@
23
23
  {% schema %}
24
24
  {
25
25
  "name": "t:sections.header_topbar.name",
26
+ "available_in": { "templates": ["header-group"] },
26
27
  "settings": [
27
28
  {
28
29
  "type": "select",
@@ -47,6 +48,7 @@
47
48
  }
48
49
  ]
49
50
  }
50
- ]
51
+ ],
52
+ "presets": [{ "name": "t:sections.header_topbar.name" }]
51
53
  }
52
54
  {% endschema %}
@@ -25,6 +25,7 @@
25
25
  {% schema %}
26
26
  {
27
27
  "name": "t:sections.product_collection.name",
28
+ "available_in": { "templates": ["index", "collection", "category"] },
28
29
  "settings": [
29
30
  {
30
31
  "type": "collection",
@@ -38,6 +39,7 @@
38
39
  "type": "zc_theme__section-heading",
39
40
  "name": "Section Heading"
40
41
  }
41
- ]
42
+ ],
43
+ "presets": [{ "name": "t:sections.product_collection.name" }]
42
44
  }
43
45
  {% endschema %}
@@ -50,7 +50,11 @@
50
50
  {
51
51
  "type": "url",
52
52
  "id": "cta_url",
53
- "label": "t:sections.promo_banner.settings.cta_url.label"
53
+ "label": "t:sections.promo_banner.settings.cta_url.label",
54
+ "default": {
55
+ "display_name": "t:sections.promo_banner.settings.cta_url.default",
56
+ "handle": "collections/all-products"
57
+ }
54
58
  }
55
59
  ],
56
60
  "blocks": [
@@ -0,0 +1,6 @@
1
+ type ProductReviews = {
2
+ [key: string]: any;
3
+ };
4
+ declare const reviews: ProductReviews;
5
+ export default reviews;
6
+ //# sourceMappingURL=reviews.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reviews.d.ts","sourceRoot":"","sources":["../../src/mocks/reviews.ts"],"names":[],"mappings":"AAAA,KAAK,cAAc,GAAG;IACpB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB,CAAC;AAIF,QAAA,MAAM,OAAO,EAAE,cAkDd,CAAC;AAEF,eAAe,OAAO,CAAC"}
@@ -0,0 +1,52 @@
1
+ // Mirrors the live /products/:product_id/reviews response so a theme renders the
2
+ // same markup with or without --storeUrl.
3
+ const reviews = {
4
+ is_reviewed: false,
5
+ allow_registered_only: false,
6
+ is_logged_in: false,
7
+ is_review_enabled: true,
8
+ reviews: [
9
+ {
10
+ id: '8858475000000094101',
11
+ user_name: 'Arun Kumar',
12
+ rating: 5,
13
+ title: 'Excellent build quality',
14
+ content: 'The product arrived well packed and works exactly as described. Battery life is great and the finish feels premium.',
15
+ last_modified_time: '14 June 2026'
16
+ },
17
+ {
18
+ id: '8858475000000094102',
19
+ user_name: 'Divya Raman',
20
+ rating: 4,
21
+ title: 'Good value for the price',
22
+ content: 'Happy with the purchase overall. Setup was simple and the sound quality is clear, though the case scratches easily.',
23
+ last_modified_time: '02 June 2026'
24
+ }
25
+ ],
26
+ user_review: null,
27
+ is_captcha_enabled: false,
28
+ average_rating: 4.5,
29
+ rating_stats: {
30
+ star_5_percentage: 50,
31
+ star_5_count: 1,
32
+ overall_average: 4.5,
33
+ star_4_percentage: 50,
34
+ star_4_count: 1,
35
+ star_3_percentage: 0,
36
+ star_3_count: 0,
37
+ star_2_percentage: 0,
38
+ star_2_count: 0,
39
+ star_1_percentage: 0,
40
+ star_1_count: 0,
41
+ total_reviews: 2
42
+ },
43
+ page_context: {
44
+ per_page: 10,
45
+ items_count: '2',
46
+ total_pages: 1,
47
+ current_page: 1,
48
+ page_param: 'page'
49
+ },
50
+ is_purchased: false
51
+ };
52
+ export default reviews;
@@ -126,7 +126,7 @@ export const STOREFRONT_ROUTES = [
126
126
  }
127
127
  ];
128
128
  export function resolveApiPath(route, params) {
129
- return route.apiPath.replace(/:([a-z_]+)/gi, (_, key) => params[key] ?? '');
129
+ return route.apiPath.replace(/:([a-z_]+)/gi, (_, key) => encodeURIComponent(params[key] ?? ''));
130
130
  }
131
131
  export function pageTemplateExists(pageTemplate) {
132
132
  return fs.existsSync(path.join(process.cwd(), 'pages', `${pageTemplate}.json`));
@@ -13,6 +13,7 @@ export declare class APIHandler {
13
13
  useMock: boolean;
14
14
  baseURL?: string;
15
15
  }): Promise<APIHandler>;
16
+ get cookieHeader(): string;
16
17
  fetch(requestUrl: string): Promise<Record<string, unknown>>;
17
18
  getMockCartData(): Record<string, unknown>;
18
19
  getMockSaveForLaterData(): Record<string, unknown>[];
@@ -27,7 +28,11 @@ export declare class APIHandler {
27
28
  wishlistAction(method: 'POST' | 'DELETE', query: string, body?: Record<string, unknown>): Promise<any>;
28
29
  getQuote(): Promise<any>;
29
30
  getProductRecommendations(productId: string): Promise<any>;
30
- getProductReviews(productId: string): Promise<any>;
31
+ getProductReviews(productId: string, query?: string): Promise<{
32
+ [key: string]: any;
33
+ }>;
34
+ submitProductReview(method: 'POST' | 'PUT', productId: string, data: Record<string, unknown>): Promise<unknown>;
35
+ getReviewForEdit(productId: string): Promise<unknown>;
31
36
  bulkAddToCart(data: Record<string, unknown>): Promise<any>;
32
37
  saveForLater(method: 'PUT' | 'DELETE', query: string): Promise<unknown>;
33
38
  moveToCart(query: string): Promise<unknown>;
@@ -1 +1 @@
1
- {"version":3,"file":"api-handler.d.ts","sourceRoot":"","sources":["../../src/utils/api-handler.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"api-handler.d.ts","sourceRoot":"","sources":["../../src/utils/api-handler.ts"],"names":[],"mappings":"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"}
@@ -9,6 +9,7 @@ import blog from '../mocks/blog.js';
9
9
  import category from '../mocks/category.js';
10
10
  import categories from '../mocks/categories.js';
11
11
  import globalObjectsMock from '../mocks/global-objects.js';
12
+ import productReviews from '../mocks/reviews.js';
12
13
  function mockJson(data) {
13
14
  return {
14
15
  json: async () => data
@@ -20,6 +21,30 @@ function withPageTitle(data, title) {
20
21
  page_title: title
21
22
  };
22
23
  }
24
+ function rewriteStoreUrls(value, origin) {
25
+ if (!origin)
26
+ return value;
27
+ if (typeof value === 'string') {
28
+ if (value === origin)
29
+ return '/';
30
+ if (value.startsWith(origin) &&
31
+ /^[/?#]/.test(value.slice(origin.length))) {
32
+ return value.slice(origin.length);
33
+ }
34
+ return value;
35
+ }
36
+ if (Array.isArray(value)) {
37
+ return value.map((item) => rewriteStoreUrls(item, origin));
38
+ }
39
+ if (value && typeof value === 'object') {
40
+ const out = {};
41
+ for (const [k, v] of Object.entries(value)) {
42
+ out[k] = rewriteStoreUrls(v, origin);
43
+ }
44
+ return out;
45
+ }
46
+ return value;
47
+ }
23
48
  export class APIHandler {
24
49
  useMock;
25
50
  cookies = [];
@@ -37,23 +62,41 @@ export class APIHandler {
37
62
  }
38
63
  return handler;
39
64
  }
65
+ // Stores cookies by name so a refreshed session replaces the old one instead
66
+ // of piling up duplicates.
67
+ #storeCookies(response) {
68
+ const setCookies = typeof response.headers.getSetCookie === 'function'
69
+ ? response.headers.getSetCookie()
70
+ : (response.headers.get('set-cookie') ?? '')
71
+ .split(/,(?=\s*[^;,=\s]+=)/)
72
+ .filter(Boolean);
73
+ for (const cookie of setCookies) {
74
+ const pair = cookie.split(';')[0].trim();
75
+ const name = pair.split('=')[0];
76
+ if (!name)
77
+ continue;
78
+ this.cookies = this.cookies.filter((existing) => existing.split('=')[0] !== name);
79
+ this.cookies.push(pair);
80
+ }
81
+ }
82
+ get cookieHeader() {
83
+ return this.cookies.join('; ');
84
+ }
40
85
  async #cookieSetter() {
41
86
  try {
42
87
  const response = await fetch(`${this.baseURL}/home?format=json`);
43
- const setCookieHeader = response.headers.get('set-cookie');
44
- setCookieHeader?.split(',').forEach((cookie) => {
45
- this.cookies.push(cookie.split(';')[0]);
46
- });
88
+ this.#storeCookies(response);
47
89
  }
48
90
  catch (e) {
49
91
  console.warn('Could not establish store session cookies.', e);
50
92
  }
51
93
  }
52
94
  async #makeRequest(requestUrl) {
53
- const response = await fetch(`${this.baseURL}${requestUrl}?format=json`, {
95
+ const separator = requestUrl.includes('?') ? '&' : '?';
96
+ const response = await fetch(`${this.baseURL}${requestUrl}${separator}format=json`, {
54
97
  method: 'GET',
55
98
  headers: {
56
- Cookie: this.cookies.join(';')
99
+ Cookie: this.cookieHeader
57
100
  }
58
101
  });
59
102
  return response;
@@ -95,8 +138,9 @@ export class APIHandler {
95
138
  });
96
139
  }
97
140
  if (requestUrl.match(/^\/products\/[^/]+/)) {
98
- const productHandle = requestUrl.split('/').pop();
99
- const product = products.find((item) => item.handle === productHandle || item.id === productHandle);
141
+ const productHandle = decodeURIComponent(requestUrl.split('?')[0].split('/').pop() ?? '');
142
+ // Unknown handles fall back to the first mock product so the PDP still renders.
143
+ const product = products.find((item) => item.handle === productHandle || item.id === productHandle) ?? products[0];
100
144
  return mockJson({
101
145
  product,
102
146
  page_title: product?.title ?? 'Product'
@@ -150,17 +194,25 @@ export class APIHandler {
150
194
  try {
151
195
  let response;
152
196
  if (this.useMock) {
153
- response = this.#makeMockRequest(requestUrl);
197
+ response = this.#makeMockRequest(requestUrl.split('?')[0]);
154
198
  }
155
199
  else {
156
200
  response = await this.#makeRequest(requestUrl);
157
201
  }
158
202
  if (response instanceof Response) {
159
203
  if (!response.ok) {
160
- throw new Error(`Failed to fetch data from ${requestUrl} (${response.status})`);
204
+ const detail = (await response.text().catch(() => ''))
205
+ .trim()
206
+ .slice(0, 300);
207
+ throw new Error(`Failed to fetch data from ${requestUrl} (${response.status})${detail ? `: ${detail}` : ''}`);
161
208
  }
162
209
  }
163
- const json = (await response.json());
210
+ let json = (await response.json());
211
+ // Live responses carry absolute store URLs (menu items, product links...).
212
+ // Make them relative so navigation stays on the local dev server.
213
+ if (!this.useMock && this.baseURL) {
214
+ json = rewriteStoreUrls(json, this.baseURL);
215
+ }
164
216
  if (json.staticAssetsUrl) {
165
217
  json.staticAssetsUrl = '/assets';
166
218
  }
@@ -178,7 +230,7 @@ export class APIHandler {
178
230
  const requestConfig = {
179
231
  method: reqMethod,
180
232
  headers: {
181
- Cookie: this.cookies.join(';')
233
+ Cookie: this.cookieHeader
182
234
  }
183
235
  };
184
236
  if (reqMethod === 'DELETE') {
@@ -196,10 +248,7 @@ export class APIHandler {
196
248
  requestConfig.body = formData;
197
249
  }
198
250
  const result = await fetch(requestUrl, requestConfig);
199
- const setCookieHeader = result.headers.get('set-cookie');
200
- setCookieHeader?.split(',').forEach((cookie) => {
201
- this.cookies.push(cookie.split(';')[0]);
202
- });
251
+ this.#storeCookies(result);
203
252
  return result.json();
204
253
  }
205
254
  #findVariantInProducts(variantId) {
@@ -252,17 +301,14 @@ export class APIHandler {
252
301
  }
253
302
  async #proxyRequest(method, path, body = null, query = '') {
254
303
  const url = `${this.baseURL}${path}${query ? `?${query}` : ''}`;
255
- const headers = { Cookie: this.cookies.join(';') };
304
+ const headers = { Cookie: this.cookieHeader };
256
305
  const init = { method, headers };
257
306
  if (body !== null && method !== 'GET' && method !== 'DELETE') {
258
307
  headers['Content-Type'] = 'application/json';
259
308
  init.body = JSON.stringify(body);
260
309
  }
261
310
  const res = await fetch(url, init);
262
- const setCookieHeader = res.headers.get('set-cookie');
263
- setCookieHeader?.split(',').forEach((cookie) => {
264
- this.cookies.push(cookie.split(';')[0]);
265
- });
311
+ this.#storeCookies(res);
266
312
  return res.json();
267
313
  }
268
314
  async addToCart(data) {
@@ -343,7 +389,7 @@ export class APIHandler {
343
389
  return this.#computeMockCart();
344
390
  }
345
391
  const res = await fetch(`${this.baseURL}/storefront/api/v1/cart`, {
346
- headers: { Cookie: this.cookies.join(';') }
392
+ headers: { Cookie: this.cookieHeader }
347
393
  });
348
394
  return res.json();
349
395
  }
@@ -371,7 +417,7 @@ export class APIHandler {
371
417
  ];
372
418
  }
373
419
  const res = await fetch(`${this.baseURL}/storefront/api/v1/currencies`, {
374
- headers: { Cookie: this.cookies.join(';') }
420
+ headers: { Cookie: this.cookieHeader }
375
421
  });
376
422
  return res.json();
377
423
  }
@@ -379,7 +425,7 @@ export class APIHandler {
379
425
  if (this.useMock) {
380
426
  return { code: 0, message: 'success' };
381
427
  }
382
- const res = await fetch(`${this.baseURL}/updatecurrency?currency_code=${encodeURIComponent(currencyCode)}`, { method: 'PUT', headers: { Cookie: this.cookies.join(';') } });
428
+ const res = await fetch(`${this.baseURL}/updatecurrency?currency_code=${encodeURIComponent(currencyCode)}`, { method: 'PUT', headers: { Cookie: this.cookieHeader } });
383
429
  return res.json();
384
430
  }
385
431
  async getPortalUser() {
@@ -387,7 +433,7 @@ export class APIHandler {
387
433
  return { current_user: null };
388
434
  }
389
435
  const res = await fetch(`${this.baseURL}/portal/user`, {
390
- headers: { Cookie: this.cookies.join(';') }
436
+ headers: { Cookie: this.cookieHeader }
391
437
  });
392
438
  return res.json();
393
439
  }
@@ -395,7 +441,7 @@ export class APIHandler {
395
441
  if (this.useMock) {
396
442
  return { variants: {} };
397
443
  }
398
- const res = await fetch(`${this.baseURL}/wishlists/wishlist-enabled?variant_ids=${encodeURIComponent(variantIds)}`, { headers: { Cookie: this.cookies.join(';') } });
444
+ const res = await fetch(`${this.baseURL}/wishlists/wishlist-enabled?variant_ids=${encodeURIComponent(variantIds)}`, { headers: { Cookie: this.cookieHeader } });
399
445
  return res.json();
400
446
  }
401
447
  async wishlistAction(method, query, body) {
@@ -415,7 +461,7 @@ export class APIHandler {
415
461
  }
416
462
  const res = await fetch(url, {
417
463
  method,
418
- headers: { Cookie: this.cookies.join(';') },
464
+ headers: { Cookie: this.cookieHeader },
419
465
  body: fetchBody
420
466
  });
421
467
  return res.json();
@@ -425,7 +471,7 @@ export class APIHandler {
425
471
  return { code: 0, quote: null };
426
472
  }
427
473
  const res = await fetch(`${this.baseURL}/storefront/api/v1/quote`, {
428
- headers: { Cookie: this.cookies.join(';') }
474
+ headers: { Cookie: this.cookieHeader }
429
475
  });
430
476
  return res.json();
431
477
  }
@@ -434,18 +480,28 @@ export class APIHandler {
434
480
  return { recommendations: [], sections: {} };
435
481
  }
436
482
  const res = await fetch(`${this.baseURL}/products/${productId}/recommendations`, {
437
- headers: { Cookie: this.cookies.join(';') }
483
+ headers: { Cookie: this.cookieHeader }
438
484
  });
439
485
  return res.json();
440
486
  }
441
- async getProductReviews(productId) {
487
+ async getProductReviews(productId, query = '') {
442
488
  if (this.useMock) {
443
- return { reviews: [], page_context: { total_count: 0 } };
489
+ return { ...productReviews };
444
490
  }
445
- const res = await fetch(`${this.baseURL}/products/${productId}/reviews`, {
446
- headers: { Cookie: this.cookies.join(';') }
447
- });
448
- return res.json();
491
+ const response = await this.#proxyRequest('GET', `/products/${encodeURIComponent(productId)}/reviews`, null, query);
492
+ return rewriteStoreUrls(response, this.baseURL ?? '');
493
+ }
494
+ async submitProductReview(method, productId, data) {
495
+ if (this.useMock) {
496
+ return { code: 0, message: 'Review submitted' };
497
+ }
498
+ return this.#proxyRequest(method, `/products/${encodeURIComponent(productId)}/reviews`, data);
499
+ }
500
+ async getReviewForEdit(productId) {
501
+ if (this.useMock) {
502
+ return { code: 0, rating: 0, title: '', content: '' };
503
+ }
504
+ return this.#proxyRequest('GET', `/editreview/${encodeURIComponent(productId)}`);
449
505
  }
450
506
  async bulkAddToCart(data) {
451
507
  if (this.useMock) {
@@ -1 +1 @@
1
- {"version":3,"file":"editor-attributes.d.ts","sourceRoot":"","sources":["../../src/utils/editor-attributes.ts"],"names":[],"mappings":"AAAA,wBAAgB,yBAAyB,CACvC,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,WAAW,CAAC,EAAE,MAAM,GACnB,MAAM,CAUR;AAED,wBAAgB,6BAA6B,CAC3C,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,WAAW,CAAC,EAAE,MAAM,GACnB,MAAM,CAYR"}
1
+ {"version":3,"file":"editor-attributes.d.ts","sourceRoot":"","sources":["../../src/utils/editor-attributes.ts"],"names":[],"mappings":"AAAA,wBAAgB,yBAAyB,CACvC,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,WAAW,CAAC,EAAE,MAAM,GACnB,MAAM,CASR;AAED,wBAAgB,6BAA6B,CAC3C,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,WAAW,CAAC,EAAE,MAAM,GACnB,MAAM,CAWR"}
@@ -2,10 +2,9 @@ export function buildSectionComponentAttr(sectionType, sectionKey, jsonType, dis
2
2
  return JSON.stringify({
3
3
  id: `section_${sectionKey}`,
4
4
  'data-zcommerce-section-id': '',
5
- 'data-zcommerce-component-name': sectionType,
5
+ 'data-zcommerce-component-name': displayName || sectionType,
6
6
  'data-zcommerce-component-key': `section_${sectionKey}`,
7
7
  'data-zcommerce-json-type': jsonType,
8
- 'data-zcommerce-display-name': displayName || sectionType,
9
8
  settingid: `setting_section_${sectionType}`
10
9
  });
11
10
  }
@@ -14,10 +13,9 @@ export function buildBlockZcommerceAttributes(blockType, blockKey, sectionKey, j
14
13
  const attrJson = JSON.stringify({
15
14
  id,
16
15
  'data-zcommerce-section-id': `section_${sectionKey}`,
17
- 'data-zcommerce-component-name': blockType,
16
+ 'data-zcommerce-component-name': displayName || blockType,
18
17
  'data-zcommerce-component-key': id,
19
18
  'data-zcommerce-json-type': jsonType,
20
- 'data-zcommerce-display-name': displayName || blockType,
21
19
  settingid: `setting_block_${blockType}`
22
20
  }).replace(/'/g, '&#39;');
23
21
  return `data-zcommerce-component='${attrJson}'`;
@@ -1,5 +1,5 @@
1
1
  import type { ErrorTraceType } from 'types/common';
2
- import type { ThemeValidationResult, ValidationIssue } from './theme-validation.js';
2
+ import { type ThemeValidationResult, type ValidationIssue } from './theme-validation.js';
3
3
  export type DevErrorKind = 'compilation' | 'validation' | 'runtime';
4
4
  export type DevErrorReport = {
5
5
  kind: DevErrorKind;
@@ -1 +1 @@
1
- {"version":3,"file":"error-report.d.ts","sourceRoot":"","sources":["../../src/utils/error-report.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,KAAK,EACV,qBAAqB,EACrB,eAAe,EAEhB,MAAM,uBAAuB,CAAC;AAE/B,MAAM,MAAM,YAAY,GAAG,aAAa,GAAG,YAAY,GAAG,SAAS,CAAC;AAEpE,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,EAAE,YAAY,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACnE,MAAM,CAAC,EAAE,eAAe,EAAE,CAAC;IAC3B,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AA4CF,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,qBAAqB,EAC7B,GAAG,SAAgB,GAClB,MAAM,CAoCR;AAED,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,cAAc,EACrB,eAAe,EAAE,MAAM,EACvB,GAAG,SAAgB,GAClB,MAAM,CAwCR;AAED,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,MAAM,EACf,GAAG,SAAgB,GAClB,MAAM,CAeR;AA2BD,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,qBAAqB,EAC7B,GAAG,SAAgB,GAClB,cAAc,CAahB;AAED,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,MAAM,EACf,GAAG,SAAgB,GAClB,cAAc,CAOhB;AAID,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,cAAc,GACrB,OAAO,CAAC,MAAM,CAAC,CAiBjB"}
1
+ {"version":3,"file":"error-report.d.ts","sourceRoot":"","sources":["../../src/utils/error-report.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,EACL,KAAK,qBAAqB,EAC1B,KAAK,eAAe,EAGrB,MAAM,uBAAuB,CAAC;AAE/B,MAAM,MAAM,YAAY,GAAG,aAAa,GAAG,YAAY,GAAG,SAAS,CAAC;AAEpE,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,EAAE,YAAY,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACnE,MAAM,CAAC,EAAE,eAAe,EAAE,CAAC;IAC3B,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AA0EF,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,qBAAqB,EAC7B,GAAG,SAAgB,GAClB,MAAM,CA4CR;AAED,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,cAAc,EACrB,eAAe,EAAE,MAAM,EACvB,GAAG,SAAgB,GAClB,MAAM,CAwCR;AAED,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,MAAM,EACf,GAAG,SAAgB,GAClB,MAAM,CAeR;AA8CD,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,qBAAqB,EAC7B,GAAG,SAAgB,GAClB,cAAc,CAchB;AAED,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,MAAM,EACf,GAAG,SAAgB,GAClB,cAAc,CAOhB;AAID,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,cAAc,GACrB,OAAO,CAAC,MAAM,CAAC,CAiBjB"}
@@ -3,10 +3,14 @@ import path from 'path';
3
3
  import { fileURLToPath } from 'url';
4
4
  import { Liquid } from 'liquidjs';
5
5
  import { escapeExpression } from './escape-expression.js';
6
+ import { validationErrors } from './theme-validation.js';
6
7
  const CATEGORY_LABELS = {
7
8
  translation: 'Missing translation keys',
8
9
  json: 'Invalid JSON files',
9
- schema: 'Invalid section/block schema settings',
10
+ schema: 'Invalid section/block schemas',
11
+ page: 'Invalid page or section-group JSON',
12
+ liquid: 'Liquid references the engine cannot render',
13
+ theme_settings: 'Invalid theme settings',
10
14
  asset_size: 'Oversized assets',
11
15
  required_file: 'Missing required files'
12
16
  };
@@ -25,7 +29,10 @@ const FIX_HINTS = {
25
29
  'Resolve each listed issue before previewing the theme.',
26
30
  'Required files: `layouts/theme.liquid`, `i18n/en.json`, `i18n/en.schema.json`.',
27
31
  'Every `"t:..."` key used in the theme must exist in `i18n/en.schema.json` or `i18n/en.json` depending on where the key is used.',
28
- 'Settings of type select, collection, category, menu, or blog need a valid `default` object.',
32
+ 'Required files also include `settings/schema.json` and `settings/theme.json`.',
33
+ 'Section and block schemas must use engine setting types. select, radio, url, menu, collection, category, product, blog, image_picker, and media_picker need a structured `default`.',
34
+ 'Page JSON `order` entries must exist in `sections`, each section `type` must have a liquid file, and every `block_order` entry must exist and be allowed by that schema.',
35
+ 'Do not use `{% paginate %}`, `{% form %}`, `{% stylesheet %}`, or `{% include %}`. Snippet, section, and section-group names must match real files.',
29
36
  'Files in `assets/` must be under 1 MB each.'
30
37
  ],
31
38
  runtime: [
@@ -40,19 +47,39 @@ function encodeMarkdown(markdown) {
40
47
  function buildFixSection(kind) {
41
48
  return FIX_HINTS[kind].map((hint) => `- ${hint}`).join('\n');
42
49
  }
50
+ /** Groups identical problems (ignoring array indices) so each is stated once. */
51
+ function groupByMessage(issues) {
52
+ const groups = new Map();
53
+ for (const issue of issues) {
54
+ const key = issue.message.replace(/\[\d+\]/g, '[]');
55
+ const where = issue.message.match(/^(\S*\[\d+\]\S*)\s/)?.[1];
56
+ const group = groups.get(key) ?? { message: key, entries: [] };
57
+ if (!group.entries.some((e) => e.file === issue.file && e.where === where)) {
58
+ group.entries.push({ file: issue.file, where });
59
+ }
60
+ groups.set(key, group);
61
+ }
62
+ return [...groups.values()];
63
+ }
43
64
  export function buildValidationMarkdown(result, cwd = process.cwd()) {
65
+ const blockingIssues = validationErrors(result.issues);
44
66
  const grouped = new Map();
45
- for (const issue of result.issues) {
67
+ for (const issue of blockingIssues) {
46
68
  const list = grouped.get(issue.category) ?? [];
47
69
  list.push(issue);
48
70
  grouped.set(issue.category, list);
49
71
  }
50
72
  const issueSections = [];
51
73
  for (const [category, issues] of grouped) {
52
- issueSections.push(`### ${CATEGORY_LABELS[category]}`);
53
- for (const issue of issues) {
54
- const location = issue.file ? ` — \`${issue.file}\`` : '';
55
- issueSections.push(`- ${issue.message}${location}`);
74
+ issueSections.push(`### ${CATEGORY_LABELS[category]} (${issues.length})`);
75
+ for (const group of groupByMessage(issues)) {
76
+ issueSections.push(`- **${group.message}** — ${group.entries.length} file${group.entries.length === 1 ? '' : 's'}`);
77
+ for (const entry of group.entries) {
78
+ if (entry.file) {
79
+ const where = entry.where ? ` (\`${entry.where}\`)` : '';
80
+ issueSections.push(` - \`${entry.file}\`${where}`);
81
+ }
82
+ }
56
83
  }
57
84
  issueSections.push('');
58
85
  }
@@ -60,7 +87,7 @@ export function buildValidationMarkdown(result, cwd = process.cwd()) {
60
87
  '# Zoho Commerce Theme CLI — Validation error',
61
88
  '',
62
89
  '## Summary',
63
- `Theme validation failed with **${result.issues.length}** issue(s) in \`${cwd}\`.`,
90
+ `Theme validation failed with **${blockingIssues.length}** issue(s) in \`${cwd}\`.`,
64
91
  '',
65
92
  '## Issues',
66
93
  ...issueSections,
@@ -118,26 +145,39 @@ function groupValidationIssues(issues) {
118
145
  }
119
146
  const htmlParts = [];
120
147
  for (const [category, categoryIssues] of grouped) {
121
- htmlParts.push(`<h3>${CATEGORY_LABELS[category]}</h3><ul>`);
122
- for (const issue of categoryIssues) {
123
- const fileLink = issue.file
124
- ? ` <a href="#" data-file="${escapeExpression(issue.file)}" class="open-file-link">${escapeExpression(issue.file)}</a>`
148
+ htmlParts.push(`<h3>${CATEGORY_LABELS[category]} <span class="issue-count">${categoryIssues.length}</span></h3>`);
149
+ for (const group of groupByMessage(categoryIssues)) {
150
+ const files = group.entries.filter((entry) => entry.file);
151
+ const count = files.length > 0
152
+ ? `<span class="issue-files-count">${files.length} file${files.length === 1 ? '' : 's'}</span>`
125
153
  : '';
126
- htmlParts.push(`<li>${escapeExpression(issue.message)}${fileLink}</li>`);
154
+ htmlParts.push(`<div class="issue-group"><div class="issue-message">${escapeExpression(group.message)}${count}</div>`);
155
+ if (files.length > 0) {
156
+ htmlParts.push('<ul class="issue-files">');
157
+ for (const entry of files) {
158
+ const file = escapeExpression(entry.file ?? '');
159
+ const where = entry.where
160
+ ? ` <span class="issue-where">${escapeExpression(entry.where)}</span>`
161
+ : '';
162
+ htmlParts.push(`<li><a href="#" data-file="${file}" class="open-file-link">${file}</a>${where}</li>`);
163
+ }
164
+ htmlParts.push('</ul>');
165
+ }
166
+ htmlParts.push('</div>');
127
167
  }
128
- htmlParts.push('</ul>');
129
168
  }
130
169
  return htmlParts.join('\n');
131
170
  }
132
171
  export function buildValidationErrorReport(result, cwd = process.cwd()) {
172
+ const blockingIssues = validationErrors(result.issues);
133
173
  return {
134
174
  kind: 'validation',
135
- title: result.issues.length === 1
175
+ title: blockingIssues.length === 1
136
176
  ? '1 validation issue found'
137
- : `${result.issues.length} validation issues found`,
177
+ : `${blockingIssues.length} validation issues found`,
138
178
  summary: 'Resolve the issues below to continue previewing this theme. The development server is running, but pages are blocked until validation passes.',
139
- issues: result.issues,
140
- context: groupValidationIssues(result.issues),
179
+ issues: blockingIssues,
180
+ context: groupValidationIssues(blockingIssues),
141
181
  markdown: buildValidationMarkdown(result, cwd)
142
182
  };
143
183
  }
@@ -1,3 +1,3 @@
1
- export declare function loadThemeGlobals(): Record<string, unknown>;
1
+ export declare function loadThemeGlobals(live?: boolean): Record<string, unknown>;
2
2
  export declare function mergePageData(globals: Record<string, unknown>, pageData: Record<string, unknown>): Record<string, unknown>;
3
3
  //# sourceMappingURL=load-theme-globals.d.ts.map