Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 1 | #!/usr/bin/env node |
| 2 | // Makes g1t's emoji data from Emojibase (https://emojibase.dev, MIT): | |
| 3 | // | |
| 4 | // npm pack emojibase-data@17.0.0 && tar xzf emojibase-data-17.0.0.tgz | |
| 5 | // node scripts/emoji/generate.mjs ./package | |
| 6 | // | |
| 7 | // It writes two files, both checked in so nothing downloads at build time: | |
| 8 | // | |
| 9 | // - apps/web/app/components/emoji/data.json: every emoji the picker shows, | |
| 10 | // with its name, shortcodes, keywords, group and skin tones. The picker | |
| 11 | // loads it on first open, so it never adds to the site's main bundle. | |
| 12 | // - services/chat/src/standard-emoji.ts: the standard shortcodes, which a | |
| 13 | // workspace's own emoji may not take. | |
| 14 | // | |
| 15 | // Shortcodes are the familiar chat set (iamcal's), then GitHub's for any | |
| 16 | // that set lacks. | |
| 17 | import { readFileSync, writeFileSync } from "node:fs"; | |
| 18 | import { dirname, join, resolve } from "node:path"; | |
| 19 | import { fileURLToPath } from "node:url"; | |
| 20 | ||
| 21 | const source = process.argv[2]; | |
| 22 | if (!source) { | |
| 23 | console.error("Usage: node scripts/emoji/generate.mjs <emojibase-data package folder>"); | |
| 24 | process.exit(1); | |
| 25 | } | |
| 26 | const root = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); | |
| 27 | const read = (path) => JSON.parse(readFileSync(join(source, path), "utf8")); | |
| 28 | ||
| 29 | const version = read("package.json").version; | |
| 30 | const compact = read("en/compact.json"); | |
| 31 | const iamcal = read("en/shortcodes/iamcal.json"); | |
| 32 | const github = read("en/shortcodes/github.json"); | |
| 33 | ||
| 34 | /** The groups the picker shows, in order; Emojibase's 2 (skin tone components) is left out. */ | |
| 35 | const GROUPS = [ | |
| 36 | [0, "Smileys & emotion"], | |
| 37 | [1, "People & body"], | |
| 38 | [3, "Animals & nature"], | |
| 39 | [4, "Food & drink"], | |
| 40 | [5, "Travel & places"], | |
| 41 | [6, "Activities"], | |
| 42 | [7, "Objects"], | |
| 43 | [8, "Symbols"], | |
| 44 | [9, "Flags"], | |
| 45 | ]; | |
| 46 | const groupIndex = new Map(GROUPS.map(([id], index) => [id, index])); | |
| 47 | const TONES = ["light skin tone", "medium-light skin tone", "medium skin tone", "medium-dark skin tone", "dark skin tone"]; | |
| 48 | ||
| 49 | // An emoji shown as one by default needs no variation selector after it | |
| 50 | // ("👍", not "👍" + U+FE0F): one spelling, so reactions compare equal. | |
| 51 | const VS16 = String.fromCodePoint(0xfe0f); | |
| 52 | const plain = (unicode) => | |
| 53 | unicode.endsWith(VS16) && /^\p{Emoji_Presentation}$/u.test(unicode.slice(0, -1)) ? unicode.slice(0, -1) : unicode; | |
| 54 | ||
| 55 | const codesOf = (hexcode) => { | |
| 56 | const list = []; | |
| 57 | for (const set of [iamcal[hexcode], github[hexcode]]) { | |
| 58 | for (const code of [set ?? []].flat()) if (!list.includes(code)) list.push(code); | |
| 59 | } | |
| 60 | return list; | |
| 61 | }; | |
| 62 | ||
| 63 | const entries = compact | |
| 64 | .filter((emoji) => groupIndex.has(emoji.group)) | |
| 65 | .sort((a, b) => a.order - b.order) | |
| 66 | .map((emoji) => { | |
| 67 | const entry = [plain(emoji.unicode), emoji.label, codesOf(emoji.hexcode).join(" "), (emoji.tags ?? []).join(" "), groupIndex.get(emoji.group)]; | |
| 68 | // Only the five single-tone variants: two-person emoji have mixed ones too. | |
| 69 | const skins = TONES.map((tone) => (emoji.skins ?? []).find((skin) => skin.label.endsWith(`: ${tone}`))?.unicode).map((skin) => skin && plain(skin)); | |
| 70 | if (skins.every(Boolean)) entry.push(skins); | |
| 71 | return entry; | |
| 72 | }); | |
| 73 | ||
| 74 | const data = { source: `emojibase-data ${version} (MIT)`, groups: GROUPS.map(([, label]) => label), emoji: entries }; | |
| 75 | writeFileSync(join(root, "apps/web/app/components/emoji/data.json"), JSON.stringify(data)); | |
| 76 | ||
| 77 | const standard = new Set(); | |
| 78 | for (const set of [iamcal, github]) for (const codes of Object.values(set)) for (const code of [codes].flat()) standard.add(code); | |
| 79 | writeFileSync( | |
| 80 | join(root, "services/chat/src/standard-emoji.ts"), | |
| 81 | `/** | |
| 82 | * The standard emoji shortcodes, which a workspace's own emoji may not | |
| 83 | * take. Made by scripts/emoji/generate.mjs from emojibase-data ${version} | |
| 84 | * (MIT); do not edit by hand. | |
| 85 | */ | |
| 86 | export const STANDARD_SHORTCODES: ReadonlySet<string> = new Set( | |
| 87 | ${JSON.stringify([...standard].sort().join(" "))}.split(" "), | |
| 88 | ); | |
| 89 | `, | |
| 90 | ); | |
| 91 | console.log(`${entries.length} emoji, ${standard.size} shortcodes, from emojibase-data ${version}`); |
This file's history is long; its oldest lines are credited to the oldest commit read.