@schalkneethling/miyagi-core 4.6.2 → 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.
- package/api/index.js +35 -0
- package/dist/css/iframe.css +1 -1
- package/frontend/assets/css/iframe/performance.css +64 -0
- package/frontend/assets/css/iframe.css +1 -0
- package/frontend/views/component_variation.twig.miyagi +2 -2
- package/frontend/views/iframe_component.twig.miyagi +9 -0
- package/frontend/views/performance.twig.miyagi +72 -0
- package/lib/build/index.js +74 -0
- package/lib/cli/budget.js +157 -0
- package/lib/cli/index.js +2 -0
- package/lib/cli/run.js +11 -0
- package/lib/data-json/index.js +69 -0
- package/lib/default-config.js +33 -0
- package/lib/helpers.js +10 -2
- package/lib/init/args.js +39 -0
- package/lib/init/router.js +28 -0
- package/lib/init/watcher.js +17 -0
- package/lib/performance/index.js +400 -0
- package/lib/performance/measure.js +124 -0
- package/lib/performance/parse-size.js +86 -0
- package/lib/performance/report.js +102 -0
- package/lib/render/index.js +4 -0
- package/lib/render/views/iframe/component.js +25 -1
- package/lib/render/views/iframe/performance.js +74 -0
- package/lib/render/views/iframe/variation.standalone.js +8 -1
- package/lib/render/views/main/performance.js +51 -0
- package/lib/state/components.js +4 -0
- package/lib/state/file-contents.js +1 -0
- package/lib/state/menu/index.js +45 -4
- package/package.json +1 -1
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Measure on-disk, gzip-compressed, and brotli-compressed sizes of files.
|
|
3
|
+
* Transfer size is what a user actually downloads, which is almost never the raw
|
|
4
|
+
* on-disk size — gzip and brotli are reported alongside raw so users can see all
|
|
5
|
+
* three and pick the metric their hosting actually serves.
|
|
6
|
+
* We measure whichever files the user has configured in their assets — source
|
|
7
|
+
* files, bundled output from esbuild/rollup/webpack, or anything else. Point
|
|
8
|
+
* `config.assets` at the files you actually ship and the budget reflects reality.
|
|
9
|
+
* Results are cached by absolute path + mtime. In dev the file watcher changes
|
|
10
|
+
* the mtime on every save, naturally invalidating the cache.
|
|
11
|
+
* @module performance/measure
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
import zlib from "node:zlib";
|
|
16
|
+
|
|
17
|
+
const cache = new Map();
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {object} FileMeasurement
|
|
21
|
+
* @property {string} path - absolute path
|
|
22
|
+
* @property {number} raw - on-disk byte length
|
|
23
|
+
* @property {number} gzip - gzip-compressed byte length
|
|
24
|
+
* @property {number} brotli - brotli-compressed byte length
|
|
25
|
+
* @property {boolean} [missing] - true when the file could not be read
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @typedef {object} CategoryMeasurement
|
|
30
|
+
* @property {FileMeasurement[]} files - per-file measurements in this category
|
|
31
|
+
* @property {{raw: number, gzip: number, brotli: number}} totals - sum across files
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @typedef {object} Measurement
|
|
36
|
+
* @property {Record<string, CategoryMeasurement>} categories - keyed by category name
|
|
37
|
+
* @property {{raw: number, gzip: number, brotli: number}} totals - sum across categories
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Clear the cached results. Intended for tests and for watcher-driven
|
|
42
|
+
* whole-state refreshes.
|
|
43
|
+
* @returns {void}
|
|
44
|
+
*/
|
|
45
|
+
export function clearMeasureCache() {
|
|
46
|
+
cache.clear();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {string} absPath
|
|
51
|
+
* @returns {FileMeasurement}
|
|
52
|
+
*/
|
|
53
|
+
function measureFile(absPath) {
|
|
54
|
+
// Open a file descriptor first, then stat and read through it. This avoids
|
|
55
|
+
// a race where the file is modified between stat and read — both operations
|
|
56
|
+
// now refer to the same version of the file.
|
|
57
|
+
let fd;
|
|
58
|
+
try {
|
|
59
|
+
fd = fs.openSync(absPath, "r");
|
|
60
|
+
} catch {
|
|
61
|
+
return { path: absPath, raw: 0, gzip: 0, brotli: 0, missing: true };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let stat;
|
|
65
|
+
let buffer;
|
|
66
|
+
try {
|
|
67
|
+
stat = fs.fstatSync(fd);
|
|
68
|
+
buffer = Buffer.alloc(stat.size);
|
|
69
|
+
fs.readSync(fd, buffer, 0, stat.size, 0);
|
|
70
|
+
} catch {
|
|
71
|
+
fs.closeSync(fd);
|
|
72
|
+
return { path: absPath, raw: 0, gzip: 0, brotli: 0, missing: true };
|
|
73
|
+
}
|
|
74
|
+
fs.closeSync(fd);
|
|
75
|
+
|
|
76
|
+
const cacheKey = `${absPath}:${stat.mtimeMs}:${stat.size}`;
|
|
77
|
+
const cached = cache.get(cacheKey);
|
|
78
|
+
if (cached) {
|
|
79
|
+
return cached;
|
|
80
|
+
}
|
|
81
|
+
const measurement = {
|
|
82
|
+
path: absPath,
|
|
83
|
+
raw: buffer.byteLength,
|
|
84
|
+
gzip: zlib.gzipSync(buffer).byteLength,
|
|
85
|
+
// Brotli default is text mode with maximum quality which is _very_ slow on
|
|
86
|
+
// large files; use quality 6 to approximate what a CDN typically serves.
|
|
87
|
+
brotli: zlib.brotliCompressSync(buffer, {
|
|
88
|
+
params: { [zlib.constants.BROTLI_PARAM_QUALITY]: 6 },
|
|
89
|
+
}).byteLength,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
cache.set(cacheKey, measurement);
|
|
93
|
+
return measurement;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Measure a set of categorised file lists.
|
|
98
|
+
* @param {{category: string, files: string[]}[]} categorised
|
|
99
|
+
* @returns {Measurement}
|
|
100
|
+
*/
|
|
101
|
+
export function measure(categorised) {
|
|
102
|
+
/** @type {Record<string, CategoryMeasurement>} */
|
|
103
|
+
const categories = {};
|
|
104
|
+
const grand = { raw: 0, gzip: 0, brotli: 0 };
|
|
105
|
+
|
|
106
|
+
for (const { category, files } of categorised) {
|
|
107
|
+
const measured = files.map((absPath) => measureFile(absPath));
|
|
108
|
+
const totals = measured.reduce(
|
|
109
|
+
(acc, file) => ({
|
|
110
|
+
raw: acc.raw + file.raw,
|
|
111
|
+
gzip: acc.gzip + file.gzip,
|
|
112
|
+
brotli: acc.brotli + file.brotli,
|
|
113
|
+
}),
|
|
114
|
+
{ raw: 0, gzip: 0, brotli: 0 },
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
categories[category] = { files: measured, totals };
|
|
118
|
+
grand.raw += totals.raw;
|
|
119
|
+
grand.gzip += totals.gzip;
|
|
120
|
+
grand.brotli += totals.brotli;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { categories, totals: grand };
|
|
124
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Human-friendly size parser for performance budgets.
|
|
3
|
+
* Accepts either a bare number (bytes) or a string with a SI-ish unit suffix.
|
|
4
|
+
* Recognised units (case-insensitive): B, kB, KB, MB, GB. The number may be
|
|
5
|
+
* a decimal (e.g. "1.5 MB") and whitespace between number and unit is optional.
|
|
6
|
+
* Uses the decimal convention (1 kB = 1000 B) because that is what web performance
|
|
7
|
+
* budgets are traditionally expressed in — it matches the web.dev budget guidance
|
|
8
|
+
* and the way transfer sizes are reported in browser devtools.
|
|
9
|
+
* @module performance/parse-size
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const ONE_KILOBYTE = 1000;
|
|
13
|
+
const ONE_MEGABYTE = 1000 * 1000;
|
|
14
|
+
const ONE_GIGABYTE = 1000 * 1000 * 1000;
|
|
15
|
+
|
|
16
|
+
const UNIT_MULTIPLIERS = {
|
|
17
|
+
b: 1,
|
|
18
|
+
kb: ONE_KILOBYTE,
|
|
19
|
+
mb: ONE_MEGABYTE,
|
|
20
|
+
gb: ONE_GIGABYTE,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Parse a size value into bytes.
|
|
25
|
+
* @param {string|number|null|undefined} value
|
|
26
|
+
* @returns {number|null} bytes, or null when value is null/undefined
|
|
27
|
+
* @throws {TypeError} if value cannot be parsed
|
|
28
|
+
*/
|
|
29
|
+
export default function parseSize(value) {
|
|
30
|
+
if (value == null) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (typeof value === "number") {
|
|
35
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
36
|
+
throw new TypeError(`Invalid size value: ${value}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return Math.round(value);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (typeof value !== "string") {
|
|
43
|
+
throw new TypeError(`Invalid size value: ${value}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const trimmed = value.trim();
|
|
47
|
+
// Match a number (integer or decimal) optionally followed by a unit suffix.
|
|
48
|
+
// Examples: "50", "1.5 kB", "200MB", "2048 B"
|
|
49
|
+
// Group 1: the numeric part (e.g. "1.5")
|
|
50
|
+
// Group 2: the unit part, may be empty (e.g. "kB", "MB", or "")
|
|
51
|
+
const match = trimmed.match(/^(\d+(?:\.\d+)?)\s*([a-zA-Z]*)$/);
|
|
52
|
+
|
|
53
|
+
if (!match) {
|
|
54
|
+
throw new TypeError(`Invalid size value: ${value}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const amount = Number.parseFloat(match[1]);
|
|
58
|
+
const unit = (match[2] || "b").toLowerCase();
|
|
59
|
+
|
|
60
|
+
if (!(unit in UNIT_MULTIPLIERS)) {
|
|
61
|
+
throw new TypeError(`Unknown size unit "${match[2]}" in "${value}"`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return Math.round(amount * UNIT_MULTIPLIERS[unit]);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Format a number of bytes as a human-friendly string (for display / reports).
|
|
69
|
+
* @param {number|null} bytes
|
|
70
|
+
* @returns {string}
|
|
71
|
+
*/
|
|
72
|
+
export function formatSize(bytes) {
|
|
73
|
+
if (bytes == null) {
|
|
74
|
+
return "—";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (bytes < ONE_KILOBYTE) {
|
|
78
|
+
return `${bytes} B`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (bytes < ONE_MEGABYTE) {
|
|
82
|
+
return `${(bytes / ONE_KILOBYTE).toFixed(2)} kB`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return `${(bytes / ONE_MEGABYTE).toFixed(2)} MB`;
|
|
86
|
+
}
|
|
@@ -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
|
+
}
|
package/lib/render/index.js
CHANGED
|
@@ -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
|
};
|
|
@@ -32,6 +32,8 @@ export default async function renderIframeComponent({
|
|
|
32
32
|
global.state.fileContents[path.join(component.paths.dir.full, "README.md")];
|
|
33
33
|
const componentSchema =
|
|
34
34
|
global.state.fileContents[component.paths.schema.full];
|
|
35
|
+
const componentDataJson =
|
|
36
|
+
global.state.fileContents[component.paths.data.full] ?? null;
|
|
35
37
|
const componentTemplate = hasTemplate
|
|
36
38
|
? global.state.fileContents[component.paths.tpl.full]
|
|
37
39
|
: null;
|
|
@@ -103,6 +105,16 @@ export default async function renderIframeComponent({
|
|
|
103
105
|
),
|
|
104
106
|
}
|
|
105
107
|
: null,
|
|
108
|
+
data: componentDataJson
|
|
109
|
+
? {
|
|
110
|
+
string: JSON.stringify(componentDataJson, null, 2),
|
|
111
|
+
type: "json",
|
|
112
|
+
file: path.join(
|
|
113
|
+
global.config.components.folder,
|
|
114
|
+
component.paths.data.short,
|
|
115
|
+
),
|
|
116
|
+
}
|
|
117
|
+
: null,
|
|
106
118
|
};
|
|
107
119
|
|
|
108
120
|
await renderVariations({
|
|
@@ -205,13 +217,24 @@ async function renderVariations({
|
|
|
205
217
|
}
|
|
206
218
|
}
|
|
207
219
|
|
|
220
|
+
let dataJson;
|
|
221
|
+
|
|
222
|
+
if (fileContents?.data?.string) {
|
|
223
|
+
try {
|
|
224
|
+
dataJson = JSON.stringify(JSON.parse(fileContents.data.string));
|
|
225
|
+
} catch (err) {
|
|
226
|
+
log("error", null, err);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
208
230
|
return Promise.all(promises)
|
|
209
231
|
.then(async () => {
|
|
210
232
|
const themeMode = getThemeMode(cookies);
|
|
211
233
|
const renderFileTabs = !!(
|
|
212
234
|
fileContents.schema ||
|
|
213
235
|
fileContents.mocks ||
|
|
214
|
-
fileContents.template
|
|
236
|
+
fileContents.template ||
|
|
237
|
+
fileContents.data
|
|
215
238
|
);
|
|
216
239
|
const componentsEntry = global.state.components.find(
|
|
217
240
|
({ shortPath }) => shortPath === component.paths.dir.short,
|
|
@@ -259,6 +282,7 @@ async function renderVariations({
|
|
|
259
282
|
? validatedMocks[0].data.map((error) => error.message).join("\n")
|
|
260
283
|
: null,
|
|
261
284
|
mocks: mocksJson,
|
|
285
|
+
dataJson,
|
|
262
286
|
template: fileContents.template,
|
|
263
287
|
renderInformation: renderFileTabs || variations.length > 0,
|
|
264
288
|
renderFileTabs,
|
|
@@ -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
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import config from "../../../default-config.js";
|
|
3
|
+
import { resolveDataJson } from "../../../data-json/index.js";
|
|
3
4
|
import { getUserUiConfig } from "../../helpers.js";
|
|
4
5
|
import resolveAssets from "../../helpers/resolve-assets.js";
|
|
5
6
|
import applyOverrides from "../../helpers/apply-overrides.js";
|
|
@@ -13,6 +14,7 @@ import applyOverrides from "../../helpers/apply-overrides.js";
|
|
|
13
14
|
* @param {Function} [object.cb] - callback function
|
|
14
15
|
* @param {object} [object.cookies]
|
|
15
16
|
* @param {object} [object.overrides] - query-param override values
|
|
17
|
+
* @param {string} [object.variation] - the variation name
|
|
16
18
|
* @returns {Promise} gets resolved when the variation has been rendered
|
|
17
19
|
*/
|
|
18
20
|
export default async function renderIframeVariationStandalone({
|
|
@@ -23,6 +25,7 @@ export default async function renderIframeVariationStandalone({
|
|
|
23
25
|
cb,
|
|
24
26
|
cookies,
|
|
25
27
|
overrides,
|
|
28
|
+
variation,
|
|
26
29
|
}) {
|
|
27
30
|
if (overrides) {
|
|
28
31
|
const schema =
|
|
@@ -31,6 +34,9 @@ export default async function renderIframeVariationStandalone({
|
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
const directoryPath = component.paths.dir.short;
|
|
37
|
+
const { json: dataJson, id: dataJsonId } = await resolveDataJson(component, {
|
|
38
|
+
variation,
|
|
39
|
+
});
|
|
34
40
|
|
|
35
41
|
return new Promise((resolve, reject) => {
|
|
36
42
|
global.app.render(
|
|
@@ -58,7 +64,8 @@ export default async function renderIframeVariationStandalone({
|
|
|
58
64
|
"component_variation.twig.miyagi",
|
|
59
65
|
{
|
|
60
66
|
html: result,
|
|
61
|
-
|
|
67
|
+
dataJson,
|
|
68
|
+
dataJsonId,
|
|
62
69
|
cssFiles,
|
|
63
70
|
jsFilesHead,
|
|
64
71
|
jsFilesBody,
|
|
@@ -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
|
+
}
|
package/lib/state/components.js
CHANGED
|
@@ -116,6 +116,10 @@ function addToRoutes({ name, shortPath, fullPath }, partials = []) {
|
|
|
116
116
|
full: path.join(fullPath, "README.md"),
|
|
117
117
|
short: path.join(shortPath, "README.md"),
|
|
118
118
|
},
|
|
119
|
+
data: {
|
|
120
|
+
full: path.join(fullPath, "data.json"),
|
|
121
|
+
short: path.join(shortPath, "data.json"),
|
|
122
|
+
},
|
|
119
123
|
},
|
|
120
124
|
type: "components",
|
|
121
125
|
};
|
|
@@ -69,6 +69,7 @@ async function getFilePaths(sourceTree) {
|
|
|
69
69
|
`${files.mocks.name}.${files.mocks.extension[0]}`,
|
|
70
70
|
`${files.mocks.name}.${files.mocks.extension[1]}`,
|
|
71
71
|
`${files.schema.name}.${files.schema.extension}`,
|
|
72
|
+
"data.json",
|
|
72
73
|
]) ||
|
|
73
74
|
helpers.fileIsDocumentationFile(entry.path)
|
|
74
75
|
) {
|
package/lib/state/menu/index.js
CHANGED
|
@@ -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)
|
|
230
|
-
|
|
231
|
-
|
|
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.
|
|
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)",
|