Skip to content

Commit 80b1eab

Browse files
committed
feat(scripts): 新增懒侧字节门禁与逐族上限
1 parent 4292dee commit 80b1eab

3 files changed

Lines changed: 249 additions & 12 deletions

File tree

‎docs/standards/performance.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,14 @@
3333
- CI 中检查包大小,超标则警告
3434
- 新功能不能使指标退化
3535

36+
### 懒侧字节门禁(批 7 · 与首屏预算**互相独立**)
37+
38+
- **口径**:懒加载 chunk(仅动态可达的 `.js`)单独判定 —— ① **族前缀清单等式**(每个懒侧 chunk 必须归属某个已登记族,**未归族即红**)② **逐族 gzip 上限** ③ **懒侧总 gzip 上限**(另加 chunk 数上限)。三条都只许降。
39+
- **判据键是族前缀**(`vendor-katex-*` 等):Vite 的 chunk 名带内容 hash(不稳定)⇒ 键只能是去掉 8 位 hash 的名字加 `-`。
40+
- **基线来源**:`scripts/lazyBudget.json` —— 首次写入值 = 2026-09-13 实测 **36 chunk / 698.86 kB**(**冻结现状,不是目标值**;写乐观「预算值」会立刻红)。
41+
- **硬约束**:上表「页面包大小 (JS) < 200KB (gzip)」与懒侧预算是**两个独立读数** —— 懒侧判定不得影响首屏读数,两者也不得并成一个预算。
42+
- **工具**:`node scripts/check-bundle-budget.mjs --no-build`(人类可读,**分别**打印两个 pass);加 `--json` 得平级的 `firstScreen` / `lazyBudget` 两栏。退出码 `1` = **任一**预算超标(首屏 / 懒侧)。
43+
3644
### 第二部分:测量与 Profiling
3745

3846
**先测量,后优化。不要凭感觉优化。**

‎scripts/check-bundle-budget.mjs‎

Lines changed: 88 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@
1414
* 3. gzip = `zlib.gzipSync(buf, { level: 6 })`;kB = **十进制 ÷1000**(vite 自己打印的口径,不是 KiB ÷1024)。
1515
* 冻结校准点:今天单一 chunk 构建下入口 chunk = **654,722 B = 654.72 kB**,与 vite 打印值逐字相等
1616
* (计划者 / Task 1 / 控制方三方独立复现)。**若本脚本与此值不符,先怀疑脚本,绝不改数字。**
17+
* 4. **懒侧(仅动态可达的 `.js`)有独立的第二个预算**(批 7 §C9.6 / §C10.4):族前缀**清单等式** +
18+
* **逐族 gzip 上限** + **懒侧总 gzip 上限**(另加 chunk 数上限),基线 = `scripts/lazyBudget.json` 的
19+
* **实测冻结值**、只许降(写「预算值」式的乐观目标会立刻红)。判据键是**族前缀**(`vendor-katex-*`)而非
20+
* chunk 名(后者带内容 hash、不稳定)。🔴 首屏与懒侧是**两个独立读数**:懒侧判定不得影响首屏字段。
1721
*
1822
* 仪器陷阱(本批实测,勿改回):
1923
* - 静态边在产物里长 `from"./x-abc.js"`:**引号必须紧跟** `from` / `import`,故 `import(` 天然不匹配;
@@ -26,10 +30,13 @@
2630
*
2731
* 副作用:默认执行 `cd app && npm run build`(写 `app/dist/`);`--no-build` 只读既有产物。
2832
* `--self-test` 在 `os.tmpdir()` 下建夹具并在结束时删除,**不碰真实 `app/dist`**。
29-
* 用法:node scripts/check-bundle-budget.mjs [--no-build] [--budget <kB>] [--json] [--self-test] [--dist <目录>]
33+
* 用法:node scripts/check-bundle-budget.mjs [--no-build] [--budget <kB>] [--json] [--self-test] [--dist <目录>] [--no-lazy]
3034
* `--dist` 供自检夹具 / 备用产物目录使用(默认 `app/dist`),与构建互斥、须配 `--no-build`。
3135
* `--json` 与默认构建同用会把 vite 输出混进 stdout;机器消费请配 `--no-build`。
32-
* 退出码:0 = 达标 · 1 = 超预算 · 2 = 构建失败 / 产物缺失 / 自检失败。
36+
* `--no-lazy` **只**给 `--dist` 的夹具用,且**不得**指向默认产物目录 ⇒ CI / husky / 收口读数都不带它,
37+
* **真实产物上懒侧门禁恒开**(它不是「可以关掉的守卫」)。
38+
* 退出码:0 = **首屏与懒侧两个预算都达标** · 1 = **任一预算超标(首屏 / 懒侧)** ·
39+
* 2 = 构建失败 / 产物缺失 / 自检失败。两个读数**互相独立**:懒侧判定**不得**影响首屏字段(批 7 §C11.5)。
3340
*/
3441
import { readFileSync, existsSync, statSync, mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
3542
import { join, resolve, dirname } from "node:path";
@@ -48,16 +55,58 @@ const opt = (f, dflt) => (argv.indexOf(f) >= 0 && argv[argv.indexOf(f) + 1] !==
4855
const NO_BUILD = has("--no-build");
4956
const AS_JSON = has("--json");
5057
const SELF_TEST = has("--self-test");
58+
const NO_LAZY = has("--no-lazy");
5159
const DIST = resolve(opt("--dist", join(APP, "dist")));
5260
const BUDGET_KB = Number(opt("--budget", "200"));
5361
const BUDGET_SOURCE = "docs/standards/performance.md:28";
62+
const LAZY_BUDGET = join(dirname(SELF), "lazyBudget.json");
5463

5564
/** 全部失败路径的统一出口:**必须** exit 2(1 只留给「超预算」),并给出可执行的下一条命令。 */
5665
function fail(msg) {
5766
console.error(`❌ 首屏预算守卫:${msg}`);
5867
process.exit(2);
5968
}
6069

70+
/**
71+
* 懒侧基线(冻结值):**缺失即失败**,绝不静默放行 —— 「读不到基线就当达标」是这类门禁最典型的失效形态。
72+
*/
73+
function loadLazyBudget(p) {
74+
if (!existsSync(p) || !statSync(p).isFile()) fail(`找不到懒侧基线 ${rel(p)} —— 基线缺失时不得静默按「达标」放行。`);
75+
const b = JSON.parse(readFileSync(p, "utf8"));
76+
const ok = Array.isArray(b.families) && b.families.length > 0 && Number.isFinite(b.lazyTotalGzipBytesMax) && Number.isFinite(b.lazyChunkCountMax) && b.families.every((f) => typeof f.prefix === "string" && Number.isFinite(f.gzipBytesMax));
77+
if (!ok) fail(`懒侧基线 ${rel(p)} 结构不完整(需要 families[{prefix,gzipBytesMax}] + lazyTotalGzipBytesMax + lazyChunkCountMax)。`);
78+
return b;
79+
}
80+
81+
/** 族前缀 = 去掉**8 位定长**内容 hash 的 chunk 名 + `-`。hash 字母表含 `-`(实测 `CaptureFloatPanel-CJ-lF7pj.js`)。 */
82+
const HASH_SUFFIX = /-[A-Za-z0-9_-]{8}\.js$/;
83+
const familyPrefixOf = (name) => (HASH_SUFFIX.test(name) ? name.replace(HASH_SUFFIX, "") + "-" : null);
84+
85+
/**
86+
* 懒侧四条判据:① **清单等式**(每个懒侧 chunk 都必须归属某个已登记族 —— 没有 `others` 白名单,就是要让
87+
* 产物演进**红**而不是静默通过)② **逐族 gzip 上限** ③ **总 gzip 上限** ④ chunk 数上限。
88+
* 每条**各产生一条具名 reason**(C9.6 第 3 条:必须红在具名断言上)。
89+
*/
90+
function judgeLazy(r, base) {
91+
const fams = new Map(base.families.map((f) => [f.prefix, { prefix: f.prefix, gzipBytesMax: f.gzipBytesMax, chunks: [], gzipBytes: 0 }]));
92+
const unlisted = [];
93+
for (const c of r.lazy) {
94+
const f = fams.get(familyPrefixOf(c.name));
95+
if (!f) { unlisted.push(c.name); continue; }
96+
f.chunks.push(c.name);
97+
f.gzipBytes += c.gzipBytes;
98+
}
99+
const families = [...fams.values()];
100+
const totalBytes = r.lazy.reduce((a, c) => a + c.gzipBytes, 0);
101+
const overFamilies = families.filter((f) => f.gzipBytes > f.gzipBytesMax);
102+
const reasons = [];
103+
if (unlisted.length) reasons.push(`① 族前缀清单等式:未归族 ${unlisted.length} 个(${unlisted.join(", ")})`);
104+
for (const f of overFamilies) reasons.push(`② 逐族上限:${f.prefix} gzip ${fmtB(f.gzipBytes)} B > 上限 ${fmtB(f.gzipBytesMax)} B`);
105+
if (totalBytes > base.lazyTotalGzipBytesMax) reasons.push(`③ 懒侧总 gzip:${fmtB(totalBytes)} B > 上限 ${fmtB(base.lazyTotalGzipBytesMax)} B`);
106+
if (r.lazy.length > base.lazyChunkCountMax) reasons.push(`④ 懒侧 chunk 数:${r.lazy.length} > 上限 ${base.lazyChunkCountMax}`);
107+
return { families, unlisted, overFamilies, totalBytes, totalMax: base.lazyTotalGzipBytesMax, count: r.lazy.length, countMax: base.lazyChunkCountMax, reasons, pass: reasons.length === 0 };
108+
}
109+
61110
/** 参考项:HTML 用 modulepreload 声明但静态闭包没覆盖的 .js —— 值得人看一眼,不影响判定。 */
62111
function preloadUncovered(dist, eager) {
63112
const html = readFileSync(join(dist, "index.html"), "utf8");
@@ -66,7 +115,7 @@ function preloadUncovered(dist, eager) {
66115
return [...new Set(tags.filter((n) => n.endsWith(".js") && !eager.has(n)))];
67116
}
68117

69-
function printHuman(r, pass) {
118+
function printHuman(r, pass, lazy) {
70119
console.log(`首屏预算守卫 · 产物 ${rel(r.dist)}`);
71120
console.log(` 单位:kB = 字节 ÷ 1000(十进制,vite 口径;勿与 ÷1024 的 KiB 混用,混用会产生假 Δ)· gzip = zlib level 6`);
72121
console.log(` 口径:module 入口 + 只沿静态 ESM import 可达的 .js,动态 import 不计 · 预算 ${BUDGET_KB} kB gzip(来源 ${BUDGET_SOURCE})`);
@@ -75,9 +124,13 @@ function printHuman(r, pass) {
75124
console.log(` ${name.padEnd(34)} ${fmtB(c.bytes).padStart(10)} B → gzip ${fmtB(c.gzip).padStart(9)} B = ${kB(c.gzip).padStart(8)} kB`);
76125
}
77126
console.log(` 首屏 JS 合计:原始 ${fmtB(r.rawBytes)} B · gzip ${fmtB(r.eagerBytes)} B = ${kB(r.eagerBytes)} kB`);
78-
console.log(` ⇒ ${pass ? "✅ 达标" : "❌ 超标"}:${pass ? "余量" : "超出"} ${kB(Math.abs(BUDGET_KB * 1000 - r.eagerBytes))} kB${pass ? "" : `(${(r.eagerBytes / 1000 / BUDGET_KB).toFixed(2)}× 预算)`}`);
127+
console.log(` ⇒ 首屏预算 ${pass ? "✅ 达标" : "❌ 超标"}:${pass ? "余量" : "超出"} ${kB(Math.abs(BUDGET_KB * 1000 - r.eagerBytes))} kB${pass ? "" : `(${(r.eagerBytes / 1000 / BUDGET_KB).toFixed(2)}× 预算)`}`);
79128
const lazyBytes = r.lazy.reduce((a, c) => a + c.gzipBytes, 0);
80-
console.log(`懒加载 chunk(仅动态可达,不计入预算):${r.lazy.length} 个 · gzip ${fmtB(lazyBytes)} B = ${kB(lazyBytes)} kB`);
129+
console.log(`懒加载 chunk(仅动态可达,独立于首屏预算):${r.lazy.length} 个 · gzip ${fmtB(lazyBytes)} B = ${kB(lazyBytes)} kB`);
130+
if (lazy) {
131+
console.log(` 懒侧口径:族前缀清单等式 + 逐族 gzip 上限 + 懒侧总 gzip 上限 · 基线 ${rel(LAZY_BUDGET)}(实测冻结值,只许降)· 未归族与超上限都红`);
132+
console.log(` ⇒ 懒侧预算 ${lazy.pass ? "✅ 达标" : "❌ 超标"}:gzip ${kB(lazy.totalBytes)} kB / 上限 ${kB(lazy.totalMax)} kB · chunk ${lazy.count} 个 / 上限 ${lazy.countMax} 个 · 未归族 ${lazy.unlisted.length} 个 · 超上限族 ${lazy.overFamilies.length} 个`);
133+
} else console.log(` ⇒ 懒侧预算 ⏭ 未判(--no-lazy,仅供 --dist 夹具;真实产物上恒判)`);
81134
console.log(`不计入预算但须报告(CSS / 字体):index.html ${fmtB(r.htmlBytes)} B = ${kB(r.htmlBytes)} kB · gzip ${kB(r.htmlGzip)} kB`);
82135
const byExt = new Map();
83136
for (const a of r.excluded) {
@@ -90,7 +143,7 @@ function printHuman(r, pass) {
90143
}
91144
}
92145

93-
const jsonOf = (r, pass) => ({
146+
const jsonOf = (r, pass, lazy) => ({
94147
dist: rel(r.dist),
95148
budgetKb: BUDGET_KB,
96149
budgetSource: BUDGET_SOURCE,
@@ -99,20 +152,38 @@ const jsonOf = (r, pass) => ({
99152
count: r.eager.size,
100153
chunks: [...r.eager].map(([name, c]) => ({ name, bytes: c.bytes, gzipBytes: c.gzip, gzipKb: Number(kB(c.gzip)) })),
101154
totalBytes: r.eagerBytes,
102-
totalKb: Number(kB(r.eagerBytes)),
155+
totalKb: Number(kB(r.eagerBytes)), pass,
103156
},
104157
lazy: { count: r.lazy.length, chunks: r.lazy, totalBytes: r.lazy.reduce((a, c) => a + c.gzipBytes, 0) },
158+
lazyBudget: lazy
159+
? {
160+
pass: lazy.pass,
161+
baseline: rel(LAZY_BUDGET),
162+
count: lazy.count,
163+
countMax: lazy.countMax,
164+
totalBytes: lazy.totalBytes,
165+
totalKb: Number(kB(lazy.totalBytes)),
166+
totalMaxBytes: lazy.totalMax,
167+
unlisted: lazy.unlisted,
168+
fails: lazy.reasons,
169+
families: lazy.families.map((f) => ({ prefix: f.prefix, chunkCount: f.chunks.length, gzipBytes: f.gzipBytes, gzipBytesMax: f.gzipBytesMax, pass: f.gzipBytes <= f.gzipBytesMax })),
170+
}
171+
: { pass: true, skipped: "--no-lazy(仅供 --dist 夹具;真实产物上恒判)" },
105172
excludedFromBudget: { indexHtml: { bytes: r.htmlBytes, gzipBytes: r.htmlGzip }, assets: r.excluded },
106173
pass,
107174
});
108175

109-
/** 报表 + 退出码:0 = 达标 · 1 = 超预算。超预算时把差额与倍数一并说清(可执行、不含糊)。 */
176+
/**
177+
* 报表 + 退出码:0 = **两个预算都达标** · 1 = **任一超标**(首屏 / 懒侧,各自点名)。
178+
* 🔴 顶层 `pass` 是**首屏** pass(既有消费方的兼容字段);懒侧见 `lazyBudget.pass`。
179+
*/
110180
function finish(r) {
111181
const pass = r.eagerBytes / 1000 < BUDGET_KB;
182+
const lazy = NO_LAZY ? null : judgeLazy(r, loadLazyBudget(LAZY_BUDGET));
112183
if (AS_JSON) {
113-
console.log(JSON.stringify(jsonOf(r, pass), null, 2));
184+
console.log(JSON.stringify(jsonOf(r, pass, lazy), null, 2));
114185
} else {
115-
printHuman(r, pass);
186+
printHuman(r, pass, lazy);
116187
const uncovered = preloadUncovered(r.dist, r.eager);
117188
if (uncovered.length) console.log(`ℹ️ modulepreload 声明但静态闭包未覆盖 ${uncovered.length} 个:${uncovered.join(", ")}(参考项,不影响判定)`);
118189
}
@@ -122,7 +193,8 @@ function finish(r) {
122193
`(${(r.eagerBytes / 1000 / BUDGET_KB).toFixed(2)}×)—— 退出码 1。降首屏只能靠 import() 切断静态边;manualChunks 只切文件、一字节不降。`,
123194
);
124195
}
125-
process.exit(pass ? 0 : 1);
196+
if (lazy && !lazy.pass) console.error(`❌ 懒侧 gzip 预算超标(退出码 1)—— 具名断言:${lazy.reasons.join(";")}。基线 ${rel(LAZY_BUDGET)} 只许降(§C9.6 第 2 条)。`);
197+
process.exit(pass && (!lazy || lazy.pass) ? 0 : 1);
126198
}
127199

128200
/**
@@ -166,7 +238,8 @@ function selfTest() {
166238
for (const d of ["empty", "noscript/assets", "partial/assets"]) mkdirSync(join(dir, d), { recursive: true });
167239
w("noscript/index.html", `<!doctype html><html><body></body></html>`);
168240
w("partial/index.html", `<script type="module" src="/assets/gone.js"></script>`);
169-
const child = (...args) => spawnSync(process.execPath, [SELF, "--no-build", "--dist", ...args], { encoding: "utf8" });
241+
// 夹具产物是人工造的(`dyn-only.js` 之类不可能命中真实基线)⇒ 夹具子进程显式关懒侧判定,本自检继续只证**首屏**口径与退出码契约 ⇒ 读数逐字不变。⚠️ `--no-lazy` 必须排在**所有带值旗标之后**(`opt()` 取旗标的下一个 argv 元素;紧跟 `--dist` 会把它吃掉)。
242+
const child = (...args) => spawnSync(process.execPath, [SELF, "--no-build", "--dist", ...args, "--no-lazy"], { encoding: "utf8" });
170243
const cases = [
171244
["dist 不存在", 2, [join(dir, "nope")]],
172245
["空 dist(无 index.html)", 2, [join(dir, "empty")]],
@@ -207,6 +280,9 @@ if (!Number.isFinite(BUDGET_KB) || BUDGET_KB <= 0) {
207280
fail(`--budget 需要正数(实得 "${opt("--budget", "")}")—— 预算解析失败时绝不能静默按「超标」处理。`);
208281
}
209282
if (has("--dist") && !NO_BUILD) fail(`--dist 与构建互斥:--dist 用于既有产物或自检夹具,请配 --no-build。`);
283+
// `--no-lazy` 是**夹具通道**而不是「守卫开关」:只许与 `--dist` 同用,且不得指向默认产物目录
284+
// ⇒ 真实产物(CI / husky / 收口读数)上懒侧门禁**恒开**。
285+
if (NO_LAZY && (!has("--dist") || DIST === resolve(join(APP, "dist")))) fail(`--no-lazy 只许配 --dist 且不得指向默认产物目录 ${rel(join(APP, "dist"))} —— 真实产物上懒侧门禁恒判。`);
210286
// 度量已搬进 `./lib/bundleMeasure.mjs`:那边的 `fail()` **只抛** `BudgetError`(度量模块不得持有进程
211287
// 出口),这里把它桥回本件的统一出口(`console.error` + `exit 2`,文案逐字不变)。用**专属类型**是为了
212288
// 让其余异常继续原样上抛 —— 拆前它们就是未捕获栈 + exit 1,无差别转 exit 2 才是真的行为变化。

‎scripts/lazyBudget.json‎

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
{
2+
"generatedFrom": "node scripts/check-bundle-budget.mjs --no-build --json(2026-09-13 实测;批 7 T9)",
3+
"unit": "gzip = zlib.gzipSync level 6 · 族前缀 = 去掉 8 位内容 hash 的 chunk 名 + '-'(chunk 名带内容 hash,不稳定,不能当键)",
4+
"semantics": "全部字段 = 当次实测的**冻结值**,只许降(批 7 §C9.6 第 2 条 / §C10.4);写乐观「预算值」会立刻红",
5+
"lazyChunkCountMax": 36,
6+
"lazyTotalGzipBytesMax": 698859,
7+
"families": [
8+
{
9+
"prefix": "ActionPage-",
10+
"gzipBytesMax": 8191
11+
},
12+
{
13+
"prefix": "AiConversationDock-",
14+
"gzipBytesMax": 2807
15+
},
16+
{
17+
"prefix": "CaptureFloatPanel-",
18+
"gzipBytesMax": 4538
19+
},
20+
{
21+
"prefix": "CaptureOverlayPanel-",
22+
"gzipBytesMax": 1402
23+
},
24+
{
25+
"prefix": "ChatPage-",
26+
"gzipBytesMax": 11382
27+
},
28+
{
29+
"prefix": "ConfirmDialog-",
30+
"gzipBytesMax": 765
31+
},
32+
{
33+
"prefix": "GoalsPage-",
34+
"gzipBytesMax": 12349
35+
},
36+
{
37+
"prefix": "KnowledgePage-",
38+
"gzipBytesMax": 27621
39+
},
40+
{
41+
"prefix": "NoteCardFlowView-",
42+
"gzipBytesMax": 787
43+
},
44+
{
45+
"prefix": "NoteMarkdown-",
46+
"gzipBytesMax": 2948
47+
},
48+
{
49+
"prefix": "NotesPage-",
50+
"gzipBytesMax": 47047
51+
},
52+
{
53+
"prefix": "RefineLaunchDialog-",
54+
"gzipBytesMax": 7184
55+
},
56+
{
57+
"prefix": "RefineStrategyPicker-",
58+
"gzipBytesMax": 2710
59+
},
60+
{
61+
"prefix": "ReviewPage-",
62+
"gzipBytesMax": 6815
63+
},
64+
{
65+
"prefix": "SessionCardFlowView-",
66+
"gzipBytesMax": 767
67+
},
68+
{
69+
"prefix": "SessionNotePreview-",
70+
"gzipBytesMax": 4899
71+
},
72+
{
73+
"prefix": "SessionProofView-",
74+
"gzipBytesMax": 1069
75+
},
76+
{
77+
"prefix": "SessionScreenCard-",
78+
"gzipBytesMax": 255
79+
},
80+
{
81+
"prefix": "SessionTriTrackView-",
82+
"gzipBytesMax": 4325
83+
},
84+
{
85+
"prefix": "SessionsPage-",
86+
"gzipBytesMax": 21012
87+
},
88+
{
89+
"prefix": "SettingsPage-",
90+
"gzipBytesMax": 17762
91+
},
92+
{
93+
"prefix": "TaskConversationView-",
94+
"gzipBytesMax": 2923
95+
},
96+
{
97+
"prefix": "ViewSwitcher-",
98+
"gzipBytesMax": 562
99+
},
100+
{
101+
"prefix": "colorPalette-",
102+
"gzipBytesMax": 947
103+
},
104+
{
105+
"prefix": "controls-",
106+
"gzipBytesMax": 400
107+
},
108+
{
109+
"prefix": "engine-",
110+
"gzipBytesMax": 200
111+
},
112+
{
113+
"prefix": "entityLabel-",
114+
"gzipBytesMax": 250
115+
},
116+
{
117+
"prefix": "registry-",
118+
"gzipBytesMax": 1966
119+
},
120+
{
121+
"prefix": "shift-",
122+
"gzipBytesMax": 212
123+
},
124+
{
125+
"prefix": "useColumnFlip-",
126+
"gzipBytesMax": 817
127+
},
128+
{
129+
"prefix": "useDbRefresh-",
130+
"gzipBytesMax": 383
131+
},
132+
{
133+
"prefix": "vendor-canvas-",
134+
"gzipBytesMax": 58784
135+
},
136+
{
137+
"prefix": "vendor-editor-",
138+
"gzipBytesMax": 208585
139+
},
140+
{
141+
"prefix": "vendor-gsap-",
142+
"gzipBytesMax": 41161
143+
},
144+
{
145+
"prefix": "vendor-katex-",
146+
"gzipBytesMax": 144412
147+
},
148+
{
149+
"prefix": "vendor-md-",
150+
"gzipBytesMax": 50622
151+
}
152+
]
153+
}

0 commit comments

Comments
 (0)