mod: adapt Themelings to use hermes-decomp (Rust) instead of hermes-dec (Python), remove colors/icons/canvas/deminify, keep only code tracking

This commit is contained in:
user 2026-07-20 11:59:06 -03:00
parent 81c5563e3d
commit 124e4cb3b1
36 changed files with 98 additions and 1756 deletions

View file

@ -2,5 +2,3 @@
bot_token="Bot ................."
# these are required
code_webhook="https://discord.com/api/webhooks/.../......"
color_webhook="https://discord.com/api/webhooks/.../......"
icons_webhook="https://discord.com/api/webhooks/.../......"

View file

@ -29,9 +29,7 @@ jobs:
ref: data
path: data
- uses: actions/setup-python@v6
with:
python-version: "3.14.1"
- uses: dtolnay/rust-toolchain@stable
- uses: oven-sh/setup-bun@v2
@ -56,8 +54,6 @@ jobs:
if: ${{ steps.check.outputs.version != 'false' }}
env:
code_webhook: ${{ secrets.CODE_WEBHOOK }}
color_webhook: ${{ secrets.COLOR_WEBHOOK }}
icons_webhook: ${{ secrets.ICONS_WEBHOOK }}
bot_token: ${{ secrets.BOT_TOKEN }}
working-directory: main
run: bun run update:prod ${{ github.event.inputs.quiet == 'true' && '--quiet' || '' }}

View file

@ -1,14 +1,11 @@
<div align="center">
<a href="https://discord.gg/ZXWT9yRueq">
<img alt="Discord server" src="https://img.shields.io/discord/1226954624372707348?style=for-the-badge&logo=discord&logoColor=%23fff&labelColor=%231e1e2e&color=%23cdd6f4&cacheSeconds=90">
</a>
<h1>🎨 Themelings</h1>
<h1> Themelings</h1>
</div>
A repo that tracks the [Discord](https://github.com/discord) Android app's code, color and icon changes!
A repo that tracks the [Discord](https://github.com/discord) Android app's code changes!
For a closer look at what's scraped, take a look at the [data](https://github.com/nexpid/Themelings/tree/data) branch
## Credits
- [hermes-dec](https://github.com/P1sec/hermes-dec) — tool for decompiling hermes React Native bytecode
- [hermes-decomp](https://github.com/SymbioticSec/hermes-decomp) — tool for decompiling hermes React Native bytecode

View file

@ -4,5 +4,3 @@ NODE_ENV="test"
bot_token="Bot ................."
# all values below are optional, they are overrides for the base .env file
code_webhook="https://discord.com/api/webhooks/.../......"
color_webhook="https://discord.com/api/webhooks/.../......"
icons_webhook="https://discord.com/api/webhooks/.../......"

View file

@ -7,21 +7,14 @@
"update:commit": "bun run --env-file local.env src/update/index.ts --commit-anyway",
"update:mock": "bun run --env-file local.env src/update/index.ts --mock",
"update:prod": "bun run src/update/index.ts",
"canvas:test": "bun run --watch src/canvas/index.test.ts",
"deminify:test": "bun run src/update/deminify.test.ts",
"colors:test": "bun run src/update/tasks/colors.test.ts",
"icons:test": "bun run src/update/tasks/icons.test.ts",
"format": "biome check --fix ."
},
"dependencies": {
"@napi-rs/canvas": "^0.1.97",
"color": "^5.0.3",
"discord-api-types": "^0.38.46"
},
"devDependencies": {
"@biomejs/biome": "^2.4.11",
"@types/bun": "^1.3.12",
"@types/color": "^4.2.1"
"@types/bun": "^1.3.12"
},
"peerDependencies": {
"typescript": "^6.0.2"

View file

@ -1,97 +0,0 @@
import { join } from "node:path";
import { type Image, loadImage } from "@napi-rs/canvas";
import { type Diff, DiffType } from "../types";
export type Item =
| {
type: "color";
colors: string[][];
}
| {
type: "image";
image: Image;
}
| {
type: "blob";
blob: string;
};
export interface Row {
title?: string;
subtitle?: string;
item: Item;
}
export interface Section {
header: string;
columns: Row[][];
}
function placeholderFile(file: string) {
return file.endsWith(".lottie") ? join(import.meta.dir, "placeholders/lottie.png") : file;
}
const numColumns = 5;
const maxRows = 3;
async function makeSection(diffs: Map<string, Diff>, isFile: boolean, type: DiffType, header: string) {
const filtered = [...diffs.entries()].filter(([, diff]) => diff.type === type);
const section: Section = {
header,
columns: Array.from({ length: numColumns }, () => []),
};
let i = 0;
for (const [name, diff] of filtered) {
const columnInd = i++ % numColumns;
const column = section.columns[columnInd],
nextColumn = section.columns[i % numColumns];
if (!column) throw new Error(`Couldn't find column of index ${columnInd}`);
if (!nextColumn || nextColumn.length >= maxRows) {
column.push({
item: {
type: "blob",
blob: `+${(filtered.length - i + 1).toLocaleString("en-US")} ${header.toLowerCase()}`,
},
});
break;
}
const changed = diff.type === DiffType.Changed;
const item: Item = isFile
? { type: "image", image: await loadImage(placeholderFile(diff.source)) }
: { type: "color", colors: [changed && diff.oldSource.split(","), diff.source.split(",")].filter((x) => !!x) };
column.push(
...[
changed &&
isFile && {
subtitle: name,
item: {
type: "image",
image: await loadImage(placeholderFile(diff.oldSource)),
} as Item,
},
{
title: name,
subtitle: diff.type === DiffType.Renamed ? diff.oldName : undefined,
item,
},
].filter((x) => !!x),
);
}
section.columns = section.columns.filter((x) => x.length);
return section;
}
export async function makeSections(diffs: Map<string, Diff>, isFile = false) {
return [
await makeSection(diffs, isFile, DiffType.Added, "Added"),
await makeSection(diffs, isFile, DiffType.Changed, "Changed"),
await makeSection(diffs, isFile, DiffType.Renamed, "Renamed"),
await makeSection(diffs, isFile, DiffType.Removed, "Removed"),
].filter((x) => x.columns[0]);
}

View file

@ -1,12 +0,0 @@
import mock from "../mock";
import { drawSections } from ".";
import { makeSections } from "./factory";
await Bun.write(
"temp/files.png",
new Uint8Array(drawSections(await makeSections(mock.icons, true)).toBuffer("image/png")),
);
await Bun.write(
"temp/colors.png",
new Uint8Array(drawSections(await makeSections(mock.semantic)).toBuffer("image/png")),
);

View file

@ -1,232 +0,0 @@
import { readdir } from "node:fs/promises";
import { join } from "node:path";
import { createCanvas, GlobalFonts } from "@napi-rs/canvas";
import { file } from "bun";
import type { Row, Section } from "./factory";
import { measureText, transformSvg, wrapText } from "./utils";
async function bulkRegister(family: string, path: string) {
for (const item of await readdir(path)) GlobalFonts.registerFromPath(join(path, item), family);
}
await bulkRegister("GG Sans", join(import.meta.dir, "fonts/ggsans"));
await bulkRegister("GG Mono", join(import.meta.dir, "fonts/ggmono"));
function calcColumnWidth(column: { textWidth: number; itemWidth: number }[]) {
return Math.max(0, ...column.map(({ textWidth, itemWidth }) => Math.max(0, textWidth, itemWidth)));
}
function calcColumnHeight(column: Row[]) {
const texts = column
.map((row) => Number(!!row.title) + Number(!!row.subtitle) + Number(row.item.type === "blob"))
.reduce((a, b) => a + b, 0);
const items = texts + column.length - 1;
return texts * textSizes.title + column.length * itemSize + items * layout.card.gapY;
}
function calcSectionHeight(columns: Row[][]) {
return Math.max(0, ...columns.map((column) => calcColumnHeight(column)));
}
const layout = {
background: {
padding: 24,
gap: 18,
},
section: {
gap: 8,
},
card: {
radius: 24,
padding: 12,
gapX: 16,
gapY: 8,
},
blob: {
radius: 24 - 12,
},
color: {
radius: 24,
},
};
const textSizes = {
header: 24,
title: 18,
blob: 24,
};
const colors = {
background: "#1c1d23", // BACKGROUND_BASE_LOWEST
section: "#26272f", // CARD_BACKGROUND_DEFAULT
blob: "#6c6f7c1f", // BACKGROUND_MOD_MUTED
textNormal: "#c7c8ce", // TEXT_NORMAL
textMuted: "#818491", // TEXT_MUTED
watermark: "#2f3035", // TEXT_OVERLAY_DARK
};
const itemSize = 128;
const blobSize = itemSize + layout.card.gapY + textSizes.title;
const watermark = await transformSvg(await file(join(import.meta.dir, "placeholders/watermark.svg")).text(), {
color: colors.watermark,
height: layout.background.padding,
});
const mctx = createCanvas(1, 1).getContext("2d");
export function drawSections(sections: Section[]) {
const headerFont = `600 ${textSizes.header}px GG Sans`;
const titleFont = `400 ${textSizes.title}px GG Mono`;
const sects = sections.map(({ header, columns }) => ({
headerWidth: measureText(mctx, headerFont, header).width,
header,
columns: columns.map((column) =>
column.map(({ title, subtitle, item }) => ({
title,
subtitle,
textWidth: Math.max(
0,
title ? measureText(mctx, titleFont, title).width : 0,
subtitle ? measureText(mctx, titleFont, subtitle).width : 0,
),
item,
itemWidth:
item.type === "blob"
? blobSize
: item.type === "image"
? (item.image.width / item.image.height) * itemSize
: itemSize,
})),
),
}));
const columnWidths = Array.from({ length: Math.max(0, ...sects.map(({ columns }) => columns.length)) }, (_, i) =>
Math.max(0, ...sects.map(({ columns }) => (columns[i] ? calcColumnWidth(columns[i]) : 0))),
);
const sectionWidth =
columnWidths.reduce((a, b) => a + b, 0) + layout.card.gapX * (columnWidths.length - 1) + layout.card.padding * 2;
const width = Math.max(0, ...sects.map(({ headerWidth }) => headerWidth), sectionWidth);
const height =
sects
.map(
({ columns }) =>
textSizes.header +
layout.section.gap +
layout.card.padding * 2 +
calcSectionHeight(columns) +
layout.background.gap,
)
.reduce((a, b) => a + b, 0) - layout.background.gap;
const canvas = createCanvas(layout.background.padding * 2 + width, layout.background.padding * 2 + height);
const ctx = canvas.getContext("2d");
ctx.textBaseline = "top";
ctx.fillStyle = colors.background;
ctx.fillRect(0, 0, canvas.width, canvas.height);
let x = layout.background.padding,
y = layout.background.padding;
for (const sect of sects) {
ctx.fillStyle = colors.textNormal;
ctx.font = headerFont;
ctx.fillText(sect.header, x, y);
y += textSizes.header + layout.section.gap;
const sectionHeight = calcSectionHeight(sect.columns);
ctx.fillStyle = colors.section;
ctx.beginPath();
ctx.roundRect(x, y, sectionWidth, sectionHeight + layout.card.padding * 2, layout.card.radius);
ctx.fill();
y += layout.card.padding;
x += layout.card.padding;
let wx = x;
for (let i = 0; i < sect.columns.length; i++) {
let wy = y;
const column = sect.columns[i];
const columnWidth = columnWidths[i];
if (!columnWidth) throw new Error(`Failed to get columnWidth for ${i}, ${columnWidth}`);
const centerX = wx + columnWidth / 2;
for (const row of column) {
ctx.font = titleFont;
if (row.subtitle) {
ctx.fillStyle = colors.textMuted;
ctx.fillText(row.subtitle, centerX - row.textWidth / 2, wy);
wy += textSizes.title + layout.card.gapY;
}
if (row.title) {
ctx.fillStyle = colors.textNormal;
ctx.fillText(row.title, centerX - row.textWidth / 2, wy);
wy += textSizes.title + layout.card.gapY;
}
const midX = centerX - row.itemWidth / 2;
if (row.item.type === "blob") {
const blobHeight = sectionHeight - wy + y;
ctx.fillStyle = colors.blob;
ctx.beginPath();
ctx.roundRect(wx, wy, columnWidth, blobHeight, layout.blob.radius);
ctx.fill();
ctx.font = `600 ${textSizes.blob}px GG Sans`;
ctx.fillStyle = colors.textNormal;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
const wrapped = wrapText(ctx, row.item.blob, columnWidth);
const height = textSizes.blob / 2;
for (let i = 0; i < wrapped.length; i++)
ctx.fillText(
wrapped[i],
wx + columnWidth / 2,
wy + blobHeight / 2 - height * (wrapped.length - 1) + textSizes.blob * i,
);
ctx.textAlign = "left";
ctx.textBaseline = "top";
} else if (row.item.type === "image") {
ctx.drawImage(row.item.image, midX, wy, row.itemWidth, itemSize);
wy += itemSize + layout.card.gapY;
} else if (row.item.type === "color") {
for (let i = 0; i < row.item.colors.length; i++) {
const colors = row.item.colors[i];
for (let j = 0; j < colors.length; j++) {
const color = colors[j];
ctx.save();
ctx.beginPath();
ctx.rect(
Math.floor(midX + (itemSize / colors.length) * j),
Math.floor(wy + (itemSize / row.item.colors.length) * i),
Math.ceil(itemSize / colors.length),
Math.ceil(itemSize / row.item.colors.length),
);
ctx.clip();
ctx.beginPath();
ctx.fillStyle = color;
ctx.roundRect(midX, wy, itemSize, itemSize, layout.color.radius);
ctx.fill();
ctx.restore();
}
}
wy += itemSize + layout.card.gapY;
}
}
wx += columnWidth + layout.card.gapX;
}
y += sectionHeight;
x -= layout.card.padding;
y += layout.card.padding;
y += layout.background.gap;
}
// watermark
ctx.drawImage(watermark, canvas.width - watermark.width - 1, 1);
return canvas;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 888 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3 KiB

View file

@ -1,19 +0,0 @@
<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_502_7157)">
<path d="M40.5906 40.9053H37.4034C35.5643 40.8801 33.8102 40.1266 32.5258 38.8101C31.2415 37.4935 30.5316 35.7213 30.552 33.8821V30.5995C30.547 30.2361 30.6136 29.8753 30.748 29.5376C30.8824 29.1999 31.082 28.892 31.3355 28.6315C31.5889 28.371 31.8912 28.163 32.225 28.0193C32.5588 27.8756 32.9177 27.7991 33.2811 27.7941H48.3199V23.8817H21.6013C19.1775 29.3399 16.6965 35.0081 15.8759 37.241C15.3658 38.5335 14.4808 39.6439 13.3347 40.4295C12.1886 41.2151 10.8338 41.64 9.44434 41.6496V43.997C11.5705 46.0661 14.4168 47.2293 17.3836 47.2414H41.3921C43.2279 47.2364 44.9871 46.5049 46.2852 45.2067C47.5833 43.9086 48.3148 42.1495 48.3199 40.3136V39.8174C47.2437 40.5272 45.9829 40.9055 44.6938 40.9053H40.5906ZM24.5022 35.5616C24.0309 35.5613 23.5705 35.4204 23.1798 35.1568C22.7892 34.8932 22.4861 34.519 22.3095 34.0822C22.1328 33.6453 22.0906 33.1656 22.1883 32.7046C22.286 32.2436 22.519 31.8222 22.8577 31.4945C23.1963 31.1668 23.6251 30.9476 24.0891 30.8651C24.553 30.7826 25.0311 30.8405 25.4619 31.0314C25.8928 31.2222 26.2569 31.5374 26.5075 31.9365C26.7581 32.3356 26.8839 32.8004 26.8687 33.2714C26.8488 33.8857 26.5908 34.4682 26.1491 34.8956C25.7074 35.3231 25.1168 35.5619 24.5022 35.5616Z" fill="#AABFF2"/>
<path d="M24.5022 30.8281C23.8746 30.8281 23.2727 31.0775 22.8289 31.5213C22.3851 31.9651 22.1357 32.567 22.1357 33.1946C22.1357 33.8223 22.3851 34.4242 22.8289 34.868C23.2727 35.3118 23.8746 35.5611 24.5022 35.5611C25.1299 35.5611 25.7318 35.3118 26.1756 34.868C26.6194 34.4242 26.8687 33.8223 26.8687 33.1946C26.8687 32.567 26.6194 31.9651 26.1756 31.5213C25.7318 31.0775 25.1299 30.8281 24.5022 30.8281Z" fill="black"/>
<path d="M48.7969 27.7939H33.2811C32.9176 27.7989 32.5588 27.8754 32.2249 28.0191C31.8911 28.1628 31.5888 28.3709 31.3354 28.6314C31.082 28.8919 30.8823 29.1998 30.7479 29.5375C30.6135 29.8751 30.5469 30.236 30.5519 30.5994V33.882C30.5316 35.7211 31.2414 37.4933 32.5258 38.8099C33.8101 40.1265 35.5642 40.88 37.4033 40.9051H44.6937C45.9828 40.9053 47.2437 40.5271 48.3198 39.8173C48.3874 39.7863 48.4514 39.7479 48.5106 39.7028C49.4379 39.0611 50.1975 38.2062 50.7255 37.2098C51.2536 36.2135 51.5346 35.105 51.5451 33.9774V30.5994C51.5502 29.8637 51.2641 29.1558 50.7493 28.6302C50.2344 28.1046 49.5326 27.804 48.7969 27.7939ZM42.0028 37.8516H38.9301C38.766 37.8727 38.5993 37.8586 38.4411 37.8103C38.2829 37.7619 38.1368 37.6804 38.0126 37.5712C37.8883 37.4619 37.7888 37.3274 37.7206 37.1767C37.6525 37.0259 37.6172 36.8624 37.6172 36.697C37.6172 36.5315 37.6525 36.368 37.7206 36.2172C37.7888 36.0665 37.8883 35.932 38.0126 35.8228C38.1368 35.7135 38.2829 35.632 38.4411 35.5836C38.5993 35.5353 38.766 35.5212 38.9301 35.5423H42.0028C42.1668 35.5212 42.3336 35.5353 42.4918 35.5836C42.65 35.632 42.7961 35.7135 42.9203 35.8228C43.0445 35.932 43.1441 36.0665 43.2122 36.2172C43.2804 36.368 43.3157 36.5315 43.3157 36.697C43.3157 36.8624 43.2804 37.0259 43.2122 37.1767C43.1441 37.3274 43.0445 37.4619 42.9203 37.5712C42.7961 37.6804 42.65 37.7619 42.4918 37.8103C42.3336 37.8586 42.1668 37.8727 42.0028 37.8516ZM47.4419 37.8516H46.2014C46.0373 37.8727 45.8706 37.8586 45.7124 37.8103C45.5542 37.7619 45.4081 37.6804 45.2838 37.5712C45.1596 37.4619 45.0601 37.3274 44.9919 37.1767C44.9237 37.0259 44.8885 36.8624 44.8885 36.697C44.8885 36.5315 44.9237 36.368 44.9919 36.2172C45.0601 36.0665 45.1596 35.932 45.2838 35.8228C45.4081 35.7135 45.5542 35.632 45.7124 35.5836C45.8706 35.5353 46.0373 35.5212 46.2014 35.5423H47.3656C47.6462 35.5785 47.9041 35.7157 48.091 35.9282C48.2779 36.1407 48.381 36.414 48.381 36.697C48.381 36.98 48.2779 37.2533 48.091 37.4658C47.9041 37.6783 47.6462 37.8154 47.3656 37.8516H47.4419Z" fill="#CBDAF7"/>
<path d="M42.0025 35.5426H38.9299C38.7658 35.5215 38.5991 35.5356 38.4409 35.5839C38.2826 35.6323 38.1365 35.7138 38.0123 35.823C37.8881 35.9323 37.7886 36.0668 37.7204 36.2175C37.6522 36.3683 37.6169 36.5318 37.6169 36.6972C37.6169 36.8627 37.6522 37.0262 37.7204 37.177C37.7886 37.3277 37.8881 37.4622 38.0123 37.5715C38.1365 37.6807 38.2826 37.7622 38.4409 37.8106C38.5991 37.8589 38.7658 37.873 38.9299 37.8519H42.0025C42.1666 37.873 42.3333 37.8589 42.4915 37.8106C42.6497 37.7622 42.7958 37.6807 42.9201 37.5715C43.0443 37.4622 43.1438 37.3277 43.212 37.177C43.2802 37.0262 43.3154 36.8627 43.3154 36.6972C43.3154 36.5318 43.2802 36.3683 43.212 36.2175C43.1438 36.0668 43.0443 35.9323 42.9201 35.823C42.7958 35.7138 42.6497 35.6323 42.4915 35.5839C42.3333 35.5356 42.1666 35.5215 42.0025 35.5426ZM47.3653 35.5426H46.2011C46.0371 35.5215 45.8703 35.5356 45.7121 35.5839C45.5539 35.6323 45.4078 35.7138 45.2836 35.823C45.1594 35.9323 45.0598 36.0668 44.9917 36.2175C44.9235 36.3683 44.8882 36.5318 44.8882 36.6972C44.8882 36.8627 44.9235 37.0262 44.9917 37.177C45.0598 37.3277 45.1594 37.4622 45.2836 37.5715C45.4078 37.6807 45.5539 37.7622 45.7121 37.8106C45.8703 37.8589 46.0371 37.873 46.2011 37.8519H47.3653C47.646 37.8157 47.9039 37.6786 48.0908 37.4661C48.2776 37.2535 48.3807 36.9802 48.3807 36.6972C48.3807 36.4143 48.2776 36.141 48.0908 35.9284C47.9039 35.7159 47.646 35.5788 47.3653 35.5426Z" fill="#AABFF2"/>
<path d="M4.27186 39.703C3.98205 39.5692 3.70151 39.4162 3.43213 39.245V50.276L6.39026 52.3562L9.34839 50.276V41.5924C7.47436 41.65 5.65222 40.9718 4.27186 39.703Z" fill="#EF8843"/>
<path d="M13.9298 35.9623C13.567 36.8635 13.0245 37.6814 12.3354 38.3663C11.6463 39.0511 10.825 39.5884 9.92146 39.9455C9.01795 40.3026 8.05119 40.4721 7.08009 40.4435C6.10899 40.4149 5.15388 40.1889 4.27295 39.7792C5.65032 41.0549 7.4728 41.7401 9.34948 41.6877C10.7389 41.6781 12.0938 41.2532 13.2399 40.4676C14.386 39.682 15.2709 38.5716 15.781 37.2791C16.6398 35.1034 19.1209 29.4353 21.5064 23.9198H19.1018C19.1018 23.9198 15.781 31.2483 13.9298 35.9623Z" fill="#D0D9EA"/>
<path d="M27.6132 10.3699L29.2354 10.5035L27.6132 10.3699ZM21.6206 10.5798C22.8051 10.4405 23.9968 10.3704 25.1895 10.3699C23.9968 10.3704 22.8051 10.4405 21.6206 10.5798ZM41.6787 18.5763C43.9769 18.6256 46.2678 18.8553 48.5301 19.2633C47.0224 17.4885 41.2397 11.6295 29.2354 10.5798C37.919 12.66 41.1252 17.603 41.6787 18.5763ZM13.9295 35.9625C15.7807 31.2485 19.1014 23.8819 19.1014 23.8819C19.1014 23.8819 24.9795 19.4351 37.0602 18.6717C32.8615 12.9463 25.9338 11.0378 21.6397 10.5798C6.90632 12.1638 0.0167358 23.3475 0.0167358 32.2791C-0.0724653 33.6375 0.196029 34.9956 0.795442 36.2179C1.39486 37.4401 2.30424 38.4839 3.4329 39.245C4.31641 39.8074 5.31205 40.1701 6.35026 40.3078C7.38847 40.4455 8.44423 40.3549 9.4438 40.0423C10.4434 39.7297 11.3626 39.2027 12.1374 38.498C12.9122 37.7933 13.5238 36.928 13.9295 35.9625ZM1.48626 30.5424C1.55559 29.1182 2.17949 27.7777 3.2245 26.8076C4.26951 25.8375 5.65266 25.3148 7.07808 25.3514C8.47156 25.6583 9.68862 26.5006 10.4669 27.6965C11.2451 28.8925 11.5223 30.3464 11.2385 31.7448C11.179 33.1555 10.5714 34.4873 9.54484 35.4568C8.51831 36.4263 7.15397 36.9569 5.74215 36.9358C5.04224 36.7968 4.37667 36.5209 3.78364 36.124C3.1906 35.7271 2.68178 35.217 2.28636 34.623C1.89095 34.029 1.61672 33.3627 1.47942 32.6625C1.34211 31.9622 1.34444 31.2418 1.48626 30.5424Z" fill="#EBEFF7"/>
<path d="M37.0222 18.6716C37.9001 18.6716 38.7971 18.6716 39.7322 18.5571H41.6407C41.0873 17.5838 37.8238 12.6408 29.1975 10.5606L27.6134 10.3698H25.1897C23.997 10.3703 22.8054 10.4404 21.6208 10.5797C25.8958 11.095 32.8236 12.8508 37.0222 18.6716Z" fill="#FC964B"/>
<path d="M39.732 18.5568C38.7969 18.5568 37.8236 18.5568 37.022 18.6713C24.9414 19.4347 19.1015 23.8814 19.1015 23.8814H60C56.6153 21.4338 52.6848 19.8485 48.5492 19.2629C46.2869 18.8548 43.996 18.6251 41.6978 18.5758L39.732 18.5568ZM5.74218 36.9353C7.15401 36.9565 8.51835 36.4258 9.54488 35.4563C10.5714 34.4868 11.1791 33.155 11.2386 31.7443C11.5223 30.3459 11.2452 28.892 10.4669 27.6961C9.68865 26.5001 8.47159 25.6579 7.07811 25.3509C5.65269 25.3143 4.26954 25.837 3.22453 26.8071C2.17952 27.7772 1.55562 29.1178 1.48629 30.542C1.34447 31.2413 1.34215 31.9618 1.47945 32.662C1.61675 33.3623 1.89098 34.0285 2.2864 34.6226C2.68182 35.2166 3.19064 35.7267 3.78367 36.1235C4.37671 36.5204 5.04227 36.7963 5.74218 36.9353ZM2.38328 30.7519C2.40525 30.2738 2.52164 29.8049 2.72573 29.372C2.92982 28.9392 3.21757 28.551 3.5724 28.2299C3.92722 27.9087 4.3421 27.661 4.79311 27.501C5.24413 27.341 5.72235 27.2719 6.20022 27.2976C7.10905 27.5245 7.89481 28.0943 8.39288 28.8876C8.89095 29.681 9.06267 30.6363 8.87208 31.5534C8.82746 32.5001 8.41204 33.3912 7.71563 34.0341C7.01922 34.6769 6.09779 35.0198 5.15056 34.9887C4.22557 34.787 3.41698 34.2296 2.89927 33.437C2.38156 32.6444 2.19623 31.68 2.38328 30.7519Z" fill="#647096"/>
<path d="M5.1509 34.9891C6.09813 35.0203 7.01956 34.6774 7.71597 34.0345C8.41237 33.3917 8.82779 32.5006 8.87241 31.5539C9.05991 30.6235 8.87512 29.6566 8.35774 28.8609C7.84035 28.0652 7.03161 27.5041 6.10513 27.298C5.15609 27.272 4.23469 27.6192 3.53884 28.2651C2.843 28.9109 2.42823 29.804 2.38361 30.7523C2.19657 31.6804 2.38189 32.6448 2.89961 33.4374C3.41732 34.2301 4.22591 34.7874 5.1509 34.9891Z" fill="#FC964B"/>
</g>
<defs>
<clipPath id="clip0_502_7157">
<rect width="60" height="60" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 687 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

View file

@ -1,7 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="257" height="48" fill="none">
<path fill="#fff" d="M23.8 48q-4.9 0-9.2-1.9A25 25 0 0 1 1.9 33.3Q-.1 29 0 24A23.5 23.5 0 0 1 14.8 1.9q4.4-2 9.5-1.9a25 25 0 0 1 16.6 6.2q3.3 3 5.2 6.8 2 4 1.9 8.6 0 7-4.3 10.7a15 15 0 0 1-10.3 3.6H30q-.7 0-1.2.6t-.5 1.1.6 1.6q.7.8.7 2.4 0 2-1.8 4.3-2 2.1-4 2.1m-13-21.8q1.3 0 2.4-1t1-2.5-1-2.5-2.5-1-2.4 1-1 2.5q0 1.5 1 2.5t2.4 1m7-9.6q1.5 0 2.5-1 1-1.2 1-2.5 0-1.5-1-2.5-1.1-1-2.5-1t-2.4 1-1 2.5 1 2.4 2.4 1m12 0q1.4 0 2.4-1 1-1.2 1-2.5 0-1.5-1-2.5t-2.4-1-2.4 1-1 2.5 1 2.4q.9 1 2.4 1M37 26q1.5 0 2.5-1t1-2.5-1-2.5-2.5-1q-1.4 0-2.4 1t-1 2.5 1 2.5 2.4 1m50.5 9V12h6.4v8.8h.1q.7-1.3 2.2-2.2 1.5-.7 3.2-.7 2.5 0 4.2 1.6a6 6 0 0 1 1.6 4.3V35h-6.6v-9.5q0-1.2-.6-1.8a2 2 0 0 0-1.6-.6q-1 0-1.7.7-.6.8-.6 2V35zm91 0V12h6.6v23zm10.1 0V18.4h6.6V35zm3.3-18.5a3.4 3.4 0 0 1-3.5-3.5q0-1.5 1-2.5t2.5-1a3.4 3.4 0 0 1 3.6 3.5q0 1.5-1 2.5t-2.6 1m56.2 19.1q-2.7 0-4.6-.8-2-.7-3.8-2.5l3.4-3.3q1.2 1 2.4 1.6 1.2.4 2.8.4.9 0 1.3-.2t.5-.7-.5-.7l-3-.8q-3-.8-4.6-2-1.5-1.4-1.4-3.3 0-2.5 2-4 2.2-1.5 5.5-1.5 2.2 0 4 .7 2 .5 3.7 2l-3.3 3.3q-1-.8-2.1-1.1-1-.4-2.3-.4-.8 0-1.2.2t-.4.6.5.6l3 .8q3 .8 4.6 2 1.5 1.3 1.5 3.4 0 2.5-2.2 4a10 10 0 0 1-5.8 1.7M79.2 35h-6.7V18.3h-6v-6.2h18.7v6.2h-6z"/>
<path fill="#fff" fill-rule="evenodd" d="M228 42.4q-3 0-5-1.2-2.1-1-3.6-3.5l4.6-2.2q.7 1.1 1.6 1.7a4 4 0 0 0 2.2.5q1.7 0 2.6-1t.9-2.8v-1h-.1q-.9.9-2.2 1.4a7 7 0 0 1-2.7.6 7 7 0 0 1-5.5-2.3 9 9 0 0 1-2-6.1q0-3.8 2.1-6.2a7 7 0 0 1 5.7-2.4q1.5 0 2.7.5t2 1.5v-1.5h6.3v14q0 5-2.4 7.5-2.4 2.4-7.1 2.5m2.4-13.7q-.9 1-2.2 1a3 3 0 0 1-2.2-1q-.9-.8-.9-2.3t.9-2.4 2.2-.9 2.2 1q.8.8.8 2.3t-.8 2.3m-114 6.9q-4.2 0-6.6-2.5a9 9 0 0 1-2.5-6.3q0-3.9 2.5-6.4a9 9 0 0 1 6.6-2.5q3.7 0 6.1 2.3a8 8 0 0 1 2.5 7.1l-.2 1h-11.1q.2.8.7 1.5 1 1.2 2.8 1.2 1 0 2-.5a6 6 0 0 0 1.9-1.4l3.4 3q-1.6 1.8-3.5 2.6-2 .9-4.6.9m1.9-12.2q.8.6.9 1.7h-5.6q.2-.9.7-1.4.8-1 2.1-1 1.2 0 1.9.7" clip-rule="evenodd"/>
<path fill="#fff" d="M127.6 35V18.4h6.3v2.4q.7-1.4 2.2-2.1 1.4-.9 3.2-.9 1.7 0 3 .8t2 2.1q1-1.5 2.5-2.2 1.5-.6 3.1-.7 3 0 4.6 1.8 1.6 1.7 1.6 4.7V35h-6.6v-9.3q0-1.3-.5-2t-1.6-.6a2 2 0 0 0-1.6.8 3 3 0 0 0-.7 2V35h-6.6v-9.3q0-1.3-.5-2t-1.6-.6a2 2 0 0 0-1.6.8 3 3 0 0 0-.7 2V35z"/>
<path fill="#fff" fill-rule="evenodd" d="M167.4 35.6q-4.2 0-6.6-2.5a9 9 0 0 1-2.5-6.3q0-3.9 2.6-6.4a9 9 0 0 1 6.6-2.5q3.6 0 6 2.3a8 8 0 0 1 2.5 6.3l-.1 1.7h-11.2q.3 1 .8 1.6.9 1.2 2.7 1.2 1.2 0 2.1-.5a6 6 0 0 0 1.8-1.4l3.4 3q-1.5 1.8-3.5 2.6t-4.6.9m2-12.2q.6.6.8 1.7h-5.6l.7-1.4q.9-1 2.2-1 1 0 1.8.7" clip-rule="evenodd"/>
<path fill="#fff" d="M198.8 35V18.4h6.3v2.4h.1q.9-1.5 2.4-2.2t3-.8q2.7 0 4.3 1.7a6 6 0 0 1 1.6 4.4V35h-6.6v-9.5q0-1-.6-1.7t-1.6-.7-1.7.7q-.6.8-.6 2V35z"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

View file

@ -1,45 +0,0 @@
import { type CanvasRenderingContext2D, loadImage } from "@napi-rs/canvas";
export function measureText(ctx: CanvasRenderingContext2D, font: string, text: string) {
ctx.font = font;
return ctx.measureText(text);
}
export function wrapText(ctx: CanvasRenderingContext2D, text: string, maxWidth: number) {
// https://stackoverflow.com/a/16599668
const words = text.split(" ");
let lines: string[] = [],
line = words[0];
for (let i = 1; i < words.length; i++) {
const word = words[i];
const newText = `${line} ${word}`;
const width = ctx.measureText(newText).width;
if (width < maxWidth) line = newText;
else {
lines.push(line);
line = word;
}
}
lines.push(line);
return lines;
}
export async function transformSvg(svg: string, config: { color?: string; height?: number; width?: number }) {
const text = config.color ? svg.replace(/fill="#[a-z0-9]{3,6}"/gi, `fill="${config.color}"`) : svg;
const image = await loadImage(Buffer.from(text));
if (config.width && config.height) {
image.width = config.width;
image.height = config.height;
} else if (config.width) {
image.height = (image.height / image.width) * config.width;
image.width = config.width;
} else if (config.height) {
image.width = (image.width / image.height) * config.height;
image.height = config.height;
}
return image;
}

View file

@ -1,125 +1,6 @@
import Color from "color";
import { type CodeDiff, type Diff, DiffType } from "./types";
import { type CodeDiff, DiffType } from "./types";
const randomColor = () =>
`#${Math.floor(Math.random() * 0xffffff)
.toString(16)
.padStart(6, "0")}`;
const randomColors = () => {
const base = Color(randomColor());
return [
base.lighten(0.6).hex(),
base.desaturate(0.3).darken(0.5).hex(),
base.darken(0.6).hex(),
base.darken(0.8).hex(),
].join(",");
};
const semNum = 20;
// mock data for "bun run update:mock" and "bun run canvas:test"
export default {
semantic: new Map<string, Diff>([
...Array.from({ length: semNum }, (_, i) => [
`MOCK_SEMANTIC_SAMPLE_${i + 1}`,
{
type: DiffType.Added,
source: randomColors(),
label: "☀️ #aaaaaa, 🌙 #bbbbbb",
} as Diff,
]),
...Array.from({ length: semNum }, (_, i) => [
`MOCK_SEMANTIC_SAMPLE_${i + 1 + semNum}`,
{
type: DiffType.Changed,
source: randomColors(),
label: "☀️ #aaaaaa, 🌙 #bbbbbb",
oldSource: randomColors(),
oldLabel: "☀️ #888888, 🌙 #999999",
} as Diff,
]),
...Array.from({ length: semNum }, (_, i) => [
`MOCK_SEMANTIC_SAMPLE_${i + 1 + semNum * 2}`,
{
type: DiffType.Renamed,
oldName: `mocked_samentic_sample_${i + 1 + semNum * 2}`,
source: randomColors(),
} as Diff,
]),
...Array.from({ length: semNum }, (_, i) => [
`MOCK__SEMANTIC__REMOVED__SAMPLE__${i + 1 + semNum * 3}`,
{
type: DiffType.Removed,
source: randomColors(),
label: "☀️ #aaaaaa, 🌙 #bbbbbb",
} as Diff,
]),
] as [string, Diff][]),
semanticBig: new Map<string, Diff>(
Array.from({ length: 35 }, (_, i) => [
`SOME_EXAMPLE_SEMANTIC_${i}`,
{
type: DiffType.Changed,
source: randomColors(),
label: "☀️ #aaaaaa, 🌙 #bbbbbb",
oldSource: randomColors(),
oldLabel: "☀️ #888888, 🌙 #999999",
} as Diff,
]),
),
raw: new Map(),
icons: new Map([
[
"FirstMockIcon",
{
type: DiffType.Added,
source: "src/canvas/mock/FirstMockIcon.png",
label: "c6f6357f",
} as Diff,
],
[
"SvgMockIcon",
{
type: DiffType.Added,
source: "src/canvas/mock/SvgMockIcon.svg",
label: "5d534560",
} as Diff,
],
[
"LottieMockIcon",
{
type: DiffType.Added,
source: "src/canvas/mock/LottieMockIcon.lottie",
label: "340efc52",
} as Diff,
],
[
"SecondMockIcon",
{
type: DiffType.Changed,
source: "src/canvas/mock/SecondMockIconNew.png",
label: "76ffeecd",
oldSource: "src/canvas/mock/SecondMockIcon.png",
oldLabel: "6e235744",
} as Diff,
],
[
"ThirdMockIcon",
{
type: DiffType.Renamed,
oldName: "thirdmkocicon",
source: "src/canvas/mock/ThirdMockIcon.png",
} as Diff,
],
[
"FourthMockIcon",
{
type: DiffType.Removed,
source: "src/canvas/mock/FourthMockIcon.png",
label: "cf4e1340",
} as Diff,
],
]),
code: new Map([
[
"app/foomodule.tsx",

View file

@ -5,25 +5,6 @@ export enum DiffType {
Removed,
}
export type Diff =
| {
type: DiffType.Added | DiffType.Removed;
source: string;
label?: string;
}
| {
type: DiffType.Changed;
source: string;
label?: string;
oldSource: string;
oldLabel?: string;
}
| {
type: DiffType.Renamed;
oldName: string;
source: string;
};
export type CodeDiff =
| {
type: DiffType.Added | DiffType.Removed;
@ -35,22 +16,6 @@ export type CodeDiff =
size: number;
};
export type Differs = Record<"semantic" | "raw" | "icons", Map<string, Diff> | undefined> & {
export type Differs = {
code: Map<string, CodeDiff> | undefined;
};
export type RawColors = Record<string, string>;
export type Icons = Record<
string,
{
file: string;
hash: string;
scales: number[];
width: number | null;
height: number | null;
}
>;
export type Semantic = Record<string, [string, { raw: string; opacity: number }]>;
export type SemanticColors = Record<string, Semantic>;

View file

@ -1,252 +0,0 @@
import { deminify } from "./deminify";
const SNIPPET = `r7 = function(a0, a1, a2, a3, a4, a5, a6) { // Environment: r3
r4 = a1;
r2 = a5;
r5 = a6;
var _closure1_slot0 = r4;
r0 = a2;
var _closure1_slot1 = r0;
var _closure1_slot2 = r5;
r0 = global;
r7 = r0.Object;
r6 = r7.defineProperty;
r1 = {};
r0 = true;
r1['value'] = r0;
r0 = '__esModule';
r0 = r6.bind(r7)(r2, r0, r1);
r0 = 0;
r6 = r5[r0];
r1 = a3;
r0 = undefined;
r1 = r1.bind(r0)(r6);
r1 = 1;
r1 = r5[r1];
r1 = r4.bind(r0)(r1);
r1 = r1.jsx;
var _closure1_slot3 = r1;
r1 = {};
r6 = function(a0) { // Original name: openLazy, environment: r3
_fun32591: for(var _fun32591_ip = 0; ; ) switch(_fun32591_ip) {
case 0:
r3 = a0;
r2 = r3.importer;
r4 = r3.hideActionSheet;
r1 = undefined;
if(!(r4 === r1)) { _fun32591_ip = 25; continue _fun32591 }
case 23:
r4 = true;
case 25:
var _closure2_slot0 = r4;
r3 = r3.isDismissable;
if(!(r3 === r1)) { _fun32591_ip = 41; continue _fun32591 }
case 39:
r3 = false;
case 41:
var _closure2_slot1 = r3;
r2 = r2.bind(r1)();
r1 = r2.then;
r0 = function(a0) { // Environment: r0
_fun32592: for(var _fun32592_ip = 0; ; ) switch(_fun32592_ip) {
case 0:
r0 = _closure2_slot0;
if(!r0) { _fun32592_ip = 45; continue _fun32592 }
case 10:
r2 = _closure1_slot1;
r1 = _closure1_slot2;
r0 = 2;
r1 = r1[r0];
r0 = undefined;
r1 = r2.bind(r0)(r1);
r0 = r1.hideActionSheet;
r0 = r0.bind(r1)();
case 45:
r2 = _closure1_slot1;
r1 = _closure1_slot2;
r0 = 3;
r1 = r1[r0];
r0 = undefined;
r3 = r2.bind(r0)(r1);
r2 = r3.dispatch;
r1 = {};
r5 = 'ALERT_OPEN';
r1['type'] = r5;
r5 = a0;
r1['alert'] = r5;
r4 = _closure2_slot1;
r1['isDismissable'] = r4;
r1 = r2.bind(r3)(r1);
return r0;
}
};
r0 = r1.bind(r2)(r0);
return r0;
}
};
r1['openLazy'] = r6;
r6 = function() { // Original name: close, environment: r3
r2 = _closure1_slot1;
r1 = _closure1_slot2;
r0 = 3;
r1 = r1[r0];
r0 = undefined;
r3 = r2.bind(r0)(r1);
r2 = r3.dispatch;
r1 = {};
r4 = 'ALERT_CLOSE';
r1['type'] = r4;
r1 = r2.bind(r3)(r1);
return r0;
};
r1['close'] = r6;
r6 = function(a0) { // Original name: show, environment: r3
_fun32594: for(var _fun32594_ip = 0; ; ) switch(_fun32594_ip) {
case 0:
r7 = a0;
r3 = this;
r5 = r7.hideActionSheet;
r4 = r7.isDismissable;
r1 = r7.confirmText;
r0 = undefined;
if(!(r1 === r0)) { _fun32594_ip = 90; continue _fun32594 }
case 32:
r10 = _closure1_slot0;
r11 = _closure1_slot2;
r2 = 4;
r8 = r11[r2];
r8 = r10.bind(r0)(r8);
r9 = r8.intl;
r8 = r9.string;
r2 = r11[r2];
r2 = r10.bind(r0)(r2);
r2 = r2.t;
r2 = r2.BddRzc;
r1 = r8.bind(r9)(r2);
case 90:
var _closure2_slot0 = r1;
r2 = {'hideActionSheet': 0, 'isDismissable': 0, 'confirmText': 0};
r13 = null;
r14 = r2;
r1 = silentSetPrototypeOf(r14, r13);
r14 = {};
r13 = r7;
r12 = r2;
r1 = copyDataProperties(r14, r13, r12);
var _closure2_slot1 = r1;
r2 = r3.openLazy;
r1 = {};
r6 = function() { // Original name: importer, environment: r6
r2 = _closure1_slot0;
r0 = _closure1_slot2;
r1 = 6;
r1 = r0[r1];
r3 = undefined;
r2 = r2.bind(r3)(r1);
r1 = 5;
r1 = r0[r1];
r0 = r0.paths;
r2 = r2.bind(r3)(r1, r0);
r1 = r2.then;
r0 = function(a0) { // Environment: r0
r1 = a0;
r1 = r1.default;
var _closure4_slot0 = r1;
r0 = function(a0) { // Environment: r0
r3 = _closure1_slot3;
r2 = _closure4_slot0;
r1 = {};
r5 = a0;
r6 = r1;
r0 = copyDataProperties(r6, r5);
r5 = _closure2_slot1;
r6 = r1;
r4 = copyDataProperties(r6, r5);
r4 = _closure2_slot0;
r0 = 'confirmText';
r1[r0] = r4;
r0 = undefined;
r0 = r3.bind(r0)(r2, r1);
return r0;
};
return r0;
};
r0 = r1.bind(r2)(r0);
return r0;
};
r1['importer'] = r6;
r1['hideActionSheet'] = r5;
r1['isDismissable'] = r4;
r1 = r2.bind(r3)(r1);
return r0;
}
};
r1['show'] = r6;
r3 = function(a0) { // Original name: confirm, environment: r3
r1 = this;
var _closure2_slot0 = r1;
r1 = a0;
var _closure2_slot1 = r1;
r1 = global;
r2 = r1.Promise;
r1 = r2.prototype;
r1 = Object.create(r1, {constructor: {value: r2}});
r3 = function(a0) { // Environment: r0
r0 = a0;
var _closure3_slot0 = r0;
r3 = _closure2_slot0;
r2 = r3.show;
r1 = {};
r0 = function() { // Original name: onConfirm, environment: r5
r2 = _closure3_slot0;
r0 = undefined;
r1 = true;
r1 = r2.bind(r0)(r1);
return r0;
};
r1['onConfirm'] = r0;
r9 = _closure1_slot0;
r10 = _closure1_slot2;
r6 = 4;
r7 = r10[r6];
r0 = undefined;
r7 = r9.bind(r0)(r7);
r8 = r7.intl;
r7 = r8.string;
r6 = r10[r6];
r6 = r9.bind(r0)(r6);
r6 = r6.t;
r6 = r6.ETE/oK;
r6 = r7.bind(r8)(r6);
r1['cancelText'] = r6;
r5 = function() { // Original name: onCancel, environment: r5
r2 = _closure3_slot0;
r0 = undefined;
r1 = false;
r1 = r2.bind(r0)(r1);
return r0;
};
r1['onCancel'] = r5;
r11 = _closure2_slot1;
r12 = r1;
r4 = copyDataProperties(r12, r11);
r1 = r2.bind(r3)(r1);
return r0;
};
r4 = r1;
r0 = new r4[r2](r3, r2);
r0 = r0 instanceof Object ? r0 : r1;
return r0;
};
r1['confirm'] = r3;
r3 = 7;
r3 = r5[r3];
r5 = r4.bind(r0)(r3);
r4 = r5.fileFinishedImporting;
r3 = 'actions/native/AlertActionCreators.tsx';
r3 = r4.bind(r5)(r3);
r2['default'] = r1;
return r0;
};`;
await Bun.write("temp/deminified.js", deminify(SNIPPET, "actions/native/AlertActionCreators.js").res);

View file

@ -1,128 +0,0 @@
import { writeFileSync } from "node:fs";
import { extname } from "node:path";
import { log } from "./progress";
const variableMatch = /\br(\d{1,2})\b/g;
const argumentMatch = /\ba(\d{1,2})\b/g;
const funMatch = /\b(_fun)(\d+)(_ip)?(?: = (\d+))?\b/g;
const caseMatch = /^case (\d+):/g;
const functNameMatch = /(function\*?) ?(\(.*?\) {) \/\/ Original name: +\??(.+), environment: \w+$/g;
const functOpenMatch = /function\*? ?\((.*?)\) {/;
const functCloseMatch = /^\s+};/;
const stringMatch = /(["'`])(?:\\.|[^\\])*?\1/g;
// biome-ignore lint/suspicious/noControlCharactersInRegex: this is needed
const nullstringMatch = /(["'`])\x00(\d+)\x00\1/g;
const nativeModuleNames = [
"global",
"require",
"metroImportDefault",
"metroImportAll",
"moduleObject",
"exports",
"dependencyMap",
];
export function deminify(code: string, path: string) {
const cleaned = code
.split(" = ")
.slice(1)
.join(" = ")
.slice(0, -1)
.replace(/ \/\/ Environment: \w+$/gm, "")
.split("\n");
const lines: string[] = [];
let depth = 0;
const localArgs = new Map<number, number>();
const funMap = new Map<string, string>();
const caseMap = new Map<string, number>();
caseMap.set("0", 0);
const varReplacer = (_: string, id: string) => `var${Number(id) + 1}`;
const argReplacer = (_: string, id: string) => {
const n = Number(id),
depth = localArgs.get(n);
if (!depth) log(`WARNING! arg${n} does not have any depth (${path})`);
if (depth === 1) return nativeModuleNames[n] ?? `native${n + 1}`;
return `arg${n + 1}`;
};
const funReplacer = (_: string, prefix: string, id: string, suffix: string, caseId?: string) => {
const num = funMap.get(id) ?? String(funMap.size + 1).padStart(4, "0");
funMap.set(id, num);
const text = `${prefix}${num}${suffix ?? ""}`;
if (!caseId) return text;
const caseNum = caseMap.get(caseId) ?? caseMap.size + 1;
caseMap.set(caseId, caseNum);
return `${text} = ${caseNum}`;
};
const caseReplacer = (_: string, id: string) => {
const num = caseMap.get(id) ?? caseMap.size + 1;
caseMap.set(id, num);
return `case ${num}:`;
};
const functNameReplacer = (_: string, prefix: string, stuff: string, name: string) => `${prefix} ${name}${stuff}`;
let j = 1;
for (const raw of cleaned) {
const nullStrings = new Map<string, string>();
const line = raw.replace(stringMatch, (match, quot: string) => {
const index = nullStrings.size + 1;
nullStrings.set(String(index), match);
return `${quot}\x00${index}\x00${quot}`;
});
// check for start of function
if (functOpenMatch.test(line)) {
const openedArgs = line.match(functOpenMatch)?.[1]?.split(", ")?.length || 0;
depth++;
for (let i = 0; i < openedArgs; i++) {
localArgs.set(i, depth);
}
}
// check for end of function
if (functCloseMatch.test(line)) {
for (const arg of localArgs.keys()) {
const n = localArgs.get(arg);
if (n === depth) localArgs.set(arg, n - 1);
}
depth--;
}
// sanity check
const y = j++;
if (depth < 0) {
writeFileSync("temp/depthcrashed.js", cleaned.join("\n"));
throw new Error(`parsing failed, depth went below 0 (LINE: ${y}, FILE: ${path})`);
}
const parsed = line
.replace(variableMatch, varReplacer)
.replace(argumentMatch, argReplacer)
.replace(funMatch, funReplacer)
.replace(caseMatch, caseReplacer)
.replace(functNameMatch, functNameReplacer)
.replace(nullstringMatch, (_, __, id: string) => {
const nullString = nullStrings.get(id);
if (nullString) return nullString;
writeFileSync("temp/nullcrashed.js", cleaned.join("\n"));
throw new Error(`parsing failed, nullstring ${id} doesn't exist (LINE: ${y}, FILE: ${path})`);
});
lines.push(parsed);
}
const final = lines.join("\n");
return {
final,
res: `// ${path}\n${extname(path).startsWith(".ts") ? "export default" : "module.exports ="} (${final})();`,
};
}

View file

@ -5,7 +5,7 @@ export const gitChanged = new Set<string>();
export async function fetchGitChanged() {
for (const changed of (
await Bun.$`git status -z -- ':!oldicons' ':!icons' ':!source'`.cwd("../data").quiet().then(handleShellErr)
await Bun.$`git status -z -- ':!source'`.cwd("../data").quiet().then(handleShellErr)
)
.text()
.split("\x00")
@ -27,16 +27,13 @@ export async function commit(files: string[], message: string) {
if (process.env.NODE_ENV === "test" && !commitAnyway) {
await fetchGitChanged();
} else {
// unstage all files
await Bun.$`git restore --staged .`.cwd("../data").nothrow().quiet().then(handleShellErr);
await fetchGitChanged();
// stage files
await Bun.$`git add ${{ raw: files.map((x) => Bun.$.escape(x)).join(" ") }}`
.cwd("../data")
.nothrow()
.quiet()
.then(handleShellErr);
// commiiiiiit
await Bun.$`git commit -m ${message}`
.cwd("../data")
.nothrow()

View file

@ -5,11 +5,8 @@ import { commit } from "./git";
import { log, makeProgress, wrapPromise } from "./progress";
import { apksFolder, codePath, cuteVersion, isMock, isQuiet, oprevFiles, prevFiles, version } from "./shared";
import codeTask from "./tasks/code";
import colors from "./tasks/colors";
import decompile from "./tasks/decompile";
import diffs from "./tasks/diffs";
import icons from "./tasks/icons";
import { webhook } from "./tasks/webhook";
import { formatError, handleShellErr, join } from "./utils";
export async function runTasks() {
@ -27,34 +24,21 @@ export async function runTasks() {
code_getting: "Finding file imports",
code_remaking: "Recreating code",
code_pushing: "Committing source",
colors: "Parsing colors",
icons: "Icon parser",
icons_getting: "Writing icons.json",
icons_copying: "Copying images",
diff: "Diffs",
diff_raw: "Diffing raw colors",
diff_semantic: "Diffing semantic colors",
diff_icons: "Diffing icons",
diff_code: "Diffing code",
webhook: "Sending webhook messages",
},
true,
);
let differs: Differs | undefined;
if (!isMock) {
// discard changes
try {
progress.start("preinit");
// reset to remote branch (without pulling latest commit)
progress.start("preinit_discard");
await Bun.$`git reset --hard`.cwd("../data").nothrow().quiet().then(handleShellErr);
if (await exists(join("../data", "oldicons")))
await rm(join("../data", "oldicons"), { force: true, recursive: true });
await Bun.write("../data/version.txt", version); // lel
await Bun.write("../data/version.txt", version);
// unstage all files
await Bun.$`git restore --staged .`.cwd("../data").nothrow().quiet().then(handleShellErr);
progress.update("preinit_discard", true);
@ -83,13 +67,9 @@ export async function runTasks() {
const code = (await Bun.file(codePath).text()).replace(/\r/g, "").split("\n");
await Promise.allSettled([
wrapPromise(codeTask(progress, code), progress, "code"),
wrapPromise(colors(code), progress, "colors"),
wrapPromise(icons(progress, code), progress, "icons"),
]);
if (progress.someFailed("code", "colors", "icons"))
throw new Error(`Failed at parser tasks!\n${progress.prettyErrors("code", "colors", "icons")}`);
await wrapPromise(codeTask(progress, code), progress, "code");
if (progress.someFailed("code"))
throw new Error(`Failed at parser tasks!\n${progress.prettyErrors("code")}`);
while (!progress.isFinished("decompile_gzip")) {
await Bun.sleep(1000);
@ -111,6 +91,7 @@ export async function runTasks() {
if (differs && !isQuiet) {
try {
const { webhook } = await import("./tasks/webhook");
await wrapPromise(webhook(differs), progress, "webhook");
} catch (e: any) {
throw new Error(`Failed to send webhook messages!\n${formatError(e)}`);
@ -120,5 +101,4 @@ export async function runTasks() {
}
await commit(["version.txt"], `chore: bump app version to ${cuteVersion}`);
await rm("../data/oldicons", { force: true, recursive: true });
}

View file

@ -6,9 +6,10 @@ export const maxDiffChanges = 20;
export const workFolder = "tmp";
export const apksFolder = join(workFolder, "apks");
export const codePath = join(workFolder, "code.js");
export const modulesPath = join(workFolder, "modules");
export const apkSplits = ["base", "config.hdpi", "config.xxhdpi"] as const;
export const oprevFiles = ["semantic.json", "raw.json", "icons.json", "source.jsonl"] as const;
export const oprevFiles = ["source.jsonl"] as const;
export const prevFiles = new Map<(typeof oprevFiles)[number], ArrayBuffer>();
export const version = await Bun.file("../data/version.txt").text();

View file

@ -1,70 +1,40 @@
import { mkdir, rm } from "node:fs/promises";
import { deminify } from "../deminify";
import { mkdir, rm, readdir, stat } from "node:fs/promises";
import { join as pathJoin } from "node:path";
import { commit } from "../git";
import type { Progress } from "../progress";
import { commitAnyway, cuteVersion } from "../shared";
import { discordPath, join, listRequiredDirs, sortEntries } from "../utils";
import { commitAnyway, cuteVersion, modulesPath } from "../shared";
import { join, sortEntries } from "../utils";
// scary code matching below, be warned
const moduleStartIndentation = " ".repeat(4);
const pathCheckRegex = / = '(.*?)';$/;
const lineImportRegex = / = r\d{1,2}\.fileFinishedImporting;$/;
export default async function code(progress: Progress, code: string[]) {
export default async function code(progress: Progress, _code: string[]) {
progress.start("code_getting");
const files = new Map<string, { final: string; res: string }>();
let moduleStart: number | null = null;
for (let i = 0; i < code.length; i++) {
const line = code[i];
// find the start of the file import based on the indentation
if (
new RegExp(`^${moduleStartIndentation}r\\d{1,2} = function\\(`).test(line) &&
line.includes(" { // Environment:")
) {
moduleStart = i;
}
// easiest way to check
const path = code[i + 1]?.match(pathCheckRegex)?.[1];
if (line.match(lineImportRegex) && path) {
const start = moduleStart;
if (!start) throw `moduleStart was null for ${start}~${i}; null ~ ${code[i]}`;
moduleStart = null;
// find the end of the file import based on the indentation
const moduleEndThing = `${moduleStartIndentation}};`;
let moduleEnd: number | null = null;
for (let j = i; j < code.length; j++) {
if (code[j] === moduleEndThing) {
moduleEnd = i = j;
break;
}
}
if (!moduleEnd) throw `moduleEnd was null for ${start}~${i}; ${code[start]} ~ ${code[i]}`;
const realPath = discordPath(path);
files.set(
realPath,
deminify(
code
.slice(start, moduleEnd + 1)
.map((line) => line.replace(new RegExp(`^${moduleStartIndentation}`), ""))
.join("\n"),
realPath,
),
);
}
if (!(await stat(modulesPath).catch(() => false))) {
throw new Error(`Modules directory not found at ${modulesPath}`);
}
async function scanDir(dir: string, basePath: string): Promise<Map<string, number>> {
const files = new Map<string, number>();
const entries = await readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = join(dir, entry.name);
if (entry.isDirectory()) {
const subFiles = await scanDir(fullPath, basePath);
for (const [name, size] of subFiles) files.set(name, size);
} else if (entry.isFile()) {
const relativePath = fullPath.replace(basePath, "").replace(/^\//, "");
const stats = await stat(fullPath);
files.set(relativePath, stats.size);
}
}
return files;
}
const files = await scanDir(modulesPath, modulesPath);
await Bun.write(
"../data/source.jsonl",
sortEntries([...files.entries()])
.map(([file, text]) => `{ "file": ${JSON.stringify(file)}, "size": ${text.final.length} }`)
.map(([file, size]) => `{ "file": ${JSON.stringify(file)}, "size": ${size} }`)
.join("\n"),
);
@ -76,15 +46,17 @@ export default async function code(progress: Progress, code: string[]) {
const filePrefix = "../data/source";
await rm(filePrefix, { recursive: true, force: true });
const dirs = listRequiredDirs([...files.keys()]);
await Promise.all(dirs.map((dir) => mkdir(join(filePrefix, dir), { recursive: true })));
await Promise.all(files.entries().map(([file, text]) => Bun.write(join(filePrefix, file), text.res)));
await Promise.all(
[...files.keys()].map(async (file) => {
const dest = join(filePrefix, file);
await mkdir(pathJoin(dest, ".."), { recursive: true });
await Bun.write(dest, Bun.file(join(modulesPath, file)));
}),
);
progress.update("code_remaking", true);
progress.start("code_pushing");
// hope the files get written in this time idk :P blehhh
await Bun.sleep(1500);
await commit(["source.jsonl", "source/*"], `chore: update source for ${cuteVersion}`);

View file

@ -1,14 +0,0 @@
import { sortObj } from "../utils";
import { convertSimpleSemantic, getInternalRawColors, getInternalSemanticColors } from "./colors";
const codeFile = Bun.file("tmp/code.js");
if (!(await codeFile.exists())) throw "Missing decompiled code.js";
const code = (await codeFile.text()).replace(/\r/g, "").split("\n");
const raw = getInternalRawColors(code);
await Bun.write("temp/raw.json", JSON.stringify(sortObj(raw), undefined, 4));
const semantic = getInternalSemanticColors(code, raw);
await Bun.write("temp/semantic.json", JSON.stringify(sortObj(semantic), undefined, 4));
await Bun.write("temp/semantic_simple.json", JSON.stringify(sortObj(convertSimpleSemantic(semantic)), null, 4));

View file

@ -1,133 +0,0 @@
import { runInNewContext } from "node:vm";
import Color, { type ColorInstance } from "color";
import type { RawColors, SemanticColors } from "../../types";
import { commit } from "../git";
import { cuteVersion } from "../shared";
import { sortObj } from "../utils";
function hex(color: ColorInstance) {
return (color.alpha() === 1 ? color.hex() : color.hexa()).toLowerCase();
}
// FIXME fixes an issue introduced in hermes-dec commit 1c112f85e3fb49442c57a0ed31b83bb3825db639,
// where code such as "rX[rY] = z" is instead parsed as "rX[Y] = z"
function fixHermesDecIndex(code: string) {
return code.replace(/r(\d+)\[(\d+)\] =/g, "r$1[r$2] =");
}
// more scary code matching below, be warned
export function evalMetroModule<T>(code: string[], importName: string, imports: object = {}): T {
const hookLine = code.findIndex((x) => x.includes(importName));
if (hookLine === -1) throw new Error(`Cannot find hookLine for ${importName}!`);
// usually fileFinishedImporting is called 5 lines before the function ends, so we pray
const endLine = hookLine + 5;
let startLine: number | null = null;
for (let i = hookLine; i > 0; i--)
if (code[i].includes("// Environment:")) {
startLine = i;
break;
}
if (startLine === null) throw new Error(`Failed to find startLine for ${importName}!`);
const snippet = fixHermesDecIndex(code.slice(startLine, endLine).join("\n"));
const fn = runInNewContext(snippet, {
global: {
Object: Object.assign(Object, {
freeze(obj: any) {
return obj;
},
}),
},
});
const exports: any = {};
fn(
null, // global
() => ({
fileFinishedImporting() {},
...imports,
}), // require
null, // metroImportDefault
null, // metroImportAll
null, // moduleObject
exports, // exports
[], // dependencyMap
);
return exports;
}
export function getInternalRawColors(code: string[]) {
const module = evalMetroModule<{
_private: {
RawColors: RawColors;
};
}>(code, "/tokens/colors/generated/raw-color-definitions.tsx'", {});
const raw: RawColors = {};
for (const [key, color] of Object.entries(module._private.RawColors)) {
raw[key] = hex(Color(color));
}
return raw;
}
export function getInternalSemanticColors(code: string[], raw: Record<string, string>) {
const module = evalMetroModule<{
_private: {
SemanticColors: Record<string, Record<string, { raw: string; opacity: number }>>;
};
}>(code, "/tokens/colors/generated/native/generated-definitions.tsx'", {
_private: {
Themes: new Proxy(
{},
{
get(_, key) {
return key.toString().toLowerCase();
},
},
),
},
});
const semantic: SemanticColors = {};
for (const [key, colors] of Object.entries(module._private.SemanticColors)) {
const color: SemanticColors[string] = {};
for (const [theme, val] of Object.entries(colors)) {
if (typeof val !== "object" || !("raw" in val)) continue;
color[theme] = [hex(Color(raw[val.raw]).alpha(val.opacity)), { raw: val.raw, opacity: val.opacity }];
}
semantic[key] = color;
}
return semantic;
}
export function convertSimpleSemantic(semantic: SemanticColors) {
const simpleSemantic: Record<string, Record<string, string>> = {};
for (const [key, colors] of Object.entries(semantic)) {
const color: Record<string, string> = {};
for (const [theme, [clr]] of Object.entries(colors)) color[theme] = clr;
simpleSemantic[key] = color;
}
return simpleSemantic;
}
export default async function colors(code: string[]) {
const raw = getInternalRawColors(code);
await Bun.write("../data/raw.json", JSON.stringify(sortObj(raw), null, 4));
const semantic = getInternalSemanticColors(code, raw);
await Bun.write("../data/semantic.json", JSON.stringify(sortObj(semantic), null, 4));
await Bun.write("../data/semantic_simple.json", JSON.stringify(sortObj(convertSimpleSemantic(semantic)), null, 4));
await commit(["raw.json", "semantic.json", "semantic_simple.json"], `chore: update colors for ${cuteVersion}`);
}

View file

@ -1,37 +1,52 @@
import { exists } from "node:fs/promises";
import { commit } from "../git";
import type { Progress } from "../progress";
import { codePath, commitAnyway, cuteVersion, workFolder } from "../shared";
import { codePath, commitAnyway, cuteVersion, modulesPath, workFolder } from "../shared";
import { handleShellErr, join } from "../utils";
const gzipWorkerURL = new URL("decompile-gzip.ts", import.meta.url).href;
const decompilerPy = "src/hermes_dec/decompilation/hbc_decompiler.py";
export default async function decompile(progress: Progress, pathToBundle: string) {
const pathToDecompiler = join(workFolder, "decompiler");
progress.start("decompile_downloading");
if (!(await exists(pathToDecompiler))) {
await Bun.$`git clone https://github.com/P1sec/hermes-dec.git --depth=1 ${pathToDecompiler}`
await Bun.$`git clone https://github.com/SymbioticSec/hermes-decomp.git --depth=1 ${pathToDecompiler}`
.quiet()
.nothrow()
.then(handleShellErr);
progress.update("decompile_downloading", true);
} else progress.update("decompile_downloading", null);
const { exitCode: hasPython } = await Bun.$`python --version`.nothrow().quiet();
if (hasPython !== 0) throw new Error("Cannot use Python! Are you sure it's installed?");
const { exitCode: hasCargo } = await Bun.$`cargo --version`.nothrow().quiet();
if (hasCargo !== 0) throw new Error("Cannot use Cargo! Are you sure Rust is installed?");
progress.start("decompile_decompiling");
if (!(await Bun.file(codePath).exists())) {
await Bun.$`python ${join(pathToDecompiler, decompilerPy)} ${pathToBundle} ${codePath}`
const decompilerBin = join(pathToDecompiler, "target/release/hermes-decomp");
if (!(await Bun.file(decompilerBin).exists())) {
await Bun.$`cargo build --release -p hermes-decomp`
.cwd(pathToDecompiler)
.quiet()
.nothrow()
.then(handleShellErr);
progress.update("decompile_decompiling", true);
} else progress.update("decompile_decompiling", null);
}
if (!(await Bun.file(codePath).exists())) {
await Bun.$`${decompilerBin} decompile ${pathToBundle} --output ${codePath}`
.quiet()
.nothrow()
.then(handleShellErr);
}
if (!(await exists(modulesPath))) {
await Bun.$`${decompilerBin} extract ${pathToBundle} --output ${modulesPath}`
.quiet()
.nothrow()
.then(handleShellErr);
}
progress.update("decompile_decompiling", true);
// background step
if (process.env.NODE_ENV !== "test" && !commitAnyway) {
const gzFile = "code.js.gz";

View file

@ -1,199 +1,9 @@
import {
type CodeDiff,
type Diff,
type Differs,
DiffType,
type Icons,
type RawColors,
type Semantic,
type SemanticColors,
} from "../../types";
import { type CodeDiff, type Differs, DiffType } from "../../types";
import { fetchGitChanged, gitChanged } from "../git";
import { type Progress, wrapPromise } from "../progress";
import { diffAnyway, prevFiles } from "../shared";
import { join, parseJsonl } from "../utils";
async function diffRaw() {
if (!prevFiles.has("raw.json")) {
throw new Error("Missing prevFile: raw.json");
}
const oldRaw = JSON.parse(new TextDecoder().decode(prevFiles.get("raw.json"))) as RawColors;
const newRaw: RawColors = await Bun.file(join("../data", "raw.json")).json();
const renamed = new Set<string>();
const changes = new Map<string, Diff>();
for (const [name, source] of Object.entries(newRaw))
if (!oldRaw[name]) {
const oldName = Object.entries(oldRaw).find(([key, val]) => !newRaw[key] && val === source)?.[0];
if (!oldName)
changes.set(name, {
type: DiffType.Added,
source,
});
else {
renamed.add(oldName);
changes.set(name, {
type: DiffType.Renamed,
oldName,
source,
});
}
} else if (oldRaw[name].toLowerCase() !== source.toLowerCase())
changes.set(name, {
type: DiffType.Changed,
source,
oldSource: oldRaw[name],
});
for (const [name, source] of Object.entries(oldRaw))
if (!(newRaw[name] || renamed.has(name)))
changes.set(name, {
type: DiffType.Removed,
source,
});
return changes;
}
function getSemanticLabel(semantic: Semantic) {
const light = semantic.light,
dark = semantic.darker ?? semantic.dark;
if (dark && light) return `☀️ ${light[0]}, 🌙 ${dark[0]}`;
return Object.entries(semantic)
.map(([name, [color]]) => `${name} ${color}`)
.join(", ");
}
async function diffSemantic() {
if (!prevFiles.has("semantic.json")) {
throw new Error("Missing prevFile: semantic.json");
}
const oldSemantic = JSON.parse(new TextDecoder().decode(prevFiles.get("semantic.json"))) as SemanticColors;
const newSemantic: SemanticColors = await Bun.file(join("../data", "semantic.json")).json();
const renamed = new Set<string>();
const changes = new Map<string, Diff>();
for (const [name, semantic] of Object.entries(newSemantic)) {
const source = Object.values(semantic)
.map(([color]) => color)
.join(",");
if (!oldSemantic[name]) {
const themes = Object.keys(semantic).length;
const oldName = Object.entries(oldSemantic).find(
([key, val]) =>
!newSemantic[key] &&
Object.keys(val).length === themes &&
Object.entries(val).every(([skey, sval]) => sval[0] === semantic[skey][0]),
)?.[0];
if (!oldName)
changes.set(name, {
type: DiffType.Added,
source,
label: getSemanticLabel(semantic),
});
else {
renamed.add(oldName);
changes.set(name, {
type: DiffType.Renamed,
oldName,
source,
});
}
} else
for (const [theme, [color]] of Object.entries(semantic))
if (!oldSemantic[name][theme])
changes.set(`${name}.${theme}`, {
type: DiffType.Added,
source: color,
});
else if (oldSemantic[name][theme][0].toLowerCase() !== color.toLowerCase())
changes.set(`${name}.${theme}`, {
type: DiffType.Changed,
source: color,
oldSource: oldSemantic[name][theme][0],
});
}
for (const [name, semantic] of Object.entries(oldSemantic)) {
if (renamed.has(name)) continue;
const source = Object.values(semantic)
.map(([color]) => color)
.join(",");
if (!newSemantic[name])
changes.set(name, {
type: DiffType.Removed,
source,
label: getSemanticLabel(semantic),
});
else
for (const [theme, [source]] of Object.entries(semantic))
if (!newSemantic[name][theme])
changes.set(`${name}.${theme}`, {
type: DiffType.Removed,
source,
});
}
return changes;
}
async function diffIcons() {
if (!prevFiles.has("icons.json")) {
throw new Error("Missing prevFile: icons.json");
}
const oldIcons = JSON.parse(new TextDecoder().decode(prevFiles.get("icons.json"))) as Icons;
const newIcons: Icons = await Bun.file(join("../data", "icons.json")).json();
const iconDir = {
old: join("../data", "oldicons"),
new: join("../data", "icons"),
};
const renamed = new Set<string>();
const changes = new Map<string, Diff>();
for (const [name, icon] of Object.entries(newIcons)) {
const label = icon.hash.slice(0, 8);
if (!oldIcons[name]) {
const oldName = Object.entries(oldIcons).find(([key, val]) => !newIcons[key] && val.hash === icon.hash)?.[0];
if (!oldName)
changes.set(name, {
type: DiffType.Added,
source: join(iconDir.new, icon.file),
label,
});
else {
renamed.add(oldName);
changes.set(name, {
type: DiffType.Renamed,
oldName,
source: join(iconDir.new, icon.file),
});
}
} else if (oldIcons[name].hash !== icon.hash)
changes.set(name, {
type: DiffType.Changed,
source: join(iconDir.new, icon.file),
label,
oldSource: join(iconDir.old, oldIcons[name].file),
oldLabel: oldIcons[name].hash.slice(0, 8),
});
}
for (const [name, icon] of Object.entries(oldIcons))
if (!(newIcons[name] || renamed.has(name)))
changes.set(name, {
type: DiffType.Removed,
source: join(iconDir.old, icon.file),
label: icon.hash.slice(0, 8),
});
return changes;
}
function parseSource(text: string) {
const source = parseJsonl(text) as { file: string; size: number }[];
return Object.fromEntries(
@ -245,9 +55,6 @@ export default async function diffs(progress: Progress) {
if (!(gitChanged.has("version.txt") || diffAnyway)) {
progress.update("diff", null);
progress.update("diff_raw", null);
progress.update("diff_semantic", null);
progress.update("diff_icons", null);
progress.update("diff_code", null);
return;
}
@ -255,27 +62,6 @@ export default async function diffs(progress: Progress) {
const differs = {} as Differs;
await Promise.all([
gitChanged.has("raw.json")
? wrapPromise(
diffRaw().then((x) => (differs.raw = x)),
progress,
"diff_raw",
)
: progress.update("diff_raw", null),
gitChanged.has("semantic.json")
? wrapPromise(
diffSemantic().then((x) => (differs.semantic = x)),
progress,
"diff_semantic",
)
: progress.update("diff_semantic", null),
gitChanged.has("icons.json")
? wrapPromise(
diffIcons().then((x) => (differs.icons = x)),
progress,
"diff_icons",
)
: progress.update("diff_icons", null),
gitChanged.has("source.jsonl")
? wrapPromise(
diffCode().then((x) => (differs.code = x)),

View file

@ -1,10 +0,0 @@
import { sortObj } from "../utils";
import { parseAssets } from "./icons";
const codeFile = Bun.file("tmp/code.js");
if (!(await codeFile.exists())) throw "Missing decompiled code.js";
const code = (await codeFile.text()).replace(/\r/g, "").split("\n");
const { icons } = await parseAssets(code);
await Bun.write("temp/icons.json", JSON.stringify(sortObj(icons), undefined, 4));

View file

@ -1,114 +0,0 @@
import { copyFile, mkdir, readdir, rename } from "node:fs/promises";
import { basename } from "node:path";
import { runInNewContext } from "node:vm";
import type { Icons } from "../../types";
import { commit } from "../git";
import type { Progress } from "../progress";
import { apkSplits, apksFolder, cuteVersion } from "../shared";
import { discordPath, join, listRequiredDirs, sortObj } from "../utils";
const infoObjRegex = /({.+?})/;
const scalesArrayRegex = /(\[.+?\])/;
// a bit less scary code matching below, be warned
export async function parseAssets(code: string[]) {
const retrievedAssets: {
httpServerLocation: string;
width: number;
height: number;
scales: number[];
hash: string;
name: string;
type: "png" | "svg" | "lottie";
}[] = [];
for (let i = 0; i < code.length; i++) {
// next line has asset info, after that are scales
const line = code[i],
infoLine = code[i + 1],
scalesLine = code[i + 2];
if (line.includes(".registerAsset") && infoLine?.includes("'httpServerLocation'") && scalesLine?.includes("[")) {
const infoText = infoLine.match(infoObjRegex)?.[1];
if (!infoText) throw new Error(`Failed to find infoText for ${infoLine} (line ${i + 1})`);
const scalesText = scalesLine.match(scalesArrayRegex)?.[1];
if (!scalesText) throw new Error(`Failed to find scalesText for ${scalesLine} (line ${i + 2})`);
const info = runInNewContext(`(${infoText})`);
if (typeof info !== "object") continue;
const scales = runInNewContext(`(${scalesText})`);
if (!Array.isArray(scales)) continue;
info.scales = scales;
if (
["httpServerLocation", "hash", "name", "type"].every((x) => x in info) &&
["svg", "png", "lottie"].includes(info.type) &&
!info.httpServerLocation.includes("node_modules/.pnpm")
) {
retrievedAssets.push(info);
}
}
}
// get files of all apks by hierarchy
const apkPaths = new Map<string, string>();
for (const split of apkSplits) {
const folder = join(apksFolder, split);
for (const path of await readdir(folder, { recursive: true })) apkPaths.set(basename(path), join(folder, path));
}
const icons: Icons = {};
const toCopy: { from: string; to: string }[] = [];
for (const asset of retrievedAssets) {
const baseName = [...asset.httpServerLocation.split("/").slice(2), asset.name]
.join("_")
.toLowerCase()
.replace(/\W+/g, "");
const path = `${baseName}.${asset.type}`;
const apkPath = apkPaths.get(path);
if (apkPath) {
const realPath = join(
discordPath(asset.httpServerLocation.split("/").slice(2).join("/")),
`${asset.name}.${asset.type}`,
);
icons[asset.name] = {
file: realPath,
hash: asset.hash,
scales: asset.scales,
width: asset.width ?? null,
height: asset.height ?? null,
};
toCopy.push({
from: apkPath,
to: join("../data/icons", realPath),
});
}
}
return { icons, toCopy };
}
export default async function icons(progress: Progress, code: string[]) {
progress.start("icons_getting");
const { icons, toCopy } = await parseAssets(code);
await Bun.write("../data/icons.json", JSON.stringify(sortObj(icons), undefined, 4));
progress.update("icons_getting", true);
progress.start("icons_copying");
// don't delete the old icons folder so we can display the old images
await rename("../data/icons", "../data/oldicons");
const dirs = listRequiredDirs(toCopy.map((x) => x.to));
await Promise.all(dirs.map((dir) => mkdir(dir, { recursive: true })));
await Promise.all(toCopy.map(({ from, to }) => copyFile(from, to)));
await commit(["icons.json", "icons"], `chore: update icons for ${cuteVersion}`);
progress.update("icons_copying", true);
}

View file

@ -1,18 +1,7 @@
import { basename } from "node:path";
import type { Canvas } from "@napi-rs/canvas";
import {
type APIMessageTopLevelComponent,
ComponentType,
MessageFlags,
type RESTPostAPIWebhookWithTokenJSONBody,
RouteBases,
Routes,
} from "discord-api-types/v10";
import { drawSections } from "../../canvas";
import { makeSections } from "../../canvas/factory";
import { type CodeDiff, type Diff, type Differs, DiffType } from "../../types";
import { type CodeDiff, type Differs, DiffType } from "../../types";
import { cuteVersion, maxDiffChanges, version } from "../shared";
import { assert, formatBytes, sortEntries } from "../utils";
import { formatBytes, sortEntries } from "../utils";
function fileBase(path: string, other?: string) {
if (!other) return `... ${basename(path)}`;
@ -48,42 +37,27 @@ interface FormattedDiff {
capped: boolean;
}
function formatDiff(diffs: Map<string, Diff | CodeDiff>): FormattedDiff {
function formatDiff(diffs: Map<string, CodeDiff>): FormattedDiff {
const entries = [...diffs.entries()];
const sections = {
Added: sortEntries(entries)
.map(
([name, diff]) =>
diff.type === DiffType.Added &&
("size" in diff
? `+ ${fileBase(name)} (${formatBytes(diff.size)})`
: `+ ${name}: ${diff.label || diff.source}`),
)
.filter((x) => typeof x === "string"),
Changed: sortEntries(entries)
.map(
([name, diff]) =>
diff.type === DiffType.Changed &&
`- ${name}: ${diff.oldLabel || diff.oldSource}\n+ ${name}: ${diff.label || diff.source}`,
diff.type === DiffType.Added && `+ ${fileBase(name)} (${formatBytes(diff.size)})`,
)
.filter((x) => typeof x === "string"),
Renamed: sortEntries(entries)
.map(
([name, diff]) =>
diff.type === DiffType.Renamed &&
("size" in diff
? `- ${fileBase(diff.oldName, name)}\n+ ${fileBase(name, diff.oldName)}`
: `- ${diff.oldName}\n+ ${name}`),
`- ${fileBase(diff.oldName, name)}\n+ ${fileBase(name, diff.oldName)}`,
)
.filter((x) => typeof x === "string"),
Removed: sortEntries(entries)
.map(
([name, diff]) =>
diff.type === DiffType.Removed &&
("size" in diff
? `- ${fileBase(name)} (${formatBytes(diff.size)})`
: `- ${name}: ${diff.label || diff.source}`),
diff.type === DiffType.Removed && `- ${fileBase(name)} (${formatBytes(diff.size)})`,
)
.filter((x) => typeof x === "string"),
};
@ -98,174 +72,26 @@ function formatDiff(diffs: Map<string, Diff | CodeDiff>): FormattedDiff {
};
}
interface WebhookEmbed {
title: string;
body: FormattedDiff;
image?: Canvas;
footer: string;
key: string;
}
async function sendWebhook(webhook: string, role: string, embeds: WebhookEmbed[], color?: number) {
const body = new FormData();
const images = new Map<number, string>(),
diffs = new Map<number, string>();
let fi = 0;
for (let i = 0; i < embeds.length; i++) {
const embed = embeds[i];
if (embed.image) {
const key = fi++;
const filename = `${embed.key}.png`;
images.set(i, filename);
body.append(`files[${key}]`, new Blob([embed.image.toBuffer("image/png")], { type: "image/png" }), filename);
}
if (embed.body.capped) {
const key = fi++;
const filename = `${embed.key}-changes.diff`;
diffs.set(i, filename);
body.append(`files[${key}]`, new Blob([embed.body.file], { type: "text/plain" }), filename);
}
}
body.append(
"payload_json",
JSON.stringify({
flags: MessageFlags.IsComponentsV2,
components: [
{
type: ComponentType.TextDisplay,
content: `<@&${role}>`,
},
...embeds.map(
({ title, body, image, footer }, i) =>
({
type: ComponentType.Container,
accent_color: color,
components: [
{
type: ComponentType.TextDisplay,
content: `## ${title}\n-# ${version} (${cuteVersion})`,
},
...body.embed.map((content) => ({
type: ComponentType.TextDisplay,
content,
})),
body.capped && {
type: ComponentType.File,
file: {
url: `attachment://${diffs.get(i)}`,
},
},
body.capped && {
type: ComponentType.Separator,
},
image && {
type: ComponentType.MediaGallery,
items: [
{
media: {
url: `attachment://${images.get(i)}`,
},
},
],
},
{
type: ComponentType.TextDisplay,
content: footer,
},
].filter((x) => !!x),
}) as APIMessageTopLevelComponent,
),
],
allowed_mentions: process.env.NODE_ENV === "test" ? { parse: [] } : { roles: [role] },
} as RESTPostAPIWebhookWithTokenJSONBody),
);
const url = new URL(webhook);
url.searchParams.set("wait", "true");
url.searchParams.set("with_components", "true");
const res = await fetch(url, {
method: "POST",
body,
});
const data = await res.text();
if (!res.ok)
throw new Error(`Failed to send webhook message with embeds ${embeds.map((x) => x.title).join(", ")}: ${data}`);
const discordToken = process.env.bot_token;
if (!discordToken) return;
// this can fail quietly
try {
const message = JSON.parse(data);
const route = `${RouteBases.api}/${Routes.channelMessageCrosspost(message.channel_id, message.id)}`;
await fetch(route, {
method: "POST",
headers: {
Authorization: discordToken,
},
});
} catch {}
}
export async function webhook(diffs: Differs) {
if (diffs.raw?.size || diffs.semantic?.size)
await sendWebhook(
assert(process.env.color_webhook, "Missing color webhook env"),
"1227327297795657850",
[
diffs.raw?.size && {
title: "Raw colors",
body: formatDiff(diffs.raw),
image: drawSections(await makeSections(diffs.raw)),
footer: makeFooter(diffs.raw.size, "raw color"),
key: "raw-colors",
},
diffs.semantic?.size && {
title: "Semantic colors",
body: formatDiff(diffs.semantic),
image: drawSections(await makeSections(diffs.semantic)),
footer: makeFooter(diffs.semantic.size, "semantic color"),
key: "semantic-colors",
},
].filter((x) => !!x),
0xd8ef9e,
);
if (diffs.icons?.size)
await sendWebhook(
assert(process.env.icons_webhook, "Missing icons webhook env"),
"1227327765079003217",
[
{
title: "Icons",
body: formatDiff(diffs.icons),
image: drawSections(await makeSections(diffs.icons, true)),
footer: makeFooter(diffs.icons.size, "icon"),
key: "icons",
},
],
0xf4b8f7,
);
if (diffs.code?.size)
await sendWebhook(
assert(process.env.code_webhook, "Missing code webhook env"),
"1233861867059941387",
[
if (diffs.code?.size) {
const body = JSON.stringify({
content: `Code changes for **${version} (${cuteVersion})**`,
embeds: [
{
title: "Code",
body: formatDiff(diffs.code),
footer: makeFooter(diffs.code.size, "code"),
key: "code",
description: formatDiff(diffs.code).embed.join("\n"),
footer: { text: makeFooter(diffs.code.size, "code") },
},
],
0x86faf3,
);
});
const webhookUrl = process.env.code_webhook;
if (webhookUrl) {
await fetch(webhookUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body,
});
}
}
}