@schalkneethling/miyagi-core 4.7.0 → 4.8.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.
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Generate a Markdown performance-budget report.
3
+ * Mirrors the shape of lib/validator/html-report.js so the two read the same at
4
+ * a glance: one summary table, then per-category detail tables. The report is
5
+ * a stable artefact that can be committed by CI or linked from PRs.
6
+ * @module performance/report
7
+ */
8
+
9
+ import path from "node:path";
10
+ import { formatSize } from "./parse-size.js";
11
+
12
+ const STATUS_LABEL = {
13
+ ok: "OK",
14
+ warn: "WARN",
15
+ exceed: "EXCEED",
16
+ unbudgeted: "—",
17
+ };
18
+
19
+ /**
20
+ * @param {"raw"|"gzip"|"brotli"} compression
21
+ * @returns {string}
22
+ */
23
+ function compressionLabel(compression) {
24
+ return compression.charAt(0).toUpperCase() + compression.slice(1);
25
+ }
26
+
27
+ /**
28
+ * @param {import("./index.js").PerformanceResult} result
29
+ * @param {object} [options]
30
+ * @param {string} [options.cwd] - used to relativize reported file paths
31
+ * @returns {string} Markdown
32
+ */
33
+ export function generatePerformanceReport(result, options = {}) {
34
+ const cwd = options.cwd || process.cwd();
35
+ const { measurement, evaluations, compression, summary } = result;
36
+ const lines = [];
37
+
38
+ lines.push("# Performance Budget Report");
39
+ lines.push("");
40
+ lines.push(`**Date:** ${new Date().toISOString().split("T")[0]}`);
41
+ lines.push(`**Compression:** ${compressionLabel(compression)}`);
42
+ lines.push(
43
+ `**OK:** ${summary.ok} | **Warn:** ${summary.warn} | **Exceed:** ${summary.exceed} | **Unbudgeted:** ${summary.unbudgeted}`,
44
+ );
45
+ lines.push("");
46
+
47
+ // Evaluation summary
48
+ lines.push("## Evaluations");
49
+ lines.push("");
50
+ lines.push("| Category | Item | Actual | Budget | Status |");
51
+ lines.push("|----------|------|--------|--------|--------|");
52
+
53
+ for (const row of evaluations) {
54
+ let itemLabel;
55
+ if (row.key === "total") {
56
+ itemLabel = "Total";
57
+ } else if (row.key === "perPage") {
58
+ itemLabel = "Per page";
59
+ } else {
60
+ itemLabel = escapePipe(row.label);
61
+ }
62
+
63
+ lines.push(
64
+ `| ${escapePipe(row.category)} | ${itemLabel} | ${formatSize(row.actual)} | ${formatSize(row.budget)} | ${STATUS_LABEL[row.status]} |`,
65
+ );
66
+ }
67
+ lines.push("");
68
+
69
+ // Per-category file breakdown
70
+ for (const [category, data] of Object.entries(measurement.categories)) {
71
+ if (data.files.length === 0) {
72
+ continue;
73
+ }
74
+
75
+ lines.push(`## ${escapePipe(category)}`);
76
+ lines.push("");
77
+ lines.push("| File | Raw | Gzip | Brotli |");
78
+ lines.push("|------|-----|------|--------|");
79
+
80
+ for (const file of data.files) {
81
+ const rel = path.relative(cwd, file.path);
82
+ const label = file.missing ? `${rel} (missing)` : rel;
83
+ lines.push(
84
+ `| ${escapePipe(label)} | ${formatSize(file.raw)} | ${formatSize(file.gzip)} | ${formatSize(file.brotli)} |`,
85
+ );
86
+ }
87
+ lines.push(
88
+ `| **Total** | ${formatSize(data.totals.raw)} | ${formatSize(data.totals.gzip)} | ${formatSize(data.totals.brotli)} |`,
89
+ );
90
+ lines.push("");
91
+ }
92
+
93
+ return lines.join("\n");
94
+ }
95
+
96
+ /**
97
+ * @param {string} text
98
+ * @returns {string}
99
+ */
100
+ function escapePipe(text) {
101
+ return String(text).replace(/\\/g, "\\\\").replace(/\|/g, "\\|");
102
+ }
@@ -15,6 +15,8 @@ import renderMainDocs from "./views/main/docs.js";
15
15
  import renderMainIndex from "./views/main/index.js";
16
16
  import iframeDesignTokens from "./views/iframe/design-tokens/index.js";
17
17
  import renderMainDesignTokens from "./views/main/design-tokens.js";
18
+ import renderMainPerformance from "./views/main/performance.js";
19
+ import renderIframePerformance from "./views/iframe/performance.js";
18
20
 
19
21
  export default {
20
22
  renderMainIndex,
@@ -28,11 +30,13 @@ export default {
28
30
  renderIframeDocs,
29
31
  renderIframeIndex,
30
32
  renderMainDesignTokens,
33
+ renderMainPerformance,
31
34
  iframe: {
32
35
  designTokens: {
33
36
  colors: iframeDesignTokens.colors,
34
37
  sizes: iframeDesignTokens.sizes,
35
38
  typography: iframeDesignTokens.typography,
36
39
  },
40
+ performance: renderIframePerformance,
37
41
  },
38
42
  };
@@ -0,0 +1,74 @@
1
+ import config from "../../../default-config.js";
2
+ import { getUserUiConfig, getThemeMode } from "../../helpers.js";
3
+ import { runPerformance } from "../../../performance/index.js";
4
+ import { formatSize } from "../../../performance/parse-size.js";
5
+
6
+ /**
7
+ * Renders the iframe-side Performance panel. Computed on-request so the
8
+ * numbers reflect the current on-disk state without any extra cache plumbing;
9
+ * `measure.js` has an mtime cache so repeat renders are cheap.
10
+ * @param {object} o
11
+ * @param {object} o.res
12
+ * @param {Function} [o.cb]
13
+ * @param {object} o.cookies
14
+ * @returns {Promise<void>}
15
+ */
16
+ export default async function renderIframePerformance({ res, cb, cookies }) {
17
+ const themeMode = getThemeMode(cookies);
18
+
19
+ const result = runPerformance({ config: global.config });
20
+
21
+ const viewData = {
22
+ compression: result.compression,
23
+ evaluations: result.evaluations.map((row) => ({
24
+ ...row,
25
+ actualFormatted: formatSize(row.actual),
26
+ budgetFormatted: formatSize(row.budget),
27
+ ratioPercent:
28
+ row.ratio == null ? null : Math.round(row.ratio * 100),
29
+ })),
30
+ categories: Object.entries(result.measurement.categories).map(
31
+ ([category, data]) => ({
32
+ category,
33
+ files: data.files.map((file) => ({
34
+ path: file.path,
35
+ raw: formatSize(file.raw),
36
+ gzip: formatSize(file.gzip),
37
+ brotli: formatSize(file.brotli),
38
+ missing: !!file.missing,
39
+ })),
40
+ totals: {
41
+ raw: formatSize(data.totals.raw),
42
+ gzip: formatSize(data.totals.gzip),
43
+ brotli: formatSize(data.totals.brotli),
44
+ },
45
+ }),
46
+ ),
47
+ summary: result.summary,
48
+ };
49
+
50
+ await res.render(
51
+ "performance.twig.miyagi",
52
+ {
53
+ ...viewData,
54
+ isBuild: global.config.isBuild,
55
+ lang: global.config.ui.lang,
56
+ miyagiDev: !!process.env.MIYAGI_DEVELOPMENT,
57
+ projectName: config.projectName,
58
+ userUiConfig: getUserUiConfig(cookies),
59
+ theme: themeMode
60
+ ? Object.assign(global.config.ui.theme, { mode: themeMode })
61
+ : global.config.ui.theme,
62
+ uiTextDirection: global.config.ui.textDirection,
63
+ },
64
+ (html) => {
65
+ if (res.send) {
66
+ res.send(html);
67
+ }
68
+
69
+ if (cb) {
70
+ cb(null, html);
71
+ }
72
+ },
73
+ );
74
+ }
@@ -0,0 +1,51 @@
1
+ import config from "../../../default-config.js";
2
+ import { getUserUiConfig, getThemeMode } from "../../helpers.js";
3
+
4
+ /**
5
+ * Renders the main Performance page — the standard chrome (menu etc.) with
6
+ * the iframe pointed at the Performance panel view.
7
+ * @param {object} object
8
+ * @param {object} object.res
9
+ * @param {Function} [object.cb]
10
+ * @param {object} [object.cookies]
11
+ * @returns {void}
12
+ */
13
+ export default function renderMainPerformance({ res, cb, cookies }) {
14
+ const themeMode = getThemeMode(cookies);
15
+
16
+ res.render(
17
+ "main.twig.miyagi",
18
+ {
19
+ lang: global.config.ui.lang,
20
+ folders: global.state.menu,
21
+ components: global.state.components,
22
+ flatUrlPattern: global.config.isBuild
23
+ ? "/show-{{component}}.html"
24
+ : "/show?file={{component}}",
25
+ iframeSrc: "/iframe/performance",
26
+ showAll: true,
27
+ projectName: config.projectName,
28
+ userProjectName: global.config.projectName,
29
+ indexPath: global.config.indexPath.embedded,
30
+ miyagiDev: !!process.env.MIYAGI_DEVELOPMENT,
31
+ isBuild: global.config.isBuild,
32
+ userUiConfig: getUserUiConfig(cookies),
33
+ theme: themeMode
34
+ ? Object.assign(global.config.ui.theme, { mode: themeMode })
35
+ : global.config.ui.theme,
36
+ basePath: global.config.isBuild ? global.config.build.basePath : "/",
37
+ uiTextDirection: global.config.ui.textDirection,
38
+ requestedComponent: "performance",
39
+ requestedVariation: null,
40
+ },
41
+ (html) => {
42
+ if (res.send) {
43
+ res.send(html);
44
+ }
45
+
46
+ if (cb) {
47
+ cb(null, html);
48
+ }
49
+ },
50
+ );
51
+ }
@@ -221,18 +221,59 @@ export const getMenu = function (sourceTree) {
221
221
 
222
222
  const docsMenu = getDocsMenu(sourceTree.docs);
223
223
  const designTokensMenu = getDesignTokensMenu();
224
+ const performanceMenu = getPerformanceMenu();
224
225
 
225
- if (!docsMenu && !designTokensMenu && componentsMenu)
226
+ if (!docsMenu && !designTokensMenu && !performanceMenu && componentsMenu) {
226
227
  return componentsMenu.children;
228
+ }
227
229
 
228
230
  const menus = [];
229
- if (designTokensMenu) menus.push(designTokensMenu);
230
- if (componentsMenu) menus.push(componentsMenu);
231
- if (docsMenu) menus.push(docsMenu);
231
+ if (designTokensMenu) {
232
+ menus.push(designTokensMenu);
233
+ }
234
+ if (componentsMenu) {
235
+ menus.push(componentsMenu);
236
+ }
237
+ if (docsMenu) {
238
+ menus.push(docsMenu);
239
+ }
240
+ if (performanceMenu) {
241
+ menus.push(performanceMenu);
242
+ }
232
243
 
233
244
  return menus;
234
245
  };
235
246
 
247
+ /**
248
+ * @returns {object|null}
249
+ */
250
+ function getPerformanceMenu() {
251
+ // Dev-mode only. In build output there is no live dev-server to compute
252
+ // perf against, so the entry would dead-link.
253
+ if (global.config.isBuild) {
254
+ return null;
255
+ }
256
+ if (!global.config.performance?.enabled) {
257
+ return null;
258
+ }
259
+
260
+ return {
261
+ topLevel: true,
262
+ name: "Performance",
263
+ id: "performance",
264
+ type: "directory",
265
+ shortPath: "performance",
266
+ children: [
267
+ {
268
+ section: "performance",
269
+ type: "file",
270
+ name: "budget",
271
+ url: "/iframe/performance",
272
+ },
273
+ ],
274
+ };
275
+ }
276
+
236
277
  /**
237
278
  * @returns {object}
238
279
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@schalkneethling/miyagi-core",
3
- "version": "4.7.0",
3
+ "version": "4.8.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)",