@ubean/cli 0.5.1 → 0.6.0-beta.1

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/dist/index.d.ts CHANGED
@@ -43,6 +43,8 @@ interface BundleBudgetEntry {
43
43
  file: string;
44
44
  bytes: number;
45
45
  gzip: number;
46
+ /** Brotli size, reported for context; no gate reads it yet. */
47
+ brotli?: number;
46
48
  isEntry?: boolean;
47
49
  }
48
50
  interface BundleBaseline {
@@ -51,18 +53,37 @@ interface BundleBaseline {
51
53
  entries: BundleBudgetEntry[];
52
54
  totalBytes: number;
53
55
  totalGzip: number;
56
+ totalBrotli?: number;
54
57
  entryGzip: number;
55
58
  }
59
+ interface BundleBudgetOptions {
60
+ /** Allowed relative gzip growth for total and entry. Defaults to 0.05. */
61
+ maxIncrease?: number;
62
+ /**
63
+ * Absolute gzip ceilings in bytes. Checked against the current build alone, so they hold
64
+ * even when the committed baseline was recorded from a smaller tree.
65
+ */
66
+ maxTotalGzip?: number;
67
+ maxEntryGzip?: number;
68
+ maxChunkGzip?: number;
69
+ }
70
+ interface BundleBudgetViolation {
71
+ kind: 'total' | 'entry' | 'chunk' | 'missing';
72
+ /** Measured gzip size in bytes. */
73
+ actual: number;
74
+ /** Ceiling that was exceeded, in bytes. */
75
+ limit: number;
76
+ file?: string;
77
+ }
56
78
  export declare function readViteManifest(manifestPath: string): Record<string, ManifestChunk>;
57
79
  export declare function summarizeBundle(outDir: string, manifest: Record<string, ManifestChunk>): BundleBaseline;
58
- export declare function compareBundleBaseline(current: Pick<BundleBaseline, 'totalGzip' | 'entryGzip'>, committed: Pick<BundleBaseline, 'totalGzip' | 'entryGzip'>, options?: {
59
- maxIncrease?: number;
60
- }): {
80
+ export declare function compareBundleBaseline(current: Pick<BundleBaseline, 'totalGzip' | 'entryGzip'> & Partial<Pick<BundleBaseline, 'entries'>>, committed: Pick<BundleBaseline, 'totalGzip' | 'entryGzip'> & Partial<Pick<BundleBaseline, 'entries'>>, options?: BundleBudgetOptions): {
61
81
  ok: boolean;
62
82
  maxIncrease: number;
63
83
  totalRatio: number;
64
84
  entryRatio: number;
65
85
  messages: string[];
86
+ violations: BundleBudgetViolation[];
66
87
  };
67
88
  export declare function writeBundleBaseline(filePath: string, baseline: BundleBaseline): void;
68
89
  export declare function findClientManifest(cwd: string): {
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
1
  import { n as getScaffoldManifest, t as SCAFFOLD_CONTRACT_VERSION } from "./scaffold-manifest-TrH6OCX8.js";
2
- import { A as summarizeBundle, C as toCamelCase, D as compareBundleBaseline, E as createFsOps, O as findClientManifest, S as renderTemplate, T as toPascalCase, _ as renderCronTemplate, a as scaffold, b as renderPageTemplate, d as CRON_TEMPLATE, f as LAYOUT_TEMPLATE, g as renderApiTemplate, h as PLUGIN_TEMPLATE, i as recoverScaffold, j as writeBundleBaseline, k as readViteManifest, m as PAGE_TEMPLATE, n as listScaffoldableFiles, p as MIDDLEWARE_TEMPLATE, t as deleteScaffold, u as API_TEMPLATE, v as renderLayoutTemplate, w as toKebabCase, x as renderPluginTemplate, y as renderMiddlewareTemplate } from "./page-Um0fHKa9.js";
2
+ import { A as readViteManifest, C as toCamelCase, D as compareBundleBaseline, E as createFsOps, M as writeBundleBaseline, O as findClientManifest, S as renderTemplate, T as toPascalCase, _ as renderCronTemplate, a as scaffold, b as renderPageTemplate, d as CRON_TEMPLATE, f as LAYOUT_TEMPLATE, g as renderApiTemplate, h as PLUGIN_TEMPLATE, i as recoverScaffold, j as summarizeBundle, m as PAGE_TEMPLATE, n as listScaffoldableFiles, p as MIDDLEWARE_TEMPLATE, t as deleteScaffold, u as API_TEMPLATE, v as renderLayoutTemplate, w as toKebabCase, x as renderPluginTemplate, y as renderMiddlewareTemplate } from "./page-C2Bde9EG.js";
3
3
  export { API_TEMPLATE, CRON_TEMPLATE, LAYOUT_TEMPLATE, MIDDLEWARE_TEMPLATE, PAGE_TEMPLATE, PLUGIN_TEMPLATE, SCAFFOLD_CONTRACT_VERSION, compareBundleBaseline, createFsOps, deleteScaffold, findClientManifest, getScaffoldManifest, listScaffoldableFiles, readViteManifest, recoverScaffold, renderApiTemplate, renderCronTemplate, renderLayoutTemplate, renderMiddlewareTemplate, renderPageTemplate, renderPluginTemplate, renderTemplate, scaffold, summarizeBundle, toCamelCase, toKebabCase, toPascalCase, writeBundleBaseline };
@@ -1,9 +1,21 @@
1
1
  import { existsSync, mkdirSync, promises, readFileSync, statSync, writeFileSync } from "node:fs";
2
- import { gzipSync } from "node:zlib";
2
+ import { brotliCompressSync, gzipSync } from "node:zlib";
3
3
  import { dirname, join, resolve } from "pathe";
4
4
  import { dirname as dirname$1, extname, join as join$1, relative as relative$1, resolve as resolve$1 } from "node:path";
5
5
  import { getLogger } from "@ubean/shared/logger";
6
6
  //#region src/analyze-lib.ts
7
+ const CHUNK_VIOLATION_DISPLAY_LIMIT = 5;
8
+ /**
9
+ * 内容哈希规范化:`assets/chunks/Foo-AbC12dEf.js` → `assets/chunks/Foo.<hash>.js`。
10
+ *
11
+ * 用于按**名字**对照两次构建的 chunk 名单 —— 名称稳定、哈希不稳定,比名字才有意义。
12
+ */
13
+ function normalizeChunkFileName(file) {
14
+ return file.replace(/-[A-Za-z0-9_-]{8}\./g, ".<hash>.");
15
+ }
16
+ function formatKilobytes(bytes) {
17
+ return `${(bytes / 1024).toFixed(1)} kB`;
18
+ }
7
19
  function readViteManifest(manifestPath) {
8
20
  return JSON.parse(readFileSync(manifestPath, "utf8"));
9
21
  }
@@ -13,18 +25,19 @@ function summarizeBundle(outDir, manifest) {
13
25
  if (!chunk.file || chunk.file.endsWith(".css")) continue;
14
26
  const abs = join(outDir, chunk.file);
15
27
  if (!existsSync(abs)) continue;
16
- const bytes = statSync(abs).size;
17
- const gzip = gzipSync(readFileSync(abs)).length;
28
+ const source = readFileSync(abs);
18
29
  entries.push({
19
30
  file: chunk.file,
20
- bytes,
21
- gzip,
31
+ bytes: statSync(abs).size,
32
+ gzip: gzipSync(source).length,
33
+ brotli: brotliCompressSync(source).length,
22
34
  isEntry: chunk.isEntry
23
35
  });
24
36
  }
25
37
  entries.sort((a, b) => b.gzip - a.gzip);
26
38
  const totalBytes = entries.reduce((sum, e) => sum + e.bytes, 0);
27
39
  const totalGzip = entries.reduce((sum, e) => sum + e.gzip, 0);
40
+ const totalBrotli = entries.reduce((sum, e) => sum + (e.brotli ?? 0), 0);
28
41
  const entryGzip = entries.filter((e) => e.isEntry).reduce((sum, e) => sum + e.gzip, 0);
29
42
  return {
30
43
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -32,24 +45,73 @@ function summarizeBundle(outDir, manifest) {
32
45
  entries,
33
46
  totalBytes,
34
47
  totalGzip,
48
+ totalBrotli,
35
49
  entryGzip
36
50
  };
37
51
  }
38
52
  function compareBundleBaseline(current, committed, options = {}) {
39
53
  const maxIncrease = options.maxIncrease ?? .05;
40
- const totalRatio = committed.totalGzip === 0 ? current.totalGzip === 0 ? 0 : Infinity : current.totalGzip / committed.totalGzip - 1;
41
- const entryRatio = committed.entryGzip === 0 ? current.entryGzip === 0 ? 0 : Infinity : current.entryGzip / committed.entryGzip - 1;
54
+ const totalRatio = growthRatio(current.totalGzip, committed.totalGzip);
55
+ const entryRatio = growthRatio(current.entryGzip, committed.entryGzip);
42
56
  const messages = [];
43
- if (totalRatio > maxIncrease) messages.push(`total gzip ${(current.totalGzip / 1024).toFixed(1)} kB exceeds baseline ${(committed.totalGzip / 1024).toFixed(1)} kB by ${(totalRatio * 100).toFixed(1)}% (max ${(maxIncrease * 100).toFixed(0)}%)`);
44
- if (entryRatio > maxIncrease) messages.push(`entry gzip ${(current.entryGzip / 1024).toFixed(1)} kB exceeds baseline ${(committed.entryGzip / 1024).toFixed(1)} kB by ${(entryRatio * 100).toFixed(1)}% (max ${(maxIncrease * 100).toFixed(0)}%)`);
57
+ const violations = [];
58
+ if (totalRatio > maxIncrease) messages.push(`total gzip ${formatKilobytes(current.totalGzip)} exceeds baseline ${formatKilobytes(committed.totalGzip)} by ${(totalRatio * 100).toFixed(1)}% (max ${(maxIncrease * 100).toFixed(0)}%)`);
59
+ if (entryRatio > maxIncrease) messages.push(`entry gzip ${formatKilobytes(current.entryGzip)} exceeds baseline ${formatKilobytes(committed.entryGzip)} by ${(entryRatio * 100).toFixed(1)}% (max ${(maxIncrease * 100).toFixed(0)}%)`);
60
+ if (options.maxTotalGzip !== void 0 && current.totalGzip > options.maxTotalGzip) {
61
+ violations.push({
62
+ kind: "total",
63
+ actual: current.totalGzip,
64
+ limit: options.maxTotalGzip
65
+ });
66
+ messages.push(`total gzip ${formatKilobytes(current.totalGzip)} exceeds the absolute budget ${formatKilobytes(options.maxTotalGzip)}`);
67
+ }
68
+ if (options.maxEntryGzip !== void 0 && current.entryGzip > options.maxEntryGzip) {
69
+ violations.push({
70
+ kind: "entry",
71
+ actual: current.entryGzip,
72
+ limit: options.maxEntryGzip
73
+ });
74
+ messages.push(`entry gzip ${formatKilobytes(current.entryGzip)} exceeds the absolute budget ${formatKilobytes(options.maxEntryGzip)}`);
75
+ }
76
+ const oversizedChunks = options.maxChunkGzip === void 0 ? [] : (current.entries ?? []).filter((entry) => entry.gzip > options.maxChunkGzip);
77
+ for (const entry of oversizedChunks) violations.push({
78
+ kind: "chunk",
79
+ actual: entry.gzip,
80
+ limit: options.maxChunkGzip,
81
+ file: entry.file
82
+ });
83
+ if (oversizedChunks.length > 0) {
84
+ const shown = oversizedChunks.slice(0, CHUNK_VIOLATION_DISPLAY_LIMIT).map((entry) => `${entry.file} ${formatKilobytes(entry.gzip)}`).join(", ");
85
+ const rest = oversizedChunks.length - CHUNK_VIOLATION_DISPLAY_LIMIT;
86
+ messages.push(`${oversizedChunks.length} chunk(s) exceed the absolute budget ${formatKilobytes(options.maxChunkGzip)}: ${shown}${rest > 0 ? ` (+${rest} more)` : ""}`);
87
+ }
88
+ const currentNames = new Set((current.entries ?? []).map((entry) => normalizeChunkFileName(entry.file)));
89
+ const missing = committed.entries === void 0 ? [] : committed.entries.map((entry) => normalizeChunkFileName(entry.file)).filter((name) => !currentNames.has(name));
90
+ const uniqueMissing = [...new Set(missing)];
91
+ for (const file of uniqueMissing) violations.push({
92
+ kind: "missing",
93
+ actual: 0,
94
+ limit: -1,
95
+ file
96
+ });
97
+ if (uniqueMissing.length > 0) {
98
+ const shown = uniqueMissing.slice(0, CHUNK_VIOLATION_DISPLAY_LIMIT).join(", ");
99
+ const rest = uniqueMissing.length - CHUNK_VIOLATION_DISPLAY_LIMIT;
100
+ messages.push(`${uniqueMissing.length} chunk(s) present in the baseline are missing from this build: ${shown}${rest > 0 ? ` (+${rest} more)` : ""}`);
101
+ }
45
102
  return {
46
103
  ok: messages.length === 0,
47
104
  maxIncrease,
48
105
  totalRatio,
49
106
  entryRatio,
50
- messages
107
+ messages,
108
+ violations
51
109
  };
52
110
  }
111
+ function growthRatio(current, committed) {
112
+ if (committed === 0) return current === 0 ? 0 : Infinity;
113
+ return current / committed - 1;
114
+ }
53
115
  function writeBundleBaseline(filePath, baseline) {
54
116
  mkdirSync(dirname(filePath), { recursive: true });
55
117
  writeFileSync(filePath, `${JSON.stringify(baseline, null, 2)}\n`, "utf8");
@@ -1733,4 +1795,4 @@ const pageCommand = {
1733
1795
  }
1734
1796
  };
1735
1797
  //#endregion
1736
- export { summarizeBundle as A, toCamelCase as C, compareBundleBaseline as D, createFsOps as E, findClientManifest as O, renderTemplate as S, toPascalCase as T, renderCronTemplate as _, scaffold as a, renderPageTemplate as b, scaffoldStarterTemplate as c, CRON_TEMPLATE as d, LAYOUT_TEMPLATE as f, renderApiTemplate as g, PLUGIN_TEMPLATE as h, recoverScaffold as i, writeBundleBaseline as j, readViteManifest as k, scaffoldUnifyTemplate as l, PAGE_TEMPLATE as m, listScaffoldableFiles as n, scaffoldBlogTemplate as o, MIDDLEWARE_TEMPLATE as p, pageCommand as r, scaffoldMinimalTemplate as s, deleteScaffold as t, API_TEMPLATE as u, renderLayoutTemplate as v, toKebabCase as w, renderPluginTemplate as x, renderMiddlewareTemplate as y };
1798
+ export { readViteManifest as A, toCamelCase as C, compareBundleBaseline as D, createFsOps as E, writeBundleBaseline as M, findClientManifest as O, renderTemplate as S, toPascalCase as T, renderCronTemplate as _, scaffold as a, renderPageTemplate as b, scaffoldStarterTemplate as c, CRON_TEMPLATE as d, LAYOUT_TEMPLATE as f, renderApiTemplate as g, PLUGIN_TEMPLATE as h, recoverScaffold as i, summarizeBundle as j, formatKilobytes as k, scaffoldUnifyTemplate as l, PAGE_TEMPLATE as m, listScaffoldableFiles as n, scaffoldBlogTemplate as o, MIDDLEWARE_TEMPLATE as p, pageCommand as r, scaffoldMinimalTemplate as s, deleteScaffold as t, API_TEMPLATE as u, renderLayoutTemplate as v, toKebabCase as w, renderPluginTemplate as x, renderMiddlewareTemplate as y };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ubean/cli",
3
- "version": "0.5.1",
3
+ "version": "0.6.0-beta.1",
4
4
  "description": "CLI for ubean (ubean-next binary: dev, build, prepare, init, preview, page, env, config, devtools, api/layout/middleware/cron/plugin scaffolds) plus the Vite dev server runner",
5
5
  "bin": {
6
6
  "ubean-next": "./bin/ubean-next.mjs"
@@ -24,15 +24,15 @@
24
24
  }
25
25
  },
26
26
  "dependencies": {
27
- "@ubean/app": "0.5.1",
28
- "@ubean/build": "0.5.1",
29
- "@ubean/client": "0.5.1",
30
- "@ubean/config": "0.5.1",
31
- "@ubean/i18n": "0.5.1",
32
- "@ubean/islands": "0.5.1",
33
- "@ubean/preset": "0.5.1",
34
- "@ubean/scan": "0.5.1",
35
- "@ubean/shared": "0.5.1",
27
+ "@ubean/app": "0.6.0-beta.1",
28
+ "@ubean/build": "0.6.0-beta.1",
29
+ "@ubean/client": "0.6.0-beta.1",
30
+ "@ubean/config": "0.6.0-beta.1",
31
+ "@ubean/i18n": "0.6.0-beta.1",
32
+ "@ubean/islands": "0.6.0-beta.1",
33
+ "@ubean/preset": "0.6.0-beta.1",
34
+ "@ubean/scan": "0.6.0-beta.1",
35
+ "@ubean/shared": "0.6.0-beta.1",
36
36
  "citty": "^0.2.2",
37
37
  "kolorist": "^1.8.0",
38
38
  "pathe": "^2.0.3"
@@ -40,13 +40,14 @@
40
40
  "devDependencies": {
41
41
  "@types/node": "^26.5.0",
42
42
  "@vitejs/plugin-vue": "^6.0.8",
43
+ "env-runner": "0.2.3",
43
44
  "typescript": "npm:typescript-native-bridge@latest",
44
45
  "vite": "npm:@voidzero-dev/vite-plus-core@0.3.1",
45
46
  "vite-plus": "0.3.1"
46
47
  },
47
48
  "peerDependencies": {
48
- "@ubean/content": "0.5.1",
49
- "@ubean/devtools": "0.5.1",
49
+ "@ubean/content": "0.6.0-beta.1",
50
+ "@ubean/devtools": "0.6.0-beta.1",
50
51
  "@vitejs/plugin-vue": "^6.0.8",
51
52
  "vite": "npm:@voidzero-dev/vite-plus-core@0.3.1"
52
53
  },