@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 CHANGED
@@ -17,6 +17,8 @@ import {
17
17
  validateComponentHtml as validateComponentHtmlImpl,
18
18
  } from "../lib/validator/html.js";
19
19
  import { generateMarkdownReport } from "../lib/validator/html-report.js";
20
+ import { runPerformance } from "../lib/performance/index.js";
21
+ import { generatePerformanceReport } from "../lib/performance/report.js";
20
22
 
21
23
  /**
22
24
  * @param {object} obj
@@ -327,6 +329,39 @@ export const validateHtml = async (options = {}) => {
327
329
  };
328
330
  };
329
331
 
332
+ /**
333
+ * Run the performance-budget check programmatically.
334
+ * @param {object} [options]
335
+ * @param {boolean} [options.html] - include post-build HTML pages
336
+ * @param {string} [options.buildFolder] - required when html is true
337
+ * @param {"raw"|"gzip"|"brotli"} [options.compression] - override config compression
338
+ * @returns {Promise<object>}
339
+ */
340
+ export const getPerformance = async (options = {}) => {
341
+ global.app = await init("api");
342
+
343
+ if (options.compression) {
344
+ global.config.performance = {
345
+ ...(global.config.performance || {}),
346
+ compression: options.compression,
347
+ };
348
+ }
349
+
350
+ const result = runPerformance({
351
+ config: global.config,
352
+ html: Boolean(options.html),
353
+ buildFolder: options.buildFolder,
354
+ });
355
+
356
+ return {
357
+ success: result.summary.exceed === 0,
358
+ data: {
359
+ result,
360
+ report: generatePerformanceReport(result),
361
+ },
362
+ };
363
+ };
364
+
330
365
  /**
331
366
  * @param {object} obj
332
367
  * @param {string|null} obj.component
@@ -4,4 +4,4 @@
4
4
  --json-tree-js-container-border-color
5
5
  );--json-tree-js-button-text-color:var(--json-tree-js-color-white);--json-tree-js-button-background-color-hover:var(
6
6
  --json-tree-js-container-border-color
7
- );--json-tree-js-button-text-color-hover:var(--json-tree-js-color-snow-white);--json-tree-js-button-background-color-active:#616b79;--json-tree-js-button-text-color-active:var(--json-tree-js-color-snow-white);--json-tree-js-border-radius:0.5rem;--json-tree-js-border-style-scrollbar:inset 0 0 6px var(--json-tree-js-color-dark-gray);--json-tree-js-border-size:0.5px;--json-tree-js-spacing:10px;--json-tree-js-spacing-font-size:1em;--json-tree-js-transition:all 0.3s}div.json-tree-js{background-color:var(--color-Code-background);border:.0625rem solid var(--color-Outline);box-sizing:border-box;color:var(--json-tree-js-color-white);display:inline-block;font-size:var(--json-tree-js-spacing-font-size);font-weight:var(--json-tree-js-text-bold-weight);line-height:1.5;margin:0!important;padding:var(--json-tree-js-spacing);width:100%}div.json-tree-js div.no-click{pointer-events:none!important}div.json-tree-js *,div.json-tree-js :after,div.json-tree-js :before{box-sizing:border-box}}@layer miyagi{div.json-tree-js :is(.down-arrow,.right-arrow,.no-arrow){appearance:none;background:none;border:none;display:inline-flex;font:inherit;height:1.5em;padding:0;width:1.5em}div.json-tree-js .no-arrow{pointer-events:none;visibility:hidden}div.json-tree-js :is(.down-arrow,.right-arrow){align-items:center;cursor:pointer;justify-content:center;transform:translateY(-.125em)}div.json-tree-js :is(.down-arrow,.right-arrow):after{border:var(--toggle-border);border-color:currentcolor;border-inline-end-width:.25em;border-top-width:.25em;content:"";display:inline-block;flex:0 0 var(--toggle-width);font-size:.4em;height:var(--toggle-height)}div.json-tree-js :is(.down-arrow,.right-arrow):hover{opacity:.7}div.json-tree-js .down-arrow:after{transform:translateY(-25%) rotate(135deg)}div.json-tree-js .right-arrow:after{transform:translateX(-25%) rotate(45deg)}div.json-tree-js div.title-bar{display:flex;margin-bottom:var(--json-tree-js-spacing)}div.json-tree-js div.title-bar div.controls,div.json-tree-js div.title-bar div.title{display:none}div.json-tree-js div.object-type-title{font-weight:var(--json-tree-js-header-bold-weight);text-align:left!important}div.json-tree-js div.object-type-title span.array{color:var(--json-tree-js-color-array)}div.json-tree-js div.object-type-title span.object{color:var(--json-tree-js-color-object)}div.json-tree-js div.object-type-title span.count{font-weight:var(--json-tree-js-text-bold-weight);margin-left:calc(var(--json-tree-js-spacing)/2)}div.json-tree-js div.object-type-contents{margin-left:calc(var(--json-tree-js-spacing)*2);margin-top:calc(var(--json-tree-js-spacing)/2);text-align:left!important}div.json-tree-js div.object-type-contents div.object-type-value{margin-bottom:calc(var(--json-tree-js-spacing)/2);margin-top:calc(var(--json-tree-js-spacing)/2)}div.json-tree-js div.object-type-contents div.object-type-value span.split{margin-left:calc(var(--json-tree-js-spacing)/2);margin-right:calc(var(--json-tree-js-spacing)/2)}div.json-tree-js div.object-type-contents div.object-type-value span.boolean,div.json-tree-js div.object-type-contents div.object-type-value span.date,div.json-tree-js div.object-type-contents div.object-type-value span.decimal,div.json-tree-js div.object-type-contents div.object-type-value span.function,div.json-tree-js div.object-type-contents div.object-type-value span.null,div.json-tree-js div.object-type-contents div.object-type-value span.number,div.json-tree-js div.object-type-contents div.object-type-value span.string,div.json-tree-js div.object-type-contents div.object-type-value span.unknown{transition:var(--json-tree-js-transition);transition-property:opacity}div.json-tree-js div.object-type-contents div.object-type-value span.boolean:not(.no-hover):hover,div.json-tree-js div.object-type-contents div.object-type-value span.date:not(.no-hover):hover,div.json-tree-js div.object-type-contents div.object-type-value span.decimal:not(.no-hover):hover,div.json-tree-js div.object-type-contents div.object-type-value span.function:not(.no-hover):hover,div.json-tree-js div.object-type-contents div.object-type-value span.null:not(.no-hover):hover,div.json-tree-js div.object-type-contents div.object-type-value span.number:not(.no-hover):hover,div.json-tree-js div.object-type-contents div.object-type-value span.string:not(.no-hover):hover,div.json-tree-js div.object-type-contents div.object-type-value span.unknown:not(.no-hover):hover{cursor:pointer;opacity:.7}div.json-tree-js div.object-type-contents div.object-type-value span.comma{color:var(--json-tree-js-color-white);font-weight:var(--json-tree-js-text-bold-weight)}div.json-tree-js div.object-type-contents div.object-type-value span.boolean{color:var(--json-tree-js-color-boolean)}div.json-tree-js div.object-type-contents div.object-type-value span.decimal{color:var(--json-tree-js-color-decimal)}div.json-tree-js div.object-type-contents div.object-type-value span.number{color:var(--json-tree-js-color-number)}div.json-tree-js div.object-type-contents div.object-type-value span.string{color:var(--json-tree-js-color-string)}div.json-tree-js div.object-type-contents div.object-type-value span.date{color:var(--json-tree-js-color-date)}div.json-tree-js div.object-type-contents div.object-type-value span.array{color:var(--json-tree-js-color-array);font-weight:var(--json-tree-js-header-bold-weight)}div.json-tree-js div.object-type-contents div.object-type-value span.object{color:var(--json-tree-js-color-object);font-weight:var(--json-tree-js-header-bold-weight)}div.json-tree-js div.object-type-contents div.object-type-value span.null{color:var(--json-tree-js-color-null);font-style:italic}div.json-tree-js div.object-type-contents div.object-type-value span.function{color:var(--json-tree-js-color-function);font-style:italic}div.json-tree-js div.object-type-contents div.object-type-value span.unknown{color:var(--json-tree-js-color-unknown);font-style:italic}div.json-tree-js div.object-type-contents div.object-type-value span.count{font-weight:var(--json-tree-js-text-bold-weight);margin-left:calc(var(--json-tree-js-spacing)/2)}.custom-scroll-bars::-webkit-scrollbar{width:12px}.custom-scroll-bars::-webkit-scrollbar-thumb,.custom-scroll-bars::-webkit-scrollbar-track{box-shadow:var(--json-tree-js-border-style-scrollbar)}.custom-scroll-bars::-webkit-scrollbar-thumb{background:var(--json-tree-js-color-white)}.custom-scroll-bars::-webkit-scrollbar-thumb:hover{background-color:var(--json-tree-js-color-white)}.custom-scroll-bars::-webkit-scrollbar-thumb:active{background-color:var(--json-tree-js-color-lighter-gray)}}@layer miyagi{.Colors{display:grid;gap:3em;grid-template-columns:repeat(auto-fill,minmax(14em,1fr))}.Colors-button{--size:3em;display:flex;flex-wrap:wrap}.Colors-button:after,.Colors-button:before{display:block}.Colors--all .Colors-button:before{margin-inline-end:10px;width:var(--size)}.Colors--all .Colors-button:before,.Colors--decoration .Colors-button:before{background:var(--color);border:1px solid var(--backdrop,transparent);content:"";height:var(--size);order:-1}.Colors--decoration .Colors-button:before{width:100%}.Colors--all .Colors-button:after,.Colors--typo .Colors-button:after{color:var(--color);font-size:var(--size);font-weight:700;order:-1;-webkit-text-stroke:1px var(--backdrop,transparent);text-stroke:1px var(--backdrop,transparent)}.Colors--all .Colors-button:after{content:"Aa"}.Colors--typo .Colors-button:after{content:"AaBbCc"}.Colors-prop,.Colors-value{flex:1 0 100%;margin:10px 0 0}.Fonts-item:not(:first-child){margin-top:3em}.Fonts-button{color:inherit}.Fonts-button:before{content:"The quick brown fox jumps over the lazy dog";font-family:var(--font-family);font-feature-settings:var(--font-feature-settings);font-kerning:var(--font-kerning);font-size:var(--font-size);font-size-adjust:var(--font-size-adjust);font-stretch:var(--font-stretch);font-style:var(--font-style);font-variant:var(--font-variant);font-variant-caps:var(--font-variant-caps);font-weight:var(--font-weight);letter-spacing:var(--letter-spacing);line-height:var(--line-height);text-shadow:var(--text-shadow);text-transform:var(--text-transform)}.Fonts-details{display:flex;list-style:none;padding:0}.Fonts-details>li:not(:last-child){border-inline-end:.0625rem solid currentcolor;margin-inline-end:.5em;padding-inline-end:.5em}.Spacings{align-items:flex-start;display:flex;flex-direction:column}.Spacings-item:not(:first-child){margin-top:3em}.Spacings-button{color:inherit}.Spacings-button:before{background:currentcolor;content:"";display:block;height:var(--spacing);width:var(--spacing)}.CustomPropsGroup{list-style:none;margin:3em 0;padding:0}.CustomProp{border:.0625rem solid transparent}.CustomProp,.CustomProp[aria-selected=true]{position:relative}.CustomProp-prop{display:block;margin:10px 0 0;word-break:break-all}.CustomProp-button{appearance:none;background:none;border:none;font:inherit;padding:0;text-align:start;width:fit-content}.CustomProp-button:focus{outline:none}.CustomProp-button:not([aria-expanded=true]):focus .CustomProp-prop,.CustomProp-button:not([aria-expanded=true]):hover .CustomProp-prop{text-decoration:underline}.CustomProp-details{background:var(--color-Tooltip-background);bottom:calc(100% + 15px);box-shadow:0 4px 10px rgba(0,0,0,.1);box-sizing:border-box;display:none;font-size:14px;gap:.5rem 1rem;grid-template-columns:fit-content(50%) 1fr;inset-inline-start:0;line-height:1.5;margin:0;outline:.0625rem solid var(--color-Tooltip-outline);padding:15px;position:absolute;white-space:nowrap;z-index:1}}@layer miyagi{}@layer miyagi{}@layer miyagi{@keyframes fadeIn{0%{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}.CustomProp-button:hover+.CustomProp-details,.CustomProp-details:not([hidden]){animation:fadeIn .15s ease;display:grid}.CustomProp-details:after,.CustomProp-details:before{border:10px solid transparent;content:"";display:block;height:0;inset-inline-start:15px;position:absolute;width:0}.CustomProp-details:before{border-top-color:var(--color-Tooltip-outline);top:100%}.CustomProp-details:after{border-top-color:var(--color-Tooltip-background);top:calc(100% - 2px)}.CustomProp-detailsProperty{font-weight:600}.CustomProp-detailsValue{margin:0}}html{--iframe-spacing:clamp(0.75rem,4vi,2.5rem);height:100%}body{background:var(--color-Iframe-background);color:var(--color-Iframe-text);font-family:var(--font-family);margin:0;min-height:100%}.Wrapper{box-sizing:border-box;padding:var(--iframe-spacing);width:100%}a{color:var(--color-Iframe-link);text-decoration:underline}a:focus-visible,button:focus-visible,summary:focus-visible{border-radius:.25em;outline:3px solid currentcolor;outline-offset:2px}code[class*=language-],pre[class*=language-]{border-radius:0;box-shadow:none;text-shadow:none}.Code code,.Documentation code,:not(pre)>code[class*=language-]{font-size-adjust:.525;text-shadow:none}.Code code,.Documentation,.ErrorMessage,.Information,:not(pre)>code[class*=language-]{line-height:3.125ex}.Code code,.Documentation code,.Information code,:not(pre)>code[class*=language-]{font-family:Menlo,Monaco,monospace}.Documentation h1{font-size:2.4em;font-weight:700;line-height:1;text-transform:capitalize}.Documentation>*+*{margin-top:3.125ex}.Documentation i{font-style:italic}.Documentation table{border-spacing:0}.Documentation th{border-block-end:.0625rem solid currentcolor;text-align:left}.Documentation :is(th,td){padding:.25em .5em}.Documentation tr:not(:last-child) td{border-block-end:.0625rem solid var(--color-Outline)}.Information-val{margin-inline-start:0}.Component-variationHeader{display:flex;font-size:14px;gap:16px;inset-inline-end:40px;line-height:1;position:absolute;top:13px}.Documentation>:first-child{margin-top:0}.Documentation p{max-width:64ch}.SectionTitle{align-items:center;display:flex;font-size:1.6em;margin:2em 0 1em;scroll-margin-top:1.5em}.SectionTitle:after{background:var(--color-Outline);content:"";flex:1;height:.0625rem;margin-inline-start:20px}.Information-wrapper+.Information-wrapper{margin-top:30px}.Information-attr{color:var(--color-Iframe-text-secondary);font-family:var(--font-family);font-size:.875em;letter-spacing:.0375em;text-decoration:none;text-transform:uppercase}.Information-attr:not(:first-child){margin-top:1em}.Documentation code,.Information code{font-weight:600}.Status:before{display:inline-block;margin-inline-end:.5em}.Status--valid{color:var(--color-Positive)}.Status--valid:before{content:"✓"}.Status--invalid{color:var(--color-Negative)}.Status--invalid:before{content:"✗"}.Error{align-items:center;display:flex;height:100%;justify-content:center;margin:0}.Error,.ErrorMessage{color:var(--color-Negative)}.ErrorMessage{margin:1.5em 0}.Iframe-newTabLink{align-items:center;display:flex;font-weight:400}.Iframe-newTabLink:before{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' aria-hidden='true' viewBox='0 0 24 24'%3E%3Cpath d='M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6m4-3h6v6m-11 5L21 3'/%3E%3C/svg%3E");content:"";display:block;height:1em;margin-inline-end:.5em;width:1em}.Code,:not(pre)>code[class*=language-],pre[class*=language-]{background:var(--color-Code-background)}:not(pre)>code[class*=language-],pre[class*=language-]{border-color:var(--color-Outline)}.Code,:not(pre)>code[class*=language-]{border:.0625rem solid var(--color-Outline);padding:calc(var(--iframe-spacing)/2)}.Code,pre[class*=language-]{font-size:.875em}.Tabs-tab summary{cursor:default;list-style-type:none;padding-block:10px;padding-inline-start:16px;position:relative}.Tabs-tab summary:before{border:var(--toggle-border);border-color:currentcolor;border-inline-end-width:.25em;border-top-width:.25em;content:"";display:block;font-size:.4em;height:var(--toggle-height);inset-inline-start:.25rem;position:absolute;top:50%;transition:var(--toggle-transition);width:var(--toggle-width)}.Tabs-tab summary::-webkit-details-marker{display:none}.Tabs-tab:not([open]) summary:before{transform:var(--toggle-transition-closed)}.Tabs-tab[open] summary:before{transform:var(--toggle-transition-opened)}.Component:not(:last-child){margin-bottom:calc(var(--iframe-spacing)*2)}.Component-iframeWrapper{height:calc(100vh - var(--iframe-spacing)*2.5);width:100%}.Component-iframeWrapper:not(.has-fixedHeight){outline:.0625rem solid var(--color-Outline);resize:both}.Component-iframe{height:100%;width:100%}.Component-head{display:flex;flex-wrap:wrap;gap:10px;justify-content:space-between;padding:0 0 20px}.Component-headMeta{align-items:center;display:flex;gap:16px}.Component-mockValidation{appearance:none;background:none;border:none;color:var(--color-Iframe-link);cursor:pointer;font-family:var(--font-family);font-size:inherit;line-height:1;margin:0;padding:0;text-decoration:underline}.Component-mockData{background:light-dark(#f2f2f2,#1f1f1f);border:none;box-sizing:border-box;height:calc(100vb - var(--iframe-spacing)*2);max-width:70ch;overscroll-behavior:contain;padding:var(--iframe-spacing);width:calc(100vi - var(--iframe-spacing)*2)}.Component-mockData::backdrop{background:light-dark(hsla(0,0%,100%,.8),rgba(0,0,0,.8))}.Component-mockData:not([open]){display:none}.Component-mockDataHeading{margin-block:0 1em}.Component-closeMockData{appearance:none;background:none;border:0;color:var(--color-Iframe-link);cursor:pointer;inset-block-start:1rem;inset-inline-end:1rem;line-height:1;margin:0;padding:0;position:absolute;text-decoration:underline}.Component-closeMockData,.Component-file{font-family:var(--font-family);font-size:.875em}.Component-file{align-items:center;color:var(--color-Iframe-text-secondary);display:inline-flex;flex-wrap:wrap;letter-spacing:.0375em;text-decoration:none;text-transform:uppercase}.Component-file:active,.Component-file:focus,.Component-file:hover{text-decoration:underline}.Component-fileFolders{color:var(--color-Iframe-text-tertiary)}.ComponentView{border:.0625rem solid var(--color-Outline);box-sizing:border-box;height:calc(100vh - var(--iframe-spacing)*2);overflow:hidden;resize:both;width:100%}.ComponentView-iframe{height:100%;width:100%}[data-mode=presentation] .DeveloperInformation{display:none}@media screen and (prefers-reduced-motion){*,:after,:before{animation:none!important;transition:none!important}}@media (prefers-color-scheme:dark){.Iframe-newTabLink:before{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' aria-hidden='true' viewBox='0 0 24 24'%3E%3Cpath d='M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6m4-3h6v6m-11 5L21 3'/%3E%3C/svg%3E")}}
7
+ );--json-tree-js-button-text-color-hover:var(--json-tree-js-color-snow-white);--json-tree-js-button-background-color-active:#616b79;--json-tree-js-button-text-color-active:var(--json-tree-js-color-snow-white);--json-tree-js-border-radius:0.5rem;--json-tree-js-border-style-scrollbar:inset 0 0 6px var(--json-tree-js-color-dark-gray);--json-tree-js-border-size:0.5px;--json-tree-js-spacing:10px;--json-tree-js-spacing-font-size:1em;--json-tree-js-transition:all 0.3s}div.json-tree-js{background-color:var(--color-Code-background);border:.0625rem solid var(--color-Outline);box-sizing:border-box;color:var(--json-tree-js-color-white);display:inline-block;font-size:var(--json-tree-js-spacing-font-size);font-weight:var(--json-tree-js-text-bold-weight);line-height:1.5;margin:0!important;padding:var(--json-tree-js-spacing);width:100%}div.json-tree-js div.no-click{pointer-events:none!important}div.json-tree-js *,div.json-tree-js :after,div.json-tree-js :before{box-sizing:border-box}}@layer miyagi{div.json-tree-js :is(.down-arrow,.right-arrow,.no-arrow){appearance:none;background:none;border:none;display:inline-flex;font:inherit;height:1.5em;padding:0;width:1.5em}div.json-tree-js .no-arrow{pointer-events:none;visibility:hidden}div.json-tree-js :is(.down-arrow,.right-arrow){align-items:center;cursor:pointer;justify-content:center;transform:translateY(-.125em)}div.json-tree-js :is(.down-arrow,.right-arrow):after{border:var(--toggle-border);border-color:currentcolor;border-inline-end-width:.25em;border-top-width:.25em;content:"";display:inline-block;flex:0 0 var(--toggle-width);font-size:.4em;height:var(--toggle-height)}div.json-tree-js :is(.down-arrow,.right-arrow):hover{opacity:.7}div.json-tree-js .down-arrow:after{transform:translateY(-25%) rotate(135deg)}div.json-tree-js .right-arrow:after{transform:translateX(-25%) rotate(45deg)}div.json-tree-js div.title-bar{display:flex;margin-bottom:var(--json-tree-js-spacing)}div.json-tree-js div.title-bar div.controls,div.json-tree-js div.title-bar div.title{display:none}div.json-tree-js div.object-type-title{font-weight:var(--json-tree-js-header-bold-weight);text-align:left!important}div.json-tree-js div.object-type-title span.array{color:var(--json-tree-js-color-array)}div.json-tree-js div.object-type-title span.object{color:var(--json-tree-js-color-object)}div.json-tree-js div.object-type-title span.count{font-weight:var(--json-tree-js-text-bold-weight);margin-left:calc(var(--json-tree-js-spacing)/2)}div.json-tree-js div.object-type-contents{margin-left:calc(var(--json-tree-js-spacing)*2);margin-top:calc(var(--json-tree-js-spacing)/2);text-align:left!important}div.json-tree-js div.object-type-contents div.object-type-value{margin-bottom:calc(var(--json-tree-js-spacing)/2);margin-top:calc(var(--json-tree-js-spacing)/2)}div.json-tree-js div.object-type-contents div.object-type-value span.split{margin-left:calc(var(--json-tree-js-spacing)/2);margin-right:calc(var(--json-tree-js-spacing)/2)}div.json-tree-js div.object-type-contents div.object-type-value span.boolean,div.json-tree-js div.object-type-contents div.object-type-value span.date,div.json-tree-js div.object-type-contents div.object-type-value span.decimal,div.json-tree-js div.object-type-contents div.object-type-value span.function,div.json-tree-js div.object-type-contents div.object-type-value span.null,div.json-tree-js div.object-type-contents div.object-type-value span.number,div.json-tree-js div.object-type-contents div.object-type-value span.string,div.json-tree-js div.object-type-contents div.object-type-value span.unknown{transition:var(--json-tree-js-transition);transition-property:opacity}div.json-tree-js div.object-type-contents div.object-type-value span.boolean:not(.no-hover):hover,div.json-tree-js div.object-type-contents div.object-type-value span.date:not(.no-hover):hover,div.json-tree-js div.object-type-contents div.object-type-value span.decimal:not(.no-hover):hover,div.json-tree-js div.object-type-contents div.object-type-value span.function:not(.no-hover):hover,div.json-tree-js div.object-type-contents div.object-type-value span.null:not(.no-hover):hover,div.json-tree-js div.object-type-contents div.object-type-value span.number:not(.no-hover):hover,div.json-tree-js div.object-type-contents div.object-type-value span.string:not(.no-hover):hover,div.json-tree-js div.object-type-contents div.object-type-value span.unknown:not(.no-hover):hover{cursor:pointer;opacity:.7}div.json-tree-js div.object-type-contents div.object-type-value span.comma{color:var(--json-tree-js-color-white);font-weight:var(--json-tree-js-text-bold-weight)}div.json-tree-js div.object-type-contents div.object-type-value span.boolean{color:var(--json-tree-js-color-boolean)}div.json-tree-js div.object-type-contents div.object-type-value span.decimal{color:var(--json-tree-js-color-decimal)}div.json-tree-js div.object-type-contents div.object-type-value span.number{color:var(--json-tree-js-color-number)}div.json-tree-js div.object-type-contents div.object-type-value span.string{color:var(--json-tree-js-color-string)}div.json-tree-js div.object-type-contents div.object-type-value span.date{color:var(--json-tree-js-color-date)}div.json-tree-js div.object-type-contents div.object-type-value span.array{color:var(--json-tree-js-color-array);font-weight:var(--json-tree-js-header-bold-weight)}div.json-tree-js div.object-type-contents div.object-type-value span.object{color:var(--json-tree-js-color-object);font-weight:var(--json-tree-js-header-bold-weight)}div.json-tree-js div.object-type-contents div.object-type-value span.null{color:var(--json-tree-js-color-null);font-style:italic}div.json-tree-js div.object-type-contents div.object-type-value span.function{color:var(--json-tree-js-color-function);font-style:italic}div.json-tree-js div.object-type-contents div.object-type-value span.unknown{color:var(--json-tree-js-color-unknown);font-style:italic}div.json-tree-js div.object-type-contents div.object-type-value span.count{font-weight:var(--json-tree-js-text-bold-weight);margin-left:calc(var(--json-tree-js-spacing)/2)}.custom-scroll-bars::-webkit-scrollbar{width:12px}.custom-scroll-bars::-webkit-scrollbar-thumb,.custom-scroll-bars::-webkit-scrollbar-track{box-shadow:var(--json-tree-js-border-style-scrollbar)}.custom-scroll-bars::-webkit-scrollbar-thumb{background:var(--json-tree-js-color-white)}.custom-scroll-bars::-webkit-scrollbar-thumb:hover{background-color:var(--json-tree-js-color-white)}.custom-scroll-bars::-webkit-scrollbar-thumb:active{background-color:var(--json-tree-js-color-lighter-gray)}}@layer miyagi{.Colors{display:grid;gap:3em;grid-template-columns:repeat(auto-fill,minmax(14em,1fr))}.Colors-button{--size:3em;display:flex;flex-wrap:wrap}.Colors-button:after,.Colors-button:before{display:block}.Colors--all .Colors-button:before{margin-inline-end:10px;width:var(--size)}.Colors--all .Colors-button:before,.Colors--decoration .Colors-button:before{background:var(--color);border:1px solid var(--backdrop,transparent);content:"";height:var(--size);order:-1}.Colors--decoration .Colors-button:before{width:100%}.Colors--all .Colors-button:after,.Colors--typo .Colors-button:after{color:var(--color);font-size:var(--size);font-weight:700;order:-1;-webkit-text-stroke:1px var(--backdrop,transparent);text-stroke:1px var(--backdrop,transparent)}.Colors--all .Colors-button:after{content:"Aa"}.Colors--typo .Colors-button:after{content:"AaBbCc"}.Colors-prop,.Colors-value{flex:1 0 100%;margin:10px 0 0}.Fonts-item:not(:first-child){margin-top:3em}.Fonts-button{color:inherit}.Fonts-button:before{content:"The quick brown fox jumps over the lazy dog";font-family:var(--font-family);font-feature-settings:var(--font-feature-settings);font-kerning:var(--font-kerning);font-size:var(--font-size);font-size-adjust:var(--font-size-adjust);font-stretch:var(--font-stretch);font-style:var(--font-style);font-variant:var(--font-variant);font-variant-caps:var(--font-variant-caps);font-weight:var(--font-weight);letter-spacing:var(--letter-spacing);line-height:var(--line-height);text-shadow:var(--text-shadow);text-transform:var(--text-transform)}.Fonts-details{display:flex;list-style:none;padding:0}.Fonts-details>li:not(:last-child){border-inline-end:.0625rem solid currentcolor;margin-inline-end:.5em;padding-inline-end:.5em}.Spacings{align-items:flex-start;display:flex;flex-direction:column}.Spacings-item:not(:first-child){margin-top:3em}.Spacings-button{color:inherit}.Spacings-button:before{background:currentcolor;content:"";display:block;height:var(--spacing);width:var(--spacing)}.CustomPropsGroup{list-style:none;margin:3em 0;padding:0}.CustomProp{border:.0625rem solid transparent}.CustomProp,.CustomProp[aria-selected=true]{position:relative}.CustomProp-prop{display:block;margin:10px 0 0;word-break:break-all}.CustomProp-button{appearance:none;background:none;border:none;font:inherit;padding:0;text-align:start;width:fit-content}.CustomProp-button:focus{outline:none}.CustomProp-button:not([aria-expanded=true]):focus .CustomProp-prop,.CustomProp-button:not([aria-expanded=true]):hover .CustomProp-prop{text-decoration:underline}.CustomProp-details{background:var(--color-Tooltip-background);bottom:calc(100% + 15px);box-shadow:0 4px 10px rgba(0,0,0,.1);box-sizing:border-box;display:none;font-size:14px;gap:.5rem 1rem;grid-template-columns:fit-content(50%) 1fr;inset-inline-start:0;line-height:1.5;margin:0;outline:.0625rem solid var(--color-Tooltip-outline);padding:15px;position:absolute;white-space:nowrap;z-index:1}}@layer miyagi{}@layer miyagi{}@layer miyagi{@keyframes fadeIn{0%{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}.CustomProp-button:hover+.CustomProp-details,.CustomProp-details:not([hidden]){animation:fadeIn .15s ease;display:grid}.CustomProp-details:after,.CustomProp-details:before{border:10px solid transparent;content:"";display:block;height:0;inset-inline-start:15px;position:absolute;width:0}.CustomProp-details:before{border-top-color:var(--color-Tooltip-outline);top:100%}.CustomProp-details:after{border-top-color:var(--color-Tooltip-background);top:calc(100% - 2px)}.CustomProp-detailsProperty{font-weight:600}.CustomProp-detailsValue{margin:0}.PerformanceTable{border-collapse:collapse;margin-block:1em;width:100%}}@layer miyagi{.PerformanceTable :is(th,td){padding:.35em .75em;text-align:start;vertical-align:top}.PerformanceTable thead th{border-block-end:.0625rem solid currentcolor;font-size:.875em;font-weight:700;letter-spacing:.0375em;text-transform:uppercase}.PerformanceTable tbody tr:not(:last-child) :is(td,th){border-block-end:.0625rem solid var(--color-Outline)}.PerformanceTable-totals :is(th,td){font-weight:700}.PerformanceTable tr[data-missing=true] td{color:var(--color-Iframe-text-secondary)}.PerformanceStatus{border-radius:.25em;display:inline-block;font-size:.875em;font-weight:700;letter-spacing:.0375em;padding:.15em .6em;text-transform:uppercase}.PerformanceStatus--ok{background:color-mix(in srgb,var(--color-Positive) 20%,transparent);color:var(--color-Positive)}.PerformanceStatus--warn{background:color-mix(in srgb,var(--color-Iframe-text-secondary) 25%,transparent);color:var(--color-Iframe-text)}.PerformanceStatus--exceed{background:color-mix(in srgb,var(--color-Negative) 20%,transparent);color:var(--color-Negative)}.PerformanceStatus--unbudgeted{color:var(--color-Iframe-text-secondary)}}html{--iframe-spacing:clamp(0.75rem,4vi,2.5rem);height:100%}body{background:var(--color-Iframe-background);color:var(--color-Iframe-text);font-family:var(--font-family);margin:0;min-height:100%}.Wrapper{box-sizing:border-box;padding:var(--iframe-spacing);width:100%}a{color:var(--color-Iframe-link);text-decoration:underline}a:focus-visible,button:focus-visible,summary:focus-visible{border-radius:.25em;outline:3px solid currentcolor;outline-offset:2px}code[class*=language-],pre[class*=language-]{border-radius:0;box-shadow:none;text-shadow:none}.Code code,.Documentation code,:not(pre)>code[class*=language-]{font-size-adjust:.525;text-shadow:none}.Code code,.Documentation,.ErrorMessage,.Information,:not(pre)>code[class*=language-]{line-height:3.125ex}.Code code,.Documentation code,.Information code,:not(pre)>code[class*=language-]{font-family:Menlo,Monaco,monospace}.Documentation h1{font-size:2.4em;font-weight:700;line-height:1;text-transform:capitalize}.Documentation>*+*{margin-top:3.125ex}.Documentation i{font-style:italic}.Documentation table{border-spacing:0}.Documentation th{border-block-end:.0625rem solid currentcolor;text-align:left}.Documentation :is(th,td){padding:.25em .5em}.Documentation tr:not(:last-child) td{border-block-end:.0625rem solid var(--color-Outline)}.Information-val{margin-inline-start:0}.Component-variationHeader{display:flex;font-size:14px;gap:16px;inset-inline-end:40px;line-height:1;position:absolute;top:13px}.Documentation>:first-child{margin-top:0}.Documentation p{max-width:64ch}.SectionTitle{align-items:center;display:flex;font-size:1.6em;margin:2em 0 1em;scroll-margin-top:1.5em}.SectionTitle:after{background:var(--color-Outline);content:"";flex:1;height:.0625rem;margin-inline-start:20px}.Information-wrapper+.Information-wrapper{margin-top:30px}.Information-attr{color:var(--color-Iframe-text-secondary);font-family:var(--font-family);font-size:.875em;letter-spacing:.0375em;text-decoration:none;text-transform:uppercase}.Information-attr:not(:first-child){margin-top:1em}.Documentation code,.Information code{font-weight:600}.Status:before{display:inline-block;margin-inline-end:.5em}.Status--valid{color:var(--color-Positive)}.Status--valid:before{content:"✓"}.Status--invalid{color:var(--color-Negative)}.Status--invalid:before{content:"✗"}.Error{align-items:center;display:flex;height:100%;justify-content:center;margin:0}.Error,.ErrorMessage{color:var(--color-Negative)}.ErrorMessage{margin:1.5em 0}.Iframe-newTabLink{align-items:center;display:flex;font-weight:400}.Iframe-newTabLink:before{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' aria-hidden='true' viewBox='0 0 24 24'%3E%3Cpath d='M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6m4-3h6v6m-11 5L21 3'/%3E%3C/svg%3E");content:"";display:block;height:1em;margin-inline-end:.5em;width:1em}.Code,:not(pre)>code[class*=language-],pre[class*=language-]{background:var(--color-Code-background)}:not(pre)>code[class*=language-],pre[class*=language-]{border-color:var(--color-Outline)}.Code,:not(pre)>code[class*=language-]{border:.0625rem solid var(--color-Outline);padding:calc(var(--iframe-spacing)/2)}.Code,pre[class*=language-]{font-size:.875em}.Tabs-tab summary{cursor:default;list-style-type:none;padding-block:10px;padding-inline-start:16px;position:relative}.Tabs-tab summary:before{border:var(--toggle-border);border-color:currentcolor;border-inline-end-width:.25em;border-top-width:.25em;content:"";display:block;font-size:.4em;height:var(--toggle-height);inset-inline-start:.25rem;position:absolute;top:50%;transition:var(--toggle-transition);width:var(--toggle-width)}.Tabs-tab summary::-webkit-details-marker{display:none}.Tabs-tab:not([open]) summary:before{transform:var(--toggle-transition-closed)}.Tabs-tab[open] summary:before{transform:var(--toggle-transition-opened)}.Component:not(:last-child){margin-bottom:calc(var(--iframe-spacing)*2)}.Component-iframeWrapper{height:calc(100vh - var(--iframe-spacing)*2.5);width:100%}.Component-iframeWrapper:not(.has-fixedHeight){outline:.0625rem solid var(--color-Outline);resize:both}.Component-iframe{height:100%;width:100%}.Component-head{display:flex;flex-wrap:wrap;gap:10px;justify-content:space-between;padding:0 0 20px}.Component-headMeta{align-items:center;display:flex;gap:16px}.Component-mockValidation{appearance:none;background:none;border:none;color:var(--color-Iframe-link);cursor:pointer;font-family:var(--font-family);font-size:inherit;line-height:1;margin:0;padding:0;text-decoration:underline}.Component-mockData{background:light-dark(#f2f2f2,#1f1f1f);border:none;box-sizing:border-box;height:calc(100vb - var(--iframe-spacing)*2);max-width:70ch;overscroll-behavior:contain;padding:var(--iframe-spacing);width:calc(100vi - var(--iframe-spacing)*2)}.Component-mockData::backdrop{background:light-dark(hsla(0,0%,100%,.8),rgba(0,0,0,.8))}.Component-mockData:not([open]){display:none}.Component-mockDataHeading{margin-block:0 1em}.Component-closeMockData{appearance:none;background:none;border:0;color:var(--color-Iframe-link);cursor:pointer;inset-block-start:1rem;inset-inline-end:1rem;line-height:1;margin:0;padding:0;position:absolute;text-decoration:underline}.Component-closeMockData,.Component-file{font-family:var(--font-family);font-size:.875em}.Component-file{align-items:center;color:var(--color-Iframe-text-secondary);display:inline-flex;flex-wrap:wrap;letter-spacing:.0375em;text-decoration:none;text-transform:uppercase}.Component-file:active,.Component-file:focus,.Component-file:hover{text-decoration:underline}.Component-fileFolders{color:var(--color-Iframe-text-tertiary)}.ComponentView{border:.0625rem solid var(--color-Outline);box-sizing:border-box;height:calc(100vh - var(--iframe-spacing)*2);overflow:hidden;resize:both;width:100%}.ComponentView-iframe{height:100%;width:100%}[data-mode=presentation] .DeveloperInformation{display:none}@media screen and (prefers-reduced-motion){*,:after,:before{animation:none!important;transition:none!important}}@media (prefers-color-scheme:dark){.Iframe-newTabLink:before{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' aria-hidden='true' viewBox='0 0 24 24'%3E%3Cpath d='M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6m4-3h6v6m-11 5L21 3'/%3E%3C/svg%3E")}}
@@ -0,0 +1,64 @@
1
+ .PerformanceTable {
2
+ border-collapse: collapse;
3
+ margin-block: 1em;
4
+ width: 100%;
5
+ }
6
+
7
+ .PerformanceTable :is(th, td) {
8
+ padding: 0.35em 0.75em;
9
+ text-align: start;
10
+ vertical-align: top;
11
+ }
12
+
13
+ .PerformanceTable thead th {
14
+ border-block-end: 0.0625rem solid currentcolor;
15
+ font-weight: bold;
16
+ text-transform: uppercase;
17
+ font-size: 0.875em;
18
+ letter-spacing: 0.0375em;
19
+ }
20
+
21
+ .PerformanceTable tbody tr:not(:last-child) :is(td, th) {
22
+ border-block-end: 0.0625rem solid var(--color-Outline);
23
+ }
24
+
25
+ .PerformanceTable-totals :is(th, td) {
26
+ font-weight: bold;
27
+ }
28
+
29
+ .PerformanceTable tr[data-missing="true"] td {
30
+ color: var(--color-Iframe-text-secondary);
31
+ }
32
+
33
+ .PerformanceStatus {
34
+ display: inline-block;
35
+ padding: 0.15em 0.6em;
36
+ border-radius: 0.25em;
37
+ font-size: 0.875em;
38
+ font-weight: bold;
39
+ letter-spacing: 0.0375em;
40
+ text-transform: uppercase;
41
+ }
42
+
43
+ .PerformanceStatus--ok {
44
+ background: color-mix(in srgb, var(--color-Positive) 20%, transparent);
45
+ color: var(--color-Positive);
46
+ }
47
+
48
+ .PerformanceStatus--warn {
49
+ background: color-mix(
50
+ in srgb,
51
+ var(--color-Iframe-text-secondary) 25%,
52
+ transparent
53
+ );
54
+ color: var(--color-Iframe-text);
55
+ }
56
+
57
+ .PerformanceStatus--exceed {
58
+ background: color-mix(in srgb, var(--color-Negative) 20%, transparent);
59
+ color: var(--color-Negative);
60
+ }
61
+
62
+ .PerformanceStatus--unbudgeted {
63
+ color: var(--color-Iframe-text-secondary);
64
+ }
@@ -4,6 +4,7 @@
4
4
  @import url("./iframe/accordion-tabs.css") layer(miyagi);
5
5
  @import url("./iframe/jsontree.js.css") layer(miyagi);
6
6
  @import url("./iframe/styleguide/index.css") layer(miyagi);
7
+ @import url("./iframe/performance.css") layer(miyagi);
7
8
 
8
9
  html {
9
10
  --iframe-spacing: clamp(0.75rem, 4vi, 2.5rem);
@@ -24,8 +24,8 @@
24
24
  {% if assets.js %}
25
25
  <script src="{{ assets.js }}"></script>
26
26
  {% endif %}
27
- {% if mockDataResolved is defined %}
28
- <script type="application/json" id="miyagi-mock-data">{{ mockDataResolved|replace({'</script>': '<\\/script>'})|raw }}</script>
27
+ {% if dataJson is defined and dataJson %}
28
+ <script type="application/json" id="{{ dataJsonId }}">{{ dataJson|replace({'</script>': '<\\/script>'})|raw }}</script>
29
29
  {% endif %}
30
30
  <script>
31
31
  {{ theme.js }}
@@ -34,6 +34,12 @@
34
34
  <div id="json-tree-resolved-mocks" data-jsontree-js="jsontree.mocks"></div>
35
35
  </details>
36
36
  {% endif %}
37
+ {% if dataJson %}
38
+ <details class="Tabs-tab">
39
+ <summary>Data</summary>
40
+ <div id="json-tree-resolved-data" data-jsontree-js="jsontree.data"></div>
41
+ </details>
42
+ {% endif %}
37
43
  {% if template %}
38
44
  <details class="Tabs-tab">
39
45
  <summary>Template</summary>
@@ -135,6 +141,9 @@
135
141
  },
136
142
  "mocks": {
137
143
  "data": {{ mocks|default({})|replace({'</script>': '<\\/script>'})|raw }}
144
+ },
145
+ "data": {
146
+ "data": {{ dataJson|default({})|replace({'</script>': '<\\/script>'})|raw }}
138
147
  }
139
148
  };
140
149
  </script>
@@ -0,0 +1,72 @@
1
+ {% extends "@miyagi/layouts/iframe_default.twig.miyagi" %}
2
+ {% block body %}
3
+ <div class="Wrapper">
4
+ <div class="Documentation">
5
+ <h1>Performance budget</h1>
6
+ <p>Measured at <strong>{{ compression }}</strong>. OK: {{ summary.ok }} · Warn: {{ summary.warn }} · Exceed: {{ summary.exceed }} · Unbudgeted: {{ summary.unbudgeted }}.</p>
7
+
8
+ <h2>Evaluations</h2>
9
+ <table class="PerformanceTable">
10
+ <thead>
11
+ <tr>
12
+ <th scope="col">Category</th>
13
+ <th scope="col">Item</th>
14
+ <th scope="col">Actual</th>
15
+ <th scope="col">Budget</th>
16
+ <th scope="col">Status</th>
17
+ </tr>
18
+ </thead>
19
+ <tbody>
20
+ {% for row in evaluations %}
21
+ <tr data-status="{{ row.status }}">
22
+ <td>{{ row.category }}</td>
23
+ <td>{% if row.key == "total" %}Total{% else %}{{ row.label }}{% endif %}</td>
24
+ <td>{{ row.actualFormatted }}</td>
25
+ <td>{{ row.budgetFormatted }}</td>
26
+ <td>
27
+ <span class="PerformanceStatus PerformanceStatus--{{ row.status }}">
28
+ {% if row.status == "ok" %}OK{% elseif row.status == "warn" %}WARN{% elseif row.status == "exceed" %}EXCEED{% else %}—{% endif %}
29
+ </span>
30
+ {% if row.ratioPercent is not null %}
31
+ <small>({{ row.ratioPercent }}%)</small>
32
+ {% endif %}
33
+ </td>
34
+ </tr>
35
+ {% endfor %}
36
+ </tbody>
37
+ </table>
38
+
39
+ {% for cat in categories %}
40
+ {% if cat.files|length > 0 %}
41
+ <h2>{{ cat.category }}</h2>
42
+ <table class="PerformanceTable">
43
+ <thead>
44
+ <tr>
45
+ <th scope="col">File</th>
46
+ <th scope="col">Raw</th>
47
+ <th scope="col">Gzip</th>
48
+ <th scope="col">Brotli</th>
49
+ </tr>
50
+ </thead>
51
+ <tbody>
52
+ {% for file in cat.files %}
53
+ <tr{% if file.missing %} data-missing="true"{% endif %}>
54
+ <td>{{ file.path }}{% if file.missing %} <em>(missing)</em>{% endif %}</td>
55
+ <td>{{ file.raw }}</td>
56
+ <td>{{ file.gzip }}</td>
57
+ <td>{{ file.brotli }}</td>
58
+ </tr>
59
+ {% endfor %}
60
+ <tr class="PerformanceTable-totals">
61
+ <th scope="row">Total</th>
62
+ <td>{{ cat.totals.raw }}</td>
63
+ <td>{{ cat.totals.gzip }}</td>
64
+ <td>{{ cat.totals.brotli }}</td>
65
+ </tr>
66
+ </tbody>
67
+ </table>
68
+ {% endif %}
69
+ {% endfor %}
70
+ </div>
71
+ </div>
72
+ {% endblock %}
@@ -7,6 +7,8 @@ import appConfig from "../default-config.js";
7
7
  import { t } from "../i18n/index.js";
8
8
  import log from "../logger.js";
9
9
  import { getVariationData } from "../mocks/index.js";
10
+ import { runPerformance } from "../performance/index.js";
11
+ import { generatePerformanceReport } from "../performance/report.js";
10
12
 
11
13
  /**
12
14
  * Module for creating a static build
@@ -176,6 +178,12 @@ export default () => {
176
178
  .then(async () => {
177
179
  await createJsonOutputFile(buildFolder, paths);
178
180
 
181
+ const performanceError = await writePerformanceReport(buildFolder);
182
+ if (performanceError) {
183
+ reject(performanceError);
184
+ return;
185
+ }
186
+
179
187
  resolve(
180
188
  t("buildDone").replace(
181
189
  "{{count}}",
@@ -190,6 +198,71 @@ export default () => {
190
198
  });
191
199
  });
192
200
 
201
+ /**
202
+ * Runs the performance-budget check against the just-built output, logs a
203
+ * summary, and (when configured) writes a markdown report. Never throws —
204
+ * a missing budget configuration is treated as "feature disabled". The only
205
+ * way this returns an Error is when `performance.report.failOnExceed` is on
206
+ * and a budget has actually been exceeded.
207
+ * @param {string} buildFolder
208
+ * @returns {Promise<Error|null>}
209
+ */
210
+ async function writePerformanceReport(buildFolder) {
211
+ const perfConfig = global.config.performance;
212
+ if (!perfConfig || !perfConfig.enabled) {
213
+ return null;
214
+ }
215
+
216
+ let result;
217
+ try {
218
+ result = runPerformance({
219
+ config: global.config,
220
+ html: true,
221
+ buildFolder,
222
+ });
223
+ } catch (error) {
224
+ log("warn", `Performance budget check skipped: ${error.message}`);
225
+ return null;
226
+ }
227
+
228
+ const configuredOutput = perfConfig.report?.output;
229
+ if (configuredOutput) {
230
+ // A bare filename (no directory component) lands inside the build folder
231
+ // alongside output.json; anything with a directory is honoured as-is.
232
+ const outputPath =
233
+ path.dirname(configuredOutput) === "."
234
+ ? path.join(buildFolder, configuredOutput)
235
+ : path.resolve(configuredOutput);
236
+
237
+ try {
238
+ await writeFile(
239
+ outputPath,
240
+ generatePerformanceReport(result),
241
+ "utf-8",
242
+ );
243
+ log("info", null, `Wrote ${outputPath}.`);
244
+ } catch (error) {
245
+ log("warn", `Failed to write performance report: ${error.message}`);
246
+ }
247
+ }
248
+
249
+ const { exceed, warn, ok, unbudgeted } = result.summary;
250
+ const summaryLine = `Performance budget (${result.compression}): ${ok} ok, ${warn} warn, ${exceed} exceed, ${unbudgeted} unbudgeted.`;
251
+
252
+ if (exceed > 0) {
253
+ log("warn", summaryLine);
254
+ if (perfConfig.report?.failOnExceed) {
255
+ return new Error(
256
+ `Performance budget exceeded in ${exceed} categor${exceed === 1 ? "y" : "ies"}.`,
257
+ );
258
+ }
259
+ } else {
260
+ log("info", summaryLine);
261
+ }
262
+
263
+ return null;
264
+ }
265
+
193
266
  /**
194
267
  * Creates an "output.json" file with the given array as content
195
268
  * @param {string} buildFolder
@@ -665,6 +738,7 @@ export default () => {
665
738
  component,
666
739
  componentData: data.resolved,
667
740
  componentDeclaredAssets: data.$assets || null,
741
+ variation,
668
742
  cb: async (err, response) => {
669
743
  if (err) {
670
744
  if (typeof err === "string") {
@@ -0,0 +1,157 @@
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
+ }
package/lib/cli/index.js CHANGED
@@ -3,9 +3,11 @@ import componentImport from "./component.js";
3
3
  import drupalAssetsImport from "./drupal-assets.js";
4
4
  import doctorImport from "./doctor.js";
5
5
  import validateHtmlImport from "./validate-html.js";
6
+ import budgetImport from "./budget.js";
6
7
 
7
8
  export const lint = lintImport;
8
9
  export const component = componentImport;
9
10
  export const drupalAssets = drupalAssetsImport;
10
11
  export const doctor = doctorImport;
11
12
  export const validateHtml = validateHtmlImport;
13
+ export const budgetCli = budgetImport;
package/lib/cli/run.js CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  drupalAssets,
11
11
  doctor,
12
12
  validateHtml as validateHtmlCli,
13
+ budgetCli,
13
14
  } from "./index.js";
14
15
  import { EXIT_CODES, MiyagiError } from "../errors.js";
15
16
 
@@ -235,6 +236,15 @@ async function runDoctorCommand(args) {
235
236
  return await doctor(args);
236
237
  }
237
238
 
239
+ /**
240
+ * @param {object} args
241
+ * @returns {Promise<object>}
242
+ */
243
+ async function runBudgetCommand(args) {
244
+ applyCliEnv(args);
245
+ return await budgetCli(args);
246
+ }
247
+
238
248
  /**
239
249
  * @param {Error|MiyagiError|string} error
240
250
  * @param {string[]} argv
@@ -281,6 +291,7 @@ export async function runCli(argv = process.argv) {
281
291
  validateHtml: runValidateHtmlCommand,
282
292
  drupalAssets: runDrupalAssetsCommand,
283
293
  doctor: runDoctorCommand,
294
+ budget: runBudgetCommand,
284
295
  },
285
296
  argv,
286
297
  );