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 26 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
9265182
feat(frontend): tabler-iconsの使用されていないアイコンを削除するように
kakkokari-gtyih Jan 25, 2025
69fb609
fix
kakkokari-gtyih Jan 25, 2025
b027172
fix
kakkokari-gtyih Jan 25, 2025
3e593c4
fix
kakkokari-gtyih Jan 25, 2025
9ad37e5
fix
kakkokari-gtyih Jan 25, 2025
021fb1a
fix
kakkokari-gtyih Jan 25, 2025
0d6d24d
Update Changelog
kakkokari-gtyih Jan 25, 2025
b254633
enhance: tablerのCSSを使用されているクラスのみに限定
kakkokari-gtyih Jan 25, 2025
8cbdf27
使用するアイコンパッケージをそろえる
kakkokari-gtyih Jan 25, 2025
68d2ef0
Update CONTRIBUTING.md
kakkokari-gtyih Jan 25, 2025
27256b3
Update CONTRIBUTING.md
kakkokari-gtyih Jan 25, 2025
0a9a947
spdx
kakkokari-gtyih Jan 25, 2025
907c0be
typo
kakkokari-gtyih Jan 25, 2025
8e8e307
Merge branch 'develop' into feat-tabler-subset
kakkokari-gtyih Jan 26, 2025
84dc996
fix: サブセットから除外される書き方をしている部分を修正
kakkokari-gtyih Jan 26, 2025
8252036
Merge branch 'develop' into feat-tabler-subset
kakkokari-gtyih Jan 26, 2025
2b03592
fix: 同じunicodeに複数のアイコンclassが割り当てられている場合に対応
kakkokari-gtyih Jan 28, 2025
7ed6656
remove debug code
kakkokari-gtyih Jan 28, 2025
b045572
Merge branch 'develop' into feat-tabler-subset
kakkokari-gtyih Jan 28, 2025
4f765b0
Update CHANGELOG.md
kakkokari-gtyih Jan 28, 2025
a27a023
Merge remote-tracking branch 'msky/develop' into feat-tabler-subset
kakkokari-gtyih Feb 15, 2025
2304562
Merge remote-tracking branch 'msky/develop' into feat-tabler-subset
kakkokari-gtyih Feb 24, 2025
a04a15a
fix merge error
kakkokari-gtyih Feb 24, 2025
8f12b41
setup renovate
kakkokari-gtyih Feb 24, 2025
1437f18
Merge branch 'develop' into feat-tabler-subset
kakkokari-gtyih Mar 3, 2025
73383ad
fix: woff2ではなくwoffに変換していたのを修正
kakkokari-gtyih Mar 3, 2025
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
@@ -1,6 +1,7 @@
## 2025.3.0

### General
- Enhance: UIのアイコンデータの読み込みを軽量化
- Enhance: プロキシアカウントをシステムアカウントとして作成するように
- 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 @@ -26,6 +26,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": "6.0.2",
"@rollup/pluginutils": "5.1.4",
"@tabler/icons-webfont": "https://github.com/misskey-dev/tabler-icons/archive/refs/tags/3.30.0-mi.1932+ab127beee.tar.gz",
"@twemoji/parser": "15.1.1",
"@vitejs/plugin-vue": "5.2.1",
"@vue/compiler-sfc": "3.5.13",
"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.2.0",
"@tabler/icons-webfont": "https://github.com/misskey-dev/tabler-icons/archive/refs/tags/3.30.0-mi.1932+ab127beee.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 @@ -25,7 +25,6 @@
"@rollup/plugin-replace": "6.0.2",
"@rollup/pluginutils": "5.1.4",
"@syuilo/aiscript": "0.19.0",
"@tabler/icons-webfont": "https://github.com/misskey-dev/tabler-icons/archive/refs/tags/3.30.0-mi.1932+ab127beee.tar.gz",
"@twemoji/parser": "15.1.1",
"@vitejs/plugin-vue": "5.2.1",
"@vue/compiler-sfc": "3.5.13",
Expand All @@ -47,6 +46,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 @@ -96,6 +96,7 @@
"@storybook/types": "8.6.2",
"@storybook/vue3": "8.6.2",
"@storybook/vue3-vite": "8.6.2",
"@tabler/icons-webfont": "https://github.com/misskey-dev/tabler-icons/archive/refs/tags/3.30.0-mi.1932+ab127beee.tar.gz",
"@testing-library/vue": "8.1.0",
"@types/canvas-confetti": "1.9.0",
"@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,
},
},
},
];
30 changes: 30 additions & 0 deletions packages/icons-subsetter/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"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.13.8",
"@types/wawoff2": "1.0.2",
"@typescript-eslint/eslint-plugin": "8.25.0",
"@typescript-eslint/parser": "8.25.0"
},
"dependencies": {
"@tabler/icons-webfont": "https://github.com/misskey-dev/tabler-icons/archive/refs/tags/3.30.0-mi.1932+ab127beee.tar.gz",
"harfbuzzjs": "0.4.5",
"tiny-glob": "0.2.9",
"tsx": "4.19.3",
"typescript": "5.8.2",
"wawoff2": "2.0.1"
},
"files": [
"built"
]
}
141 changes: 141 additions & 0 deletions packages/icons-subsetter/src/generator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/*
* 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 iconClasses = Array.from(rgMap.entries()).filter(([_, unicode]) => parseInt(unicode, 16) === icon);
if (iconClasses.length > 1) {
console.warn(`[WARN] Multiple classes for the same unicode: ${iconClasses.map(([cls]) => cls).join(', ')}. Maybe it's deprecated?`);
}
const iconSelector = iconClasses.map(([className]) => `.${className}::before`).join(', ');
cssRules.push(`${iconSelector} { 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