zz-meridian 0.23.0 → 0.24.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/package.json +1 -1
- package/payload/CHANGELOG.md +6 -0
- package/payload/package.json +1 -1
- package/payload/scripts/lib/budgets.ts +8 -4
- package/payload/scripts/lib/sizes.ts +8 -4
- package/payload/scripts/sizes.ts +6 -4
- package/payload/scripts/verify.config.ts +3 -2
- package/payload/tests/verify-sizes.test.ts +7 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zz-meridian",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"description": "Bring ZZ Meridian, a dashboard design system, into a Next.js project, or start a new dashboard on it. Copies the files in; nothing depends on this package at runtime.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/payload/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
Every release of ZZ Meridian, newest first. Versions follow semver: a removed or renamed token, prop or card is major; a new card, token or variant is minor; a corrected value is a patch. Each entry says what breaks and what to do instead.
|
|
4
4
|
|
|
5
|
+
## [0.24.0] · 2026-10-20
|
|
6
|
+
|
|
7
|
+
### New
|
|
8
|
+
|
|
9
|
+
- **First-load JS can be held to each route's baseline alone** ([#76](https://github.com/zhixuan312/zz-meridian/issues/76)), as every page's HTML is since 0.19.0. Set `budgets.firstLoadKb: null` in `scripts/verify.config.ts`: a route `scripts/verify.baseline.json` holds is then held to `firstLoadGrowthPct` over its own figure and nothing else, and the 820 KiB cap binds only a route the baseline does not hold yet. A product whose largest page sits near the cap no longer fails `pnpm verify` for a change its own baseline allows, and no longer derives a cap from the baseline to say the same rule twice. Leaving the key out keeps the cap on every route, as before; the size line says which applies (`cap 820 KiB for a route the baseline does not hold`).
|
|
10
|
+
|
|
5
11
|
## [0.23.0] · 2026-10-20
|
|
6
12
|
|
|
7
13
|
### New
|
package/payload/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zz-meridian-template",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "ZZ Meridian: a dashboard design system and starter. Six layers, from DTCG tokens through four layers of React components to product workspaces, and a Design Atlas, in a light and a dark theme.",
|
|
@@ -24,7 +24,8 @@ export type NavigationCheck = {
|
|
|
24
24
|
};
|
|
25
25
|
export type Budgets = {
|
|
26
26
|
navigation: { warm: ReadinessBudget; cold: ReadinessBudget; afterLive: ReadinessBudget };
|
|
27
|
-
|
|
27
|
+
/** `null`: each route the baseline holds is held to it alone, and the default cap binds only a route it does not. */
|
|
28
|
+
firstLoadKb: number | null;
|
|
28
29
|
firstLoadGrowthPct: number;
|
|
29
30
|
htmlGrowthPct: number;
|
|
30
31
|
htmlKb: Record<string, number>;
|
|
@@ -33,7 +34,7 @@ export type Budgets = {
|
|
|
33
34
|
export type BudgetsConfig = {
|
|
34
35
|
budgets?: {
|
|
35
36
|
navigation?: { warm?: ReadinessBudget; cold?: ReadinessBudget; afterLive?: ReadinessBudget };
|
|
36
|
-
firstLoadKb?: number;
|
|
37
|
+
firstLoadKb?: number | null;
|
|
37
38
|
firstLoadGrowthPct?: number;
|
|
38
39
|
htmlGrowthPct?: number;
|
|
39
40
|
htmlKb?: Record<string, number>;
|
|
@@ -47,6 +48,9 @@ const COLD: ReadinessBudget = {
|
|
|
47
48
|
interactiveMs: { desktop: 2000, phone: 4000 },
|
|
48
49
|
};
|
|
49
50
|
|
|
51
|
+
/** The first-load cap, in KiB, for a route the baseline does not hold, and for every route unless `firstLoadKb` is null. */
|
|
52
|
+
export const FIRST_LOAD_CAP_KB = 820;
|
|
53
|
+
|
|
50
54
|
/** Each page's HTML is held to its own baseline; an absolute HTML cap is a project's route, so the defaults set none. */
|
|
51
55
|
export const DEFAULT_BUDGETS: Budgets = {
|
|
52
56
|
navigation: {
|
|
@@ -58,7 +62,7 @@ export const DEFAULT_BUDGETS: Budgets = {
|
|
|
58
62
|
cold: COLD,
|
|
59
63
|
afterLive: COLD,
|
|
60
64
|
},
|
|
61
|
-
firstLoadKb:
|
|
65
|
+
firstLoadKb: FIRST_LOAD_CAP_KB,
|
|
62
66
|
firstLoadGrowthPct: 5,
|
|
63
67
|
htmlGrowthPct: 5,
|
|
64
68
|
htmlKb: {},
|
|
@@ -92,7 +96,7 @@ export function budgetsOf(config: BudgetsConfig): Budgets {
|
|
|
92
96
|
const cold = b.navigation?.cold ?? DEFAULT_BUDGETS.navigation.cold;
|
|
93
97
|
return {
|
|
94
98
|
navigation: { warm, cold, afterLive: b.navigation?.afterLive ?? cold },
|
|
95
|
-
firstLoadKb: b.firstLoadKb ?? DEFAULT_BUDGETS.firstLoadKb,
|
|
99
|
+
firstLoadKb: b.firstLoadKb === null ? null : b.firstLoadKb ?? DEFAULT_BUDGETS.firstLoadKb,
|
|
96
100
|
firstLoadGrowthPct: b.firstLoadGrowthPct ?? DEFAULT_BUDGETS.firstLoadGrowthPct,
|
|
97
101
|
htmlGrowthPct: b.htmlGrowthPct ?? DEFAULT_BUDGETS.htmlGrowthPct,
|
|
98
102
|
htmlKb: b.htmlKb ?? DEFAULT_BUDGETS.htmlKb,
|
|
@@ -19,16 +19,20 @@ export function firstLoad(
|
|
|
19
19
|
});
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* Routes over the cap or past their growth allowance over a baseline; without a baseline growth is not configured.
|
|
24
|
+
* `capUnheldOnly` holds a route the baseline holds to its baseline alone, so the cap binds only a route it does not.
|
|
25
|
+
*/
|
|
23
26
|
export function firstLoadProblems(
|
|
24
27
|
routes: { route: string; bytes: number }[],
|
|
25
|
-
{ capKiB, growthPct, baseline }: { capKiB: number; growthPct: number; baseline: Record<string, number> | null },
|
|
28
|
+
{ capKiB, growthPct, baseline, capUnheldOnly = false }: { capKiB: number; growthPct: number; baseline: Record<string, number> | null; capUnheldOnly?: boolean },
|
|
26
29
|
): { problems: string[]; growth: 'checked' | 'not-configured' } {
|
|
27
30
|
const problems: string[] = [];
|
|
28
31
|
for (const { route, bytes } of routes) {
|
|
29
|
-
if (bytes > capKiB * KIB) problems.push(`${route}: first-load JS ${kib(bytes)} KiB over the ${capKiB} KiB cap`);
|
|
30
32
|
const base = baseline?.[route];
|
|
31
|
-
|
|
33
|
+
const held = base !== undefined && base > 0;
|
|
34
|
+
if (!(capUnheldOnly && held) && bytes > capKiB * KIB) problems.push(`${route}: first-load JS ${kib(bytes)} KiB over the ${capKiB} KiB cap`);
|
|
35
|
+
if (!held) continue;
|
|
32
36
|
const grew = ((bytes - base) / base) * 100;
|
|
33
37
|
if (grew > growthPct) problems.push(`${route}: first-load JS grew ${grew.toFixed(1)}% over its baseline (cap ${growthPct}%)`);
|
|
34
38
|
}
|
package/payload/scripts/sizes.ts
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
*
|
|
7
7
|
* First-load comes from the build's own `diagnostics/route-bundle-stats.json`, read once, with each chunk's size from
|
|
8
8
|
* disk: every chunk a route loads counted once, uncompressed. It is checked against `budgets.firstLoadKb` and, when
|
|
9
|
-
* `scripts/verify.baseline.json` exists, against `budgets.firstLoadGrowthPct` over it
|
|
9
|
+
* `scripts/verify.baseline.json` exists, against `budgets.firstLoadGrowthPct` over it; with `firstLoadKb: null` a route
|
|
10
|
+
* the baseline holds is held to it alone, and the default cap binds only a route it does not. A page's HTML is what a person
|
|
10
11
|
* downloads: the complete streamed response from the app running at `--base`, gzipped. Every page in the baseline's
|
|
11
12
|
* `html` is held to `budgets.htmlGrowthPct` over its own figure, and a route in `budgets.htmlKb` also to that absolute
|
|
12
13
|
* cap. Without a baseline a growth check is `not-configured`, a warning and never a pass; without `--base` the HTML is
|
|
@@ -28,7 +29,7 @@ import fs from 'node:fs';
|
|
|
28
29
|
import path from 'node:path';
|
|
29
30
|
import { gzipSync } from 'node:zlib';
|
|
30
31
|
|
|
31
|
-
import { budgetsOf, type BudgetsConfig } from './lib/budgets.ts';
|
|
32
|
+
import { FIRST_LOAD_CAP_KB, budgetsOf, type BudgetsConfig } from './lib/budgets.ts';
|
|
32
33
|
import { distDirOf } from './lib/next-config.ts';
|
|
33
34
|
import { firstLoad, firstLoadProblems, htmlGrowthProblems, htmlProblems, htmlRoutes } from './lib/sizes.ts';
|
|
34
35
|
import verifyConfig from './verify.config.ts';
|
|
@@ -112,11 +113,12 @@ async function main(): Promise<number> {
|
|
|
112
113
|
|
|
113
114
|
const budgets = budgetsOf(verifyConfig as BudgetsConfig);
|
|
114
115
|
const baseline = baselineOf();
|
|
115
|
-
const
|
|
116
|
+
const capKiB = budgets.firstLoadKb ?? FIRST_LOAD_CAP_KB;
|
|
117
|
+
const size = firstLoadProblems(routes, { capKiB, growthPct: budgets.firstLoadGrowthPct, baseline: baseline?.firstLoad ?? null, capUnheldOnly: budgets.firstLoadKb === null });
|
|
116
118
|
const largest = routes.reduce((a, b) => (b.bytes > a.bytes ? b : a), routes[0]);
|
|
117
119
|
const lines: string[] = [];
|
|
118
120
|
const firstStatus: Status = size.problems.length ? 'FAIL' : size.growth === 'not-configured' ? 'warn' : 'ok';
|
|
119
|
-
lines.push(`${firstStatus === 'FAIL' ? 'FAIL ' : firstStatus === 'warn' ? 'warn ' : 'ok '} first-load ${routes.length} routes, largest ${largest.route} ${kib(largest.bytes)} KiB · cap ${budgets.firstLoadKb}
|
|
121
|
+
lines.push(`${firstStatus === 'FAIL' ? 'FAIL ' : firstStatus === 'warn' ? 'warn ' : 'ok '} first-load ${routes.length} routes, largest ${largest.route} ${kib(largest.bytes)} KiB · cap ${capKiB} KiB${budgets.firstLoadKb === null ? ' for a route the baseline does not hold' : ''}`
|
|
120
122
|
+ ` · growth ${size.growth === 'checked' ? `within ${budgets.firstLoadGrowthPct}% of the baseline` : 'not-configured (no scripts/verify.baseline.json)'}`);
|
|
121
123
|
for (const p of size.problems) lines.push(`FAIL ${p}`);
|
|
122
124
|
|
|
@@ -101,11 +101,12 @@ type VerifyConfig = {
|
|
|
101
101
|
* `scripts/verify.baseline.json`, the warm, cold and after-live navigation times, and no prefetch on a closed phone drawer.
|
|
102
102
|
* Every page's HTML, gzipped through the end of the streamed response, is held to `htmlGrowthPct` (5%) over its own
|
|
103
103
|
* figure in the baseline's `html`; `htmlKb` adds an absolute cap in KiB, the same gzipped figure, for a page a project
|
|
104
|
-
* wants pinned, and has no default.
|
|
104
|
+
* wants pinned, and has no default. `firstLoadKb: null` holds first-load JS the same way: each route the baseline holds
|
|
105
|
+
* to its own figure alone, with the default cap binding only a route the baseline does not hold yet.
|
|
105
106
|
*/
|
|
106
107
|
budgets?: {
|
|
107
108
|
navigation?: { warm?: ReadinessBudget; cold?: ReadinessBudget; afterLive?: ReadinessBudget };
|
|
108
|
-
firstLoadKb?: number;
|
|
109
|
+
firstLoadKb?: number | null;
|
|
109
110
|
firstLoadGrowthPct?: number;
|
|
110
111
|
htmlGrowthPct?: number;
|
|
111
112
|
htmlKb?: Record<string, number>;
|
|
@@ -22,6 +22,13 @@ describe('first-load JS', () => {
|
|
|
22
22
|
expect(firstLoadProblems([{ route: '/y', bytes: 535 * KiB }], { capKiB: 820, growthPct: 5, baseline: null })).toEqual({ problems: [], growth: 'not-configured' });
|
|
23
23
|
expect(firstLoadProblems([{ route: '/y', bytes: 535 * KiB }], { capKiB: 1, growthPct: 5, baseline: null }).problems).toHaveLength(1);
|
|
24
24
|
});
|
|
25
|
+
it('with the cap for unheld routes only, holds a route the baseline holds to its baseline alone (#76)', () => {
|
|
26
|
+
const routes = [{ route: '/x', bytes: 852 * KiB }, { route: '/y', bytes: 900 * KiB }, { route: '/z', bytes: 700 * KiB }];
|
|
27
|
+
const got = firstLoadProblems(routes, { capKiB: 820, growthPct: 5, baseline: { '/x': 850 * KiB, '/z': 600 * KiB }, capUnheldOnly: true });
|
|
28
|
+
expect(got.problems).toEqual(['/y: first-load JS 900 KiB over the 820 KiB cap', '/z: first-load JS grew 16.7% over its baseline (cap 5%)']);
|
|
29
|
+
expect(budgetsOf({ budgets: { firstLoadKb: null } }).firstLoadKb).toBeNull();
|
|
30
|
+
expect(budgetsOf({}).firstLoadKb).toBe(820);
|
|
31
|
+
});
|
|
25
32
|
});
|
|
26
33
|
|
|
27
34
|
describe('HTML and prefetch', () => {
|