@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
@@ -0,0 +1,544 @@
1
+ /**
2
+ * Schema checks aligned with storefront_template_engine SchemaValidator
3
+ * on patch_storefront_logging_and_error_handling. color_scheme is included
4
+ * because SchemaSettingsUtil resolves it. Findings the storefront still
5
+ * renders are warnings.
6
+ */
7
+ export const MAX_BLOCK_NESTING_DEPTH = 5;
8
+ export const EDITABLE_PAGE_KEYS = [
9
+ 'index',
10
+ 'cart',
11
+ 'product',
12
+ 'page',
13
+ 'blog',
14
+ 'blogs',
15
+ 'collection',
16
+ 'category',
17
+ 'checkout',
18
+ 'orders',
19
+ 'search',
20
+ '404'
21
+ ];
22
+ export const SECTION_GROUP_KEYS = ['header-group', 'footer-group'];
23
+ const ALLOWED_AVAILABLE_IN = new Set([
24
+ ...EDITABLE_PAGE_KEYS,
25
+ ...SECTION_GROUP_KEYS
26
+ ]);
27
+ /** Engine SchemaValidator.KNOWN_SETTING_TYPES plus color_scheme. */
28
+ export const SUPPORTED_SETTING_TYPES = new Set([
29
+ 'checkbox',
30
+ 'radio',
31
+ 'select',
32
+ 'collection',
33
+ 'blog',
34
+ 'form',
35
+ 'category',
36
+ 'product',
37
+ 'menu',
38
+ 'color',
39
+ 'color_scheme',
40
+ 'range',
41
+ 'image_picker',
42
+ 'media_picker',
43
+ 'url',
44
+ 'text',
45
+ 'image',
46
+ 'richtext',
47
+ 'inline_richtext',
48
+ 'header'
49
+ ]);
50
+ const OPTION_SETTING_TYPES = new Set(['select', 'radio']);
51
+ const RESOURCE_SETTING_TYPES = new Set([
52
+ 'collection',
53
+ 'category',
54
+ 'product',
55
+ 'blog',
56
+ 'menu'
57
+ ]);
58
+ const META_VALUES = new Set(['desktop', 'mobile']);
59
+ const THEME_BLOCK_PREFIX = 'zc_theme__';
60
+ const APP_BLOCK_PREFIX = 'zc_app__';
61
+ export function isRecord(value) {
62
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
63
+ }
64
+ function isNonEmptyString(value) {
65
+ return typeof value === 'string' && value.trim().length > 0;
66
+ }
67
+ function isPositiveInteger(value) {
68
+ return typeof value === 'number' && Number.isInteger(value) && value > 0;
69
+ }
70
+ function push(issues, message, severity = 'error') {
71
+ issues.push({ message, severity });
72
+ }
73
+ export function validateSchemaDocument(schema, options = {}) {
74
+ const issues = [];
75
+ if (!isNonEmptyString(schema.name)) {
76
+ push(issues, 'name is required and must be a non-empty string');
77
+ }
78
+ if ('tag' in schema && typeof schema.tag !== 'string') {
79
+ push(issues, 'tag must be a string');
80
+ }
81
+ if ('class' in schema && typeof schema.class !== 'string') {
82
+ push(issues, 'class must be a string');
83
+ }
84
+ if ('max_blocks' in schema && !isPositiveInteger(schema.max_blocks)) {
85
+ push(issues, 'max_blocks must be a positive integer');
86
+ }
87
+ const requirementSeverity = options.requirementSeverity ?? 'warning';
88
+ validateAvailableIn(schema, issues, options.requireAvailableIn === true, requirementSeverity);
89
+ validateSettingsArray(schema.settings, 'settings', issues);
90
+ validateSchemaBlocks(schema.blocks, 'blocks', issues, 1);
91
+ validatePresets(schema, issues, options.requirePresets === true, options.themeBlockSchemas, requirementSeverity);
92
+ return issues;
93
+ }
94
+ function validateAvailableIn(schema, issues, required, requirementSeverity) {
95
+ if (!('available_in' in schema)) {
96
+ if (required) {
97
+ push(issues, 'available_in is required and must list at least one page in available_in.templates', requirementSeverity);
98
+ }
99
+ return;
100
+ }
101
+ if (!isRecord(schema.available_in)) {
102
+ push(issues, 'available_in must be an object', 'warning');
103
+ return;
104
+ }
105
+ const templates = schema.available_in.templates;
106
+ if (templates === undefined || templates === null) {
107
+ if (required) {
108
+ push(issues, 'available_in.templates must contain at least one page', requirementSeverity);
109
+ }
110
+ return;
111
+ }
112
+ if (!Array.isArray(templates)) {
113
+ push(issues, 'available_in.templates must be an array', 'warning');
114
+ return;
115
+ }
116
+ if (required && templates.length === 0) {
117
+ push(issues, 'available_in.templates must contain at least one page', requirementSeverity);
118
+ }
119
+ templates.forEach((value, index) => {
120
+ const path = `available_in.templates[${index}]`;
121
+ if (typeof value !== 'string') {
122
+ push(issues, `${path} must be a string`, 'warning');
123
+ return;
124
+ }
125
+ if (!ALLOWED_AVAILABLE_IN.has(value)) {
126
+ push(issues, `${path} '${value}' is not an editable page; allowed values: ${[
127
+ ...ALLOWED_AVAILABLE_IN
128
+ ].join(', ')}`, 'warning');
129
+ }
130
+ });
131
+ }
132
+ function validateSettingsArray(settings, path, issues) {
133
+ if (settings === undefined || settings === null) {
134
+ return;
135
+ }
136
+ if (!Array.isArray(settings)) {
137
+ push(issues, `${path} must be an array`);
138
+ return;
139
+ }
140
+ const seenIds = new Set();
141
+ settings.forEach((setting, index) => {
142
+ const settingPath = `${path}[${index}]`;
143
+ if (!isRecord(setting)) {
144
+ push(issues, `${settingPath} must be an object`);
145
+ return;
146
+ }
147
+ validateSetting(setting, settingPath, issues, seenIds);
148
+ });
149
+ }
150
+ function validateSetting(setting, path, issues, seenIds) {
151
+ const type = setting.type;
152
+ if (!isNonEmptyString(type)) {
153
+ push(issues, `${path}.type is required`);
154
+ return;
155
+ }
156
+ if (!SUPPORTED_SETTING_TYPES.has(type)) {
157
+ push(issues, `${path}.type '${type}' is not a recognised setting type`);
158
+ return;
159
+ }
160
+ if ('meta' in setting) {
161
+ if (typeof setting.meta !== 'string' || !META_VALUES.has(setting.meta)) {
162
+ push(issues, `${path}.meta must be one of desktop, mobile`);
163
+ }
164
+ }
165
+ if (type === 'header') {
166
+ if (!isNonEmptyString(setting.content)) {
167
+ push(issues, `${path} (header) must have a content`);
168
+ }
169
+ return;
170
+ }
171
+ const id = setting.id;
172
+ if (!isNonEmptyString(id)) {
173
+ push(issues, `${path}.id is required for type '${type}'`);
174
+ }
175
+ else if (seenIds.has(id)) {
176
+ push(issues, `${path}.id '${id}' is duplicated in ${path.split('[')[0]}`, 'warning');
177
+ }
178
+ else {
179
+ seenIds.add(id);
180
+ }
181
+ if (OPTION_SETTING_TYPES.has(type)) {
182
+ validateOptions(setting, path, issues);
183
+ }
184
+ if (type === 'range') {
185
+ validateRange(setting, path, issues);
186
+ return;
187
+ }
188
+ validateStructuredDefault(setting, type, path, issues);
189
+ }
190
+ function validateStructuredDefault(setting, type, path, issues) {
191
+ const needsOptionDefault = OPTION_SETTING_TYPES.has(type);
192
+ const needsResourceDefault = RESOURCE_SETTING_TYPES.has(type);
193
+ const needsUrlDefault = type === 'url';
194
+ const needsImageDefault = type === 'image_picker';
195
+ const needsMediaDefault = type === 'media_picker';
196
+ if (!needsOptionDefault &&
197
+ !needsResourceDefault &&
198
+ !needsUrlDefault &&
199
+ !needsImageDefault &&
200
+ !needsMediaDefault) {
201
+ if (type === 'checkbox' && 'default' in setting && typeof setting.default !== 'boolean') {
202
+ push(issues, `${path}.default must be a boolean for type 'checkbox'`, 'warning');
203
+ }
204
+ return;
205
+ }
206
+ const defaultPath = `${path}.default`;
207
+ if (!('default' in setting)) {
208
+ push(issues, `${defaultPath} is required for type '${type}'`);
209
+ return;
210
+ }
211
+ if (!isRecord(setting.default)) {
212
+ push(issues, `${defaultPath} must be an object`);
213
+ return;
214
+ }
215
+ const def = setting.default;
216
+ if (needsOptionDefault) {
217
+ requireString(def, 'display_name', defaultPath, issues);
218
+ requireString(def, 'value', defaultPath, issues);
219
+ if (typeof def.value === 'string' && Array.isArray(setting.options)) {
220
+ const values = setting.options
221
+ .filter(isRecord)
222
+ .map((option) => option.value)
223
+ .filter((value) => typeof value === 'string');
224
+ if (values.length > 0 && !values.includes(def.value)) {
225
+ push(issues, `${defaultPath}.value '${def.value}' is not one of options (${values.join(', ')})`, 'warning');
226
+ }
227
+ }
228
+ return;
229
+ }
230
+ if (needsUrlDefault) {
231
+ requireString(def, 'display_name', defaultPath, issues);
232
+ requireString(def, 'handle', defaultPath, issues);
233
+ return;
234
+ }
235
+ if (needsResourceDefault) {
236
+ if (type === 'menu') {
237
+ requireString(def, 'display_name', defaultPath, issues);
238
+ }
239
+ requireString(def, 'handle', defaultPath, issues);
240
+ return;
241
+ }
242
+ if (needsImageDefault || needsMediaDefault) {
243
+ requireString(def, 'url', defaultPath, issues);
244
+ optionalBoolean(def, 'enable_customisation', defaultPath, issues);
245
+ if (needsMediaDefault) {
246
+ optionalBoolean(def, 'is_video', defaultPath, issues);
247
+ }
248
+ }
249
+ }
250
+ function requireString(def, key, defaultPath, issues) {
251
+ if (typeof def[key] !== 'string') {
252
+ push(issues, `${defaultPath}.${key} is required and must be a string`);
253
+ }
254
+ }
255
+ function optionalBoolean(def, key, defaultPath, issues) {
256
+ if (key in def && typeof def[key] !== 'boolean') {
257
+ push(issues, `${defaultPath}.${key} must be a boolean`);
258
+ }
259
+ }
260
+ function validateOptions(setting, path, issues) {
261
+ if (!Array.isArray(setting.options)) {
262
+ push(issues, `${path}.options is required and must be an array`);
263
+ return;
264
+ }
265
+ if (setting.options.length === 0) {
266
+ push(issues, `${path}.options must not be empty`);
267
+ return;
268
+ }
269
+ setting.options.forEach((option, index) => {
270
+ const optionPath = `${path}.options[${index}]`;
271
+ if (!isRecord(option)) {
272
+ push(issues, `${optionPath} must be an object`);
273
+ return;
274
+ }
275
+ if (typeof option.value !== 'string') {
276
+ push(issues, `${optionPath}.value is required`);
277
+ }
278
+ if (typeof option.label !== 'string') {
279
+ push(issues, `${optionPath}.label is required`);
280
+ }
281
+ });
282
+ }
283
+ function validateRange(setting, path, issues) {
284
+ if ('unit' in setting && typeof setting.unit !== 'string') {
285
+ push(issues, `${path}.unit must be a string`);
286
+ }
287
+ const min = setting.min;
288
+ const max = setting.max;
289
+ if (typeof min !== 'number') {
290
+ push(issues, `${path}.min is required and must be a number for type 'range'`);
291
+ }
292
+ if (typeof max !== 'number') {
293
+ push(issues, `${path}.max is required and must be a number for type 'range'`);
294
+ }
295
+ if (typeof min === 'number' && typeof max === 'number') {
296
+ if (min >= max) {
297
+ push(issues, `${path}.min must be less than max`);
298
+ }
299
+ if ('step' in setting &&
300
+ (typeof setting.step !== 'number' || setting.step <= 0)) {
301
+ push(issues, `${path}.step must be a positive number`);
302
+ }
303
+ if (typeof setting.default === 'number') {
304
+ if (setting.default < min || setting.default > max) {
305
+ push(issues, `${path}.default must be within the min/max range`);
306
+ }
307
+ }
308
+ }
309
+ }
310
+ function validateSchemaBlocks(blocks, path, issues, depth) {
311
+ if (blocks === undefined || blocks === null) {
312
+ return;
313
+ }
314
+ if (!Array.isArray(blocks)) {
315
+ push(issues, `${path} must be an array`);
316
+ return;
317
+ }
318
+ if (depth > MAX_BLOCK_NESTING_DEPTH) {
319
+ push(issues, `${path} nesting exceeds the maximum depth of ${MAX_BLOCK_NESTING_DEPTH}`);
320
+ return;
321
+ }
322
+ blocks.forEach((block, index) => {
323
+ const blockPath = `${path}[${index}]`;
324
+ if (!isRecord(block)) {
325
+ push(issues, `${blockPath} must be an object`);
326
+ return;
327
+ }
328
+ const type = block.type;
329
+ if (!isNonEmptyString(type)) {
330
+ push(issues, `${blockPath}.type is required`);
331
+ return;
332
+ }
333
+ if (type === '@theme' ||
334
+ type === '@widget' ||
335
+ type === '@app' ||
336
+ type.startsWith(THEME_BLOCK_PREFIX) ||
337
+ type.startsWith(APP_BLOCK_PREFIX)) {
338
+ return;
339
+ }
340
+ if ('limit' in block && !isPositiveInteger(block.limit)) {
341
+ push(issues, `${blockPath}.limit must be a positive integer`);
342
+ }
343
+ if ('settings' in block) {
344
+ validateSettingsArray(block.settings, `${blockPath}.settings`, issues);
345
+ }
346
+ validateSchemaBlocks(block.blocks, `${blockPath}.blocks`, issues, depth + 1);
347
+ });
348
+ }
349
+ function validatePresets(schema, issues, required, themeBlockSchemas, requirementSeverity = 'warning') {
350
+ if (!('presets' in schema)) {
351
+ if (required) {
352
+ push(issues, 'presets must contain at least one preset with a name', requirementSeverity);
353
+ }
354
+ return;
355
+ }
356
+ if (!Array.isArray(schema.presets)) {
357
+ push(issues, 'presets must be an array');
358
+ return;
359
+ }
360
+ if (required && schema.presets.length === 0) {
361
+ push(issues, 'presets must contain at least one preset with a name', requirementSeverity);
362
+ }
363
+ schema.presets.forEach((preset, index) => {
364
+ const path = `presets[${index}]`;
365
+ if (!isRecord(preset)) {
366
+ push(issues, `${path} must be an object`);
367
+ return;
368
+ }
369
+ if (!isNonEmptyString(preset.name)) {
370
+ push(issues, `${path}.name is required`, required ? requirementSeverity : 'error');
371
+ }
372
+ if ('settings' in preset && !isRecord(preset.settings)) {
373
+ push(issues, `${path}.settings must be an object`);
374
+ }
375
+ validatePresetBlocks(preset, path, schema, issues, 1, themeBlockSchemas);
376
+ });
377
+ }
378
+ function validatePresetBlocks(preset, path, containerSchema, issues, depth, themeBlockSchemas) {
379
+ if (!('blocks' in preset)) {
380
+ return;
381
+ }
382
+ if (!isRecord(preset.blocks)) {
383
+ push(issues, `${path}.blocks must be an object keyed by block name, with block_order. An array is ignored by the engine, so those preset blocks are never created`, 'warning');
384
+ return;
385
+ }
386
+ const presetBlocks = preset.blocks;
387
+ if (depth > MAX_BLOCK_NESTING_DEPTH) {
388
+ push(issues, `${path}.blocks nesting exceeds the maximum depth of ${MAX_BLOCK_NESTING_DEPTH}`);
389
+ return;
390
+ }
391
+ for (const [blockName, block] of Object.entries(presetBlocks)) {
392
+ const blockType = isRecord(block) ? block.type : undefined;
393
+ if (!isNonEmptyString(blockType)) {
394
+ push(issues, `${path}.blocks.${blockName}.type is required`);
395
+ }
396
+ else if (containerSchema &&
397
+ !isBlockTypeAllowed(containerSchema, blockType)) {
398
+ push(issues, `${path}.blocks.${blockName} type '${blockType}' is not allowed by the container schema blocks`);
399
+ }
400
+ if (isRecord(block)) {
401
+ const childSchema = isNonEmptyString(blockType)
402
+ ? resolveChildContainerSchema(containerSchema, blockType, themeBlockSchemas)
403
+ : null;
404
+ validatePresetBlocks(block, `${path}.blocks.${blockName}`, childSchema, issues, depth + 1, themeBlockSchemas);
405
+ }
406
+ }
407
+ if (!('block_order' in preset)) {
408
+ return;
409
+ }
410
+ if (!Array.isArray(preset.block_order)) {
411
+ push(issues, `${path}.block_order must be an array`);
412
+ return;
413
+ }
414
+ preset.block_order.forEach((blockName, index) => {
415
+ if (typeof blockName !== 'string' || !(blockName in presetBlocks)) {
416
+ push(issues, `${path}.block_order[${index}] '${String(blockName)}' is not a key in blocks`);
417
+ }
418
+ });
419
+ }
420
+ export function isBlockTypeAllowed(containerSchema, blockType) {
421
+ if (!containerSchema || !blockType) {
422
+ return false;
423
+ }
424
+ const blocks = containerSchema.blocks;
425
+ if (!Array.isArray(blocks)) {
426
+ return false;
427
+ }
428
+ let allowsAllThemeBlocks = false;
429
+ let allowsAppBlocks = false;
430
+ for (const blockSchema of blocks) {
431
+ if (!isRecord(blockSchema) || typeof blockSchema.type !== 'string') {
432
+ continue;
433
+ }
434
+ if (blockSchema.type === '@theme') {
435
+ allowsAllThemeBlocks = true;
436
+ }
437
+ else if (blockSchema.type === '@widget' || blockSchema.type === '@app') {
438
+ allowsAppBlocks = true;
439
+ }
440
+ else if (blockSchema.type === blockType) {
441
+ return true;
442
+ }
443
+ }
444
+ if (blockType.startsWith(THEME_BLOCK_PREFIX)) {
445
+ return allowsAllThemeBlocks;
446
+ }
447
+ if (blockType.startsWith(APP_BLOCK_PREFIX)) {
448
+ return allowsAppBlocks;
449
+ }
450
+ return false;
451
+ }
452
+ /**
453
+ * Schema whose `blocks` allow a placed block's children.
454
+ * Theme-block files are resolved by the caller via `themeBlockSchemas`.
455
+ */
456
+ export function resolveChildContainerSchema(containerSchema, blockType, themeBlockSchemas) {
457
+ if (!blockType) {
458
+ return null;
459
+ }
460
+ if (blockType.startsWith(THEME_BLOCK_PREFIX)) {
461
+ return themeBlockSchemas?.get(blockType.slice(THEME_BLOCK_PREFIX.length)) ?? null;
462
+ }
463
+ if (!containerSchema || !Array.isArray(containerSchema.blocks)) {
464
+ return null;
465
+ }
466
+ for (const blockSchema of containerSchema.blocks) {
467
+ if (isRecord(blockSchema) && blockSchema.type === blockType) {
468
+ return blockSchema;
469
+ }
470
+ }
471
+ return null;
472
+ }
473
+ export function declaredThemeBlockNames(schema) {
474
+ if (!Array.isArray(schema.blocks)) {
475
+ return [];
476
+ }
477
+ const names = [];
478
+ for (const block of schema.blocks) {
479
+ if (isRecord(block) &&
480
+ typeof block.type === 'string' &&
481
+ block.type.startsWith(THEME_BLOCK_PREFIX)) {
482
+ names.push(block.type.slice(THEME_BLOCK_PREFIX.length));
483
+ }
484
+ }
485
+ return names;
486
+ }
487
+ export function validateThemeSettingsGroups(groups) {
488
+ const issues = [];
489
+ if (!Array.isArray(groups)) {
490
+ return [
491
+ {
492
+ message: 'settings/schema.json must be an array of setting groups',
493
+ severity: 'error'
494
+ }
495
+ ];
496
+ }
497
+ groups.forEach((group, index) => {
498
+ const path = `groups[${index}]`;
499
+ if (!isRecord(group)) {
500
+ push(issues, `${path} must be an object`);
501
+ return;
502
+ }
503
+ if (!isNonEmptyString(group.name)) {
504
+ push(issues, `${path}.name is required and must be a non-empty string`);
505
+ }
506
+ validateSettingsArray(group.settings, `${path}.settings`, issues);
507
+ });
508
+ return issues;
509
+ }
510
+ export function validateThemeSettingsData(data) {
511
+ const issues = [];
512
+ if (!isRecord(data)) {
513
+ return [
514
+ {
515
+ message: 'settings/theme.json must be an object',
516
+ severity: 'error'
517
+ }
518
+ ];
519
+ }
520
+ if (!isRecord(data.presets)) {
521
+ return [
522
+ {
523
+ message: 'settings/theme.json presets must be an object',
524
+ severity: 'error'
525
+ }
526
+ ];
527
+ }
528
+ const presetNames = Object.keys(data.presets);
529
+ let selectedName;
530
+ if (typeof data.selected === 'string') {
531
+ selectedName = data.selected;
532
+ }
533
+ else if (isRecord(data.selected) && typeof data.selected.preset_name === 'string') {
534
+ selectedName = data.selected.preset_name;
535
+ }
536
+ else {
537
+ push(issues, 'settings/theme.json selected must be a preset name or { preset_name }');
538
+ return issues;
539
+ }
540
+ if (!presetNames.includes(selectedName)) {
541
+ push(issues, `settings/theme.json selected preset '${selectedName}' is not a key in presets`);
542
+ }
543
+ return issues;
544
+ }
@@ -1,4 +1,4 @@
1
1
  import { LiveReloadServer } from 'livereload';
2
2
  export declare function findAvailablePort(): Promise<number>;
3
- export declare function watchFolders(liveReloadServer: LiveReloadServer, onChange?: () => void | Promise<void>): void;
3
+ export declare function watchFolders(liveReloadServer?: LiveReloadServer, onChange?: () => void | Promise<void>): void;
4
4
  //# sourceMappingURL=serve-theme-utils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"serve-theme-utils.d.ts","sourceRoot":"","sources":["../../src/utils/serve-theme-utils.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAY9C,wBAAsB,iBAAiB,oBAWtC;AAID,wBAAgB,YAAY,CAC1B,gBAAgB,EAAE,gBAAgB,EAClC,QAAQ,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,QA2CtC"}
1
+ {"version":3,"file":"serve-theme-utils.d.ts","sourceRoot":"","sources":["../../src/utils/serve-theme-utils.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAY9C,wBAAsB,iBAAiB,oBAWtC;AAID,wBAAgB,YAAY,CAC1B,gBAAgB,CAAC,EAAE,gBAAgB,EACnC,QAAQ,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,QA2CtC"}
@@ -57,7 +57,7 @@ export function watchFolders(liveReloadServer, onChange) {
57
57
  console.error(chalk.red('Failed to revalidate theme:'), err);
58
58
  }
59
59
  }
60
- liveReloadServer.refresh('/');
60
+ liveReloadServer?.refresh('/');
61
61
  });
62
62
  }
63
63
  catch (err) {
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Page JSON, section-group JSON, and Liquid reference checks.
3
+ * Mirrors PageJsonValidator / RenderUtil.validateBlocksAllowed so a theme
4
+ * fails in the CLI before the engine rejects it at render or save.
5
+ */
6
+ import type { ValidationIssue } from './theme-validation.js';
7
+ export declare function validateLiquidSchemas(cwd: string, mode?: 'dev' | 'build'): ValidationIssue[];
8
+ export declare function validateThemeSettingsFiles(cwd: string): ValidationIssue[];
9
+ export declare function validateTemplateJson(cwd: string): ValidationIssue[];
10
+ export declare function validateLiquidReferences(cwd: string): ValidationIssue[];
11
+ //# sourceMappingURL=template-reference-validation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"template-reference-validation.d.ts","sourceRoot":"","sources":["../../src/utils/template-reference-validation.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AA8J7D,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,MAAM,EACX,IAAI,GAAE,KAAK,GAAG,OAAiB,GAC9B,eAAe,EAAE,CA6GnB;AAED,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,EAAE,CAsCzE;AAED,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,EAAE,CAsKnE;AAkOD,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,EAAE,CAkFvE"}