@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,466 @@
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 fs from 'fs-extra';
7
+ import path from 'path';
8
+ import { MAX_BLOCK_NESTING_DEPTH, declaredThemeBlockNames, isBlockTypeAllowed, isRecord, resolveChildContainerSchema, validateSchemaDocument, validateThemeSettingsData, validateThemeSettingsGroups } from './schema-validation.js';
9
+ const SCHEMA_BLOCK = /\{%-?\s*schema\s*-?%\}([\s\S]*?)\{%-?\s*endschema\s*-?%\}/;
10
+ const UNSUPPORTED_TAGS = new Set([
11
+ 'paginate',
12
+ 'endpaginate',
13
+ 'form',
14
+ 'endform',
15
+ 'stylesheet',
16
+ 'endstylesheet',
17
+ 'include',
18
+ 'layout',
19
+ 'highlight',
20
+ 'endhighlight'
21
+ ]);
22
+ const SUPPORTED_TAGS = new Set([
23
+ 'if',
24
+ 'elsif',
25
+ 'else',
26
+ 'endif',
27
+ 'unless',
28
+ 'endunless',
29
+ 'case',
30
+ 'when',
31
+ 'endcase',
32
+ 'for',
33
+ 'endfor',
34
+ 'break',
35
+ 'continue',
36
+ 'cycle',
37
+ 'tablerow',
38
+ 'endtablerow',
39
+ 'liquid',
40
+ 'assign',
41
+ 'capture',
42
+ 'endcapture',
43
+ 'increment',
44
+ 'decrement',
45
+ 'echo',
46
+ 'comment',
47
+ 'endcomment',
48
+ 'raw',
49
+ 'endraw',
50
+ 'render',
51
+ 'section',
52
+ 'sections',
53
+ 'render_blocks',
54
+ 'render_block',
55
+ 'schema',
56
+ 'endschema',
57
+ 'style',
58
+ 'endstyle',
59
+ 'javascript',
60
+ 'endjavascript'
61
+ ]);
62
+ const THEME_BLOCK_PREFIX = 'zc_theme__';
63
+ const CUSTOM_SECTION_PREFIX = 'custom_section_';
64
+ function issue(category, message, file, severity = 'error', failsBuild = false) {
65
+ return failsBuild
66
+ ? { category, message, file, severity, failsBuild }
67
+ : { category, message, file, severity };
68
+ }
69
+ function readJsonFile(filePath) {
70
+ try {
71
+ return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
72
+ }
73
+ catch {
74
+ return undefined;
75
+ }
76
+ }
77
+ function extractSchema(content) {
78
+ const match = content.match(SCHEMA_BLOCK);
79
+ if (!match) {
80
+ return { raw: null, schema: null, invalid: false };
81
+ }
82
+ const raw = match[1].trim();
83
+ if (!raw) {
84
+ return { raw: '', schema: null, invalid: true };
85
+ }
86
+ try {
87
+ const parsed = JSON.parse(raw);
88
+ if (!isRecord(parsed)) {
89
+ return { raw, schema: null, invalid: true };
90
+ }
91
+ return { raw, schema: parsed, invalid: false };
92
+ }
93
+ catch {
94
+ return { raw, schema: null, invalid: true };
95
+ }
96
+ }
97
+ function liquidBaseNames(cwd, folder) {
98
+ const names = new Set();
99
+ const dir = path.join(cwd, folder);
100
+ if (!fs.existsSync(dir)) {
101
+ return names;
102
+ }
103
+ for (const file of fs.readdirSync(dir)) {
104
+ if (file.endsWith('.liquid')) {
105
+ names.add(file.replace(/\.liquid$/, ''));
106
+ }
107
+ }
108
+ return names;
109
+ }
110
+ function loadSchemaIndex(cwd, folder) {
111
+ const index = new Map();
112
+ const dir = path.join(cwd, folder);
113
+ if (!fs.existsSync(dir)) {
114
+ return index;
115
+ }
116
+ for (const file of fs.readdirSync(dir)) {
117
+ if (!file.endsWith('.liquid')) {
118
+ continue;
119
+ }
120
+ const content = fs.readFileSync(path.join(dir, file), 'utf-8');
121
+ const { schema } = extractSchema(content);
122
+ if (schema) {
123
+ index.set(file.replace(/\.liquid$/, ''), schema);
124
+ }
125
+ }
126
+ return index;
127
+ }
128
+ export function validateLiquidSchemas(cwd, mode = 'build') {
129
+ const issues = [];
130
+ const themeBlockSchemas = loadSchemaIndex(cwd, 'blocks');
131
+ const themeBlockFiles = liquidBaseNames(cwd, 'blocks');
132
+ const sectionRequirementSeverity = mode === 'build' ? 'error' : 'warning';
133
+ // Requirement findings are warnings in dev but errors in build.
134
+ const requirementFailsBuild = mode !== 'build';
135
+ for (const folder of ['sections', 'blocks']) {
136
+ const dir = path.join(cwd, folder);
137
+ if (!fs.existsSync(dir)) {
138
+ continue;
139
+ }
140
+ for (const file of fs.readdirSync(dir).filter((name) => name.endsWith('.liquid'))) {
141
+ const relative = path.posix.join(folder, file);
142
+ const content = fs.readFileSync(path.join(dir, file), 'utf-8');
143
+ const extracted = extractSchema(content);
144
+ if (!extracted.schema && !extracted.invalid) {
145
+ if (folder === 'blocks') {
146
+ issues.push(issue('schema', 'Theme block file must contain {% schema %}...{% endschema %}', relative, 'warning'));
147
+ }
148
+ else {
149
+ issues.push(issue('schema', 'available_in is required and must list at least one page in available_in.templates', relative, sectionRequirementSeverity, requirementFailsBuild));
150
+ issues.push(issue('schema', 'presets must contain at least one preset with a name', relative, sectionRequirementSeverity, requirementFailsBuild));
151
+ }
152
+ continue;
153
+ }
154
+ if (extracted.invalid) {
155
+ issues.push(issue('schema', 'Schema block must contain valid JSON', relative));
156
+ continue;
157
+ }
158
+ if (!extracted.schema) {
159
+ continue;
160
+ }
161
+ const schemaOptions = {
162
+ requireAvailableIn: folder === 'sections',
163
+ requirePresets: folder === 'sections',
164
+ themeBlockSchemas
165
+ };
166
+ const messages = validateSchemaDocument(extracted.schema, {
167
+ ...schemaOptions,
168
+ requirementSeverity: folder === 'sections' ? sectionRequirementSeverity : 'error'
169
+ });
170
+ // Same findings, in the same order, as build would report them.
171
+ const buildMessages = requirementFailsBuild
172
+ ? validateSchemaDocument(extracted.schema, {
173
+ ...schemaOptions,
174
+ requirementSeverity: 'error'
175
+ })
176
+ : messages;
177
+ messages.forEach((finding, index) => {
178
+ issues.push(issue('schema', finding.message, relative, finding.severity, finding.severity === 'warning' &&
179
+ buildMessages[index]?.severity === 'error'));
180
+ });
181
+ for (const blockName of declaredThemeBlockNames(extracted.schema)) {
182
+ if (!themeBlockFiles.has(blockName)) {
183
+ issues.push(issue('schema', `Theme block 'zc_theme__${blockName}' has no blocks/${blockName}.liquid file`, relative));
184
+ }
185
+ }
186
+ }
187
+ }
188
+ return issues;
189
+ }
190
+ export function validateThemeSettingsFiles(cwd) {
191
+ const issues = [];
192
+ const schemaPath = path.join(cwd, 'settings', 'schema.json');
193
+ const dataPath = path.join(cwd, 'settings', 'theme.json');
194
+ if (fs.existsSync(schemaPath)) {
195
+ const parsed = readJsonFile(schemaPath);
196
+ if (parsed !== undefined) {
197
+ for (const finding of validateThemeSettingsGroups(parsed)) {
198
+ issues.push(issue('theme_settings', finding.message, 'settings/schema.json', finding.severity));
199
+ }
200
+ }
201
+ }
202
+ if (fs.existsSync(dataPath)) {
203
+ const parsed = readJsonFile(dataPath);
204
+ if (parsed !== undefined) {
205
+ for (const finding of validateThemeSettingsData(parsed)) {
206
+ issues.push(issue('theme_settings', finding.message, 'settings/theme.json', finding.severity));
207
+ }
208
+ }
209
+ }
210
+ return issues;
211
+ }
212
+ export function validateTemplateJson(cwd) {
213
+ const issues = [];
214
+ const sectionSchemas = loadSchemaIndex(cwd, 'sections');
215
+ const themeBlockSchemas = loadSchemaIndex(cwd, 'blocks');
216
+ const themeBlockFiles = liquidBaseNames(cwd, 'blocks');
217
+ const files = [];
218
+ const pagesDir = path.join(cwd, 'pages');
219
+ if (fs.existsSync(pagesDir)) {
220
+ for (const file of fs.readdirSync(pagesDir)) {
221
+ if (file.endsWith('.json')) {
222
+ files.push(path.posix.join('pages', file));
223
+ }
224
+ }
225
+ }
226
+ const sectionsDir = path.join(cwd, 'sections');
227
+ if (fs.existsSync(sectionsDir)) {
228
+ for (const file of fs.readdirSync(sectionsDir)) {
229
+ if (file.endsWith('.json')) {
230
+ files.push(path.posix.join('sections', file));
231
+ }
232
+ }
233
+ }
234
+ for (const relative of files) {
235
+ const parsed = readJsonFile(path.join(cwd, relative));
236
+ if (!isRecord(parsed)) {
237
+ continue;
238
+ }
239
+ if (typeof parsed.layout === 'string' && parsed.layout.trim()) {
240
+ const layoutFile = `layouts/${parsed.layout}.liquid`;
241
+ if (!fs.existsSync(path.join(cwd, layoutFile))) {
242
+ issues.push(issue('page', `layout '${parsed.layout}' has no ${layoutFile}`, relative));
243
+ }
244
+ }
245
+ if (!('order' in parsed) && !('sections' in parsed)) {
246
+ continue;
247
+ }
248
+ if (!Array.isArray(parsed.order)) {
249
+ issues.push(issue('page', 'order must be an array', relative));
250
+ continue;
251
+ }
252
+ if (!isRecord(parsed.sections)) {
253
+ issues.push(issue('page', 'sections must be an object', relative));
254
+ continue;
255
+ }
256
+ const sections = parsed.sections;
257
+ const seen = new Set();
258
+ parsed.order.forEach((entry, index) => {
259
+ if (typeof entry !== 'string' || !entry) {
260
+ issues.push(issue('page', `order[${index}] must be a section name`, relative));
261
+ return;
262
+ }
263
+ if (seen.has(entry)) {
264
+ issues.push(issue('page', `order lists section '${entry}' more than once`, relative, 'warning'));
265
+ }
266
+ seen.add(entry);
267
+ const section = sections[entry];
268
+ if (!isRecord(section)) {
269
+ issues.push(issue('page', `Section '${entry}' is listed in order but is missing from sections`, relative));
270
+ return;
271
+ }
272
+ const type = section.type;
273
+ if (typeof type !== 'string' || !type.trim()) {
274
+ issues.push(issue('page', `Section '${entry}' has no type`, relative));
275
+ return;
276
+ }
277
+ if (!type.startsWith(CUSTOM_SECTION_PREFIX)) {
278
+ const sectionFile = `sections/${type}.liquid`;
279
+ if (!fs.existsSync(path.join(cwd, sectionFile))) {
280
+ issues.push(issue('page', `Section '${type}' does not exist in the theme (${sectionFile})`, relative));
281
+ }
282
+ }
283
+ const sectionSchema = sectionSchemas.get(type) ?? null;
284
+ validatePlacedBlocks(section, sectionSchema, entry, relative, issues, themeBlockSchemas, themeBlockFiles, 1);
285
+ if (sectionSchema &&
286
+ typeof sectionSchema.max_blocks === 'number' &&
287
+ Array.isArray(section.block_order) &&
288
+ section.block_order.length > sectionSchema.max_blocks) {
289
+ issues.push(issue('page', `Section '${entry}' has ${section.block_order.length} blocks but max_blocks is ${sectionSchema.max_blocks}`, relative));
290
+ }
291
+ if (sectionSchema && Array.isArray(sectionSchema.blocks) && Array.isArray(section.block_order) && isRecord(section.blocks)) {
292
+ validateBlockLimits(section.blocks, section.block_order, sectionSchema.blocks, entry, relative, issues);
293
+ }
294
+ });
295
+ for (const key of Object.keys(sections)) {
296
+ if (!seen.has(key)) {
297
+ issues.push(issue('page', `Section '${key}' is in sections but not in order, so it is never rendered`, relative, 'warning'));
298
+ }
299
+ }
300
+ }
301
+ return issues;
302
+ }
303
+ function validateBlockLimits(blocks, blockOrder, schemaBlocks, containerName, file, issues) {
304
+ for (const schemaBlock of schemaBlocks) {
305
+ if (!isRecord(schemaBlock) || typeof schemaBlock.type !== 'string') {
306
+ continue;
307
+ }
308
+ if (typeof schemaBlock.limit !== 'number') {
309
+ continue;
310
+ }
311
+ const schemaType = schemaBlock.type;
312
+ const countApp = schemaType === '@widget' || schemaType === '@app';
313
+ let count = 0;
314
+ for (const key of blockOrder) {
315
+ if (typeof key !== 'string') {
316
+ continue;
317
+ }
318
+ const block = blocks[key];
319
+ if (!isRecord(block) || typeof block.type !== 'string') {
320
+ continue;
321
+ }
322
+ if (countApp && block.type.startsWith('zc_app__')) {
323
+ count += 1;
324
+ }
325
+ else if (block.type === schemaType) {
326
+ count += 1;
327
+ }
328
+ }
329
+ if (count > schemaBlock.limit) {
330
+ issues.push(issue('page', `Block type '${schemaType}' is used ${count} times in '${containerName}' but limit is ${schemaBlock.limit}`, file));
331
+ }
332
+ }
333
+ }
334
+ function validatePlacedBlocks(container, containerSchema, containerName, file, issues, themeBlockSchemas, themeBlockFiles, depth) {
335
+ const hasBlocks = 'blocks' in container;
336
+ const hasOrder = 'block_order' in container;
337
+ if (!hasBlocks && !hasOrder) {
338
+ return;
339
+ }
340
+ if (hasBlocks && !isRecord(container.blocks)) {
341
+ issues.push(issue('page', `'${containerName}'.blocks must be an object`, file));
342
+ return;
343
+ }
344
+ if (hasOrder && !Array.isArray(container.block_order)) {
345
+ issues.push(issue('page', `'${containerName}'.block_order must be an array`, file));
346
+ return;
347
+ }
348
+ if (!isRecord(container.blocks) || !Array.isArray(container.block_order)) {
349
+ issues.push(issue('page', `'${containerName}' must set both blocks and block_order`, file));
350
+ return;
351
+ }
352
+ const placedBlocks = container.blocks;
353
+ const blockOrder = container.block_order;
354
+ if (depth > MAX_BLOCK_NESTING_DEPTH) {
355
+ issues.push(issue('page', `Block nesting under '${containerName}' exceeds the maximum depth of ${MAX_BLOCK_NESTING_DEPTH}`, file));
356
+ return;
357
+ }
358
+ const referenced = new Set();
359
+ blockOrder.forEach((entry, index) => {
360
+ if (typeof entry !== 'string' || !entry) {
361
+ issues.push(issue('page', `'${containerName}'.block_order[${index}] must be a block name`, file));
362
+ return;
363
+ }
364
+ referenced.add(entry);
365
+ const block = placedBlocks[entry];
366
+ if (!isRecord(block)) {
367
+ issues.push(issue('page', `Block '${entry}' is listed in block_order but is missing from '${containerName}'.blocks`, file));
368
+ return;
369
+ }
370
+ const blockType = block.type;
371
+ if (typeof blockType !== 'string' || !blockType.trim()) {
372
+ issues.push(issue('page', `Block '${entry}' has no type`, file));
373
+ return;
374
+ }
375
+ if (blockType === '@theme' || blockType === '@widget' || blockType === '@app') {
376
+ return;
377
+ }
378
+ if (containerSchema && !isBlockTypeAllowed(containerSchema, blockType)) {
379
+ issues.push(issue('page', `Block type '${blockType}' is not allowed in '${containerName}' — declare it in the container schema blocks`, file));
380
+ }
381
+ if (blockType.startsWith(THEME_BLOCK_PREFIX) &&
382
+ !themeBlockFiles.has(blockType.slice(THEME_BLOCK_PREFIX.length))) {
383
+ const blockName = blockType.slice(THEME_BLOCK_PREFIX.length);
384
+ issues.push(issue('page', `Theme block '${blockType}' does not exist (blocks/${blockName}.liquid)`, file));
385
+ }
386
+ if (depth < MAX_BLOCK_NESTING_DEPTH &&
387
+ !blockType.startsWith('zc_app__')) {
388
+ const childSchema = blockType.startsWith(THEME_BLOCK_PREFIX)
389
+ ? themeBlockSchemas.get(blockType.slice(THEME_BLOCK_PREFIX.length)) ?? null
390
+ : resolveChildContainerSchema(containerSchema, blockType, themeBlockSchemas);
391
+ validatePlacedBlocks(block, childSchema, entry, file, issues, themeBlockSchemas, themeBlockFiles, depth + 1);
392
+ }
393
+ });
394
+ for (const key of Object.keys(placedBlocks)) {
395
+ if (!referenced.has(key)) {
396
+ issues.push(issue('page', `Block '${key}' is in '${containerName}'.blocks but not in block_order, so it is never rendered`, file, 'warning'));
397
+ }
398
+ }
399
+ }
400
+ function stripNonExecutableLiquid(content) {
401
+ return content
402
+ .replace(/\{%-?\s*schema\s*-?%\}[\s\S]*?\{%-?\s*endschema\s*-?%\}/g, '')
403
+ .replace(/\{%-?\s*comment\s*-?%\}[\s\S]*?\{%-?\s*endcomment\s*-?%\}/g, '')
404
+ .replace(/\{%-?\s*raw\s*-?%\}[\s\S]*?\{%-?\s*endraw\s*-?%\}/g, '');
405
+ }
406
+ function liquidFiles(cwd) {
407
+ const files = [];
408
+ for (const folder of ['layouts', 'sections', 'blocks', 'snippets']) {
409
+ const dir = path.join(cwd, folder);
410
+ if (!fs.existsSync(dir)) {
411
+ continue;
412
+ }
413
+ for (const file of fs.readdirSync(dir)) {
414
+ if (file.endsWith('.liquid')) {
415
+ files.push(path.posix.join(folder, file));
416
+ }
417
+ }
418
+ }
419
+ return files;
420
+ }
421
+ export function validateLiquidReferences(cwd) {
422
+ const issues = [];
423
+ const tagPattern = /\{%-?\s*([a-zA-Z_][\w]*)/g;
424
+ const renderPattern = /\{%-?\s*render\s+(['"])([^'"]+)\1/g;
425
+ const sectionPattern = /\{%-?\s*section\s+(['"])([^'"]+)\1/g;
426
+ const sectionsPattern = /\{%-?\s*sections\s+(['"])([^'"]+)\1/g;
427
+ for (const relative of liquidFiles(cwd)) {
428
+ const content = stripNonExecutableLiquid(fs.readFileSync(path.join(cwd, relative), 'utf-8'));
429
+ const reportedTags = new Set();
430
+ for (const match of content.matchAll(tagPattern)) {
431
+ const tag = match[1];
432
+ if (SUPPORTED_TAGS.has(tag) || reportedTags.has(tag)) {
433
+ continue;
434
+ }
435
+ reportedTags.add(tag);
436
+ if (UNSUPPORTED_TAGS.has(tag)) {
437
+ issues.push(issue('liquid', `Tag '{% ${tag} %}' is not supported by the storefront engine`, relative));
438
+ }
439
+ else {
440
+ issues.push(issue('liquid', `Unknown Liquid tag '{% ${tag} %}'. The storefront engine will fail when this file renders`, relative));
441
+ }
442
+ }
443
+ for (const match of content.matchAll(renderPattern)) {
444
+ const snippet = match[2];
445
+ const snippetFile = path.join(cwd, 'snippets', `${snippet}.liquid`);
446
+ if (!fs.existsSync(snippetFile)) {
447
+ issues.push(issue('liquid', `Snippet '${snippet}' does not exist (snippets/${snippet}.liquid)`, relative));
448
+ }
449
+ }
450
+ for (const match of content.matchAll(sectionPattern)) {
451
+ const sectionName = match[2];
452
+ const sectionFile = path.join(cwd, 'sections', `${sectionName}.liquid`);
453
+ if (!fs.existsSync(sectionFile)) {
454
+ issues.push(issue('liquid', `Section '${sectionName}' does not exist (sections/${sectionName}.liquid)`, relative));
455
+ }
456
+ }
457
+ for (const match of content.matchAll(sectionsPattern)) {
458
+ const groupName = match[2];
459
+ const groupFile = path.join(cwd, 'sections', `${groupName}.json`);
460
+ if (!fs.existsSync(groupFile)) {
461
+ issues.push(issue('liquid', `Section group '${groupName}' does not exist (sections/${groupName}.json)`, relative));
462
+ }
463
+ }
464
+ }
465
+ return issues;
466
+ }
@@ -0,0 +1,5 @@
1
+ type BlockSchema = Record<string, any>;
2
+ export declare function loadThemeBlockSchema(cwd: string, blockType: string): BlockSchema | null;
3
+ export declare function withThemeBlockSchema(cwd: string, entry: BlockSchema | undefined, blockType: string): BlockSchema | undefined;
4
+ export {};
5
+ //# sourceMappingURL=theme-block-schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme-block-schema.d.ts","sourceRoot":"","sources":["../../src/utils/theme-block-schema.ts"],"names":[],"mappings":"AAKA,KAAK,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAMvC,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,MAAM,GAChB,WAAW,GAAG,IAAI,CAcpB;AAID,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,WAAW,GAAG,SAAS,EAC9B,SAAS,EAAE,MAAM,GAChB,WAAW,GAAG,SAAS,CAWzB"}
@@ -0,0 +1,35 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { isThemeBlockType, parseThemeBlockFileName } from './theme-block-render.js';
4
+ const SCHEMA_PATTERN = /{%\s*schema\s*%}([\s\S]*?){%\s*endschema\s*%}/;
5
+ // Theme blocks (type "zc_theme__<name>") keep their settings in blocks/<name>.liquid,
6
+ // not in the schema of the section that lists them.
7
+ export function loadThemeBlockSchema(cwd, blockType) {
8
+ if (!isThemeBlockType(blockType))
9
+ return null;
10
+ const filePath = path.join(cwd, 'blocks', `${parseThemeBlockFileName(blockType)}.liquid`);
11
+ try {
12
+ if (!fs.existsSync(filePath))
13
+ return null;
14
+ const match = fs.readFileSync(filePath, 'utf-8').match(SCHEMA_PATTERN);
15
+ return match ? JSON.parse(match[1].trim()) : null;
16
+ }
17
+ catch {
18
+ return null;
19
+ }
20
+ }
21
+ // Merges a block entry from a section schema with the schema of its theme block file.
22
+ // Values declared on the section entry (name, limit...) win over the block file.
23
+ export function withThemeBlockSchema(cwd, entry, blockType) {
24
+ const fileSchema = loadThemeBlockSchema(cwd, blockType);
25
+ if (!fileSchema)
26
+ return entry;
27
+ return {
28
+ ...fileSchema,
29
+ ...(entry ?? {}),
30
+ settings: entry?.settings ?? fileSchema.settings ?? [],
31
+ ...(entry?.blocks ?? fileSchema.blocks
32
+ ? { blocks: entry?.blocks ?? fileSchema.blocks }
33
+ : {})
34
+ };
35
+ }
@@ -1,18 +1,25 @@
1
- export type ValidationIssueCategory = 'translation' | 'json' | 'schema' | 'asset_size' | 'required_file';
1
+ export type ValidationIssueCategory = 'translation' | 'json' | 'schema' | 'page' | 'liquid' | 'theme_settings' | 'asset_size' | 'required_file';
2
2
  export type ValidationIssue = {
3
3
  category: ValidationIssueCategory;
4
4
  message: string;
5
5
  file?: string;
6
+ /** Warnings are reported and do not block preview or build. */
7
+ severity?: 'error' | 'warning';
8
+ /** A warning in this mode that is an error in build mode. */
9
+ failsBuild?: boolean;
6
10
  };
7
11
  export type ThemeValidationResult = {
8
12
  valid: boolean;
9
13
  issues: ValidationIssue[];
10
14
  };
11
15
  export type ThemeValidationMode = 'dev' | 'build';
16
+ /** Default object keys required for structured setting types. */
12
17
  export declare const SETTING_RULES: Record<string, string[]>;
13
18
  export declare function validateTheme(cwd?: string, options?: {
14
19
  mode?: ThemeValidationMode;
15
20
  }): Promise<ThemeValidationResult>;
21
+ export declare function validationErrors(issues: ValidationIssue[]): ValidationIssue[];
22
+ export declare function validationWarnings(issues: ValidationIssue[]): ValidationIssue[];
16
23
  export declare function formatValidationIssuesForConsole(issues: ValidationIssue[]): string;
17
24
  export declare function assertThemeValid(result: ThemeValidationResult): void;
18
25
  //# sourceMappingURL=theme-validation.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"theme-validation.d.ts","sourceRoot":"","sources":["../../src/utils/theme-validation.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,uBAAuB,GAC/B,aAAa,GACb,MAAM,GACN,QAAQ,GACR,YAAY,GACZ,eAAe,CAAC;AAEpB,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,EAAE,uBAAuB,CAAC;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,eAAe,EAAE,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,KAAK,GAAG,OAAO,CAAC;AAElD,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAOlD,CAAC;AA2VF,wBAAsB,aAAa,CACjC,GAAG,SAAgB,EACnB,OAAO,GAAE;IAAE,IAAI,CAAC,EAAE,mBAAmB,CAAA;CAAO,GAC3C,OAAO,CAAC,qBAAqB,CAAC,CAehC;AAED,wBAAgB,gCAAgC,CAC9C,MAAM,EAAE,eAAe,EAAE,GACxB,MAAM,CA4BR;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,qBAAqB,GAAG,IAAI,CAOpE"}
1
+ {"version":3,"file":"theme-validation.d.ts","sourceRoot":"","sources":["../../src/utils/theme-validation.ts"],"names":[],"mappings":"AAWA,MAAM,MAAM,uBAAuB,GAC/B,aAAa,GACb,MAAM,GACN,QAAQ,GACR,MAAM,GACN,QAAQ,GACR,gBAAgB,GAChB,YAAY,GACZ,eAAe,CAAC;AAEpB,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,EAAE,uBAAuB,CAAC;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC/B,6DAA6D;IAC7D,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,eAAe,EAAE,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,KAAK,GAAG,OAAO,CAAC;AAElD,iEAAiE;AACjE,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAWlD,CAAC;AAgLF,wBAAsB,aAAa,CACjC,GAAG,SAAgB,EACnB,OAAO,GAAE;IAAE,IAAI,CAAC,EAAE,mBAAmB,CAAA;CAAO,GAC3C,OAAO,CAAC,qBAAqB,CAAC,CAyBhC;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,eAAe,EAAE,GAAG,eAAe,EAAE,CAE7E;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,eAAe,EAAE,GAAG,eAAe,EAAE,CAE/E;AAED,wBAAgB,gCAAgC,CAC9C,MAAM,EAAE,eAAe,EAAE,GACxB,MAAM,CAqCR;AA0ED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,qBAAqB,GAAG,IAAI,CAUpE"}