merge-iconfont-from-cdn.mjs 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. /**
  2. * 从两套 iconfont CDN CSS(与 setIconfont 历史配置一致)合并类名与 unicode,
  3. * 生成 iconfont.json + 双字体 iconfont.css,并下载 woff2/ttf 到 src/assets/iconfont。
  4. * 规则顺序:2298093 为基础,3882322 覆盖同名类(与页面先插 229 再插 388 时一致)。
  5. *
  6. * 使用:node scripts/merge-iconfont-from-cdn.mjs
  7. */
  8. import fs from 'node:fs';
  9. import path from 'node:path';
  10. import https from 'node:https';
  11. import { fileURLToPath } from 'node:url';
  12. const __dirname = path.dirname(fileURLToPath(import.meta.url));
  13. const root = path.resolve(__dirname, '..');
  14. const outDir = path.join(root, 'src', 'assets', 'iconfont');
  15. const URL_229_CSS = 'https://at.alicdn.com/t/font_2298093_y6u00apwst.css';
  16. const URL_388_CSS = 'https://at.alicdn.com/t/c/font_3882322_9ah7y8m9175.css';
  17. const FONT_FILES = [
  18. { url: 'https://at.alicdn.com/t/font_2298093_y6u00apwst.woff2?t=1627014681704', name: 'font-2298093.woff2' },
  19. { url: 'https://at.alicdn.com/t/font_2298093_y6u00apwst.ttf?t=1627014681704', name: 'font-2298093.ttf' },
  20. { url: 'https://at.alicdn.com/t/c/font_3882322_9ah7y8m9175.woff2?t=1676037377315', name: 'font-3882322.woff2' },
  21. { url: 'https://at.alicdn.com/t/c/font_3882322_9ah7y8m9175.ttf?t=1676037377315', name: 'font-3882322.ttf' },
  22. ];
  23. function fetchText(url) {
  24. return new Promise((resolve, reject) => {
  25. https
  26. .get(url, (res) => {
  27. if (res.statusCode === 301 || res.statusCode === 302) {
  28. fetchText(res.headers.location).then(resolve).catch(reject);
  29. return;
  30. }
  31. if (res.statusCode !== 200) {
  32. reject(new Error(`GET ${url} ${res.statusCode}`));
  33. return;
  34. }
  35. const chunks = [];
  36. res.on('data', (c) => chunks.push(c));
  37. res.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
  38. })
  39. .on('error', reject);
  40. });
  41. }
  42. function downloadFile(url, dest) {
  43. return new Promise((resolve, reject) => {
  44. const file = fs.createWriteStream(dest);
  45. https
  46. .get(url, (res) => {
  47. if (res.statusCode === 301 || res.statusCode === 302) {
  48. file.close();
  49. fs.unlink(dest, () => {});
  50. downloadFile(res.headers.location, dest).then(resolve).catch(reject);
  51. return;
  52. }
  53. if (res.statusCode !== 200) {
  54. file.close();
  55. fs.unlink(dest, () => {});
  56. reject(new Error(`GET ${url} ${res.statusCode}`));
  57. return;
  58. }
  59. res.pipe(file);
  60. file.on('finish', () => file.close(resolve));
  61. })
  62. .on('error', (e) => {
  63. file.close();
  64. fs.unlink(dest, () => {});
  65. reject(e);
  66. });
  67. });
  68. }
  69. /** @returns {Map<string, string>} className (e.g. icon-foo) -> hex 如 e670(无反斜杠) */
  70. function parseIconRules(css) {
  71. const map = new Map();
  72. const re = /\.(icon-[a-zA-Z0-9_-]+):before\s*\{[^}]*?content:\s*"([^"]+)"/g;
  73. let m;
  74. while ((m = re.exec(css))) {
  75. let hex = m[2].replace(/^\\+/, '');
  76. map.set(m[1], hex);
  77. }
  78. return map;
  79. }
  80. /** 合并:388 覆盖 229 */
  81. function mergeMaps(base229, overlay388) {
  82. const unicode = new Map(base229);
  83. const source = new Map();
  84. for (const k of base229.keys()) source.set(k, '2298093');
  85. for (const [k, v] of overlay388) {
  86. unicode.set(k, v);
  87. source.set(k, '3882322');
  88. }
  89. return { unicode, source };
  90. }
  91. function buildCss({ unicode, source }) {
  92. const lines = [
  93. '/* 由 scripts/merge-iconfont-from-cdn.mjs 生成;双字体与历史双 CDN 叠加顺序一致(388 覆盖同名类) */',
  94. '@font-face {',
  95. ' font-family: "iconfont-2298093";',
  96. " src: url('./font-2298093.woff2') format('woff2'),",
  97. " url('./font-2298093.ttf') format('truetype');",
  98. '}',
  99. '@font-face {',
  100. ' font-family: "iconfont-3882322";',
  101. " src: url('./font-3882322.woff2') format('woff2'),",
  102. " url('./font-3882322.ttf') format('truetype');",
  103. '}',
  104. '.iconfont {',
  105. ' font-size: 16px;',
  106. ' font-style: normal;',
  107. ' -webkit-font-smoothing: antialiased;',
  108. ' -moz-osx-font-smoothing: grayscale;',
  109. '}',
  110. '',
  111. ];
  112. const sorted = [...unicode.entries()].sort((a, b) => a[0].localeCompare(b[0]));
  113. for (const [cls, hex] of sorted) {
  114. const fam = source.get(cls) === '3882322' ? 'iconfont-3882322' : 'iconfont-2298093';
  115. const short = cls.replace(/^icon-/, '');
  116. lines.push(`.${cls}:before {`);
  117. lines.push(` font-family: "${fam}" !important;`);
  118. lines.push(` content: "\\${hex}";`);
  119. lines.push('}');
  120. lines.push('');
  121. }
  122. return lines.join('\n');
  123. }
  124. function buildJson({ unicode, source }) {
  125. const glyphs = [...unicode.entries()]
  126. .sort((a, b) => a[0].localeCompare(b[0]))
  127. .map(([cls, hex], idx) => {
  128. const font_class = cls.replace(/^icon-/, '');
  129. const dec = parseInt(hex, 16);
  130. return {
  131. icon_id: String(100000 + idx),
  132. name: font_class,
  133. font_class,
  134. unicode: hex,
  135. unicode_decimal: dec,
  136. _source_project: source.get(cls),
  137. };
  138. });
  139. // e-icon-picker / 后端若校验字段,去掉非标准字段
  140. for (const g of glyphs) delete g._source_project;
  141. return {
  142. id: 'merged-2298093-3882322',
  143. name: 'dvadmin3-merged',
  144. font_family: 'iconfont',
  145. css_prefix_text: 'icon-',
  146. description: '合并自 iconfont 项目 2298093 + 3882322,与菜单 icon 字段一致',
  147. glyphs,
  148. };
  149. }
  150. async function main() {
  151. fs.mkdirSync(outDir, { recursive: true });
  152. console.log('Fetching CSS…');
  153. const [css229, css388] = await Promise.all([fetchText(URL_229_CSS), fetchText(URL_388_CSS)]);
  154. const map229 = parseIconRules(css229);
  155. const map388 = parseIconRules(css388);
  156. const merged = mergeMaps(map229, map388);
  157. console.log(`Icons: 2298093=${map229.size}, 3882322=${map388.size}, merged=${merged.unicode.size}`);
  158. console.log('Downloading font files…');
  159. for (const f of FONT_FILES) {
  160. const dest = path.join(outDir, f.name);
  161. await downloadFile(f.url, dest);
  162. console.log(' saved', f.name);
  163. }
  164. const cssOut = buildCss(merged);
  165. const jsonOut = buildJson(merged);
  166. fs.writeFileSync(path.join(outDir, 'iconfont.css'), cssOut, 'utf8');
  167. fs.writeFileSync(path.join(outDir, 'iconfont.json'), JSON.stringify(jsonOut, null, 2), 'utf8');
  168. console.log('Wrote iconfont.css, iconfont.json');
  169. }
  170. main().catch((e) => {
  171. console.error(e);
  172. process.exit(1);
  173. });