Skip to content
File

Blob: scripts/generate-emoji-data.ts

typescript102 lines
1import { mkdir, rm, writeFile } from "node:fs/promises";
2import path from "node:path";
3import { createRequire } from "node:module";
4import { fileURLToPath } from "node:url";
5import { emojis as tiptapEmojis, type EmojiItem } from "@tiptap/extension-emoji";
6 
7const require = createRequire(import.meta.url);
8 
9interface EmojibaseItem {
10 emoji?: string;
11 group?: number;
12}
13 
14const APPLE_ASSET_PREFIX = "https://cdn.jsdelivr.net/npm/emoji-datasource-apple/img/apple/64/";
15const VARIATION_SELECTOR_RE = /[\uFE0E\uFE0F]/gu;
16const __dirname = path.dirname(fileURLToPath(import.meta.url));
17const sharedGeneratedDir = path.resolve(__dirname, "../src/shared/emoji/generated");
18const clientGeneratedDir = path.resolve(__dirname, "../src/client/lib/emoji/generated");
19 
20function normalizeEmoji(emoji: string): string {
21 return emoji.replace(VARIATION_SELECTOR_RE, "");
22}
23 
24function getAppleAssetName(item: EmojiItem): string | null {
25 if (!item.fallbackImage?.startsWith(APPLE_ASSET_PREFIX)) {
26 return null;
27 }
28 
29 return item.fallbackImage.slice(APPLE_ASSET_PREFIX.length);
30}
31 
32function renderModule(header: string, value: unknown): string {
33 return `${header}${JSON.stringify(value, null, 2)} as const;\n`;
34}
35 
36async function main() {
37 const emojibaseData = require("emojibase-data/en/data.json") as EmojibaseItem[];
38 const emojibaseMessages = require("emojibase-data/en/messages.json") as {
39 groups: Record<string, { message?: string }>;
40 };
41 const iconData: Record<string, string> = {};
42 const groupOverrides: Record<string, string> = {};
43 const baseEmojiMap = new Map<string, EmojiItem>();
44 
45 for (const item of tiptapEmojis) {
46 if (!item.emoji) continue;
47 
48 const normalizedEmoji = normalizeEmoji(item.emoji);
49 baseEmojiMap.set(normalizedEmoji, item);
50 
51 const asset = getAppleAssetName(item);
52 if (asset) {
53 iconData[normalizedEmoji] = asset;
54 }
55 }
56 
57 for (const emojibaseItem of emojibaseData) {
58 if (!emojibaseItem.emoji) {
59 continue;
60 }
61 
62 const baseEmoji = normalizeEmoji(emojibaseItem.emoji);
63 const baseItem = baseEmojiMap.get(baseEmoji);
64 if (!baseItem) continue;
65 
66 const groupMessage =
67 typeof emojibaseItem.group === "number"
68 ? (emojibaseMessages.groups[String(emojibaseItem.group)]?.message ?? "")
69 : "";
70 if (!baseItem.group && groupMessage) {
71 groupOverrides[baseEmoji] = groupMessage;
72 }
73 }
74 
75 await mkdir(sharedGeneratedDir, { recursive: true });
76 await mkdir(clientGeneratedDir, { recursive: true });
77 
78 await writeFile(
79 path.join(sharedGeneratedDir, "emoji-icon-data.ts"),
80 renderModule(
81 `// This file is generated by scripts/generate-emoji-data.ts\n\nexport const EMOJI_ICON_DATA = `,
82 iconData,
83 ),
84 );
85 
86 await writeFile(
87 path.join(clientGeneratedDir, "emoji-group-overrides.ts"),
88 renderModule(
89 `// This file is generated by scripts/generate-emoji-data.ts\n\nexport const EMOJI_GROUP_OVERRIDES = `,
90 groupOverrides,
91 ),
92 );
93 
94 // Older versions of this script wrote both maps under the client directory.
95 // Clean up the stale icon-data file if a previous checkout still has it so
96 // it does not shadow the shared module via tsconfig path resolution.
97 await rm(path.join(clientGeneratedDir, "emoji-icon-data.ts"), { force: true });
98 await rm(path.join(clientGeneratedDir, "emoji-tone-variants.ts"), { force: true });
99}
100 
101await main();