@schalkneethling/miyagi-core 4.4.4 → 4.5.1

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/api/index.js CHANGED
@@ -12,6 +12,11 @@ import {
12
12
  toSchemaValidationResult,
13
13
  validateSchemas,
14
14
  } from "../lib/validator/schemas.js";
15
+ import {
16
+ validateAllHtml as validateAllHtmlImpl,
17
+ validateComponentHtml as validateComponentHtmlImpl,
18
+ } from "../lib/validator/html.js";
19
+ import { generateMarkdownReport } from "../lib/validator/html-report.js";
15
20
 
16
21
  /**
17
22
  * @param {object} obj
@@ -307,6 +312,54 @@ function getComponentsObject(component) {
307
312
  * @param {Map<string, Array<object>>} params.errorMap
308
313
  * @returns {Array<object>}
309
314
  */
315
+ /**
316
+ * @param {object} [options]
317
+ * @param {object} [options.htmlValidateConfig]
318
+ * @returns {Promise<object>}
319
+ */
320
+ export const validateHtml = async (options = {}) => {
321
+ global.app = await init("api");
322
+ const results = await validateAllHtmlImpl(options);
323
+ const report = generateMarkdownReport(results);
324
+ return {
325
+ success: results.summary.failed === 0,
326
+ data: { results, report },
327
+ };
328
+ };
329
+
330
+ /**
331
+ * @param {object} obj
332
+ * @param {string|null} obj.component
333
+ * @param {object} [obj.htmlValidateConfig]
334
+ * @returns {Promise<object>}
335
+ */
336
+ export const validateHtmlComponent = async (
337
+ { component, ...options } = { component: null },
338
+ ) => {
339
+ if (!component)
340
+ return {
341
+ success: false,
342
+ message:
343
+ 'Please pass a component to `validateHtmlComponent` ({ component: "name" }).',
344
+ };
345
+
346
+ global.app = await init("api");
347
+
348
+ const componentObject = getComponentsObject(component);
349
+
350
+ if (!componentObject)
351
+ return {
352
+ success: false,
353
+ message: `Component "${component}" does not exist.`,
354
+ };
355
+
356
+ const result = await validateComponentHtmlImpl(componentObject, options);
357
+ return {
358
+ success: result.variations.every((v) => v.valid),
359
+ data: result,
360
+ };
361
+ };
362
+
310
363
  function getLintComponentErrorsInRouteOrder({ components, errorMap }) {
311
364
  return components
312
365
  .map((route) => {
package/lib/cli/index.js CHANGED
@@ -2,8 +2,10 @@ import lintImport from "./lint.js";
2
2
  import componentImport from "./component.js";
3
3
  import drupalAssetsImport from "./drupal-assets.js";
4
4
  import doctorImport from "./doctor.js";
5
+ import validateHtmlImport from "./validate-html.js";
5
6
 
6
7
  export const lint = lintImport;
7
8
  export const component = componentImport;
8
9
  export const drupalAssets = drupalAssetsImport;
9
10
  export const doctor = doctorImport;
11
+ export const validateHtml = validateHtmlImport;
package/lib/cli/run.js CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  component as createComponentViaCli,
10
10
  drupalAssets,
11
11
  doctor,
12
+ validateHtml as validateHtmlCli,
12
13
  } from "./index.js";
13
14
  import { EXIT_CODES, MiyagiError } from "../errors.js";
14
15
 
@@ -220,6 +221,11 @@ async function runDrupalAssetsCommand(args) {
220
221
  * @param {object} args
221
222
  * @returns {Promise<object>}
222
223
  */
224
+ async function runValidateHtmlCommand(args) {
225
+ applyCliEnv(args);
226
+ return await validateHtmlCli(args);
227
+ }
228
+
223
229
  async function runDoctorCommand(args) {
224
230
  applyCliEnv(args);
225
231
  return await doctor(args);
@@ -268,6 +274,7 @@ export async function runCli(argv = process.argv) {
268
274
  new: runComponentCommand,
269
275
  mocks: runMocksCommand,
270
276
  lint: runLintCommand,
277
+ validateHtml: runValidateHtmlCommand,
271
278
  drupalAssets: runDrupalAssetsCommand,
272
279
  doctor: runDoctorCommand,
273
280
  },
@@ -0,0 +1,110 @@
1
+ import path from "path";
2
+ import { writeFile } from "node:fs/promises";
3
+ import init from "./app.js";
4
+ import getConfig from "../config.js";
5
+ import log from "../logger.js";
6
+ import { t } from "../i18n/index.js";
7
+ import { EXIT_CODES } from "../errors.js";
8
+ import {
9
+ validateAllHtml,
10
+ validateComponentHtml,
11
+ validateHtmlFiles,
12
+ } from "../validator/html.js";
13
+ import { generateMarkdownReport } from "../validator/html-report.js";
14
+
15
+ /**
16
+ * @param {object} args
17
+ * @returns {Promise<object>}
18
+ */
19
+ export default async function validateHtml(args) {
20
+ process.env.NODE_ENV = "development";
21
+
22
+ const filesGlob = args.files;
23
+
24
+ if (filesGlob) {
25
+ // Files mode — validate pre-existing HTML files
26
+ const config = await getConfig(args);
27
+ global.config = config;
28
+ const results = await validateHtmlFiles(filesGlob);
29
+ return await writeReport(results, args, config);
30
+ }
31
+
32
+ // Render mode — render components and validate
33
+ const componentArg = args.component;
34
+ const config = await getConfig(args);
35
+ global.app = await init(config);
36
+
37
+ let results;
38
+
39
+ if (componentArg) {
40
+ const component = global.state.routes.find(
41
+ ({ alias }) =>
42
+ alias === path.relative(config.components.folder, componentArg),
43
+ );
44
+
45
+ if (!component) {
46
+ const message = t("htmlValidation.componentNotFound").replace("{{component}}", componentArg);
47
+ log("error", message);
48
+ return {
49
+ success: false,
50
+ code: EXIT_CODES.CLI_USAGE_ERROR,
51
+ shouldExit: true,
52
+ message,
53
+ };
54
+ }
55
+
56
+ const result = await validateComponentHtml(component);
57
+ results = {
58
+ components: [result],
59
+ summary: {
60
+ total: 1,
61
+ passed: result.variations.every((v) => v.valid) ? 1 : 0,
62
+ failed: result.variations.some((v) => !v.valid) ? 1 : 0,
63
+ errors: result.variations.reduce(
64
+ (sum, v) =>
65
+ sum + v.messages.filter((m) => m.severity === 2).length,
66
+ 0,
67
+ ),
68
+ warnings: result.variations.reduce(
69
+ (sum, v) =>
70
+ sum + v.messages.filter((m) => m.severity !== 2).length,
71
+ 0,
72
+ ),
73
+ },
74
+ };
75
+ } else {
76
+ results = await validateAllHtml();
77
+ }
78
+
79
+ return await writeReport(results, args, config);
80
+ }
81
+
82
+ /**
83
+ * @param {object} results
84
+ * @param {object} args
85
+ * @param {object} config
86
+ * @returns {Promise<object>}
87
+ */
88
+ async function writeReport(results, args, config) {
89
+ const report = generateMarkdownReport(results);
90
+ const outputPath = args.output ?? config.htmlValidation?.output ?? "html-validation-report.md";
91
+
92
+ try {
93
+ await writeFile(outputPath, report, "utf-8");
94
+ log(
95
+ "info",
96
+ t("htmlValidation.reportWritten").replace("{{path}}", outputPath),
97
+ );
98
+ } catch (error) {
99
+ log("error", t("htmlValidation.reportWriteFailed").replace("{{error}}", error.message));
100
+ }
101
+
102
+ return {
103
+ success: results.summary.failed === 0,
104
+ code:
105
+ results.summary.failed === 0
106
+ ? EXIT_CODES.SUCCESS
107
+ : EXIT_CODES.VALIDATION_ERROR,
108
+ shouldExit: true,
109
+ };
110
+ }
@@ -49,6 +49,18 @@ export default {
49
49
  lint: {
50
50
  logLevel: "error",
51
51
  },
52
+ htmlValidation: {
53
+ output: "html-validation-report.md",
54
+ htmlValidateConfig: {
55
+ extends: ["html-validate:recommended"],
56
+ rules: {
57
+ "doctype-style": "off",
58
+ "input-missing-label": "error",
59
+ "missing-doctype": "off",
60
+ "no-missing-references": "off",
61
+ },
62
+ },
63
+ },
52
64
  extensions: [],
53
65
  files: {
54
66
  css: {
package/lib/i18n/en.js CHANGED
@@ -31,6 +31,29 @@ export default {
31
31
  done: "Finished creating component {{component}}.",
32
32
  },
33
33
  },
34
+ htmlValidation: {
35
+ all: {
36
+ start: "Validating rendered HTML for all components\u2026",
37
+ valid: "All rendered HTML is valid!",
38
+ invalid: {
39
+ one: "1 component has HTML validation errors!",
40
+ other: "{{amount}} components have HTML validation errors!",
41
+ },
42
+ },
43
+ component: {
44
+ start: "Validating rendered HTML for {{component}}\u2026",
45
+ valid: "Rendered HTML is valid!",
46
+ renderFailed:
47
+ "Could not render {{component}} variation {{variation}}.",
48
+ },
49
+ files: {
50
+ start: "Validating HTML files matching {{pattern}}\u2026",
51
+ noFilesFound: "No files found matching {{pattern}}.",
52
+ },
53
+ componentNotFound: "The component {{component}} does not seem to exist.",
54
+ reportWriteFailed: "Failed to write report: {{error}}.",
55
+ reportWritten: "HTML validation report written to {{path}}.",
56
+ },
34
57
  linter: {
35
58
  all: {
36
59
  start: "Validating schema files and mock data for all components…",
package/lib/init/args.js CHANGED
@@ -101,6 +101,28 @@ export default function createCli(handlers, argv = process.argv) {
101
101
  }),
102
102
  commandHandler(handlers.lint),
103
103
  )
104
+ .command(
105
+ "validate-html [component]",
106
+ "Validates rendered HTML of components and generates a Markdown report",
107
+ (builder) =>
108
+ builder
109
+ .positional("component", {
110
+ description: "Optional component path to validate (render mode)",
111
+ type: "string",
112
+ })
113
+ .option("files", {
114
+ alias: "f",
115
+ description:
116
+ "Glob pattern pointing to HTML files to validate (e.g. build/miyagi/component-*.html)",
117
+ type: "string",
118
+ })
119
+ .option("output", {
120
+ alias: "o",
121
+ description: "Path for the Markdown report file",
122
+ type: "string",
123
+ }),
124
+ commandHandler(handlers.validateHtml),
125
+ )
104
126
  .command(
105
127
  "drupal-assets",
106
128
  "Resolves Drupal *.libraries.yml dependencies and updates component $assets in mock files",
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Generate a Markdown report from HTML validation results.
3
+ * @param {object} results - output from validateAllHtml, validateComponentHtml, or validateHtmlFiles
4
+ * @param {Array<object>} results.components
5
+ * @param {object} results.summary
6
+ * @returns {string} Markdown-formatted report
7
+ */
8
+ export function generateMarkdownReport(results) {
9
+ const { components, summary } = results;
10
+ const lines = [];
11
+
12
+ lines.push("# HTML Validation Report");
13
+ lines.push("");
14
+ lines.push(`**Date:** ${new Date().toISOString().split("T")[0]}`);
15
+ lines.push(
16
+ `**Total components:** ${summary.total} | **Passed:** ${summary.passed} | **Failed:** ${summary.failed}`,
17
+ );
18
+ lines.push(
19
+ `**Errors:** ${summary.errors} | **Warnings:** ${summary.warnings}`,
20
+ );
21
+ lines.push("");
22
+
23
+ // Summary table
24
+ lines.push("## Summary");
25
+ lines.push("");
26
+ lines.push("| Component | Status | Errors | Warnings |");
27
+ lines.push("|-----------|--------|--------|----------|");
28
+
29
+ for (const comp of components) {
30
+ const hasErrors = comp.variations.some((v) => !v.valid);
31
+ const status = hasErrors ? "FAIL" : "PASS";
32
+ let errors = 0;
33
+ let warnings = 0;
34
+
35
+ for (const variation of comp.variations) {
36
+ for (const msg of variation.messages) {
37
+ if (msg.severity === 2) {
38
+ errors++;
39
+ } else {
40
+ warnings++;
41
+ }
42
+ }
43
+ }
44
+
45
+ lines.push(`| ${comp.component} | ${status} | ${errors} | ${warnings} |`);
46
+ }
47
+
48
+ // Failed component details
49
+ const failedComponents = components.filter((comp) =>
50
+ comp.variations.some((v) => !v.valid),
51
+ );
52
+
53
+ if (failedComponents.length > 0) {
54
+ lines.push("");
55
+ lines.push("## Failed Components");
56
+
57
+ for (const comp of failedComponents) {
58
+ lines.push("");
59
+ lines.push(`### ${comp.component}`);
60
+
61
+ const failedVariations = comp.variations.filter((v) => !v.valid);
62
+
63
+ for (const variation of failedVariations) {
64
+ lines.push("");
65
+ lines.push(`#### ${variation.name}`);
66
+ lines.push("");
67
+ lines.push("| Line | Col | Severity | Rule | Message |");
68
+ lines.push("|------|-----|----------|------|---------|");
69
+
70
+ for (const msg of variation.messages) {
71
+ const severity = msg.severity === 2 ? "error" : "warning";
72
+ const escapedMessage = msg.message
73
+ .replace(/\\/g, "\\\\")
74
+ .replace(/\|/g, "\\|");
75
+ lines.push(
76
+ `| ${msg.line} | ${msg.column} | ${severity} | ${msg.ruleId} | ${escapedMessage} |`,
77
+ );
78
+ }
79
+ }
80
+ }
81
+ }
82
+
83
+ lines.push("");
84
+ return lines.join("\n");
85
+ }
@@ -0,0 +1,389 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { globSync } from "node:fs";
3
+ import path from "path";
4
+ import { HtmlValidate } from "html-validate";
5
+ import { getComponentData } from "../mocks/index.js";
6
+ import { t } from "../i18n/index.js";
7
+ import log from "../logger.js";
8
+
9
+ /**
10
+ * @param {string} templatePath
11
+ * @param {object} data
12
+ * @returns {Promise<string>}
13
+ */
14
+ function renderTemplate(templatePath, data) {
15
+ return new Promise((resolve, reject) => {
16
+ global.app.render(templatePath, data, (error, result) => {
17
+ if (error) {
18
+ reject(error);
19
+ } else {
20
+ resolve(result);
21
+ }
22
+ });
23
+ });
24
+ }
25
+
26
+ /**
27
+ * @param {object} htmlValidateConfig
28
+ * @returns {HtmlValidate}
29
+ */
30
+ function createValidator(htmlValidateConfig) {
31
+ return new HtmlValidate(htmlValidateConfig);
32
+ }
33
+
34
+ /**
35
+ * @param {Array<object>} componentResults
36
+ * @returns {object}
37
+ */
38
+ function buildSummary(componentResults) {
39
+ let errors = 0;
40
+ let warnings = 0;
41
+ let passed = 0;
42
+ let failed = 0;
43
+
44
+ for (const comp of componentResults) {
45
+ const hasErrors = comp.variations.some((v) => !v.valid);
46
+ if (hasErrors) {
47
+ failed++;
48
+ } else {
49
+ passed++;
50
+ }
51
+ for (const variation of comp.variations) {
52
+ for (const msg of variation.messages) {
53
+ if (msg.severity === 2) {
54
+ errors++;
55
+ } else {
56
+ warnings++;
57
+ }
58
+ }
59
+ }
60
+ }
61
+
62
+ return {
63
+ total: componentResults.length,
64
+ passed,
65
+ failed,
66
+ errors,
67
+ warnings,
68
+ };
69
+ }
70
+
71
+ /**
72
+ * Validate rendered HTML for all components.
73
+ * @param {object} [options]
74
+ * @param {object} [options.htmlValidateConfig]
75
+ * @returns {Promise<{components: Array<object>, summary: object}>}
76
+ */
77
+ export async function validateAllHtml(options = {}) {
78
+ const config =
79
+ options.htmlValidateConfig ?? global.config.htmlValidation?.htmlValidateConfig;
80
+ const validator = createValidator(config);
81
+ const components = global.state.routes.filter(
82
+ (route) => route.type === "components" && route.paths.tpl,
83
+ );
84
+
85
+ log(
86
+ "info",
87
+ t("htmlValidation.all.start"),
88
+ );
89
+
90
+ const componentResults = await Promise.all(
91
+ components.map((component) =>
92
+ validateSingleComponent(component, validator),
93
+ ),
94
+ );
95
+
96
+ const summary = buildSummary(componentResults);
97
+
98
+ if (summary.failed === 0) {
99
+ log("success", t("htmlValidation.all.valid"));
100
+ } else {
101
+ const msg =
102
+ summary.failed === 1
103
+ ? t("htmlValidation.all.invalid.one")
104
+ : t("htmlValidation.all.invalid.other").replace(
105
+ "{{amount}}",
106
+ summary.failed,
107
+ );
108
+ log("error", msg);
109
+ }
110
+
111
+ return { components: componentResults, summary };
112
+ }
113
+
114
+ /**
115
+ * Validate rendered HTML for a single component (all variations).
116
+ * @param {object} component - route object from global.state.routes
117
+ * @param {object} [options]
118
+ * @param {object} [options.htmlValidateConfig]
119
+ * @returns {Promise<{component: string, variations: Array<object>}>}
120
+ */
121
+ export async function validateComponentHtml(component, options = {}) {
122
+ const config =
123
+ options.htmlValidateConfig ?? global.config.htmlValidation?.htmlValidateConfig;
124
+ const validator = createValidator(config);
125
+
126
+ log(
127
+ "info",
128
+ t("htmlValidation.component.start").replace(
129
+ "{{component}}",
130
+ component.paths.dir.short,
131
+ ),
132
+ );
133
+
134
+ const result = await validateSingleComponent(component, validator);
135
+
136
+ const allValid = result.variations.every((v) => v.valid);
137
+ if (allValid) {
138
+ log("success", t("htmlValidation.component.valid"));
139
+ }
140
+
141
+ return result;
142
+ }
143
+
144
+ /**
145
+ * @param {object} component
146
+ * @param {HtmlValidate} validator
147
+ * @returns {Promise<{component: string, variations: Array<object>}>}
148
+ */
149
+ async function validateSingleComponent(component, validator) {
150
+ const data = await getComponentData(component);
151
+ const variations = [];
152
+
153
+ if (data && data.length > 0) {
154
+ for (const entry of data) {
155
+ try {
156
+ const html = await renderTemplate(
157
+ component.paths.tpl.full,
158
+ entry.resolved ?? {},
159
+ );
160
+ const report = await validator.validateString(html);
161
+ const messages = report.results.flatMap((r) =>
162
+ r.messages.map((msg) => ({
163
+ severity: msg.severity,
164
+ message: msg.message,
165
+ ruleId: msg.ruleId,
166
+ line: msg.line,
167
+ column: msg.column,
168
+ })),
169
+ );
170
+
171
+ variations.push({
172
+ name: entry.name,
173
+ valid: report.valid,
174
+ messages,
175
+ });
176
+ } catch (error) {
177
+ log(
178
+ "warn",
179
+ t("htmlValidation.component.renderFailed")
180
+ .replace("{{component}}", component.paths.dir.short)
181
+ .replace("{{variation}}", entry.name),
182
+ );
183
+ variations.push({
184
+ name: entry.name,
185
+ valid: false,
186
+ messages: [
187
+ {
188
+ severity: 2,
189
+ message: `Render error: ${error.message || error}`,
190
+ ruleId: "render-error",
191
+ line: 0,
192
+ column: 0,
193
+ },
194
+ ],
195
+ });
196
+ }
197
+ }
198
+ } else {
199
+ // No mock data — render with empty object for default variation
200
+ try {
201
+ const html = await renderTemplate(component.paths.tpl.full, {});
202
+ const report = await validator.validateString(html);
203
+ const messages = report.results.flatMap((r) =>
204
+ r.messages.map((msg) => ({
205
+ severity: msg.severity,
206
+ message: msg.message,
207
+ ruleId: msg.ruleId,
208
+ line: msg.line,
209
+ column: msg.column,
210
+ })),
211
+ );
212
+
213
+ variations.push({
214
+ name: "default",
215
+ valid: report.valid,
216
+ messages,
217
+ });
218
+ } catch (error) {
219
+ variations.push({
220
+ name: "default",
221
+ valid: false,
222
+ messages: [
223
+ {
224
+ severity: 2,
225
+ message: `Render error: ${error.message || error}`,
226
+ ruleId: "render-error",
227
+ line: 0,
228
+ column: 0,
229
+ },
230
+ ],
231
+ });
232
+ }
233
+ }
234
+
235
+ return {
236
+ component: component.paths.dir.short,
237
+ variations,
238
+ };
239
+ }
240
+
241
+ /**
242
+ * Parse component and variation name from a build output filename.
243
+ * Expected pattern: component-<path>-variation-<name>.html
244
+ * @param {string} filePath
245
+ * @returns {{ component: string, variation: string }}
246
+ */
247
+ function parseFileName(filePath) {
248
+ const basename = path.basename(filePath, ".html");
249
+
250
+ const variationMatch = basename.match(/^component-(.+)-variation-(.+)$/);
251
+ if (variationMatch) {
252
+ const componentPart = variationMatch[1].replace(/-/g, "/");
253
+ return {
254
+ component: componentPart,
255
+ variation: variationMatch[2],
256
+ };
257
+ }
258
+
259
+ // Fallback: use full basename as component name
260
+ return {
261
+ component: basename,
262
+ variation: "default",
263
+ };
264
+ }
265
+
266
+ /**
267
+ * Validate pre-existing HTML files matching a glob pattern.
268
+ * Files are validated as full HTML documents.
269
+ * @param {string} globPattern
270
+ * @param {object} [options]
271
+ * @param {object} [options.htmlValidateConfig]
272
+ * @returns {Promise<{components: Array<object>, summary: object}>}
273
+ */
274
+ export async function validateHtmlFiles(globPattern, options = {}) {
275
+ const baseConfig =
276
+ options.htmlValidateConfig ?? global.config?.htmlValidation?.htmlValidateConfig;
277
+
278
+ // For file mode, use full-document validation (don't disable doctype rules)
279
+ const fileConfig = {
280
+ ...baseConfig,
281
+ rules: {
282
+ ...(baseConfig?.rules ?? {}),
283
+ "doctype-style": undefined,
284
+ "missing-doctype": undefined,
285
+ },
286
+ };
287
+
288
+ // Remove undefined keys so they fall back to the preset defaults
289
+ for (const [key, value] of Object.entries(fileConfig.rules)) {
290
+ if (value === undefined) {
291
+ delete fileConfig.rules[key];
292
+ }
293
+ }
294
+
295
+ const validator = createValidator(fileConfig);
296
+
297
+ log(
298
+ "info",
299
+ t("htmlValidation.files.start").replace("{{pattern}}", globPattern),
300
+ );
301
+
302
+ const files = globSync(globPattern);
303
+
304
+ if (files.length === 0) {
305
+ log(
306
+ "warn",
307
+ t("htmlValidation.files.noFilesFound").replace(
308
+ "{{pattern}}",
309
+ globPattern,
310
+ ),
311
+ );
312
+ return { components: [], summary: buildSummary([]) };
313
+ }
314
+
315
+ // Group files by component
316
+ const componentMap = new Map();
317
+
318
+ for (const filePath of files) {
319
+ const { component, variation } = parseFileName(filePath);
320
+ if (!componentMap.has(component)) {
321
+ componentMap.set(component, []);
322
+ }
323
+ componentMap.get(component).push({ filePath, variation });
324
+ }
325
+
326
+ const componentResults = [];
327
+
328
+ for (const [componentName, fileEntries] of componentMap) {
329
+ const variations = [];
330
+
331
+ for (const { filePath, variation } of fileEntries) {
332
+ try {
333
+ const html = await readFile(filePath, "utf-8");
334
+ const report = await validator.validateString(html);
335
+ const messages = report.results.flatMap((r) =>
336
+ r.messages.map((msg) => ({
337
+ severity: msg.severity,
338
+ message: msg.message,
339
+ ruleId: msg.ruleId,
340
+ line: msg.line,
341
+ column: msg.column,
342
+ })),
343
+ );
344
+
345
+ variations.push({
346
+ name: variation,
347
+ valid: report.valid,
348
+ messages,
349
+ });
350
+ } catch (error) {
351
+ variations.push({
352
+ name: variation,
353
+ valid: false,
354
+ messages: [
355
+ {
356
+ severity: 2,
357
+ message: `File read error: ${error.message || error}`,
358
+ ruleId: "file-error",
359
+ line: 0,
360
+ column: 0,
361
+ },
362
+ ],
363
+ });
364
+ }
365
+ }
366
+
367
+ componentResults.push({
368
+ component: componentName,
369
+ variations,
370
+ });
371
+ }
372
+
373
+ const summary = buildSummary(componentResults);
374
+
375
+ if (summary.failed === 0) {
376
+ log("success", t("htmlValidation.all.valid"));
377
+ } else {
378
+ const msg =
379
+ summary.failed === 1
380
+ ? t("htmlValidation.all.invalid.one")
381
+ : t("htmlValidation.all.invalid.other").replace(
382
+ "{{amount}}",
383
+ summary.failed,
384
+ );
385
+ log("error", msg);
386
+ }
387
+
388
+ return { components: componentResults, summary };
389
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@schalkneethling/miyagi-core",
3
- "version": "4.4.4",
3
+ "version": "4.5.1",
4
4
  "description": "miyagi is a component development tool for JavaScript template engines.",
5
5
  "main": "index.js",
6
6
  "author": "Schalk Neethling <schalkneethling@duck.com>, Michael Großklaus <mail@mgrossklaus.de> (https://www.mgrossklaus.de)",
@@ -43,6 +43,7 @@
43
43
  "deepmerge": "^4.3.1",
44
44
  "directory-tree": "^3.5.2",
45
45
  "express": "^5.1.0",
46
+ "html-validate": "^10.11.2",
46
47
  "js-yaml": "^4.1.0",
47
48
  "marked": "^17.0.2",
48
49
  "twing": "7.3.1",