@schalkneethling/miyagi-core 4.8.0 → 4.9.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/api/index.js +0 -35
- package/dist/css/iframe.css +1 -1
- package/dist/css/main.css +1 -1
- package/frontend/assets/css/iframe/perf.css +35 -0
- package/frontend/assets/css/iframe.css +1 -1
- package/frontend/assets/css/main/perf.css +63 -0
- package/frontend/assets/css/main.css +1 -0
- package/frontend/views/iframe_component.twig.miyagi +17 -0
- package/frontend/views/main.twig.miyagi +12 -0
- package/lib/build/index.js +0 -73
- package/lib/cli/index.js +2 -2
- package/lib/cli/perf.js +129 -0
- package/lib/cli/run.js +4 -4
- package/lib/default-config.js +0 -33
- package/lib/init/args.js +10 -23
- package/lib/init/router.js +9 -27
- package/lib/performance/classify.js +33 -0
- package/lib/performance/component.js +122 -0
- package/lib/performance/config.js +65 -0
- package/lib/performance/html-size.js +55 -0
- package/lib/performance/index.js +130 -374
- package/lib/performance/page.js +105 -0
- package/lib/performance/render-page.js +34 -0
- package/lib/performance/routes.js +72 -0
- package/lib/performance/schema.json +79 -0
- package/lib/performance/view-data.js +86 -0
- package/lib/render/index.js +0 -4
- package/lib/render/views/iframe/component.js +14 -0
- package/lib/render/views/main/component.js +23 -1
- package/lib/state/menu/index.js +1 -35
- package/package.json +2 -1
- package/frontend/assets/css/iframe/performance.css +0 -64
- package/frontend/views/performance.twig.miyagi +0 -72
- package/lib/cli/budget.js +0 -157
- package/lib/performance/report.js +0 -102
- package/lib/render/views/iframe/performance.js +0 -74
- package/lib/render/views/main/performance.js +0 -51
package/lib/cli/budget.js
DELETED
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
import { writeFile } from "node:fs/promises";
|
|
2
|
-
import getConfig from "../config.js";
|
|
3
|
-
import log from "../logger.js";
|
|
4
|
-
import { EXIT_CODES } from "../errors.js";
|
|
5
|
-
import { runPerformance } from "../performance/index.js";
|
|
6
|
-
import { generatePerformanceReport } from "../performance/report.js";
|
|
7
|
-
import { formatSize } from "../performance/parse-size.js";
|
|
8
|
-
|
|
9
|
-
const VALID_COMPRESSIONS = ["raw", "gzip", "brotli"];
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* `miyagi budget` — on-demand performance budget check.
|
|
13
|
-
* By default walks the configured global CSS/JS and asset folders, measures
|
|
14
|
-
* them, and compares against `config.performance.budgets`. The HTML category
|
|
15
|
-
* (post-build pages) is opt-in via `--build-folder`.
|
|
16
|
-
* @param {object} args
|
|
17
|
-
* @returns {Promise<object>}
|
|
18
|
-
*/
|
|
19
|
-
export default async function budgetCli(args) {
|
|
20
|
-
const config = await getConfig(args);
|
|
21
|
-
global.config = config;
|
|
22
|
-
|
|
23
|
-
if (args.compression && !VALID_COMPRESSIONS.includes(args.compression)) {
|
|
24
|
-
log(
|
|
25
|
-
"error",
|
|
26
|
-
`Unknown --compression "${args.compression}". Use one of: ${VALID_COMPRESSIONS.join(", ")}`,
|
|
27
|
-
);
|
|
28
|
-
return {
|
|
29
|
-
success: false,
|
|
30
|
-
code: EXIT_CODES.CLI_USAGE_ERROR,
|
|
31
|
-
shouldExit: true,
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
if (args.compression) {
|
|
36
|
-
config.performance = {
|
|
37
|
-
...(config.performance || {}),
|
|
38
|
-
compression: args.compression,
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const result = runPerformance({
|
|
43
|
-
config,
|
|
44
|
-
html: Boolean(args.buildFolder),
|
|
45
|
-
buildFolder: args.buildFolder,
|
|
46
|
-
listAllPages: args.listAllPages,
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
if (args.json) {
|
|
50
|
-
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
51
|
-
} else {
|
|
52
|
-
printTable(result);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
if (args.output) {
|
|
56
|
-
const report = generatePerformanceReport(result);
|
|
57
|
-
try {
|
|
58
|
-
await writeFile(args.output, report, "utf-8");
|
|
59
|
-
log("info", `Performance report written to ${args.output}`);
|
|
60
|
-
} catch (error) {
|
|
61
|
-
log("error", `Failed to write report: ${error.message}`);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const exceeded = result.summary.exceed > 0;
|
|
66
|
-
const shouldFail =
|
|
67
|
-
args.fail || config.performance?.report?.failOnExceed;
|
|
68
|
-
|
|
69
|
-
if (exceeded && shouldFail) {
|
|
70
|
-
return {
|
|
71
|
-
success: false,
|
|
72
|
-
code: EXIT_CODES.VALIDATION_ERROR,
|
|
73
|
-
shouldExit: true,
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (exceeded) {
|
|
78
|
-
log(
|
|
79
|
-
"warn",
|
|
80
|
-
`Performance budget exceeded in ${result.summary.exceed} categor${result.summary.exceed === 1 ? "y" : "ies"}.`,
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return {
|
|
85
|
-
success: true,
|
|
86
|
-
code: EXIT_CODES.SUCCESS,
|
|
87
|
-
shouldExit: true,
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* @param {import("../performance/index.js").PerformanceResult} result
|
|
93
|
-
* @returns {void}
|
|
94
|
-
*/
|
|
95
|
-
function printTable(result) {
|
|
96
|
-
const { evaluations, compression, summary } = result;
|
|
97
|
-
|
|
98
|
-
log("info", `Performance budget — measured at ${compression}.`);
|
|
99
|
-
|
|
100
|
-
const header = ["Category", "Item", "Actual", "Budget", "Status"];
|
|
101
|
-
const rows = evaluations.map((row) => [
|
|
102
|
-
row.category,
|
|
103
|
-
row.key === "total" ? "Total" : shortLabel(row.label),
|
|
104
|
-
formatSize(row.actual),
|
|
105
|
-
formatSize(row.budget),
|
|
106
|
-
statusLabel(row.status),
|
|
107
|
-
]);
|
|
108
|
-
|
|
109
|
-
const columnWidths = header.map((heading, index) =>
|
|
110
|
-
Math.max(heading.length, ...rows.map((row) => row[index].length)),
|
|
111
|
-
);
|
|
112
|
-
|
|
113
|
-
const padCell = (text, width) => text + " ".repeat(width - text.length);
|
|
114
|
-
const renderRow = (columns) =>
|
|
115
|
-
columns.map((cell, index) => padCell(cell, columnWidths[index])).join(" ");
|
|
116
|
-
|
|
117
|
-
process.stdout.write(`${renderRow(header)}\n`);
|
|
118
|
-
process.stdout.write(
|
|
119
|
-
`${columnWidths.map((width) => "-".repeat(width)).join(" ")}\n`,
|
|
120
|
-
);
|
|
121
|
-
for (const row of rows) {
|
|
122
|
-
process.stdout.write(`${renderRow(row)}\n`);
|
|
123
|
-
}
|
|
124
|
-
process.stdout.write(
|
|
125
|
-
`\nOK: ${summary.ok} Warn: ${summary.warn} Exceed: ${summary.exceed} Unbudgeted: ${summary.unbudgeted}\n`,
|
|
126
|
-
);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const MAX_LABEL_LENGTH = 60;
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* @param {string} label
|
|
133
|
-
* @returns {string}
|
|
134
|
-
*/
|
|
135
|
-
function shortLabel(label) {
|
|
136
|
-
if (label.length <= MAX_LABEL_LENGTH) {
|
|
137
|
-
return label;
|
|
138
|
-
}
|
|
139
|
-
return `…${label.slice(-(MAX_LABEL_LENGTH - 1))}`;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* @param {string} status
|
|
144
|
-
* @returns {string}
|
|
145
|
-
*/
|
|
146
|
-
function statusLabel(status) {
|
|
147
|
-
switch (status) {
|
|
148
|
-
case "ok":
|
|
149
|
-
return "OK";
|
|
150
|
-
case "warn":
|
|
151
|
-
return "WARN";
|
|
152
|
-
case "exceed":
|
|
153
|
-
return "EXCEED";
|
|
154
|
-
default:
|
|
155
|
-
return "—";
|
|
156
|
-
}
|
|
157
|
-
}
|
|
@@ -1,102 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,74 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,51 +0,0 @@
|
|
|
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
|
-
}
|