Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,21 @@ BitFun redistributes selected material and links libraries from the following
third-party projects. These notices are included with BitFun's Desktop and CLI
release packages.

## HarmonyOS Sans Fonts

- Project: HarmonyOS Sans Fonts
- Provider: Huawei Device Co., Ltd.
- License: HarmonyOS Sans Fonts License Agreement
- Copyright: Copyright 2021 Huawei Device Co., Ltd.

Non-Apple BitFun GUI distributions bundle unmodified copies of the Base and
Simplified Chinese Regular, Medium, and Bold fonts. They do not bundle the
Traditional Chinese font family. Apple GUI distributions and Traditional
Chinese UI text use platform system fonts instead.

The complete license agreement and a font-specific notice are included only in
the non-Apple frontend artifact under `third-party/fonts/harmonyos-sans/`.

## models.dev catalog data

- Project: models.dev
Expand Down
6 changes: 3 additions & 3 deletions design-system/packages/design-tokens/src/system.tokens.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,15 @@
"family": {
"sans": {
"$type": "fontFamily",
"$value": "'Noto Sans SC', -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Hiragino Sans GB', 'Segoe UI', 'Microsoft YaHei UI', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif"
"$value": "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei UI', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif"
},
"control": {
"$type": "fontFamily",
"$value": "'SF Pro Text', -apple-system, BlinkMacSystemFont, 'Segoe UI Variable Text', 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Noto Sans SC', 'Microsoft YaHei UI', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif"
"$value": "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI Variable Text', 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei UI', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif"
},
"mono": {
"$type": "fontFamily",
"$value": "'JetBrains Mono', 'FiraCode', ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Monaco, 'Cascadia Mono', 'Cascadia Code', Consolas, 'Liberation Mono', 'Courier New', monospace"
"$value": "'JetBrains Mono', 'Fira Code', ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Monaco, 'Cascadia Mono', 'Cascadia Code', Consolas, 'Liberation Mono', 'Courier New', monospace"
}
},
"size": {
Expand Down
12 changes: 5 additions & 7 deletions design-system/packages/design-tokens/tests/contract.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -375,15 +375,13 @@ test("shared system scales preserve the migrated Web UI foundation contract", ()
},
);
const controlFontFamily = tokens["font.family.control"];
assert.equal(controlFontFamily.startsWith("'SF Pro Text'"), true);
assert.equal(controlFontFamily.startsWith("system-ui"), true);
assert.equal(controlFontFamily.includes("-apple-system"), true);
assert.equal(controlFontFamily.includes("'Segoe UI Variable Text'"), true);
assert.equal(controlFontFamily.includes("'Noto Sans SC'"), true);
assert.ok(
controlFontFamily.indexOf("'Segoe UI Variable Text'")
< controlFontFamily.indexOf("'Noto Sans SC'"),
);
assert.equal(tokens["font.family.sans"].startsWith("'Noto Sans SC'"), true);
assert.equal(controlFontFamily.includes("'Noto Sans SC'"), false);
assert.equal(tokens["font.family.sans"].startsWith("system-ui"), true);
assert.equal(tokens["font.family.mono"].startsWith("'JetBrains Mono'"), true);
assert.equal(tokens["font.family.mono"].includes("'Fira Code'"), true);
assert.equal(tokens["font.size.micro"], "10px");
assert.equal(tokens["font.size.meta"], "11px");
assert.equal(tokens["font.size.xs"], "12px");
Expand Down
23 changes: 14 additions & 9 deletions docs/performance/04-startup-and-bundle-size.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
- 审阅范围:Tauri 冷启动链路(src/apps/desktop)、前端首包与分包(src/web-ui)、静态资源(public/dist)、Rust release profile
- 方法:只读源码分析 + 现成 dist 产物统计(未运行完整构建)

> 2026-09-02 更新:下文保留 2026-07-26 的体积基线;字体项已经按平台字体档案重新实现,当前状态见第 2 节。

## 产物体积基线(现成产物实测)

| 产物 | 大小 | 说明 |
Expand All @@ -12,7 +14,7 @@
| `dist/assets`(JS/CSS) | 19 MB(JS 13.6 MB / 731 个 chunk) | 入口 `index-DwMxiWtW.js` 单文件 **5.44 MB** |
| `dist/agent-companion-pets` | **18 MB** | 10 个桌宠精灵图全量内置 |
| `dist/monaco-editor` | **14 MB** | monaco `min/vs` 全量拷贝(含 9 种 NLS 语言、tsWorker 5.7 MB) |
| `dist/fonts` | **13 MB** | Noto Sans SC 3 个字重,各 ~4.2 MB |
| `dist/fonts` | **13 MB** | 审阅时的旧内置中文字体 3 个字重,各 ~4.2 MB |
| `dist/` 根目录图片 | ~2.7 MB | BitFun-Logo.png 1.06 MB 等未压缩 PNG |
| `target/release/bitfun-desktop.exe` | 217 MB(7-24 本地构建,profile 未知,可能为 release-fast) | 前端 dist 全部嵌入二进制 |
| NSIS 安装包 | 0.2.11 → 81 MB;0.2.13 → **90.5 MB**(两个小版本 +9.4 MB) | `target/release/bundle/nsis/` |
Expand All @@ -26,7 +28,7 @@
| # | 问题 | 维度 | 预期收益 | 风险 |
|---|---|---|---|---|
| 1 | Rust 端全部后端服务**串行初始化完成后才创建窗口**,init 期间无任何窗口 | 冷启动 | **高** | 中 |
| 2 | Noto Sans SC 三个静态字重共 12.7 MB,全部打入安装包 | 体积 | ****(-8~9 MB) | |
| 2 | 旧字体无条件进入所有平台安装包 | 体积/平台一致性 | **已解决**(2026-09-02) | |
| 3 | 10 个桌宠精灵图 18 MB 全量内置 | 体积 | **高**(-10~16 MB,视方案) | 中(产品决策) |
| 4 | Monaco 全量 min 拷贝:未用 NLS 语言 1.4 MB + 未做裁剪 | 体积 | 中(-1.4 MB 起) | 低 |
| 5 | 首包 5.44 MB:三语种 bootstrap 文案 ~793 KB 全部 eager 打入入口 | 冷启动+体积 | 中(首包 -0.5 MB+,减 JS parse) | 中 |
Expand Down Expand Up @@ -60,13 +62,16 @@
2. 把"窗口创建"前移:仅 `initialize_global_config`(主题/语言 bootstrap 需要)是窗口创建的真依赖;可在 config 就绪后立即创建并显示窗口(splash 已自带),其余服务初始化移入后台任务,前端已有 `initialize_workspace_startup_state` 命令兜底(`commands.rs:2024-2047` 注明了 fallback 路径)。需要给未就绪期间到达的 invoke 加"服务就绪门闩"(如全局 `OnceCell`/`Notify` gate)。
3. `prepare_workspace_startup_bootstrap_snapshot` 的 `block_in_place/block_on`(`lib.rs:708-716`)可设超时上限或改为异步注入(前端本就有命令回退路径),避免慢盘工作区把窗口创建拖住。

### 2. 中文字体 12.7 MB【高收益/低风险】
### 2. 平台字体档案【已解决】

2026-09-02 起,正式桌面构建按目标 triple 选择字体档案:

**证据**:`dist/fonts/Noto_Sans_SC/static/` 下 Regular/Medium/SemiBold 各 4.18~4.27 MB(woff2 已压缩,brotli 二次压缩几乎无收益,≈1:1 进入安装包);`public/fonts/fonts.css` 三个字重都被 `@font-face` 声明,UI 三个字重都会被实际请求。这一项约占 90 MB 安装包的 14%。
- Apple 使用系统 `-apple-system` / SF Pro 与系统等宽字体,不输出任何产品文本字体文件。
- Windows/Linux 使用 HarmonyOS Sans Base 与 SC 的 Regular、Medium、Bold 六个未修改 TTF;许可证不允许子集化或格式转换,因此这一档案优先保证授权合规和字形一致性,而不是最小体积。
- `zh-TW` 明确使用系统字体,不内置 TC 字体。
- 编辑器/终端字体与产品正文分离;Fira Code 只进入非 Apple 档案。

**方案**(任选其一):
- 换用 Noto Sans SC **可变字体**单文件(wght 300-700 一个 woff2,约 4~5 MB),`fonts.css` 三个 `@font-face` 合并为一条 `font-weight: 400 600`,与 FiraCode-VF 的做法一致(FiraCode 已有 VF 文件)→ 净省 ~8.4 MB。
- 或按 GB2312/常用字集做子集化(pyftsubset),三字重合计可降到 2~3 MB,但存在生僻字回退风险,需保留系统字体 fallback。
`scripts/web-font-profile.mjs` 固化源文件长度和 SHA-256,并在构建时审计字体与法律文件白名单,防止旧字体、TC 字体或非批准格式重新进入产物。

### 3. 桌宠资源 18 MB 全量内置【高收益/中风险(产品决策)】

Expand Down Expand Up @@ -128,7 +133,7 @@

| 任务 | 具体内容 | 涉及文件 | 预期收益 | 风险 |
|---|---|---|---|---|
| T1 字体瘦身 | 将 Noto Sans SC 三个静态字重替换为单个可变字体 woff2,更新 `@font-face` 为 `font-weight: 400 600`;回归中英文界面渲染 | `src/web-ui/public/fonts/`(fonts.css + 字体文件) | 安装包 -8 MB | 低 |
| T1 平台字体档案(已完成) | Apple 使用系统字体且不携带产品字体;Windows/Linux 内置 HarmonyOS Sans Base + SC 六个未修改 TTF;`zh-TW` 使用系统字体 | `scripts/web-font-profile.mjs`、`src/web-ui/src/font-profiles/`、`src/web-ui/src/assets/fonts/` | Apple 移除产品字体;非 Apple 接受合规字体体积 | 已完成 |
| T2 Monaco NLS 裁剪 | `copy-monaco` 后删除 `nls.messages.{ru,ja,ko,fr,it,es,de}.js`(保留 zh-cn/zh-tw);在 `verify-monaco-assets.cjs` 中加断言防回归 | 根 `package.json` scripts、`scripts/verify-monaco-assets.cjs` | dist -1.4 MB | 低 |
| T3 图片压缩 | oxipng/pngquant 压缩 `public/` 根目录 6 个 PNG(Logo、panda 系列);`panda-pix/spritesheet.png` 转 webp;保持 `Logo-ICON-128.png` 透明度契约测试通过 | `src/web-ui/public/*.png`、`public/agent-companion-pets/panda-pix/` | dist -2.5~3 MB | 低 |
| T4 桌宠按需分发 | 产品确认后:仅内置默认宠物,其余走现有 `import_agent_companion_pet_package` 通道按需获取 | `src/web-ui/public/agent-companion-pets/`、桌宠选择 UI | 安装包 -14 MB | 中(需产品确认) |
Expand All @@ -140,4 +145,4 @@
| T10 高亮引擎统一 | 消除双 prismjs(InlineDiffPreview 改用 refractor 或懒加载),规划 highlight.js/react-syntax-highlighter 二选一 | `src/web-ui/src/flow_chat/components/InlineDiffPreview.tsx` 等 | JS -0.5~1 MB | 中 |
| T11 体积基线监控 | CI 增加 dist 分目录与安装包体积基线对比(0.2.11→0.2.13 已 +9.4 MB 无告警) | `scripts/ci/` | 防回归 | 低 |

建议实施顺序:T1/T2/T3(纯资源,低风险速赢,合计约 -12 MB)→ T5 → T8 → T9 → T7 → T6(收益最大但需设计评审)→ T4/T10/T11。
建议后续实施顺序:T2/T3(纯资源,低风险速赢)→ T5 → T8 → T9 → T7 → T6(收益最大但需设计评审)→ T4/T10/T11。
2 changes: 0 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,6 @@
"capabilities:generate": "node scripts/generate-interactive-capabilities.mjs",
"capabilities:check": "node scripts/generate-interactive-capabilities.mjs --check",
"capabilities:test": "node --test scripts/interactive-capabilities.test.mjs",
"iconography:generate": "node scripts/iconography/generate.mjs",
"iconography:check": "node scripts/iconography/generate.mjs --check",
"typography:audit": "node scripts/audit-typography-tokens.mjs",
"typography:audit:test": "node --test scripts/audit-typography-tokens.test.mjs",
"theme:color-audit": "node scripts/audit-frontend-colors.mjs --surface web-ui",
Expand Down
46 changes: 46 additions & 0 deletions scripts/audit-typography-tokens.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ const SELF_AUDIT_FILES = new Set([
'scripts/audit-typography-tokens.mjs',
'scripts/audit-typography-tokens.test.mjs',
]);
const WEB_FONT_PROFILE_STYLE_ROOT = 'src/web-ui/src/font-profiles/';
const WEB_FONT_PROFILE_STACK_PROPERTIES = new Set([
'--bf-font-family-sans',
'--bf-font-family-control',
'--bf-font-family-mono',
]);

const TYPOGRAPHY_PROPERTIES = new Set([
'font',
Expand Down Expand Up @@ -353,6 +359,38 @@ function normalizeCssValue(value) {
.trim();
}

function cssAtRuleRanges(source, atRulePattern) {
const ranges = [];
const pattern = new RegExp(`${atRulePattern}\\s*\\{`, 'gi');
for (const match of source.matchAll(pattern)) {
const openBrace = (match.index ?? 0) + match[0].lastIndexOf('{');
let depth = 1;
for (let index = openBrace + 1; index < source.length; index += 1) {
if (source[index] === '{') depth += 1;
if (source[index] === '}') depth -= 1;
if (depth === 0) {
ranges.push([openBrace, index]);
break;
}
}
}
return ranges;
}

function isBuildOwnedWebFontDeclaration(relativePath, source, offset, property, customProperty) {
if (!normalizePath(relativePath).startsWith(WEB_FONT_PROFILE_STYLE_ROOT)) return false;

if (customProperty) {
return property === 'fontFamily'
&& WEB_FONT_PROFILE_STACK_PROPERTIES.has(customProperty.toLowerCase());
}

if (property !== 'fontFamily' && property !== 'fontWeight') return false;
return cssAtRuleRanges(source, '@font-face').some(([start, end]) => (
offset >= start && offset <= end
));
}

function isStructuralZero(property, value) {
return (property === 'fontSize' || property === 'lineHeight') && /^0(?:\.0+)?$/.test(value);
}
Expand Down Expand Up @@ -383,6 +421,7 @@ function auditCssDeclarations(text, relativePath) {
// are still caught because they contain no interpolation marker.
if (value.includes('${')) continue;
if (isCanonicalTypographyValue(property, value)) continue;
if (isBuildOwnedWebFontDeclaration(relativePath, source, match.index ?? 0, property)) continue;
if (hasLocalTypographyException(lines, line)) continue;

issues.push(createIssue(
Expand Down Expand Up @@ -413,6 +452,13 @@ function auditCssDeclarations(text, relativePath) {
if (
isCanonicalTypographyValue(property, value)
|| /^var\(--_[a-z0-9-]+\)$/i.test(normalizedValue)
|| isBuildOwnedWebFontDeclaration(
relativePath,
source,
match.index ?? 0,
property,
customProperty,
)
|| hasLocalTypographyException(lines, line)
) {
continue;
Expand Down
32 changes: 32 additions & 0 deletions scripts/audit-typography-tokens.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,38 @@ test('rejects raw CSS sizes, weights, line heights, tracking, and font stacks',
assert.equal(codes.filter(code => code === 'raw-css-typography').length, 5);
});

test('accepts only build-owned font faces and stacks in Web font profiles', () => {
const issues = auditTypographyText(`
@font-face {
font-family: "BitFun HarmonyOS Sans";
font-weight: 500;
}
@layer bf.tokens.system {
:where([data-bf-design-system-root]) {
--bf-font-family-sans: "BitFun HarmonyOS Sans", system-ui, sans-serif;
--bf-font-family-control: "BitFun HarmonyOS Sans", system-ui, sans-serif;
--bf-font-family-mono: "Fira Code", monospace;
}
}
`, 'src/web-ui/src/font-profiles/harmony-bundled.css');

assert.deepEqual(issues, []);
});

test('rejects unrelated raw typography inside Web font profiles', () => {
const codes = codesFor(`
.bad {
font-family: Inter, sans-serif;
font-size: 14px;
font-weight: 650;
--feature-font-family: Inter, sans-serif;
}
`, 'src/web-ui/src/font-profiles/harmony-bundled.css');

assert.equal(codes.filter(code => code === 'raw-css-typography').length, 3);
assert.equal(codes.filter(code => code === 'raw-private-typography-token').length, 1);
});

test('rejects static TypeScript typography but accepts tokens, adapters, and dynamic previews', () => {
const codes = codesFor(`
const RAW_FONT_SIZE = 12;
Expand Down
18 changes: 18 additions & 0 deletions scripts/desktop-tauri-build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ import { extractProductConfigArg } from './product-customization/cli.mjs';
import { productBuildEnvironment } from './product-customization/projections.mjs';
import { resolveProductDefinition } from './product-customization/resolver.mjs';
import { resolveReleaseChannel } from './release-channel.mjs';
import {
WEB_FONT_PROFILE_ENV,
fontProfileForDesktopTarget,
} from './web-font-profile.mjs';

const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = join(__dirname, '..');
Expand All @@ -43,6 +47,8 @@ async function main() {
const resolution = resolveProductDefinition({ rootDir: ROOT, productConfig, member: 'desktop' });
Object.assign(process.env, productBuildEnvironment(resolution));
console.log(`[product] ${resolution.assembly.member} ${resolution.assembly.assemblyDigest}`);
const fontProfile = configureDesktopWebFontProfile(forward);
console.log(`[font-profile] ${fontProfile}`);
const releaseChannel = resolveReleaseChannel(process.env.BITFUN_RELEASE_CHANNEL);
console.log(`[release] channel=${releaseChannel.channel}`);

Expand Down Expand Up @@ -191,6 +197,18 @@ function optionValue(args, option) {
return undefined;
}

export function configureDesktopWebFontProfile(
args,
{ env = process.env, platform = process.platform } = {},
) {
const profile = fontProfileForDesktopTarget({
target: optionValue(args, '--target'),
platform,
});
env[WEB_FONT_PROFILE_ENV] = profile;
return profile;
}

export function prepareMacOSFlashgrepForSigning(
flashgrepBinary,
desktopDir,
Expand Down
37 changes: 37 additions & 0 deletions scripts/desktop-tauri-build.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,53 @@ import { tmpdir } from 'node:os';
import { join } from 'node:path';
import test from 'node:test';
import {
configureDesktopWebFontProfile,
prepareMacOSFlashgrepForSigning,
prepareTauriConfig,
shouldRetryMacDmgBuild,
} from './desktop-tauri-build.mjs';
import { resolveProductDefinition } from './product-customization/resolver.mjs';
import {
APPLE_SYSTEM_FONT_PROFILE,
HARMONY_BUNDLED_FONT_PROFILE,
WEB_FONT_PROFILE_ENV,
} from './web-font-profile.mjs';

const FAILED_BUILD = { status: 1 };
const DMG_ARGS = ['--target', 'x86_64-apple-darwin', '--bundles', 'app,dmg'];
const ROOT = join(import.meta.dirname, '..');

test('Desktop packaging selects the Web font profile from its target triple', () => {
const appleEnv = { [WEB_FONT_PROFILE_ENV]: HARMONY_BUNDLED_FONT_PROFILE };
assert.equal(
configureDesktopWebFontProfile(
['--target', 'aarch64-apple-darwin'],
{ env: appleEnv, platform: 'win32' },
),
APPLE_SYSTEM_FONT_PROFILE,
);
assert.equal(appleEnv[WEB_FONT_PROFILE_ENV], APPLE_SYSTEM_FONT_PROFILE);

const windowsEnv = { [WEB_FONT_PROFILE_ENV]: APPLE_SYSTEM_FONT_PROFILE };
assert.equal(
configureDesktopWebFontProfile(
['--target=x86_64-pc-windows-msvc'],
{ env: windowsEnv, platform: 'darwin' },
),
HARMONY_BUNDLED_FONT_PROFILE,
);
assert.equal(windowsEnv[WEB_FONT_PROFILE_ENV], HARMONY_BUNDLED_FONT_PROFILE);

assert.equal(
configureDesktopWebFontProfile([], { env: {}, platform: 'darwin' }),
APPLE_SYSTEM_FONT_PROFILE,
);
assert.equal(
configureDesktopWebFontProfile([], { env: {}, platform: 'linux' }),
HARMONY_BUNDLED_FONT_PROFILE,
);
});

test('release builds do not mutate DMGs after Tauri signs and notarizes them', () => {
const source = readFileSync(join(ROOT, 'scripts', 'desktop-tauri-build.mjs'), 'utf8');
assert.doesNotMatch(source, /patchDmgExtras/);
Expand Down
Loading
Loading