@schalkneethling/miyagi-core 4.7.0 → 4.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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);
@@ -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
@@ -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
  );
@@ -89,6 +89,39 @@ export default {
89
89
  },
90
90
  },
91
91
  namespaces: {},
92
+ // Performance budget feature — tracks asset byte size against configured
93
+ // limits and surfaces the state via `miyagi budget`, the build-time report,
94
+ // and the dev-server UI. Defaults mirror the "Slow 4G / Moto G4" tier from
95
+ // web.dev's "Your First Performance Budget" (see docs).
96
+ performance: {
97
+ enabled: true,
98
+ compression: "gzip",
99
+ report: {
100
+ failOnExceed: false,
101
+ output: "performance-report.md",
102
+ },
103
+ budgets: {
104
+ global: {
105
+ css: "35 kB",
106
+ js: "200 kB",
107
+ total: null,
108
+ },
109
+ html: {
110
+ perPage: "30 kB",
111
+ total: null,
112
+ },
113
+ folders: {
114
+ fonts: { total: "30 kB" },
115
+ images: { total: "50 kB" },
116
+ total: null,
117
+ },
118
+ perComponent: {
119
+ css: null,
120
+ js: null,
121
+ total: null,
122
+ },
123
+ },
124
+ },
92
125
  projectName: "miyagi",
93
126
  ui: {
94
127
  mode: "light",
package/lib/init/args.js CHANGED
@@ -168,6 +168,45 @@ export default function createCli(handlers, argv = process.argv) {
168
168
  () => {},
169
169
  commandHandler(handlers.doctor),
170
170
  )
171
+ .command(
172
+ "budget",
173
+ "Checks asset byte sizes against your performance budget",
174
+ (builder) =>
175
+ builder
176
+ .option("compression", {
177
+ description: "Which compression to compare against the budget",
178
+ type: "string",
179
+ choices: ["raw", "gzip", "brotli"],
180
+ })
181
+ .option("fail", {
182
+ description: "Exit with a non-zero code if any budget is exceeded",
183
+ type: "boolean",
184
+ default: false,
185
+ })
186
+ .option("json", {
187
+ description:
188
+ "Emit the full evaluation as JSON on stdout (for CI / automation)",
189
+ type: "boolean",
190
+ default: false,
191
+ })
192
+ .option("output", {
193
+ alias: "o",
194
+ description: "Also write a markdown report to this path",
195
+ type: "string",
196
+ })
197
+ .option("build-folder", {
198
+ description:
199
+ "Include post-build HTML pages from this folder (reads output.json)",
200
+ type: "string",
201
+ })
202
+ .option("list-all-pages", {
203
+ description:
204
+ "List every HTML page in the evaluation, not just those that exceed or warn",
205
+ type: "boolean",
206
+ default: false,
207
+ }),
208
+ commandHandler(handlers.budget),
209
+ )
171
210
  .help()
172
211
  .version(pkgJson.version)
173
212
  .alias("help", "h")