@zohofinance/commerce 2.1.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 (48) 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 +213 -49
  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/error-report.d.ts +1 -1
  24. package/ts-dist/utils/error-report.d.ts.map +1 -1
  25. package/ts-dist/utils/error-report.js +58 -18
  26. package/ts-dist/utils/load-theme-globals.d.ts +1 -1
  27. package/ts-dist/utils/load-theme-globals.d.ts.map +1 -1
  28. package/ts-dist/utils/load-theme-globals.js +42 -2
  29. package/ts-dist/utils/resolve-schema-settings.d.ts.map +1 -1
  30. package/ts-dist/utils/resolve-schema-settings.js +0 -1
  31. package/ts-dist/utils/resolve-schema-translations.d.ts +1 -0
  32. package/ts-dist/utils/resolve-schema-translations.d.ts.map +1 -1
  33. package/ts-dist/utils/resolve-schema-translations.js +3 -0
  34. package/ts-dist/utils/schema-validation.d.ts +34 -0
  35. package/ts-dist/utils/schema-validation.d.ts.map +1 -0
  36. package/ts-dist/utils/schema-validation.js +544 -0
  37. package/ts-dist/utils/serve-theme-utils.d.ts +1 -1
  38. package/ts-dist/utils/serve-theme-utils.d.ts.map +1 -1
  39. package/ts-dist/utils/serve-theme-utils.js +1 -1
  40. package/ts-dist/utils/template-reference-validation.d.ts +11 -0
  41. package/ts-dist/utils/template-reference-validation.d.ts.map +1 -0
  42. package/ts-dist/utils/template-reference-validation.js +466 -0
  43. package/ts-dist/utils/theme-block-schema.d.ts +5 -0
  44. package/ts-dist/utils/theme-block-schema.d.ts.map +1 -0
  45. package/ts-dist/utils/theme-block-schema.js +35 -0
  46. package/ts-dist/utils/theme-validation.d.ts +8 -1
  47. package/ts-dist/utils/theme-validation.d.ts.map +1 -1
  48. package/ts-dist/utils/theme-validation.js +84 -143
@@ -1,34 +1,21 @@
1
1
  import fs from 'fs-extra';
2
2
  import path from 'path';
3
+ import chalk from 'chalk';
3
4
  import jsonlint from 'jsonlint-mod';
5
+ import { validateLiquidReferences, validateLiquidSchemas, validateTemplateJson, validateThemeSettingsFiles } from './template-reference-validation.js';
6
+ /** Default object keys required for structured setting types. */
4
7
  export const SETTING_RULES = {
5
8
  select: ['display_name', 'value'],
6
- collection: ['display_name', 'handle'],
7
- category: ['display_name', 'handle'],
9
+ radio: ['display_name', 'value'],
10
+ url: ['display_name', 'handle'],
11
+ collection: ['handle'],
12
+ category: ['handle'],
8
13
  menu: ['display_name', 'handle'],
9
- blog: ['display_name', 'handle'],
10
- product: ['display_name', 'handle']
14
+ blog: ['handle'],
15
+ product: ['handle'],
16
+ image_picker: ['url'],
17
+ media_picker: ['url']
11
18
  };
12
- const SUPPORTED_SETTING_TYPES = new Set([
13
- 'text',
14
- 'textarea',
15
- 'richtext',
16
- 'checkbox',
17
- 'range',
18
- 'select',
19
- 'radio',
20
- 'media_picker',
21
- 'preview_layout',
22
- 'color',
23
- 'image_picker',
24
- 'url',
25
- 'header',
26
- 'category',
27
- 'collection',
28
- 'blog',
29
- 'menu',
30
- 'product'
31
- ]);
32
19
  const SKIP_DIRS = new Set([
33
20
  'node_modules',
34
21
  'ts-dist',
@@ -142,114 +129,6 @@ async function validateJsonFiles(folder, cwd, issues) {
142
129
  }
143
130
  }
144
131
  }
145
- function validateSettings(settings, file) {
146
- const issues = [];
147
- for (const setting of settings) {
148
- const settingType = String(setting.type ?? '');
149
- if (!settingType) {
150
- issues.push({
151
- category: 'schema',
152
- message: 'Setting is missing required "type" field',
153
- file
154
- });
155
- continue;
156
- }
157
- if (!SUPPORTED_SETTING_TYPES.has(settingType)) {
158
- issues.push({
159
- category: 'schema',
160
- message: `Unsupported setting type "${settingType}"`,
161
- file
162
- });
163
- continue;
164
- }
165
- if (settingType !== 'header') {
166
- const id = String(setting.id ?? '');
167
- if (!id) {
168
- issues.push({
169
- category: 'schema',
170
- message: `Setting of type "${settingType}" is missing required "id" field`,
171
- file
172
- });
173
- continue;
174
- }
175
- }
176
- const requiredKeys = SETTING_RULES[settingType];
177
- if (!requiredKeys) {
178
- continue;
179
- }
180
- const settingId = String(setting.id ?? '');
181
- const settingDefault = setting.default;
182
- if (typeof settingDefault !== 'object' || settingDefault === null) {
183
- issues.push({
184
- category: 'schema',
185
- message: `Setting id "${settingId}" (type "${settingType}"): ` +
186
- `'default' must be an object with keys ${requiredKeys.join(', ')}, ` +
187
- `found ${JSON.stringify(settingDefault)}`,
188
- file
189
- });
190
- continue;
191
- }
192
- const defaultObj = settingDefault;
193
- const missing = requiredKeys.filter((key) => !(key in defaultObj));
194
- if (missing.length > 0) {
195
- issues.push({
196
- category: 'schema',
197
- message: `Setting id "${settingId}" (type "${settingType}"): ` +
198
- `missing default keys ${missing.join(', ')}, found ${JSON.stringify(settingDefault)}`,
199
- file
200
- });
201
- }
202
- }
203
- return issues;
204
- }
205
- function processSchema(schemaJson, file) {
206
- const issues = [];
207
- if (Array.isArray(schemaJson.settings)) {
208
- issues.push(...validateSettings(schemaJson.settings, file));
209
- }
210
- if (Array.isArray(schemaJson.blocks)) {
211
- for (const block of schemaJson.blocks) {
212
- if (Array.isArray(block.settings)) {
213
- issues.push(...validateSettings(block.settings, file));
214
- }
215
- }
216
- }
217
- return issues;
218
- }
219
- function validateLiquidFiles(cwd) {
220
- const issues = [];
221
- for (const folder of ['sections', 'blocks']) {
222
- const dir = path.join(cwd, folder);
223
- if (!fs.existsSync(dir)) {
224
- continue;
225
- }
226
- const files = fs
227
- .readdirSync(dir)
228
- .filter((file) => file.endsWith('.liquid'));
229
- for (const file of files) {
230
- const filePath = path.join(dir, file);
231
- const content = fs.readFileSync(filePath, 'utf-8');
232
- const schemaMatch = content.match(/{% schema %}([\s\S]*?){% endschema %}/);
233
- if (!schemaMatch) {
234
- continue;
235
- }
236
- let schemaJson;
237
- try {
238
- schemaJson = JSON.parse(schemaMatch[1].trim());
239
- }
240
- catch (err) {
241
- issues.push({
242
- category: 'schema',
243
- message: `Invalid schema JSON: ${err.message}`,
244
- file: path.relative(cwd, filePath)
245
- });
246
- continue;
247
- }
248
- issues.push(...processSchema(schemaJson, path.relative(cwd, filePath)));
249
- }
250
- }
251
- return issues;
252
- }
253
132
  function validateAssetFileSizes(cwd) {
254
133
  const issues = [];
255
134
  const assetsDir = path.join(cwd, 'assets');
@@ -272,9 +151,11 @@ function validateAssetFileSizes(cwd) {
272
151
  const REQUIRED_FILES_COMMON = [
273
152
  { file: 'layouts/theme.liquid', description: 'main layout file' },
274
153
  { file: 'i18n/en.json', description: 'default translation fallback' },
275
- { file: 'i18n/en.schema.json', description: 'schema label translations' }
154
+ { file: 'i18n/en.schema.json', description: 'schema label translations' },
155
+ { file: 'settings/schema.json', description: 'theme settings schema' },
156
+ { file: 'settings/theme.json', description: 'theme settings data' }
276
157
  ];
277
- function validateRequiredFiles(cwd, _mode) {
158
+ function validateRequiredFiles(cwd) {
278
159
  const requiredFiles = REQUIRED_FILES_COMMON;
279
160
  return requiredFiles
280
161
  .filter(({ file }) => !fs.existsSync(path.join(cwd, file)))
@@ -286,19 +167,53 @@ function validateRequiredFiles(cwd, _mode) {
286
167
  }
287
168
  export async function validateTheme(cwd = process.cwd(), options = {}) {
288
169
  const mode = options.mode ?? 'build';
170
+ // Dev and build share one rule set. A section missing available_in or a
171
+ // named preset is logged in dev and blocks the zip on build.
172
+ if (mode !== 'dev' && mode !== 'build') {
173
+ return { valid: true, issues: [] };
174
+ }
289
175
  const issues = [
290
- ...validateRequiredFiles(cwd, mode),
176
+ ...validateRequiredFiles(cwd),
291
177
  ...validateTranslationKeys(cwd),
292
178
  ...validateAssetFileSizes(cwd),
293
- ...validateLiquidFiles(cwd)
179
+ ...validateLiquidSchemas(cwd, mode),
180
+ ...validateThemeSettingsFiles(cwd),
181
+ ...validateTemplateJson(cwd),
182
+ ...validateLiquidReferences(cwd)
294
183
  ];
295
184
  await validateJsonFiles(cwd, cwd, issues);
185
+ const errors = validationErrors(issues);
296
186
  return {
297
- valid: issues.length === 0,
187
+ valid: errors.length === 0,
298
188
  issues
299
189
  };
300
190
  }
191
+ export function validationErrors(issues) {
192
+ return issues.filter((issue) => (issue.severity ?? 'error') === 'error');
193
+ }
194
+ export function validationWarnings(issues) {
195
+ return issues.filter((issue) => issue.severity === 'warning');
196
+ }
301
197
  export function formatValidationIssuesForConsole(issues) {
198
+ const errors = validationErrors(issues);
199
+ const warnings = validationWarnings(issues);
200
+ const lines = [];
201
+ if (errors.length > 0) {
202
+ lines.push(formatIssueGroup(errors, '❌', chalk.red));
203
+ }
204
+ if (warnings.length > 0) {
205
+ const failing = warnings.filter((issue) => issue.failsBuild);
206
+ const plain = warnings.filter((issue) => !issue.failsBuild);
207
+ if (plain.length > 0) {
208
+ lines.push(formatIssueGroup(plain, '⚠️ ', chalk.yellow, 'These do not block preview or build. The storefront still renders the theme.'));
209
+ }
210
+ if (failing.length > 0) {
211
+ lines.push(formatIssueGroup(failing, '⚠️ ', chalk.red, 'These do not block preview, but they WILL CAUSE A BUILD ERROR. Fix them before building.'));
212
+ }
213
+ }
214
+ return lines.join('\n');
215
+ }
216
+ function formatIssueGroup(issues, mark, color, intro) {
302
217
  const grouped = new Map();
303
218
  for (const issue of issues) {
304
219
  const list = grouped.get(issue.category) ?? [];
@@ -308,16 +223,41 @@ export function formatValidationIssuesForConsole(issues) {
308
223
  const labels = {
309
224
  translation: 'Missing translation keys',
310
225
  json: 'Invalid JSON files',
311
- schema: 'Invalid section/block schema settings',
226
+ schema: 'Invalid section/block schemas',
227
+ page: 'Invalid page or section-group JSON',
228
+ liquid: 'Liquid references the engine cannot render',
229
+ theme_settings: 'Invalid theme settings',
312
230
  asset_size: 'Oversized assets',
313
231
  required_file: 'Missing required files'
314
232
  };
315
233
  const lines = [];
234
+ if (intro) {
235
+ lines.push(color(`\n${intro}`));
236
+ }
316
237
  for (const [category, categoryIssues] of grouped) {
317
- lines.push(`\n❌ ${labels[category]}:`);
238
+ lines.push(color(chalk.bold(`\n${mark} ${labels[category]}`) +
239
+ ` (${categoryIssues.length})`));
240
+ // Group identical problems (ignoring array indices) so each is stated once.
241
+ const byMessage = new Map();
318
242
  for (const issue of categoryIssues) {
319
- const location = issue.file ? ` (${issue.file})` : '';
320
- lines.push(` • ${issue.message}${location}`);
243
+ const key = issue.message.replace(/\[\d+\]/g, '[]');
244
+ const where = issue.message.match(/^(\S*\[\d+\]\S*)\s/)?.[1];
245
+ const list = byMessage.get(key) ?? [];
246
+ if (!list.some((e) => e.file === issue.file && e.where === where)) {
247
+ list.push({ file: issue.file, where });
248
+ }
249
+ byMessage.set(key, list);
250
+ }
251
+ for (const [message, entries] of byMessage) {
252
+ const withFiles = entries.filter((e) => e.file);
253
+ const count = withFiles.length > 0
254
+ ? chalk.dim(` — ${withFiles.length} file${withFiles.length === 1 ? '' : 's'}`)
255
+ : '';
256
+ lines.push(color(` ▸ ${message}`) + count);
257
+ for (const entry of withFiles) {
258
+ const where = entry.where ? chalk.dim(` ${entry.where}`) : '';
259
+ lines.push(color(` – ${entry.file}`) + where);
260
+ }
321
261
  }
322
262
  }
323
263
  return lines.join('\n');
@@ -326,6 +266,7 @@ export function assertThemeValid(result) {
326
266
  if (result.valid) {
327
267
  return;
328
268
  }
329
- const formatted = formatValidationIssuesForConsole(result.issues);
330
- throw new Error(`Build aborted: theme validation failed.${formatted}`);
269
+ // Callers print the issue list themselves; keep the message short so it isn't repeated.
270
+ const count = validationErrors(result.issues).length;
271
+ throw new Error(`Build aborted: theme validation failed with ${count} error${count === 1 ? '' : 's'}.`);
331
272
  }