@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.
- package/VALIDATIONS.md +122 -0
- package/package.json +2 -2
- package/public/dev-error.liquid +49 -0
- package/ts-dist/commands/build-theme.d.ts.map +1 -1
- package/ts-dist/commands/build-theme.js +38 -11
- package/ts-dist/commands/serve-theme.d.ts.map +1 -1
- package/ts-dist/commands/serve-theme.js +414 -70
- package/ts-dist/index.js +0 -0
- package/ts-dist/mocks/global-objects.d.ts.map +1 -1
- package/ts-dist/mocks/global-objects.js +1 -0
- package/ts-dist/mocks/init-theme/i18n/en.schema.json +2 -1
- package/ts-dist/mocks/init-theme/sections/footer.liquid +3 -1
- package/ts-dist/mocks/init-theme/sections/header-topbar.liquid +3 -1
- package/ts-dist/mocks/init-theme/sections/product-collection.liquid +3 -1
- package/ts-dist/mocks/init-theme/sections/promo-banner.liquid +5 -1
- package/ts-dist/mocks/reviews.d.ts +6 -0
- package/ts-dist/mocks/reviews.d.ts.map +1 -0
- package/ts-dist/mocks/reviews.js +52 -0
- package/ts-dist/routes/storefront-routes.js +1 -1
- package/ts-dist/utils/api-handler.d.ts +6 -1
- package/ts-dist/utils/api-handler.d.ts.map +1 -1
- package/ts-dist/utils/api-handler.js +91 -35
- package/ts-dist/utils/editor-attributes.d.ts.map +1 -1
- package/ts-dist/utils/editor-attributes.js +2 -4
- package/ts-dist/utils/error-report.d.ts +1 -1
- package/ts-dist/utils/error-report.d.ts.map +1 -1
- package/ts-dist/utils/error-report.js +58 -18
- package/ts-dist/utils/load-theme-globals.d.ts +1 -1
- package/ts-dist/utils/load-theme-globals.d.ts.map +1 -1
- package/ts-dist/utils/load-theme-globals.js +42 -2
- package/ts-dist/utils/resolve-schema-settings.d.ts.map +1 -1
- package/ts-dist/utils/resolve-schema-settings.js +0 -1
- package/ts-dist/utils/resolve-schema-translations.d.ts +1 -0
- package/ts-dist/utils/resolve-schema-translations.d.ts.map +1 -1
- package/ts-dist/utils/resolve-schema-translations.js +3 -0
- package/ts-dist/utils/schema-validation.d.ts +34 -0
- package/ts-dist/utils/schema-validation.d.ts.map +1 -0
- package/ts-dist/utils/schema-validation.js +544 -0
- package/ts-dist/utils/serve-theme-utils.d.ts +1 -1
- package/ts-dist/utils/serve-theme-utils.d.ts.map +1 -1
- package/ts-dist/utils/serve-theme-utils.js +1 -1
- package/ts-dist/utils/template-reference-validation.d.ts +11 -0
- package/ts-dist/utils/template-reference-validation.d.ts.map +1 -0
- package/ts-dist/utils/template-reference-validation.js +466 -0
- package/ts-dist/utils/theme-block-schema.d.ts +5 -0
- package/ts-dist/utils/theme-block-schema.d.ts.map +1 -0
- package/ts-dist/utils/theme-block-schema.js +35 -0
- package/ts-dist/utils/theme-validation.d.ts +8 -1
- package/ts-dist/utils/theme-validation.d.ts.map +1 -1
- 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
|
-
|
|
7
|
-
|
|
9
|
+
radio: ['display_name', 'value'],
|
|
10
|
+
url: ['display_name', 'handle'],
|
|
11
|
+
collection: ['handle'],
|
|
12
|
+
category: ['handle'],
|
|
8
13
|
menu: ['display_name', 'handle'],
|
|
9
|
-
blog: ['
|
|
10
|
-
product: ['
|
|
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
|
|
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
|
|
176
|
+
...validateRequiredFiles(cwd),
|
|
291
177
|
...validateTranslationKeys(cwd),
|
|
292
178
|
...validateAssetFileSizes(cwd),
|
|
293
|
-
...
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
320
|
-
|
|
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
|
-
|
|
330
|
-
|
|
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
|
}
|