@schalkneethling/miyagi-core 4.4.4 → 4.6.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.
@@ -2,6 +2,7 @@ import path from "path";
2
2
  import config from "../../../default-config.js";
3
3
  import { getUserUiConfig } from "../../helpers.js";
4
4
  import resolveAssets from "../../helpers/resolve-assets.js";
5
+ import applyOverrides from "../../helpers/apply-overrides.js";
5
6
 
6
7
  /**
7
8
  * @param {object} object - parameter object
@@ -20,7 +21,14 @@ export default async function renderIframeVariationStandalone({
20
21
  componentDeclaredAssets = null,
21
22
  cb,
22
23
  cookies,
24
+ overrides,
23
25
  }) {
26
+ if (overrides) {
27
+ const schema =
28
+ global.state.fileContents[component.paths?.schema?.full] ?? null;
29
+ componentData = applyOverrides(componentData ?? {}, overrides, schema);
30
+ }
31
+
24
32
  const directoryPath = component.paths.dir.short;
25
33
 
26
34
  return new Promise((resolve, reject) => {
@@ -49,6 +57,7 @@ export default async function renderIframeVariationStandalone({
49
57
  "component_variation.twig.miyagi",
50
58
  {
51
59
  html: result,
60
+ mockDataResolved: JSON.stringify(componentData ?? {}),
52
61
  cssFiles,
53
62
  jsFilesHead,
54
63
  jsFilesBody,
@@ -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.6.0",
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",
@@ -59,7 +60,7 @@
59
60
  "@types/yargs": "^17.0.35",
60
61
  "@vitest/coverage-v8": "^4.0.6",
61
62
  "cssnano": "^7.1.2",
62
- "eslint": "^9.39.0",
63
+ "eslint": "^10.2.0",
63
64
  "eslint-plugin-jsdoc": "^62.5.4",
64
65
  "globals": "^17.4.0",
65
66
  "gulp": "^5.0.1",