perf(dashboard): subset MDI icon font and self-host Google Fonts (#6532)

* perf(dashboard): subset MDI icon font and self-host Google Fonts

* perf(dashboard): subset MDI icon font and self-host Google Fonts

* perf(dashboard): subset MDI icon font and self-host Google Fonts

* perf(dashboard): subset MDI icon font cr fix

* chore: update lockfile
This commit is contained in:
camera-2018
2026-03-18 16:32:19 +08:00
committed by GitHub
parent 412e2e07cc
commit 4e5587998b
9 changed files with 1914 additions and 27 deletions
+4 -2
View File
@@ -5,6 +5,7 @@
"author": "CodedThemes",
"scripts": {
"dev": "vite --host",
"subset-icons": "node scripts/subset-mdi-font.mjs",
"build": "vue-tsc --noEmit && vite build",
"build-stage": "vue-tsc --noEmit && vite build --base=/vue/free/stage/",
"build-prod": "vue-tsc --noEmit && vite build --base=/vue/free/",
@@ -33,7 +34,6 @@
"monaco-editor": "^0.52.2",
"pinia": "2.1.6",
"pinyin-pro": "^3.26.0",
"remixicon": "3.5.0",
"shiki": "^3.20.0",
"stream-markdown": "^0.0.13",
"vee-validate": "4.11.3",
@@ -62,8 +62,10 @@
"prettier": "3.0.2",
"sass": "1.66.1",
"sass-loader": "13.3.2",
"subset-font": "^2.4.0",
"typescript": "5.1.6",
"vite": "6.4.1",
"vite-plugin-webfont-dl": "^3.12.0",
"vue-cli-plugin-vuetify": "2.5.8",
"vue-tsc": "1.8.8",
"vuetify-loader": "^2.0.0-alpha.9"
@@ -74,4 +76,4 @@
"lodash-es": "4.17.23"
}
}
}
}
+155 -21
View File
@@ -72,9 +72,6 @@ importers:
pinyin-pro:
specifier: ^3.26.0
version: 3.28.0
remixicon:
specifier: 3.5.0
version: 3.5.0
shiki:
specifier: ^3.20.0
version: 3.22.0
@@ -154,12 +151,18 @@ importers:
sass-loader:
specifier: 13.3.2
version: 13.3.2(sass@1.66.1)(webpack@5.105.0)
subset-font:
specifier: ^2.4.0
version: 2.4.0
typescript:
specifier: 5.1.6
version: 5.1.6
vite:
specifier: 6.4.1
version: 6.4.1(@types/node@20.19.32)(sass@1.66.1)(terser@5.46.0)
vite-plugin-webfont-dl:
specifier: ^3.12.0
version: 3.12.0(vite@6.4.1(@types/node@20.19.32)(sass@1.66.1)(terser@5.46.0))
vue-cli-plugin-vuetify:
specifier: 2.5.8
version: 2.5.8(sass-loader@13.3.2(sass@1.66.1)(webpack@5.105.0))(vue@3.3.4)(vuetify-loader@2.0.0-alpha.9(@vue/compiler-sfc@3.3.4)(vue@3.3.4)(vuetify@3.7.11)(webpack@5.105.0))(webpack@5.105.0)
@@ -199,6 +202,12 @@ packages:
'@braintree/sanitize-url@7.1.2':
resolution: {integrity: sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==}
'@cacheable/memory@2.0.8':
resolution: {integrity: sha512-FvEb29x5wVwu/Kf93IWwsOOEuhHh6dYCJF3vcKLzXc0KXIW181AOzv6ceT4ZpBHDvAfG60eqb+ekmrnLHIy+jw==}
'@cacheable/utils@2.4.0':
resolution: {integrity: sha512-PeMMsqjVq+bF0WBsxFBxr/WozBJiZKY0rUojuaCoIaKnEl3Ju1wfEwS+SV1DU/cSe8fqHIPiYJFif8T3MVt4cQ==}
'@chevrotain/cst-dts-gen@11.0.3':
resolution: {integrity: sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==}
@@ -454,6 +463,15 @@ packages:
'@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
'@keyv/bigmap@1.3.1':
resolution: {integrity: sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==}
engines: {node: '>= 18'}
peerDependencies:
keyv: ^5.6.0
'@keyv/serialize@1.1.1':
resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==}
'@mdi/font@7.2.96':
resolution: {integrity: sha512-e//lmkmpFUMZKhmCY9zdjRe4zNXfbOIJnn6xveHbaV2kSw5aJ5dLXUxcRt1Gxfi7ZYpFLUWlkG2MGSFAiqAu7w==}
@@ -519,79 +537,66 @@ packages:
resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.59.0':
resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==}
cpu: [arm]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.59.0':
resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.59.0':
resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-loong64-gnu@4.59.0':
resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==}
cpu: [loong64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-loong64-musl@4.59.0':
resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==}
cpu: [loong64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-ppc64-gnu@4.59.0':
resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-ppc64-musl@4.59.0':
resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==}
cpu: [ppc64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-riscv64-gnu@4.59.0':
resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.59.0':
resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.59.0':
resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.59.0':
resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.59.0':
resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==}
cpu: [x64]
os: [linux]
libc: [musl]
'@rollup/rollup-openbsd-x64@4.59.0':
resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==}
@@ -1258,6 +1263,9 @@ packages:
buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
cacheable@2.3.3:
resolution: {integrity: sha512-iffYMX4zxKp54evOH27fm92hs+DeC1DhXmNVN8Tr94M/iZIV42dqTHSR2Ik4TOSPyOAwKr7Yu3rN9ALoLkbWyQ==}
call-bind-apply-helpers@1.0.2:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'}
@@ -1304,6 +1312,10 @@ packages:
resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==}
engines: {node: '>=6.0'}
clean-css@5.3.3:
resolution: {integrity: sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==}
engines: {node: '>= 10.0'}
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
@@ -1760,6 +1772,9 @@ packages:
resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==}
engines: {node: ^10.12.0 || >=12.0.0}
flat-cache@6.1.20:
resolution: {integrity: sha512-AhHYqwvN62NVLp4lObVXGVluiABTHapoB57EyegZVmazN+hhGhLTn3uZbOofoTw4DSDvVCadzzyChXhOAvy8uQ==}
flatted@3.3.3:
resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
@@ -1772,6 +1787,9 @@ packages:
debug:
optional: true
fontverter@2.0.0:
resolution: {integrity: sha512-DFVX5hvXuhi1Jven1tbpebYTCT9XYnvx6/Z+HFUPb7ZRMCW+pj2clU9VMhoTPgWKPhAs7JJDSk3CW1jNUvKCZQ==}
form-data@4.0.5:
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
engines: {node: '>= 6'}
@@ -1831,6 +1849,9 @@ packages:
hachure-fill@0.5.2:
resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==}
harfbuzzjs@0.4.15:
resolution: {integrity: sha512-p1edvnlc+vpRe2kz7OKzcscf0gyFiDZpco+miDxAiiZ67cu1oNlbuOkmP/A/i1l/w938VrkF2FdZ8scNcnkPrQ==}
has-flag@4.0.0:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'}
@@ -1843,6 +1864,10 @@ packages:
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
engines: {node: '>= 0.4'}
hashery@1.5.0:
resolution: {integrity: sha512-nhQ6ExaOIqti2FDWoEMWARUqIKyjr2VcZzXShrI+A3zpeiuPWzx6iPftt44LhP74E5sW36B75N6VHbvRtpvO6Q==}
engines: {node: '>=20'}
hasown@2.0.2:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
@@ -1861,6 +1886,9 @@ packages:
resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==}
engines: {node: '>=12.0.0'}
hookified@1.15.1:
resolution: {integrity: sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==}
html-void-elements@3.0.0:
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
@@ -1970,6 +1998,9 @@ packages:
keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
keyv@5.6.0:
resolution: {integrity: sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==}
khroma@2.1.0:
resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==}
@@ -2218,6 +2249,9 @@ packages:
package-manager-detector@1.6.0:
resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==}
pako@1.0.11:
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
parent-module@1.0.1:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'}
@@ -2385,6 +2419,10 @@ packages:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
qified@0.6.0:
resolution: {integrity: sha512-tsSGN1x3h569ZSU1u6diwhltLyfUWDp3YbFHedapTmpBl0B3P6U3+Qptg7xu+v+1io1EwhdPyyRHYbEw0KN2FA==}
engines: {node: '>=20'}
querystring@0.2.1:
resolution: {integrity: sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg==}
engines: {node: '>=0.4.x'}
@@ -2410,9 +2448,6 @@ packages:
regex@6.1.0:
resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==}
remixicon@3.5.0:
resolution: {integrity: sha512-wNzWGKf4frb3tEmgvP5shry0n1OdTjjEk9RHLuRuAhfA50bvEdpKH1XWNUYrHUSjAQQkkdyIm+lf4mOuysIKTQ==}
require-from-string@2.0.2:
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
engines: {node: '>=0.10.0'}
@@ -2564,6 +2599,9 @@ packages:
stylis@4.3.6:
resolution: {integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==}
subset-font@2.4.0:
resolution: {integrity: sha512-DA/45nIj4NiseVdfHxVdVGL7hvNo3Ol6HjEm3KSYtPyDcsr6jh8Q37vSgz+A722wMfUd6nL8kgsi7uGv9DExXQ==}
supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
@@ -2760,6 +2798,11 @@ packages:
vue: ^3.0.0
vuetify: '>=3'
vite-plugin-webfont-dl@3.12.0:
resolution: {integrity: sha512-0jxsr8ycuoK/uV5Y3ytttTRhgvfZo8v3O4JZBlVc4C7QWIws/vCLVR4B3ag+TGVkLNQya6hXfY3UnZge3M8vmA==}
peerDependencies:
vite: ^2 || ^3 || ^4 || ^5 || ^6 || ^7 || ^8
vite@6.4.1:
resolution: {integrity: sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
@@ -2914,6 +2957,10 @@ packages:
resolution: {integrity: sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==}
engines: {node: '>=10.13.0'}
wawoff2@2.0.1:
resolution: {integrity: sha512-r0CEmvpH63r4T15ebFqeOjGqU4+EgTx4I510NtK35EMciSdcTxCw3Byy3JnBonz7iyIFZ0AbVo0bbFpEVuhCYA==}
hasBin: true
webpack-sources@3.3.4:
resolution: {integrity: sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==}
engines: {node: '>=10.13.0'}
@@ -2933,6 +2980,9 @@ packages:
engines: {node: '>= 8'}
hasBin: true
woff2sfnt-sfnt2woff@1.0.0:
resolution: {integrity: sha512-edK4COc1c1EpRfMqCZO1xJOvdUtM5dbVb9iz97rScvnTevqEB3GllnLWCmMVp1MfQBdF1DFg/11I0rSyAdS4qQ==}
word-wrap@1.2.5:
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
engines: {node: '>=0.10.0'}
@@ -2978,6 +3028,18 @@ snapshots:
'@braintree/sanitize-url@7.1.2': {}
'@cacheable/memory@2.0.8':
dependencies:
'@cacheable/utils': 2.4.0
'@keyv/bigmap': 1.3.1(keyv@5.6.0)
hookified: 1.15.1
keyv: 5.6.0
'@cacheable/utils@2.4.0':
dependencies:
hashery: 1.5.0
keyv: 5.6.0
'@chevrotain/cst-dts-gen@11.0.3':
dependencies:
'@chevrotain/gast': 11.0.3
@@ -3165,6 +3227,14 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
'@keyv/bigmap@1.3.1(keyv@5.6.0)':
dependencies:
hashery: 1.5.0
hookified: 1.15.1
keyv: 5.6.0
'@keyv/serialize@1.1.1': {}
'@mdi/font@7.2.96': {}
'@mermaid-js/parser@0.6.3':
@@ -4067,6 +4137,14 @@ snapshots:
buffer-from@1.1.2: {}
cacheable@2.3.3:
dependencies:
'@cacheable/memory': 2.0.8
'@cacheable/utils': 2.4.0
hookified: 1.15.1
keyv: 5.6.0
qified: 0.6.0
call-bind-apply-helpers@1.0.2:
dependencies:
es-errors: 1.3.0
@@ -4119,6 +4197,10 @@ snapshots:
chrome-trace-event@1.0.4: {}
clean-css@5.3.3:
dependencies:
source-map: 0.6.1
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
@@ -4627,10 +4709,21 @@ snapshots:
keyv: 4.5.4
rimraf: 3.0.2
flat-cache@6.1.20:
dependencies:
cacheable: 2.3.3
flatted: 3.3.3
hookified: 1.15.1
flatted@3.3.3: {}
follow-redirects@1.15.11: {}
fontverter@2.0.0:
dependencies:
wawoff2: 2.0.1
woff2sfnt-sfnt2woff: 1.0.0
form-data@4.0.5:
dependencies:
asynckit: 0.4.0
@@ -4704,6 +4797,8 @@ snapshots:
hachure-fill@0.5.2: {}
harfbuzzjs@0.4.15: {}
has-flag@4.0.0: {}
has-symbols@1.1.0: {}
@@ -4712,6 +4807,10 @@ snapshots:
dependencies:
has-symbols: 1.1.0
hashery@1.5.0:
dependencies:
hookified: 1.15.1
hasown@2.0.2:
dependencies:
function-bind: 1.1.2
@@ -4738,6 +4837,8 @@ snapshots:
highlight.js@11.11.1: {}
hookified@1.15.1: {}
html-void-elements@3.0.0: {}
iconv-lite@0.6.3:
@@ -4822,6 +4923,10 @@ snapshots:
dependencies:
json-buffer: 3.0.1
keyv@5.6.0:
dependencies:
'@keyv/serialize': 1.1.1
khroma@2.1.0: {}
langium@3.3.1:
@@ -5078,6 +5183,8 @@ snapshots:
package-manager-detector@1.6.0: {}
pako@1.0.11: {}
parent-module@1.0.1:
dependencies:
callsites: 3.1.0
@@ -5263,6 +5370,10 @@ snapshots:
punycode@2.3.1: {}
qified@0.6.0:
dependencies:
hookified: 1.15.1
querystring@0.2.1: {}
queue-microtask@1.2.3: {}
@@ -5285,8 +5396,6 @@ snapshots:
dependencies:
regex-utilities: 2.3.0
remixicon@3.5.0: {}
require-from-string@2.0.2: {}
resolve-from@4.0.0: {}
@@ -5457,6 +5566,13 @@ snapshots:
stylis@4.3.6: {}
subset-font@2.4.0:
dependencies:
fontverter: 2.0.0
harfbuzzjs: 0.4.15
lodash: 4.17.23
p-limit: 3.1.0
supports-color@7.2.0:
dependencies:
has-flag: 4.0.0
@@ -5635,6 +5751,16 @@ snapshots:
transitivePeerDependencies:
- supports-color
vite-plugin-webfont-dl@3.12.0(vite@6.4.1(@types/node@20.19.32)(sass@1.66.1)(terser@5.46.0)):
dependencies:
axios: 1.13.5
clean-css: 5.3.3
flat-cache: 6.1.20
picocolors: 1.1.1
vite: 6.4.1(@types/node@20.19.32)(sass@1.66.1)(terser@5.46.0)
transitivePeerDependencies:
- debug
vite@6.4.1(@types/node@20.19.32)(sass@1.66.1)(terser@5.46.0):
dependencies:
esbuild: 0.25.12
@@ -5765,6 +5891,10 @@ snapshots:
glob-to-regexp: 0.4.1
graceful-fs: 4.2.11
wawoff2@2.0.1:
dependencies:
argparse: 2.0.1
webpack-sources@3.3.4: {}
webpack@5.105.0:
@@ -5803,6 +5933,10 @@ snapshots:
dependencies:
isexe: 2.0.0
woff2sfnt-sfnt2woff@1.0.0:
dependencies:
pako: 1.0.11
word-wrap@1.2.5: {}
wrappy@1.0.2: {}
+291
View File
@@ -0,0 +1,291 @@
/**
* subset-mdi-font.mjs
*
* Build script that:
* 1. Scans src/ for all mdi-* icon names used in .vue/.ts files
* 2. Resolves their Unicode codepoints from @mdi/font CSS
* 3. Subsets the MDI font to include only those glyphs (via subset-font, pure JS)
* 4. Generates a minimal CSS file with only the needed icon classes
* 5. Outputs to src/assets/mdi-subset/
*
* Fallback: if any step fails, copies the original full @mdi/font CSS and fonts
* so the build never breaks.
*/
import { readFileSync, writeFileSync, copyFileSync, readdirSync, statSync, existsSync, mkdirSync } from "fs";
import { join, resolve, extname } from "path";
import { fileURLToPath } from "url";
// Derive __dirname portably from import.meta.url (works across all Node ESM versions)
const __dirname = fileURLToPath(new URL(".", import.meta.url));
const ROOT = resolve(__dirname, "..");
const SRC = join(ROOT, "src");
const MDI_CSS_PATH = join(ROOT, "node_modules/@mdi/font/css/materialdesignicons.css");
const MDI_TTF_PATH = join(ROOT, "node_modules/@mdi/font/fonts/materialdesignicons-webfont.ttf");
const MDI_WOFF2_PATH = join(ROOT, "node_modules/@mdi/font/fonts/materialdesignicons-webfont.woff2");
const MDI_WOFF_PATH = join(ROOT, "node_modules/@mdi/font/fonts/materialdesignicons-webfont.woff");
const OUT_DIR = join(ROOT, "src/assets/mdi-subset");
// Utility classes that should not be treated as icon names
const UTILITY_CLASSES = new Set([
"mdi-set", "mdi-spin", "mdi-rotate-45", "mdi-rotate-90", "mdi-rotate-135",
"mdi-rotate-180", "mdi-rotate-225", "mdi-rotate-270", "mdi-rotate-315",
"mdi-flip-h", "mdi-flip-v", "mdi-light", "mdi-dark", "mdi-inactive",
"mdi-18px", "mdi-24px", "mdi-36px", "mdi-48px",
]);
// Regex to match individual icon class definitions in MDI CSS
export const ICON_CLASS_PATTERN = /\.(mdi-[a-z][a-z0-9-]*)::before\s*\{\s*content:\s*"\\([0-9A-Fa-f]+)"\s*;?\s*}/g;
// ── Helper functions ────────────────────────────────────────────────────────
/** Recursively collect files with given extensions, skipping node_modules. */
export function* collectFiles(dir, exts) {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const full = join(dir, entry.name);
if (entry.isDirectory() && entry.name !== "node_modules") {
yield* collectFiles(full, exts);
} else if (exts.includes(extname(entry.name))) {
yield full;
}
}
}
/** Scan source files and return a Set of used mdi-* icon names. */
export function scanUsedIcons(sourceFiles) {
const iconPattern = /mdi-[a-z][a-z0-9-]*/g;
const usedIcons = new Set();
for (const file of sourceFiles) {
const content = readFileSync(file, "utf-8");
for (const match of content.matchAll(iconPattern)) {
if (!UTILITY_CLASSES.has(match[0])) {
usedIcons.add(match[0]);
}
}
}
return usedIcons;
}
/** Parse @mdi/font CSS and return a Map of icon-name → hex codepoint. */
export function parseIconCodepoints(mdiCSS) {
const iconMap = new Map();
for (const match of mdiCSS.matchAll(ICON_CLASS_PATTERN)) {
iconMap.set(match[1], match[2]);
}
return iconMap;
}
/** Resolve used icons against the codepoint map, returning resolved/missing/subsetChars. */
export function resolveUsedIcons(usedIcons, iconMap) {
const resolvedIcons = [];
const missingIcons = [];
const subsetChars = [];
for (const icon of usedIcons) {
const cp = iconMap.get(icon);
if (cp) {
resolvedIcons.push(icon);
subsetChars.push(String.fromCodePoint(parseInt(cp, 16)));
} else {
missingIcons.push(icon);
}
}
return { resolvedIcons, missingIcons, subsetChars };
}
/**
* Extract utility CSS rules (size, rotation, flip, spin, etc.) from the original MDI CSS.
* Uses a subtraction approach: removes the parts we regenerate (icon definitions,
* @font-face, base .mdi rules) and keeps everything else. This is more robust than
* relying on a fixed start marker, as it tolerates CSS reordering in future versions.
*/
export function extractUtilityCss(mdiCSS, iconClassPattern) {
let utilityCss = mdiCSS
.replace(iconClassPattern, "") // Remove icon definitions
.replace(/@font-face\s*\{[\s\S]*?}/g, "") // Remove @font-face
.replace(/\.mdi:before,\s*\.mdi-set\s*\{[\s\S]*?}/g, "") // Remove base rules
.replace(/\/\*# sourceMappingURL=.*\*\//, "") // Remove source map
.trim();
// Clean up excess blank lines left after removals
utilityCss = utilityCss.replace(/(\r\n|\n){3,}/g, "\n\n");
return utilityCss;
}
/** Build a fallback CSS that rewrites font URLs to use subset filenames. */
function buildFallbackCss() {
const mdiCSS = readFileSync(MDI_CSS_PATH, "utf-8");
return mdiCSS
// Rewrite woff/woff2 URLs to point at subset filenames
.replace(/url\("\.\.\/fonts\/materialdesignicons-webfont\.(woff2?)\?[^"]*"\)/g,
(_, ext) => `url("./materialdesignicons-webfont-subset.${ext}")`)
// Remove legacy eot/ttf sources
.replace(/url\("\.\.\/fonts\/materialdesignicons-webfont\.(eot|ttf)\?[^"]*"\)[^,]*/g, "")
// Clean up dangling commas/separators
.replace(/src:\s*,/g, "src:")
.replace(/,\s*;/g, ";");
}
// ── Fallback: copy original full MDI font if subsetting fails ───────────────
function fallbackToFullFont(reason) {
console.warn(`\n⚠️ Subsetting failed: ${reason}`);
console.warn(`⚠️ Falling back to full @mdi/font (build will not break)\n`);
// Copy original font files
if (existsSync(MDI_WOFF2_PATH)) {
copyFileSync(MDI_WOFF2_PATH, join(OUT_DIR, "materialdesignicons-webfont-subset.woff2"));
}
if (existsSync(MDI_WOFF_PATH)) {
copyFileSync(MDI_WOFF_PATH, join(OUT_DIR, "materialdesignicons-webfont-subset.woff"));
}
writeFileSync(join(OUT_DIR, "materialdesignicons-subset.css"), buildFallbackCss());
const size = existsSync(MDI_WOFF2_PATH) ? statSync(MDI_WOFF2_PATH).size : 0;
console.warn(`⚠️ Fallback complete: using full font (${(size / 1024).toFixed(1)} KB woff2)`);
}
// ── Exported entry point ────────────────────────────────────────────────────
export async function runMdiSubset() {
mkdirSync(OUT_DIR, { recursive: true });
try {
// Pre-checks
if (!existsSync(MDI_CSS_PATH)) {
throw new Error(`@mdi/font CSS not found at ${MDI_CSS_PATH}. Run 'pnpm install' first.`);
}
if (!existsSync(MDI_TTF_PATH)) {
throw new Error(`@mdi/font TTF not found at ${MDI_TTF_PATH}. Run 'pnpm install' first.`);
}
// Dynamic import subset-font (may not be installed in all environments)
let subsetFont;
try {
subsetFont = (await import("subset-font")).default;
} catch (e) {
throw new Error(`subset-font package not available: ${e.message}. Run 'pnpm install' first.`);
}
// Step 1: Scan source files for mdi-* icon names
const sourceFiles = collectFiles(SRC, [".vue", ".ts", ".js"]);
const usedIcons = scanUsedIcons(sourceFiles);
if (usedIcons.size === 0) {
throw new Error("No mdi-* icons found in source files. Something is wrong with scanning.");
}
console.log(`✅ Found ${usedIcons.size} unique mdi-* icons in source files`);
// Step 2: Parse @mdi/font CSS to get codepoints for each icon
const mdiCSS = readFileSync(MDI_CSS_PATH, "utf-8");
const iconMap = parseIconCodepoints(mdiCSS);
if (iconMap.size === 0) {
throw new Error("Could not parse any icon definitions from @mdi/font CSS. Format may have changed.");
}
console.log(`📦 MDI font CSS contains ${iconMap.size} icon definitions`);
// Step 3: Resolve codepoints for used icons
const { resolvedIcons, missingIcons, subsetChars } = resolveUsedIcons(usedIcons, iconMap);
if (missingIcons.length > 0) {
console.warn(`⚠️ ${missingIcons.length} icons not found in MDI CSS:`, missingIcons.join(", "));
}
if (resolvedIcons.length === 0) {
throw new Error("No icon codepoints could be resolved. Icon name format may have changed.");
}
console.log(`🔍 Resolved ${resolvedIcons.length} codepoints for subsetting`);
// Add space character
subsetChars.push(" ");
const subsetText = subsetChars.join("");
// Step 4: Subset font with subset-font (pure JS/WASM)
const fontBuffer = readFileSync(MDI_TTF_PATH);
console.log(`🔧 Subsetting font to woff2...`);
const woff2Buffer = await subsetFont(fontBuffer, subsetText, {
targetFormat: "woff2",
});
console.log(`🔧 Subsetting font to woff...`);
const woffBuffer = await subsetFont(fontBuffer, subsetText, {
targetFormat: "woff",
});
if (woff2Buffer.length === 0 || woffBuffer.length === 0) {
throw new Error("subset-font produced empty output. Font file may be corrupted.");
}
const outWoff2 = join(OUT_DIR, "materialdesignicons-webfont-subset.woff2");
const outWoff = join(OUT_DIR, "materialdesignicons-webfont-subset.woff");
writeFileSync(outWoff2, woff2Buffer);
writeFileSync(outWoff, woffBuffer);
// Step 5: Generate subset CSS
let css = `/* Auto-generated MDI subset – ${resolvedIcons.length} icons */
/* Do not edit manually. Run: pnpm run subset-icons */
@font-face {
font-family: "Material Design Icons";
src: url("./materialdesignicons-webfont-subset.woff2") format("woff2"),
url("./materialdesignicons-webfont-subset.woff") format("woff");
font-weight: normal;
font-style: normal;
}
.mdi:before,
.mdi-set {
display: inline-block;
font: normal normal normal 24px/1 "Material Design Icons";
font-size: inherit;
text-rendering: auto;
line-height: inherit;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
`;
for (const icon of resolvedIcons.sort()) {
const cp = iconMap.get(icon);
css += `.${icon}::before {\n content: "\\${cp}";\n}\n\n`;
}
const utilityCss = extractUtilityCss(mdiCSS, ICON_CLASS_PATTERN);
if (utilityCss) {
css += `/* Utility classes (extracted from @mdi/font) */\n${utilityCss}\n`;
} else {
console.warn("⚠️ Could not find MDI utility classes in original CSS, skipping");
}
const outCSS = join(OUT_DIR, "materialdesignicons-subset.css");
writeFileSync(outCSS, css);
// Report
const origSize = statSync(MDI_TTF_PATH).size;
const subsetWoff2Size = woff2Buffer.length;
console.log(`\n📊 Results:`);
console.log(` Original TTF font: ${(origSize / 1024).toFixed(1)} KB`);
console.log(` Subset WOFF2: ${(subsetWoff2Size / 1024).toFixed(1)} KB`);
console.log(` Reduction: ${((1 - subsetWoff2Size / origSize) * 100).toFixed(1)}%`);
console.log(` Icons included: ${resolvedIcons.length}`);
console.log(` CSS file: ${outCSS}`);
console.log(`\n✅ MDI font subset generated successfully!`);
} catch (err) {
// Fallback: any failure → use original full font so build never breaks
try {
fallbackToFullFont(err.message);
} catch (fallbackErr) {
console.error(`❌ Fallback also failed: ${fallbackErr.message}`);
console.error(`❌ Please ensure @mdi/font is installed: pnpm install`);
throw fallbackErr;
}
}
}
// ── CLI entry point: allows running directly via `node scripts/subset-mdi-font.mjs` ──
if (import.meta.url.startsWith('file:') && process.argv[1] === fileURLToPath(import.meta.url)) {
runMdiSubset().catch(err => {
console.error(err);
process.exit(1);
});
}
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,5 +1,5 @@
import { createVuetify } from 'vuetify';
import '@mdi/font/css/materialdesignicons.css';
import '@/assets/mdi-subset/materialdesignicons-subset.css';
import * as components from 'vuetify/components';
import * as directives from 'vuetify/directives';
import { PurpleTheme } from '@/theme/LightTheme';
+250
View File
@@ -0,0 +1,250 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdirSync, writeFileSync, rmSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import {
collectFiles,
scanUsedIcons,
parseIconCodepoints,
resolveUsedIcons,
extractUtilityCss,
ICON_CLASS_PATTERN,
} from '../scripts/subset-mdi-font.mjs';
// ── Helper: create a temporary directory tree for file-system tests ─────────
function makeTmpDir() {
const base = join(tmpdir(), `mdi-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
mkdirSync(base, { recursive: true });
return base;
}
// ── collectFiles ────────────────────────────────────────────────────────────
test('collectFiles yields files matching given extensions', () => {
const tmp = makeTmpDir();
writeFileSync(join(tmp, 'a.vue'), '');
writeFileSync(join(tmp, 'b.ts'), '');
writeFileSync(join(tmp, 'c.txt'), '');
const files = [...collectFiles(tmp, ['.vue', '.ts'])];
assert.equal(files.length, 2);
assert.ok(files.some(f => f.endsWith('a.vue')));
assert.ok(files.some(f => f.endsWith('b.ts')));
rmSync(tmp, { recursive: true });
});
test('collectFiles recurses into subdirectories', () => {
const tmp = makeTmpDir();
const sub = join(tmp, 'sub');
mkdirSync(sub);
writeFileSync(join(sub, 'deep.vue'), '');
const files = [...collectFiles(tmp, ['.vue'])];
assert.equal(files.length, 1);
assert.ok(files[0].endsWith('deep.vue'));
rmSync(tmp, { recursive: true });
});
test('collectFiles skips node_modules directories', () => {
const tmp = makeTmpDir();
const nm = join(tmp, 'node_modules');
mkdirSync(nm);
writeFileSync(join(nm, 'pkg.vue'), '');
writeFileSync(join(tmp, 'app.vue'), '');
const files = [...collectFiles(tmp, ['.vue'])];
assert.equal(files.length, 1);
assert.ok(files[0].endsWith('app.vue'));
rmSync(tmp, { recursive: true });
});
test('collectFiles yields nothing for empty directory', () => {
const tmp = makeTmpDir();
const files = [...collectFiles(tmp, ['.vue'])];
assert.equal(files.length, 0);
rmSync(tmp, { recursive: true });
});
// ── scanUsedIcons ───────────────────────────────────────────────────────────
test('scanUsedIcons extracts mdi-* icon names from files', () => {
const tmp = makeTmpDir();
writeFileSync(join(tmp, 'A.vue'), '<v-icon>mdi-home</v-icon><v-icon>mdi-close</v-icon>');
writeFileSync(join(tmp, 'B.vue'), 'icon="mdi-home"');
const icons = scanUsedIcons(collectFiles(tmp, ['.vue']));
assert.ok(icons instanceof Set);
assert.ok(icons.has('mdi-home'));
assert.ok(icons.has('mdi-close'));
assert.equal(icons.size, 2); // mdi-home deduplicated
rmSync(tmp, { recursive: true });
});
test('scanUsedIcons excludes utility classes', () => {
const tmp = makeTmpDir();
writeFileSync(join(tmp, 'A.vue'), 'mdi-spin mdi-rotate-90 mdi-flip-h mdi-home');
const icons = scanUsedIcons(collectFiles(tmp, ['.vue']));
assert.ok(icons.has('mdi-home'));
assert.ok(!icons.has('mdi-spin'));
assert.ok(!icons.has('mdi-rotate-90'));
assert.ok(!icons.has('mdi-flip-h'));
rmSync(tmp, { recursive: true });
});
test('scanUsedIcons returns empty set when no icons found', () => {
const tmp = makeTmpDir();
writeFileSync(join(tmp, 'A.vue'), '<div>Hello</div>');
const icons = scanUsedIcons(collectFiles(tmp, ['.vue']));
assert.equal(icons.size, 0);
rmSync(tmp, { recursive: true });
});
// ── parseIconCodepoints ─────────────────────────────────────────────────────
test('parseIconCodepoints parses icon definitions from CSS', () => {
const css = `
.mdi-home::before { content: "\\F02DC"; }
.mdi-close::before { content: "\\F0156"; }
`;
const map = parseIconCodepoints(css);
assert.equal(map.size, 2);
assert.equal(map.get('mdi-home'), 'F02DC');
assert.equal(map.get('mdi-close'), 'F0156');
});
test('parseIconCodepoints handles CSS with semicolons inside braces', () => {
const css = `.mdi-check::before { content: "\\F012C"; }`;
const map = parseIconCodepoints(css);
assert.equal(map.get('mdi-check'), 'F012C');
});
test('parseIconCodepoints returns empty map for non-matching CSS', () => {
const css = `.some-other-class { color: red; }`;
const map = parseIconCodepoints(css);
assert.equal(map.size, 0);
});
// ── resolveUsedIcons ────────────────────────────────────────────────────────
test('resolveUsedIcons separates resolved and missing icons', () => {
const usedIcons = new Set(['mdi-home', 'mdi-close', 'mdi-nonexistent']);
const iconMap = new Map([
['mdi-home', 'F02DC'],
['mdi-close', 'F0156'],
]);
const { resolvedIcons, missingIcons, subsetChars } = resolveUsedIcons(usedIcons, iconMap);
assert.ok(resolvedIcons.includes('mdi-home'));
assert.ok(resolvedIcons.includes('mdi-close'));
assert.equal(resolvedIcons.length, 2);
assert.deepEqual(missingIcons, ['mdi-nonexistent']);
// Verify subsetChars contains correct Unicode characters
assert.equal(subsetChars.length, 2);
assert.equal(subsetChars[0], String.fromCodePoint(0xF02DC));
assert.equal(subsetChars[1], String.fromCodePoint(0xF0156));
});
test('resolveUsedIcons returns all missing when iconMap is empty', () => {
const usedIcons = new Set(['mdi-home']);
const iconMap = new Map();
const { resolvedIcons, missingIcons, subsetChars } = resolveUsedIcons(usedIcons, iconMap);
assert.equal(resolvedIcons.length, 0);
assert.deepEqual(missingIcons, ['mdi-home']);
assert.equal(subsetChars.length, 0);
});
// ── extractUtilityCss ───────────────────────────────────────────────────────
test('extractUtilityCss removes icon definitions and keeps utility rules', () => {
const css = `
@font-face {
font-family: "Material Design Icons";
src: url("../fonts/materialdesignicons-webfont.woff2") format("woff2");
}
.mdi:before,
.mdi-set {
display: inline-block;
font: normal normal normal 24px/1 "Material Design Icons";
}
.mdi-home::before { content: "\\F02DC"; }
.mdi-close::before { content: "\\F0156"; }
.mdi-spin:before {
animation: mdi-spin 2s infinite linear;
}
.mdi-18px.mdi-set, .mdi-18px.mdi:before {
font-size: 18px;
}
/*# sourceMappingURL=materialdesignicons.css.map */
`;
const result = extractUtilityCss(css, ICON_CLASS_PATTERN);
// Should NOT contain icon definitions
assert.ok(!result.includes('mdi-home'));
assert.ok(!result.includes('mdi-close'));
// Should NOT contain @font-face
assert.ok(!result.includes('@font-face'));
// Should NOT contain base .mdi rules
assert.ok(!result.includes('display: inline-block'));
// Should NOT contain source map
assert.ok(!result.includes('sourceMappingURL'));
// SHOULD contain utility classes
assert.ok(result.includes('mdi-spin'));
assert.ok(result.includes('mdi-18px'));
});
test('extractUtilityCss returns empty string when only icon defs exist', () => {
const css = `
@font-face { font-family: "MDI"; src: url("font.woff2"); }
.mdi:before, .mdi-set { display: inline-block; }
.mdi-home::before { content: "\\F02DC"; }
`;
const result = extractUtilityCss(css, ICON_CLASS_PATTERN);
assert.equal(result, '');
});
test('extractUtilityCss handles empty CSS input', () => {
const result = extractUtilityCss('', ICON_CLASS_PATTERN);
assert.equal(result, '');
});
// ── ICON_CLASS_PATTERN ──────────────────────────────────────────────────────
test('ICON_CLASS_PATTERN matches standard MDI icon definitions', () => {
const css = `.mdi-home::before { content: "\\F02DC"; }`;
const matches = [...css.matchAll(ICON_CLASS_PATTERN)];
assert.equal(matches.length, 1);
assert.equal(matches[0][1], 'mdi-home');
assert.equal(matches[0][2], 'F02DC');
});
test('ICON_CLASS_PATTERN does not match non-icon classes', () => {
const css = `.some-class::before { content: "hello"; }`;
const matches = [...css.matchAll(ICON_CLASS_PATTERN)];
assert.equal(matches.length, 0);
});
+20 -3
View File
@@ -2,10 +2,26 @@ import { fileURLToPath, URL } from 'url';
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import vuetify from 'vite-plugin-vuetify';
import webfontDl from 'vite-plugin-webfont-dl';
// @ts-ignore — .mjs not in TS project scope; Vite resolves this at runtime
import { runMdiSubset } from './scripts/subset-mdi-font.mjs';
// Vite plugin: run MDI icon font subsetting (build only)
function mdiSubset() {
return {
name: 'vite-plugin-mdi-subset',
async buildStart() {
console.log('\n🔧 Running MDI icon font subsetting...');
await runMdiSubset();
},
};
}
// https://vitejs.dev/config/
export default defineConfig({
export default defineConfig(({ command }) => ({
plugins: [
// Only run MDI subsetting during production builds, skip in dev server
...(command === 'build' ? [mdiSubset()] : []),
vue({
template: {
compilerOptions: {
@@ -15,7 +31,8 @@ export default defineConfig({
}),
vuetify({
autoImport: true
})
}),
webfontDl()
],
resolve: {
alias: {
@@ -47,4 +64,4 @@ export default defineConfig({
}
}
}
});
}));