diff --git a/apps/desktop/assets/icons.zip b/apps/desktop/assets/icons.zip new file mode 100644 index 00000000..c7c82a1b Binary files /dev/null and b/apps/desktop/assets/icons.zip differ diff --git a/apps/desktop/electron.vite.config.ts b/apps/desktop/electron.vite.config.ts index b3067211..c679418c 100644 --- a/apps/desktop/electron.vite.config.ts +++ b/apps/desktop/electron.vite.config.ts @@ -14,6 +14,7 @@ export default defineConfig({ { src: 'assets/**/*', dest: 'assets', + overwrite: false, }, ], }), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a0a3ffb7..d48f02a1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -523,25 +523,25 @@ importers: scripts: dependencies: - '@emoji-mart/data': - specifier: ^1.2.1 - version: 1.2.1 archiver: specifier: ^7.0.1 version: 7.0.1 - remixicon: - specifier: ^4.5.0 - version: 4.5.0 - simple-icons: - specifier: ^13.16.0 - version: 13.16.0 + node-fetch: + specifier: ^3.3.2 + version: 3.3.2 ulid: specifier: ^2.3.0 version: 2.3.0 + unzipper: + specifier: ^0.12.3 + version: 0.12.3 devDependencies: '@types/archiver': specifier: ^6.0.3 version: 6.0.3 + '@types/unzipper': + specifier: ^0.10.10 + version: 0.10.10 ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@22.9.0)(typescript@5.6.3) @@ -889,9 +889,6 @@ packages: '@emnapi/runtime@1.3.1': resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==} - '@emoji-mart/data@1.2.1': - resolution: {integrity: sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw==} - '@esbuild/aix-ppc64@0.21.5': resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} engines: {node: '>=12'} @@ -2595,6 +2592,9 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/unzipper@0.10.10': + resolution: {integrity: sha512-jKJdNxhmCHTZsaKW5x0qjn6rB+gHk0w5VFbEKsw84i+RJqXZyfTmGnpjDcKqzMpjz7VVLsUBMtO5T3mVidpt0g==} + '@types/use-sync-external-store@0.0.6': resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} @@ -3276,6 +3276,10 @@ packages: csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + data-view-buffer@1.0.1: resolution: {integrity: sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==} engines: {node: '>= 0.4'} @@ -3435,6 +3439,9 @@ packages: resolution: {integrity: sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg==} engines: {node: '>=10'} + duplexer2@0.1.4: + resolution: {integrity: sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==} + eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -3740,6 +3747,10 @@ packages: picomatch: optional: true + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} @@ -3789,6 +3800,10 @@ packages: resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==} engines: {node: '>= 6'} + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -4575,6 +4590,10 @@ packages: node-addon-api@5.1.0: resolution: {integrity: sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==} + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -4584,10 +4603,17 @@ packages: encoding: optional: true + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + node-gyp-build-optional-packages@5.2.2: resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} hasBin: true + node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + node-releases@2.0.18: resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} @@ -5288,9 +5314,6 @@ packages: resolution: {integrity: sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==} engines: {node: '>= 0.4'} - remixicon@4.5.0: - resolution: {integrity: sha512-IP/wNQGG3JCigaeFF3DERSTqMIZBlNu1yu8clNGB7wFe7ZN/ueKMplFHL5uEbnGpCzqfY6MlxIYn2vRzub+5cQ==} - require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -5441,10 +5464,6 @@ packages: simple-get@4.0.1: resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} - simple-icons@13.16.0: - resolution: {integrity: sha512-aMg1efZ0IvYPKdvqUW0woVGSJwb199y9z7j1+6D5zPMn95eMQN0xzKAHefsqQW2K/5LwfgtFK3Gxn9n1eafX0A==} - engines: {node: '>=0.12.18'} - simple-swizzle@0.2.2: resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} @@ -5868,6 +5887,9 @@ packages: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} + unzipper@0.12.3: + resolution: {integrity: sha512-PZ8hTS+AqcGxsaQntl3IRBw65QrBI6lxzqDEL7IAo/XCEqRTKGfOX56Vea5TH9SZczRVxuzk1re04z/YjuYCJA==} + update-browserslist-db@1.1.1: resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==} hasBin: true @@ -5997,6 +6019,10 @@ packages: w3c-keyname@2.2.8: resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -6880,8 +6906,6 @@ snapshots: tslib: 2.8.1 optional: true - '@emoji-mart/data@1.2.1': {} - '@esbuild/aix-ppc64@0.21.5': optional: true @@ -8539,6 +8563,10 @@ snapshots: '@types/unist@3.0.3': {} + '@types/unzipper@0.10.10': + dependencies: + '@types/node': 22.9.0 + '@types/use-sync-external-store@0.0.6': {} '@types/verror@1.10.10': @@ -9424,6 +9452,8 @@ snapshots: csstype@3.1.3: {} + data-uri-to-buffer@4.0.1: {} + data-view-buffer@1.0.1: dependencies: call-bind: 1.0.7 @@ -9576,6 +9606,10 @@ snapshots: dotenv@9.0.2: {} + duplexer2@0.1.4: + dependencies: + readable-stream: 2.3.8 + eastasianwidth@0.2.0: {} ee-first@1.1.1: {} @@ -10100,6 +10134,11 @@ snapshots: optionalDependencies: picomatch: 4.0.2 + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + file-entry-cache@6.0.1: dependencies: flat-cache: 3.2.0 @@ -10156,6 +10195,10 @@ snapshots: combined-stream: 1.0.8 mime-types: 2.1.35 + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + forwarded@0.2.0: {} fraction.js@4.3.7: {} @@ -10914,17 +10957,27 @@ snapshots: node-addon-api@5.1.0: {} + node-domexception@1.0.0: {} + node-fetch@2.7.0(encoding@0.1.13): dependencies: whatwg-url: 5.0.0 optionalDependencies: encoding: 0.1.13 + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + node-gyp-build-optional-packages@5.2.2: dependencies: detect-libc: 2.0.3 optional: true + node-int64@0.4.0: {} + node-releases@2.0.18: {} nodemon@3.1.7: @@ -11600,8 +11653,6 @@ snapshots: es-errors: 1.3.0 set-function-name: 2.0.2 - remixicon@4.5.0: {} - require-directory@2.1.1: {} resolve-alpn@1.2.1: {} @@ -11815,8 +11866,6 @@ snapshots: once: 1.4.0 simple-concat: 1.0.1 - simple-icons@13.16.0: {} - simple-swizzle@0.2.2: dependencies: is-arrayish: 0.3.2 @@ -12306,6 +12355,14 @@ snapshots: unpipe@1.0.0: {} + unzipper@0.12.3: + dependencies: + bluebird: 3.7.2 + duplexer2: 0.1.4 + fs-extra: 11.2.0 + graceful-fs: 4.2.11 + node-int64: 0.4.0 + update-browserslist-db@1.1.1(browserslist@4.24.2): dependencies: browserslist: 4.24.2 @@ -12434,6 +12491,8 @@ snapshots: w3c-keyname@2.2.8: {} + web-streams-polyfill@3.3.3: {} + webidl-conversions@3.0.1: {} webidl-conversions@4.0.2: {} diff --git a/scripts/package.json b/scripts/package.json index 389a13e6..a64e1a5b 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -9,14 +9,14 @@ }, "devDependencies": { "@types/archiver": "^6.0.3", + "@types/unzipper": "^0.10.10", "ts-node": "^10.9.2", "typescript": "^5.6.3" }, "dependencies": { - "@emoji-mart/data": "^1.2.1", "archiver": "^7.0.1", - "remixicon": "^4.5.0", - "simple-icons": "^13.16.0", - "ulid": "^2.3.0" + "node-fetch": "^3.3.2", + "ulid": "^2.3.0", + "unzipper": "^0.12.3" } } diff --git a/scripts/src/emojis/index.ts b/scripts/src/emojis/index.ts index 971e9eb1..75ddc8e2 100644 --- a/scripts/src/emojis/index.ts +++ b/scripts/src/emojis/index.ts @@ -1,7 +1,7 @@ import fs from 'fs'; -import path from 'path'; import archiver from 'archiver'; -import { EmojiMartData } from '@emoji-mart/data'; +import unzipper from 'unzipper'; +import fetch from 'node-fetch'; import { monotonicFactory } from 'ulid'; const ulid = monotonicFactory(); @@ -10,19 +10,47 @@ type EmojiMartI18n = { categories: Record; }; -const emojiMartDataPath = 'node_modules/@emoji-mart/data/sets/15/twitter.json'; -const emojiMartData = JSON.parse( - fs.readFileSync(emojiMartDataPath, 'utf-8') -) as EmojiMartData; +type EmojiMartEmoji = { + id: string; + name: string; + keywords: string[]; + skins: EmojiMartSkin[]; + version: number; + emoticons?: string[]; +}; -const i18nPath = 'node_modules/@emoji-mart/data/i18n/en.json'; -const i18nData = JSON.parse( - fs.readFileSync(i18nPath, 'utf-8') -) as EmojiMartI18n; +type EmojiMartSkin = { + unified: string; + native: string; +}; -const EMOJIS_DIR_PATH = 'src/emojis/out'; +type EmojiMartCategory = { + id: string; + emojis: string[]; +}; + +type EmojiMartData = { + emojis: Record; + categories: Record; +}; + +const WORK_DIR_PATH = 'src/emojis/temp'; +const EMOJIS_DIR_PATH = `${WORK_DIR_PATH}/emojis`; const EMOJIS_METADATA_FILE_PATH = `${EMOJIS_DIR_PATH}/emojis.json`; -const ZIP_FILE_PATH = `src/emojis/emojis.zip`; +const EMOJIS_ZIP_FILE_PATH = 'src/emojis/emojis.zip'; + +const GITHUB_DOMAIN = 'https://github.com'; + +const EMOJI_MART_REPO = 'missive/emoji-mart'; +const EMOJI_MART_TAG = '5.6.0'; +const EMOJI_MART_DIR_PATH = `${WORK_DIR_PATH}/emoji-mart-${EMOJI_MART_TAG}`; +const EMOJI_MART_I18N_FILE_PATH = `${EMOJI_MART_DIR_PATH}/packages/emoji-mart-data/i18n/en.json`; +const EMOJI_MART_DATA_FILE_PATH = `${EMOJI_MART_DIR_PATH}/packages/emoji-mart-data/sets/15/twitter.json`; + +const TWEEMOJI_REPO = 'jdecked/twemoji'; +const TWEEMOJI_TAG = '15.1.0'; +const TWEEMOJI_DIR_PATH = `${WORK_DIR_PATH}/twemoji-${TWEEMOJI_TAG}`; +const TWEEMOJI_SVG_DIR_PATH = `${TWEEMOJI_DIR_PATH}/assets/svg`; type EmojiMetadata = { categories: EmojiCategory[]; @@ -53,10 +81,61 @@ const generateEmojiId = () => { return ulid().toLowerCase() + 'em'; }; -const getEmojiUrl = (unified: string) => { +const downloadEmojiMartRepo = async () => { + console.log(`Downloading emoji-mart repo`); + const url = `${GITHUB_DOMAIN}/${EMOJI_MART_REPO}/archive/refs/tags/v${EMOJI_MART_TAG}.zip`; + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to download ${url}`); + } + + if (fs.existsSync(EMOJI_MART_DIR_PATH)) { + fs.rmSync(EMOJI_MART_DIR_PATH, { recursive: true }); + } + + await new Promise((resolve, reject) => { + if (response.body == null) { + reject(new Error(`Failed to download ${url}`)); + return; + } + + response.body + .pipe(unzipper.Extract({ path: WORK_DIR_PATH })) + .on('close', resolve) + .on('error', reject); + }); + console.log(`Downloaded emoji-mart repo`); +}; + +const downloadTweemojiRepo = async () => { + console.log(`Downloading twemoji repo`); + const url = `${GITHUB_DOMAIN}/${TWEEMOJI_REPO}/archive/refs/tags/v${TWEEMOJI_TAG}.zip`; + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to download ${url}`); + } + + if (fs.existsSync(TWEEMOJI_DIR_PATH)) { + fs.rmSync(TWEEMOJI_DIR_PATH, { recursive: true }); + } + + await new Promise((resolve, reject) => { + if (response.body == null) { + reject(new Error(`Failed to download ${url}`)); + return; + } + + response.body + .pipe(unzipper.Extract({ path: WORK_DIR_PATH })) + .on('close', resolve) + .on('error', reject); + }); + console.log(`Downloaded twemoji repo`); +}; + +const getEmojiSkinFileName = (unified: string) => { let file = unified; - // Fix for "copyright" and "trademark" emojis if (file.substring(0, 2) == '00') { file = file.substring(2); @@ -77,7 +156,7 @@ const getEmojiUrl = (unified: string) => { } } - return `https://cdn.jsdelivr.net/gh/jdecked/twemoji@15.1.0/assets/svg/${file}.svg`; + return `${file}.svg`; }; const readMetadata = (): EmojiMetadata => { @@ -90,7 +169,8 @@ const readMetadata = (): EmojiMetadata => { ) as EmojiMetadata; }; -const generateMetadata = () => { +const generateEmojisDir = () => { + console.log(`Generating emojis dir`); const existingMetadata: EmojiMetadata = readMetadata(); const result: EmojiMetadata = { @@ -100,6 +180,15 @@ const generateMetadata = () => { const idMap: Record = {}; + const emojiMartData = JSON.parse( + fs.readFileSync(EMOJI_MART_DATA_FILE_PATH, 'utf-8') + ) as EmojiMartData; + + const i18nData = JSON.parse( + fs.readFileSync(EMOJI_MART_I18N_FILE_PATH, 'utf-8') + ) as EmojiMartI18n; + + console.log(`Processing emojis`); for (const emojiMartItem of Object.values(emojiMartData.emojis)) { const existingEmoji = Object.values(existingMetadata.emojis).find( (emoji) => emoji.code === emojiMartItem.id @@ -112,18 +201,33 @@ const generateMetadata = () => { name: emojiMartItem.name, tags: emojiMartItem.keywords, emoticons: emojiMartItem.emoticons, - skins: emojiMartItem.skins.map((skin) => ({ - id: - existingEmoji?.skins.find((s) => s.unified === skin.unified)?.id ?? - generateEmojiId(), - unified: skin.unified, - })), + skins: [], }; + for (const skin of emojiMartItem.skins) { + const existingSkin = existingEmoji?.skins.find( + (s) => s.unified === skin.unified + ); + + const skinId = existingSkin?.id ?? generateEmojiId(); + emoji.skins.push({ + id: skinId, + unified: skin.unified, + }); + + const fileName = getEmojiSkinFileName(skin.unified); + const sourceFilePath = `${TWEEMOJI_SVG_DIR_PATH}/${fileName}`; + const targetFilePath = `${EMOJIS_DIR_PATH}/${fileName}`; + if (!fs.existsSync(targetFilePath)) { + fs.copyFileSync(sourceFilePath, targetFilePath); + } + } + idMap[emoji.code] = emoji.id; result.emojis[emojiId] = emoji; } + console.log(`Processing categories`); for (const emojiMartCategory of Object.values(emojiMartData.categories)) { const i18nCategory = i18nData.categories[emojiMartCategory.id]; @@ -152,41 +256,17 @@ const generateMetadata = () => { } fs.writeFileSync(EMOJIS_METADATA_FILE_PATH, JSON.stringify(result, null, 2)); -}; - -const generateImages = async () => { - const emojis = JSON.parse( - fs.readFileSync(EMOJIS_METADATA_FILE_PATH, 'utf-8') - ) as EmojiMetadata; - - for (const emoji of Object.values(emojis.emojis)) { - for (const skin of emoji.skins) { - const fileName = `${skin.id}.svg`; - const filePath = path.join(EMOJIS_DIR_PATH, fileName); - if (fs.existsSync(filePath)) { - continue; - } - - const url = getEmojiUrl(skin.unified); - console.log(`Downloading ${url} to ${filePath}`); - - const response = await fetch(url); - if (!response.ok) { - throw new Error(`Failed to download ${url}`); - } - - const svg = await response.text(); - fs.writeFileSync(filePath, svg); - - // Rate limit - await new Promise((resolve) => setTimeout(resolve, 10)); - } - } + console.log(`Generated emojis dir`); }; const zipEmojis = async () => { + console.log(`Zipping emojis`); + if (fs.existsSync(EMOJIS_ZIP_FILE_PATH)) { + fs.rmSync(EMOJIS_ZIP_FILE_PATH); + } + await new Promise((resolve, reject) => { - const output = fs.createWriteStream(ZIP_FILE_PATH); + const output = fs.createWriteStream(EMOJIS_ZIP_FILE_PATH); const archive = archiver('zip', { zlib: { level: 9 } }); output.on('close', () => { @@ -200,16 +280,26 @@ const zipEmojis = async () => { archive.directory(EMOJIS_DIR_PATH, false); archive.finalize(); }); + console.log(`Zipped emojis`); }; const generateEmojis = async () => { - if (!fs.existsSync(EMOJIS_DIR_PATH)) { - fs.mkdirSync(EMOJIS_DIR_PATH); + if (!fs.existsSync(WORK_DIR_PATH)) { + fs.mkdirSync(WORK_DIR_PATH); } - generateMetadata(); - await generateImages(); + if (!fs.existsSync(EMOJIS_DIR_PATH)) { + fs.mkdirSync(EMOJIS_DIR_PATH); + + await downloadEmojiMartRepo(); + await downloadTweemojiRepo(); + + generateEmojisDir(); await zipEmojis(); + + console.log(`Cleaning up`); + fs.rmSync(WORK_DIR_PATH, { recursive: true }); + console.log(`All done`); }; generateEmojis(); diff --git a/scripts/src/icons/index.ts b/scripts/src/icons/index.ts index ac70ca3a..913cf277 100644 --- a/scripts/src/icons/index.ts +++ b/scripts/src/icons/index.ts @@ -1,21 +1,72 @@ import fs from 'fs'; -import path from 'path'; import archiver from 'archiver'; +import unzipper from 'unzipper'; +import fetch from 'node-fetch'; import { monotonicFactory } from 'ulid'; -import { getIconsData, getIconSlug } from 'simple-icons/sdk'; const ulid = monotonicFactory(); -const CDN_URL = 'https://cdn.jsdelivr.net/gh'; +type SimpleIconsData = { + icons: SimpleIconItem[]; +}; -const REMIX_ICONS_DIR_PATH = 'node_modules/remixicon/icons'; -const REMIX_ICON_TAGS = `${CDN_URL}/Remix-Design/RemixIcon/tags.json`; +type SimpleIconItem = { + title: string; + slug?: string; +}; -const SIMPLE_ICONS_DIR_PATH = 'node_modules/simple-icons/icons'; +const TITLE_TO_SLUG_REPLACEMENTS = { + '+': 'plus', + '.': 'dot', + '&': 'and', + đ: 'd', + ħ: 'h', + ı: 'i', + ĸ: 'k', + ŀ: 'l', + ł: 'l', + ß: 'ss', + ŧ: 't', +}; -const ICONS_DIR_PATH = 'src/icons/out'; +const TITLE_TO_SLUG_CHARS_REGEX = new RegExp( + `[${Object.keys(TITLE_TO_SLUG_REPLACEMENTS).join('')}]`, + 'g' +); + +const TITLE_TO_SLUG_RANGE_REGEX = /[^a-z\d]/g; + +const simpleIconTitleToSlug = (title: string) => + title + .toLowerCase() + .replaceAll( + TITLE_TO_SLUG_CHARS_REGEX, + (char) => + TITLE_TO_SLUG_REPLACEMENTS[ + char as keyof typeof TITLE_TO_SLUG_REPLACEMENTS + ] + ) + .normalize('NFD') + .replaceAll(TITLE_TO_SLUG_RANGE_REGEX, ''); + +const WORK_DIR_PATH = 'src/icons/temp'; +const ICONS_DIR_PATH = `${WORK_DIR_PATH}/icons`; const ICONS_METADATA_FILE_PATH = `${ICONS_DIR_PATH}/icons.json`; -const ZIP_FILE_PATH = 'src/icons/icons.zip'; +const ICONS_ZIP_FILE_PATH = 'src/icons/icons.zip'; + +const GITHUB_DOMAIN = 'https://github.com'; + +const REMIX_ICON_REPO = 'Remix-Design/RemixIcon'; +const REMIX_ICON_TAG = '4.5.0'; +const REMIX_ICON_DIR_PATH = `${WORK_DIR_PATH}/RemixIcon-${REMIX_ICON_TAG}`; +const REMIX_ICON_TAGS_FILE_PATH = `${REMIX_ICON_DIR_PATH}/tags.json`; +const REMIX_ICON_ICONS_DIR_PATH = `${REMIX_ICON_DIR_PATH}/icons`; + +const SIMPLE_ICONS_REPO = 'simple-icons/simple-icons'; +const SIMPLE_ICONS_TAG = '13.16.0'; +const SIMPLE_ICONS_DIR_PATH = `${WORK_DIR_PATH}/simple-icons-${SIMPLE_ICONS_TAG}`; +const SIMPLE_ICONS_DATA_FILE_PATH = `${SIMPLE_ICONS_DIR_PATH}/_data/simple-icons.json`; +const SIMPLE_ICONS_ICONS_DIR_PATH = `${SIMPLE_ICONS_DIR_PATH}/icons`; type IconMetadata = { categories: IconCategory[]; @@ -39,16 +90,56 @@ const generateIconId = () => { return ulid().toLowerCase() + 'ic'; }; -const fetchRemixIconTags = async (): Promise< - Record> -> => { - const response = await fetch(REMIX_ICON_TAGS); +const downloadRemixIconRepo = async () => { + console.log(`Downloading remix icon repo`); + const url = `${GITHUB_DOMAIN}/${REMIX_ICON_REPO}/archive/refs/tags/v${REMIX_ICON_TAG}.zip`; + const response = await fetch(url); if (!response.ok) { - throw new Error('Failed to fetch remix icon tags'); + throw new Error(`Failed to download ${url}`); } - const json = await response.json(); - return json as Record>; + if (fs.existsSync(REMIX_ICON_DIR_PATH)) { + fs.rmSync(REMIX_ICON_DIR_PATH, { recursive: true }); + } + + await new Promise((resolve, reject) => { + if (response.body == null) { + reject(new Error(`Failed to download ${url}`)); + return; + } + + response.body + .pipe(unzipper.Extract({ path: WORK_DIR_PATH })) + .on('close', resolve) + .on('error', reject); + }); + console.log(`Downloaded remix icon repo`); +}; + +const downloadSimpleIconsRepo = async () => { + console.log(`Downloading simple icons repo`); + const url = `${GITHUB_DOMAIN}/${SIMPLE_ICONS_REPO}/archive/refs/tags/${SIMPLE_ICONS_TAG}.zip`; + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to download ${url}`); + } + + if (fs.existsSync(SIMPLE_ICONS_DIR_PATH)) { + fs.rmSync(SIMPLE_ICONS_DIR_PATH, { recursive: true }); + } + + await new Promise((resolve, reject) => { + if (response.body == null) { + reject(new Error(`Failed to download ${url}`)); + return; + } + + response.body + .pipe(unzipper.Extract({ path: WORK_DIR_PATH })) + .on('close', resolve) + .on('error', reject); + }); + console.log(`Downloaded simple icons repo`); }; const readMetadata = (): IconMetadata => { @@ -61,16 +152,21 @@ const readMetadata = (): IconMetadata => { ) as IconMetadata; }; -const generateMetadata = async () => { +const generateIconsDir = async () => { + console.log(`Generating icons dir`); const existingMetadata = readMetadata(); + const result: IconMetadata = { categories: [], icons: {}, }; - const remixIconTags = await fetchRemixIconTags(); + console.log('Generating remix icons'); + const remixIconTags = JSON.parse( + fs.readFileSync(REMIX_ICON_TAGS_FILE_PATH, 'utf-8') + ) as Record>; - const categoryDirs = fs.readdirSync(REMIX_ICONS_DIR_PATH); + const categoryDirs = fs.readdirSync(REMIX_ICON_ICONS_DIR_PATH); for (const categoryDir of categoryDirs) { const categoryId = categoryDir.toLowerCase().replace(/\s+/g, '-'); const category: IconCategory = { @@ -81,7 +177,9 @@ const generateMetadata = async () => { const categoryTags = remixIconTags[categoryDir] ?? {}; - const iconFiles = fs.readdirSync(`${REMIX_ICONS_DIR_PATH}/${categoryDir}`); + const iconFiles = fs.readdirSync( + `${REMIX_ICON_ICONS_DIR_PATH}/${categoryDir}` + ); for (const iconFile of iconFiles) { const fileName = iconFile.replace('.svg', ''); if (fileName.endsWith('-fill')) { @@ -118,7 +216,7 @@ const generateMetadata = async () => { result.icons[iconId] = icon; category.icons.push(iconId); - const sourceFilePath = `${REMIX_ICONS_DIR_PATH}/${categoryDir}/${iconFile}`; + const sourceFilePath = `${REMIX_ICON_ICONS_DIR_PATH}/${categoryDir}/${iconFile}`; const targetFilePath = `${ICONS_DIR_PATH}/${iconId}.svg`; if (!fs.existsSync(targetFilePath)) { fs.copyFileSync(sourceFilePath, targetFilePath); @@ -134,10 +232,15 @@ const generateMetadata = async () => { icons: [], }; - const simpleIconsData = await getIconsData(); - for (const simpleIcon of simpleIconsData) { + console.log('Generating simple icons'); + const simpleIconsData = JSON.parse( + fs.readFileSync(SIMPLE_ICONS_DATA_FILE_PATH, 'utf-8') + ) as SimpleIconsData; + + for (const simpleIcon of simpleIconsData.icons) { const simpleIconTitle = simpleIcon.title; - const simpleIconSlug = getIconSlug(simpleIcon); + const simpleIconSlug = + simpleIcon.slug ?? simpleIconTitleToSlug(simpleIconTitle); const iconCode = `si-${simpleIconSlug}`; @@ -161,7 +264,7 @@ const generateMetadata = async () => { result.icons[iconId] = icon; logosCategory.icons.push(iconId); - const sourceFilePath = `${SIMPLE_ICONS_DIR_PATH}/${simpleIconSlug}.svg`; + const sourceFilePath = `${SIMPLE_ICONS_ICONS_DIR_PATH}/${simpleIconSlug}.svg`; const targetFilePath = `${ICONS_DIR_PATH}/${iconId}.svg`; if (!fs.existsSync(targetFilePath)) { fs.copyFileSync(sourceFilePath, targetFilePath); @@ -171,11 +274,12 @@ const generateMetadata = async () => { result.categories.push(logosCategory); fs.writeFileSync(ICONS_METADATA_FILE_PATH, JSON.stringify(result, null, 2)); + console.log(`Generated icons dir`); }; const zipIcons = async () => { await new Promise((resolve, reject) => { - const output = fs.createWriteStream(ZIP_FILE_PATH); + const output = fs.createWriteStream(ICONS_ZIP_FILE_PATH); const archive = archiver('zip', { zlib: { level: 9 } }); output.on('close', () => { @@ -189,15 +293,27 @@ const zipIcons = async () => { archive.directory(ICONS_DIR_PATH, false); archive.finalize(); }); + console.log(`Zipped icons`); }; const generateIcons = async () => { + if (!fs.existsSync(WORK_DIR_PATH)) { + fs.mkdirSync(WORK_DIR_PATH); + } + if (!fs.existsSync(ICONS_DIR_PATH)) { fs.mkdirSync(ICONS_DIR_PATH); } - generateMetadata(); + await downloadRemixIconRepo(); + await downloadSimpleIconsRepo(); + + generateIconsDir(); await zipIcons(); + + console.log(`Cleaning up`); + fs.rmSync(WORK_DIR_PATH, { recursive: true }); + console.log(`All done`); }; generateIcons();