Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(frontend): tabler-iconsのサブセット化 #15340

Open
wants to merge 16 commits into
base: develop
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions .github/workflows/check-spdx-license-id.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ jobs:
"packages/frontend/test"
"packages/frontend-embed/@types"
"packages/frontend-embed/src"
"packages/icons-subsetter/src"
"packages/misskey-bubble-game/src"
"packages/misskey-reversi/src"
"packages/sw/src"
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
- Enhance: AiScriptの拡張API関数において引数の型チェックをより厳格に
- Enhance: クエリパラメータでuiを一時的に変更できるように #15240
- Enhance: リモート絵文字のインポート時に詳細を確認できるように #15336
- Enhance: UIのアイコンデータの読み込みを軽量化
- Fix: 画面サイズが変わった際にナビゲーションバーが自動で折りたたまれない問題を修正
- Fix: サーバー情報メニューに区切り線が不足していたのを修正
- Fix: ノートがログインしているユーザーしか見れない場合にログインダイアログを閉じるとその後の動線がなくなる問題を修正
Expand Down
6 changes: 6 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,12 @@ Misskey uses Vue(v3) as its front-end framework.
- **When creating a new component, please use the Composition API (with [setup sugar](https://v3.vuejs.org/api/sfc-script-setup.html) and [ref sugar](https://github.com/vuejs/rfcs/discussions/369)) instead of the Options API.**
- Some of the existing components are implemented in the Options API, but it is an old implementation. Refactors that migrate those components to the Composition API are also welcome.

## Tabler Icons
アイコンは、Production Build時に使用されていないものが削除されるようになっています。

**アイコンを動的に設定する際には、 `ti-${someVal}` のような、アイコン名のみを動的に変化させる実装を行わないでください。**
必ず `ti-xxx` のような完全なクラス名を含めるようにしてください。

## nirax
niraxは、Misskeyで使用しているオリジナルのフロントエンドルーティングシステムです。
**vue-routerから影響を多大に受けているので、まずはvue-routerについて学ぶことをお勧めします。**
Expand Down
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ COPY --link ["packages/backend/package.json", "./packages/backend/"]
COPY --link ["packages/frontend-shared/package.json", "./packages/frontend-shared/"]
COPY --link ["packages/frontend/package.json", "./packages/frontend/"]
COPY --link ["packages/frontend-embed/package.json", "./packages/frontend-embed/"]
COPY --link ["packages/icons-subsetter/package.json", "./packages/icons-subsetter/"]
COPY --link ["packages/sw/package.json", "./packages/sw/"]
COPY --link ["packages/misskey-js/package.json", "./packages/misskey-js/"]
COPY --link ["packages/misskey-reversi/package.json", "./packages/misskey-reversi/"]
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"packages/frontend-shared",
"packages/frontend",
"packages/frontend-embed",
"packages/icons-subsetter",
"packages/backend",
"packages/sw",
"packages/misskey-js",
Expand Down
3 changes: 2 additions & 1 deletion packages/frontend-embed/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@
"@rollup/plugin-json": "6.1.0",
"@rollup/plugin-replace": "5.0.7",
"@rollup/pluginutils": "5.1.3",
"@tabler/icons-webfont": "3.3.0",
"@twemoji/parser": "15.1.1",
"@vitejs/plugin-vue": "5.2.0",
"@vue/compiler-sfc": "3.5.12",
"astring": "1.9.0",
"buraha": "0.0.1",
"estree-walker": "3.0.3",
"icons-subsetter": "workspace:*",
"mfm-js": "0.24.0",
"misskey-js": "workspace:*",
"frontend-shared": "workspace:*",
Expand All @@ -39,6 +39,7 @@
},
"devDependencies": {
"@misskey-dev/summaly": "5.1.0",
"@tabler/icons-webfont": "https://github.com/misskey-dev/tabler-icons/archive/refs/tags/3.29.0-mi.1913+5921534bc.tar.gz",
"@testing-library/vue": "8.1.0",
"@types/estree": "1.0.6",
"@types/micromatch": "4.0.9",
Expand Down
6 changes: 5 additions & 1 deletion packages/frontend-embed/src/boot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@
// https://vitejs.dev/config/build-options.html#build-modulepreload
import 'vite/modulepreload-polyfill';

import '@tabler/icons-webfont/dist/tabler-icons.scss';
if (import.meta.env.DEV) {
await import('@tabler/icons-webfont/dist/tabler-icons.scss');
} else {
await import('icons-subsetter/built/tabler-icons-frontendEmbed.css');
}

import '@/style.scss';
import { createApp, defineAsyncComponent } from 'vue';
Expand Down
3 changes: 2 additions & 1 deletion packages/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
"@rollup/plugin-replace": "5.0.7",
"@rollup/pluginutils": "5.1.3",
"@syuilo/aiscript": "0.19.0",
"@tabler/icons-webfont": "3.3.0",
"@twemoji/parser": "15.1.1",
"@vitejs/plugin-vue": "5.2.0",
"@vue/compiler-sfc": "3.5.12",
Expand All @@ -45,6 +44,7 @@
"estree-walker": "3.0.3",
"eventemitter3": "5.0.1",
"frontend-shared": "workspace:*",
"icons-subsetter": "workspace:*",
"idb-keyval": "6.2.1",
"insert-text-at-cursor": "0.3.0",
"is-file-animated": "1.0.2",
Expand Down Expand Up @@ -94,6 +94,7 @@
"@storybook/types": "8.4.4",
"@storybook/vue3": "8.4.4",
"@storybook/vue3-vite": "8.4.4",
"@tabler/icons-webfont": "https://github.com/misskey-dev/tabler-icons/archive/refs/tags/3.29.0-mi.1913+5921534bc.tar.gz",
"@testing-library/vue": "8.1.0",
"@types/canvas-confetti": "^1.6.4",
"@types/estree": "1.0.6",
Expand Down
6 changes: 5 additions & 1 deletion packages/frontend/src/_boot_.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@
// https://vitejs.dev/config/build-options.html#build-modulepreload
import 'vite/modulepreload-polyfill';

import '@tabler/icons-webfont/dist/tabler-icons.scss';
if (import.meta.env.DEV) {
await import('@tabler/icons-webfont/dist/tabler-icons.scss');
} else {
await import('icons-subsetter/built/tabler-icons-frontend.css');
}

import '@/style.scss';
import { mainBoot } from '@/boot/main-boot.js';
Expand Down
8 changes: 3 additions & 5 deletions packages/frontend/src/components/MkDrive.folder.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts.uploadFolder }}
</p>
<button v-if="selectMode" class="_button" :class="$style.checkboxWrapper" @click.prevent.stop="checkboxClicked">
<div :class="[$style.checkbox, { [$style.checked]: isSelected }]"></div>
<div :class="[$style.checkbox, { [$style.checked]: isSelected, 'ti ti-check': isSelected }]"></div>
</button>
</div>
</template>
Expand Down Expand Up @@ -353,16 +353,14 @@ function onContextmenu(ev: MouseEvent) {
border-color: var(--MI_THEME-accent);
background: var(--MI_THEME-accent);

&::after {
content: "\ea5e";
font-family: 'tabler-icons';
&::before {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #fff;
font-size: 12px;
line-height: 22px;
line-height: 18px;
}
}
}
Expand Down
15 changes: 15 additions & 0 deletions packages/icons-subsetter/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
## これは何

フロントエンドの各パッケージで使用されているtabler iconsのclassをスキャンし、使用されているiconのみを抽出するツールです。

なお、サブセット版に無いアイコンが呼び出された場合は本物のtabler icons フォントにフォールバックするようになっています。

このツールは本番ビルド時にのみ使用されます(開発モードでも最初の1回だけビルドが走りますが、これは型エラーを抑制するためにファイルを置いておく用の措置です)

現時点では `src/generator.ts``filesToScan` にスキャン対象のファイルが書かれています。もしこれに当てはまらないファイルをサブセットのスキャン対象とする場合はこの部分を適宜修正してください。

## 使い方

```bash
pnpm build
```
18 changes: 18 additions & 0 deletions packages/icons-subsetter/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import tsParser from '@typescript-eslint/parser';
import sharedConfig from '../shared/eslint.config.js';

// eslint-disable-next-line import/no-default-export
export default [
...sharedConfig,
{
files: ['**/*.ts', '**/*.tsx'],
languageOptions: {
parserOptions: {
parser: tsParser,
project: ['./tsconfig.json'],
sourceType: 'module',
tsconfigRootDir: import.meta.dirname,
},
},
},
];
29 changes: 29 additions & 0 deletions packages/icons-subsetter/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "icons-subsetter",
"version": "0.0.0",
"private": true,
"description": "Subset tabler-icons webfont",
"type": "module",
"scripts": {
"build": "tsx src/generator.ts",
"eslint": "eslint src/**/*.ts",
"typecheck": "tsc --noEmit",
"lint": "pnpm typecheck && pnpm eslint"
},
"devDependencies": {
"@types/node": "22.9.0",
"@typescript-eslint/eslint-plugin": "7.17.0",
"@typescript-eslint/parser": "7.17.0"
},
"dependencies": {
"@tabler/icons-webfont": "https://github.com/misskey-dev/tabler-icons/archive/refs/tags/3.29.0-mi.1913+5921534bc.tar.gz",
"harfbuzzjs": "0.4.4",
"tiny-glob": "^0.2.9",
"tsx": "4.4.0",
"typescript": "5.6.3",
"wasm-ttf2woff": "1.0.0"
},
"files": [
"built"
]
}
137 changes: 137 additions & 0 deletions packages/icons-subsetter/src/generator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/

import { promises as fsp, existsSync } from 'fs';
import path from 'path';
import glob from 'tiny-glob';
import { generateSubsettedFont } from './subsetter.js';

const filesToScan = {
frontend: 'packages/frontend/src/**/*.{ts,vue}',
//frontendShared: 'packages/frontend-shared/js/**/*.{ts}', // 現時点では該当がないのでスキップ。ここをコメントアウトするときは、各フロントエンドにこのチャンクのCSSのimportを追加すること
frontendEmbed: 'packages/frontend-embed/src/**/*.{ts,vue}',
};

async function main() {
const start = performance.now();

// 1. ビルドディレクトリを削除
if (existsSync('./built')) {
await fsp.rm('./built', { recursive: true });
}
await fsp.mkdir('./built');

// 2. tabler-icons.min.cssから、class名とUnicodeのマッピングを抽出
const css = await fsp.readFile('node_modules/@tabler/icons-webfont/dist/tabler-icons.min.css', 'utf-8');
const cssRegex = /\.(ti-[a-z0-9-]+)::?before\s*{\n?\s*content:\s*["']\\([a-fA-F0-9]+)["'];?\n?\s*}/g;
const rgMap = new Map<string, string>();
let matches: RegExpExecArray | null;
while ((matches = cssRegex.exec(css)) !== null) {
rgMap.set(matches[1], matches[2]);
}

// 3. tabler-icons-classes.cssから、.tiのルールを抽出
const classTiBaseRule = css.match(/\.ti\s*{[^}]*}/)![0];

// 4. フォールバック用のtabler-icons.woff2をコピー
const fontPath = 'node_modules/@tabler/icons-webfont/dist/fonts/';
await fsp.copyFile(fontPath + 'tabler-icons.woff2', './built/tabler-icons.woff2');

// 5. 各チャンクごとにファイルをスキャンして、使用されているアイコンを抽出
const unicodeRangeValues = new Map<string, number[]>();
for (const [key, dir] of Object.entries(filesToScan)) {
console.log(`Scanning ${key}...`);

const iconsToPack = new Set<string>();

const cwd = path.resolve(process.cwd(), '../../');
const files = await glob(dir, { cwd });
for (const file of files) {
//console.log(`Scanning ${file}`);
const content = await fsp.readFile(path.resolve(cwd, file), 'utf-8');
const classRegex = /ti-[a-z0-9-]+/g;
let matches: RegExpExecArray | null;
while ((matches = classRegex.exec(content)) !== null) {
const icon = matches[0];
if (rgMap.has(icon)) {
iconsToPack.add(icon);
}
}
}

// 6. チャンク内で使用されているアイコンのUnicodeの配列を生成
const unicodeValues = Array.from(iconsToPack).map((icon) => parseInt(rgMap.get(icon)!, 16));
unicodeRangeValues.set(key, unicodeValues);
}

// 7. Tabler Iconフォントをサブセット化
const subsettedFonts = await generateSubsettedFont(fontPath + 'tabler-icons.ttf', unicodeRangeValues);

// 8. サブセット化したフォント・CSSを書き出し
await Promise.allSettled(Array.from(subsettedFonts.entries()).map(async ([key, buffer]) => {
const cssRules = [`@font-face {
font-family: "tabler-icons";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url("./tabler-icons.woff2") format("woff2");
}`];

// サブセット化したフォントの中身がある(=unicodeRangeValuesの配列が空ではない)場合のみ、サブセットしたものに関する情報を追記
if (unicodeRangeValues.get(key)!.length > 0) {
await fsp.writeFile(`./built/tabler-icons-${key}.woff2`, buffer);

const unicodeRangeString = (() => {
const values = unicodeRangeValues.get(key)!.sort((a, b) => a - b);
const ranges = [];

for (let i = 0; i < values.length; i++) {
const start = values[i];
let end = values[i];
while (values[i + 1] === end + 1) {
end = values[i + 1];
i++;
}
if (start === end) {
ranges.push(`U+${start.toString(16)}`);
} else if (start + 1 === end) {
ranges.push(`U+${start.toString(16)}`, `U+${end.toString(16)}`);
} else {
ranges.push(`U+${start.toString(16)}-${end.toString(16)}`);
}
}

return ranges.join(', ');
})();

cssRules.push(`@font-face {
font-family: "tabler-icons";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url("./tabler-icons-${key}.woff2") format("woff2");
unicode-range: ${unicodeRangeString};
}`);

cssRules.push(classTiBaseRule);

// 使用されているアイコンのclassとの対応を追記
for (const icon of unicodeRangeValues.get(key)!) {
const iconClass = Array.from(rgMap.entries()).find(([_, unicode]) => parseInt(unicode, 16) === icon)![0];
cssRules.push(`.${iconClass}::before { content: "\\${icon.toString(16)}"; }`);
}
}

await fsp.writeFile(`./built/tabler-icons-${key}.css`, cssRules.join('\n') + '\n');
}));

const end = performance.now();
console.log(`Done in ${Math.round((end - start) * 100) / 100}ms`);
}

main().catch((err) => {
console.error(err);
process.exit(1);
});
Loading
Loading