refactor: dedupe diff logic, simplify runner, decompose canvas
- extract 4 diff fns into generic diffEntries + diffEntriesCode - drop 120s polling loop; await gzip promise directly - filter-before-map in webhook, inline color cast helpers - decompose drawSections into measure/render helpers - validate colors VM eval result shape - chain git commits via lastCommit promise - drop unused Differs fields, trim types
This commit is contained in:
parent
0f7170e1c7
commit
dc2b7299ff
10 changed files with 390 additions and 399 deletions
|
|
@ -1,8 +1,8 @@
|
||||||
import { readdir } from "node:fs/promises";
|
import { readdir } from "node:fs/promises";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { createCanvas, GlobalFonts } from "@napi-rs/canvas";
|
import { createCanvas, GlobalFonts, type SKRSContext2D } from "@napi-rs/canvas";
|
||||||
import { file } from "bun";
|
import { file } from "bun";
|
||||||
import type { Row, Section } from "./factory";
|
import type { Item, Row, Section } from "./factory";
|
||||||
import { measureText, transformSvg, wrapText } from "./utils";
|
import { measureText, transformSvg, wrapText } from "./utils";
|
||||||
|
|
||||||
async function bulkRegister(family: string, path: string) {
|
async function bulkRegister(family: string, path: string) {
|
||||||
|
|
@ -11,6 +11,37 @@ async function bulkRegister(family: string, path: string) {
|
||||||
await bulkRegister("GG Sans", join(import.meta.dir, "fonts/ggsans"));
|
await bulkRegister("GG Sans", join(import.meta.dir, "fonts/ggsans"));
|
||||||
await bulkRegister("GG Mono", join(import.meta.dir, "fonts/ggmono"));
|
await bulkRegister("GG Mono", join(import.meta.dir, "fonts/ggmono"));
|
||||||
|
|
||||||
|
const layout = {
|
||||||
|
background: { padding: 24, gap: 18 },
|
||||||
|
section: { gap: 8 },
|
||||||
|
card: { radius: 24, padding: 12, gapX: 16, gapY: 8 },
|
||||||
|
blob: { radius: 12 },
|
||||||
|
color: { radius: 24 },
|
||||||
|
};
|
||||||
|
|
||||||
|
const textSizes = { header: 24, title: 18, blob: 24 };
|
||||||
|
|
||||||
|
const colors = {
|
||||||
|
background: "#1c1d23",
|
||||||
|
section: "#26272f",
|
||||||
|
blob: "#6c6f7c1f",
|
||||||
|
textNormal: "#c7c8ce",
|
||||||
|
textMuted: "#818491",
|
||||||
|
watermark: "#2f3035",
|
||||||
|
};
|
||||||
|
|
||||||
|
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");
|
||||||
|
const headerFont = `600 ${textSizes.header}px GG Sans`;
|
||||||
|
const titleFont = `400 ${textSizes.title}px GG Mono`;
|
||||||
|
|
||||||
function calcColumnWidth(column: { textWidth: number; itemWidth: number }[]) {
|
function calcColumnWidth(column: { textWidth: number; itemWidth: number }[]) {
|
||||||
return Math.max(0, ...column.map(({ textWidth, itemWidth }) => Math.max(0, textWidth, itemWidth)));
|
return Math.max(0, ...column.map(({ textWidth, itemWidth }) => Math.max(0, textWidth, itemWidth)));
|
||||||
}
|
}
|
||||||
|
|
@ -27,81 +58,37 @@ function calcSectionHeight(columns: Row[][]) {
|
||||||
return Math.max(0, ...columns.map((column) => calcColumnHeight(column)));
|
return Math.max(0, ...columns.map((column) => calcColumnHeight(column)));
|
||||||
}
|
}
|
||||||
|
|
||||||
const layout = {
|
function measureItemWidth(item: Item) {
|
||||||
background: {
|
if (item.type === "blob") return blobSize;
|
||||||
padding: 24,
|
if (item.type === "image") return (item.image.width / item.image.height) * itemSize;
|
||||||
gap: 18,
|
return itemSize;
|
||||||
},
|
}
|
||||||
section: {
|
|
||||||
gap: 8,
|
|
||||||
},
|
|
||||||
card: {
|
|
||||||
radius: 24,
|
|
||||||
padding: 12,
|
|
||||||
gapX: 16,
|
|
||||||
gapY: 8,
|
|
||||||
},
|
|
||||||
blob: {
|
|
||||||
radius: 24 - 12,
|
|
||||||
},
|
|
||||||
color: {
|
|
||||||
radius: 24,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const textSizes = {
|
function measureRow(row: Row) {
|
||||||
header: 24,
|
return {
|
||||||
title: 18,
|
title: row.title,
|
||||||
blob: 24,
|
subtitle: row.subtitle,
|
||||||
};
|
textWidth: Math.max(
|
||||||
|
0,
|
||||||
const colors = {
|
row.title ? measureText(mctx, titleFont, row.title).width : 0,
|
||||||
background: "#1c1d23", // BACKGROUND_BASE_LOWEST
|
row.subtitle ? measureText(mctx, titleFont, row.subtitle).width : 0,
|
||||||
section: "#26272f", // CARD_BACKGROUND_DEFAULT
|
),
|
||||||
blob: "#6c6f7c1f", // BACKGROUND_MOD_MUTED
|
item: row.item,
|
||||||
textNormal: "#c7c8ce", // TEXT_NORMAL
|
itemWidth: measureItemWidth(row.item),
|
||||||
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`;
|
|
||||||
|
|
||||||
|
function measureSections(sections: Section[]) {
|
||||||
const sects = sections.map(({ header, columns }) => ({
|
const sects = sections.map(({ header, columns }) => ({
|
||||||
headerWidth: measureText(mctx, headerFont, header).width,
|
headerWidth: measureText(mctx, headerFont, header).width,
|
||||||
header,
|
header,
|
||||||
columns: columns.map((column) =>
|
columns: columns.map((column) => column.map(measureRow)),
|
||||||
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) =>
|
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))),
|
Math.max(0, ...sects.map(({ columns }) => (columns[i] ? calcColumnWidth(columns[i]) : 0))),
|
||||||
);
|
);
|
||||||
|
|
||||||
const sectionWidth =
|
const sectionWidth =
|
||||||
columnWidths.reduce((a, b) => a + b, 0) + layout.card.gapX * (columnWidths.length - 1) + layout.card.padding * 2;
|
columnWidths.reduce((a, b) => a + b, 0) + layout.card.gapX * (columnWidths.length - 1) + layout.card.padding * 2;
|
||||||
|
|
||||||
|
|
@ -118,6 +105,56 @@ export function drawSections(sections: Section[]) {
|
||||||
)
|
)
|
||||||
.reduce((a, b) => a + b, 0) - layout.background.gap;
|
.reduce((a, b) => a + b, 0) - layout.background.gap;
|
||||||
|
|
||||||
|
return { sects, columnWidths, sectionWidth, width, height };
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderBlob(ctx: SKRSContext2D, blob: string, x: number, y: number, width: number, height: number) {
|
||||||
|
ctx.fillStyle = colors.blob;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.roundRect(x, y, width, height, 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, blob, width);
|
||||||
|
const lineHeight = textSizes.blob / 2;
|
||||||
|
for (let i = 0; i < wrapped.length; i++) {
|
||||||
|
ctx.fillText(wrapped[i], x + width / 2, y + height / 2 - lineHeight * (wrapped.length - 1) + textSizes.blob * i);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.textAlign = "left";
|
||||||
|
ctx.textBaseline = "top";
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderColor(ctx: SKRSContext2D, colorGrid: string[][], x: number, y: number) {
|
||||||
|
for (let i = 0; i < colorGrid.length; i++) {
|
||||||
|
const row = colorGrid[i];
|
||||||
|
for (let j = 0; j < row.length; j++) {
|
||||||
|
ctx.save();
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.rect(
|
||||||
|
Math.floor(x + (itemSize / row.length) * j),
|
||||||
|
Math.floor(y + (itemSize / colorGrid.length) * i),
|
||||||
|
Math.ceil(itemSize / row.length),
|
||||||
|
Math.ceil(itemSize / colorGrid.length),
|
||||||
|
);
|
||||||
|
ctx.clip();
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.fillStyle = row[j];
|
||||||
|
ctx.roundRect(x, y, itemSize, itemSize, layout.color.radius);
|
||||||
|
ctx.fill();
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function drawSections(sections: Section[]) {
|
||||||
|
const { sects, columnWidths, sectionWidth, width, height } = measureSections(sections);
|
||||||
|
|
||||||
const canvas = createCanvas(layout.background.padding * 2 + width, layout.background.padding * 2 + height);
|
const canvas = createCanvas(layout.background.padding * 2 + width, layout.background.padding * 2 + height);
|
||||||
const ctx = canvas.getContext("2d");
|
const ctx = canvas.getContext("2d");
|
||||||
ctx.textBaseline = "top";
|
ctx.textBaseline = "top";
|
||||||
|
|
@ -125,8 +162,8 @@ export function drawSections(sections: Section[]) {
|
||||||
ctx.fillStyle = colors.background;
|
ctx.fillStyle = colors.background;
|
||||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
let x = layout.background.padding,
|
let x = layout.background.padding;
|
||||||
y = layout.background.padding;
|
let y = layout.background.padding;
|
||||||
for (const sect of sects) {
|
for (const sect of sects) {
|
||||||
ctx.fillStyle = colors.textNormal;
|
ctx.fillStyle = colors.textNormal;
|
||||||
ctx.font = headerFont;
|
ctx.font = headerFont;
|
||||||
|
|
@ -165,54 +202,12 @@ export function drawSections(sections: Section[]) {
|
||||||
|
|
||||||
const midX = centerX - row.itemWidth / 2;
|
const midX = centerX - row.itemWidth / 2;
|
||||||
if (row.item.type === "blob") {
|
if (row.item.type === "blob") {
|
||||||
const blobHeight = sectionHeight - wy + y;
|
renderBlob(ctx, row.item.blob, wx, wy, columnWidth, 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") {
|
} else if (row.item.type === "image") {
|
||||||
ctx.drawImage(row.item.image, midX, wy, row.itemWidth, itemSize);
|
ctx.drawImage(row.item.image, midX, wy, row.itemWidth, itemSize);
|
||||||
wy += itemSize + layout.card.gapY;
|
wy += itemSize + layout.card.gapY;
|
||||||
} else if (row.item.type === "color") {
|
} else if (row.item.type === "color") {
|
||||||
for (let i = 0; i < row.item.colors.length; i++) {
|
renderColor(ctx, row.item.colors, midX, wy);
|
||||||
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;
|
wy += itemSize + layout.card.gapY;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -225,7 +220,6 @@ export function drawSections(sections: Section[]) {
|
||||||
y += layout.background.gap;
|
y += layout.background.gap;
|
||||||
}
|
}
|
||||||
|
|
||||||
// watermark
|
|
||||||
ctx.drawImage(watermark, canvas.width - watermark.width - 1, 1);
|
ctx.drawImage(watermark, canvas.width - watermark.width - 1, 1);
|
||||||
|
|
||||||
return canvas;
|
return canvas;
|
||||||
|
|
|
||||||
11
src/mock.ts
11
src/mock.ts
|
|
@ -1,13 +1,16 @@
|
||||||
import { type CodeDiff, DiffType } from "./types";
|
import { type CodeDiff, DiffType } from "./types";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
code: new Map([
|
raw: new Map(),
|
||||||
|
semantic: new Map(),
|
||||||
|
icons: new Map(),
|
||||||
|
code: new Map<string, CodeDiff>([
|
||||||
[
|
[
|
||||||
"app/foomodule.tsx",
|
"app/foomodule.tsx",
|
||||||
{
|
{
|
||||||
type: DiffType.Added,
|
type: DiffType.Added,
|
||||||
size: 24782,
|
size: 24782,
|
||||||
} as CodeDiff,
|
},
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"app/ui/barmodule.ts",
|
"app/ui/barmodule.ts",
|
||||||
|
|
@ -15,14 +18,14 @@ export default {
|
||||||
type: DiffType.Renamed,
|
type: DiffType.Renamed,
|
||||||
oldName: "app/legacy/ui/bar_module.ts",
|
oldName: "app/legacy/ui/bar_module.ts",
|
||||||
size: 5212,
|
size: 5212,
|
||||||
} as CodeDiff,
|
},
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"app/intl/ReallyBigCoolComponent.tsx",
|
"app/intl/ReallyBigCoolComponent.tsx",
|
||||||
{
|
{
|
||||||
type: DiffType.Removed,
|
type: DiffType.Removed,
|
||||||
size: 46532,
|
size: 46532,
|
||||||
} as CodeDiff,
|
},
|
||||||
],
|
],
|
||||||
]),
|
]),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -35,8 +35,11 @@ export type CodeDiff =
|
||||||
size: number;
|
size: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Differs = Record<"semantic" | "raw" | "icons", Map<string, Diff> | undefined> & {
|
export type Differs = {
|
||||||
code: Map<string, CodeDiff> | undefined;
|
semantic: Map<string, Diff>;
|
||||||
|
raw: Map<string, Diff>;
|
||||||
|
icons: Map<string, Diff>;
|
||||||
|
code: Map<string, CodeDiff>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RawColors = Record<string, string>;
|
export type RawColors = Record<string, string>;
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,11 @@ export const gitChanged = new Set<string>();
|
||||||
export async function fetchGitChanged() {
|
export async function fetchGitChanged() {
|
||||||
for (const cwd of ["../data", "../canvas"] as const) {
|
for (const cwd of ["../data", "../canvas"] as const) {
|
||||||
try {
|
try {
|
||||||
const out = await Bun.$`git status -z`.cwd(cwd).quiet().nothrow().then((r) => r.text());
|
const out = await Bun.$`git status -z`
|
||||||
|
.cwd(cwd)
|
||||||
|
.quiet()
|
||||||
|
.nothrow()
|
||||||
|
.then((r) => r.text());
|
||||||
for (const changed of out
|
for (const changed of out
|
||||||
.split("\x00")
|
.split("\x00")
|
||||||
.filter((x) => x !== "")
|
.filter((x) => x !== "")
|
||||||
|
|
@ -16,35 +20,26 @@ export async function fetchGitChanged() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const gitQueue: Promise<void>[] = [];
|
let lastCommit: Promise<void> = Promise.resolve();
|
||||||
|
|
||||||
export async function commit(files: string[], message: string, cwd = "../data") {
|
export async function commit(files: string[], message: string, cwd = "../data") {
|
||||||
while (gitQueue[0]) {
|
lastCommit = lastCommit.then(async () => {
|
||||||
await gitQueue[0];
|
if (process.env.NODE_ENV === "test" && !commitAnyway) {
|
||||||
}
|
await fetchGitChanged();
|
||||||
|
} else {
|
||||||
const { promise, resolve } = Promise.withResolvers<void>();
|
await Bun.$`git restore --staged .`.cwd(cwd).nothrow().quiet().then(handleShellErr);
|
||||||
gitQueue.push(promise);
|
if (cwd === "../data") await fetchGitChanged();
|
||||||
|
await Bun.$`git add ${{ raw: files.map((x) => Bun.$.escape(x)).join(" ") }}`
|
||||||
if (process.env.NODE_ENV === "test" && !commitAnyway) {
|
.cwd(cwd)
|
||||||
await fetchGitChanged();
|
.nothrow()
|
||||||
} else {
|
.quiet()
|
||||||
await Bun.$`git restore --staged .`.cwd(cwd).nothrow().quiet().then(handleShellErr);
|
.then(handleShellErr);
|
||||||
if (cwd === "../data") await fetchGitChanged();
|
await Bun.$`git commit -m ${message}`
|
||||||
await Bun.$`git add ${{ raw: files.map((x) => Bun.$.escape(x)).join(" ") }}`
|
.cwd(cwd)
|
||||||
.cwd(cwd)
|
.nothrow()
|
||||||
.nothrow()
|
.quiet()
|
||||||
.quiet()
|
.then((e) => void e);
|
||||||
.then(handleShellErr);
|
}
|
||||||
await Bun.$`git commit -m ${message}`
|
});
|
||||||
.cwd(cwd)
|
return lastCommit;
|
||||||
.nothrow()
|
|
||||||
.quiet()
|
|
||||||
.then((e) => void e);
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve();
|
|
||||||
|
|
||||||
const promInd = gitQueue.indexOf(promise);
|
|
||||||
if (promInd !== -1) gitQueue.splice(promInd, 1);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import { exists, rm } from "node:fs/promises";
|
|
||||||
import mock from "../mock";
|
import mock from "../mock";
|
||||||
import type { Differs } from "../types";
|
import type { Differs } from "../types";
|
||||||
import { commit } from "./git";
|
import { commit } from "./git";
|
||||||
|
|
@ -8,7 +7,6 @@ import codeTask from "./tasks/code";
|
||||||
import colorsTask from "./tasks/colors";
|
import colorsTask from "./tasks/colors";
|
||||||
import decompile from "./tasks/decompile";
|
import decompile from "./tasks/decompile";
|
||||||
import diffs from "./tasks/diffs";
|
import diffs from "./tasks/diffs";
|
||||||
import iconsTask from "./tasks/icons";
|
|
||||||
import { formatError, handleShellErr, join } from "./utils";
|
import { formatError, handleShellErr, join } from "./utils";
|
||||||
|
|
||||||
export async function runTasks() {
|
export async function runTasks() {
|
||||||
|
|
@ -40,7 +38,9 @@ export async function runTasks() {
|
||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
let differs: Differs | undefined;
|
let differs: Differs = { raw: new Map(), semantic: new Map(), icons: new Map(), code: new Map() };
|
||||||
|
|
||||||
|
let gzipDone: Promise<void> | undefined;
|
||||||
|
|
||||||
if (!isMock) {
|
if (!isMock) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -50,11 +50,19 @@ export async function runTasks() {
|
||||||
await Bun.$`git reset --hard`.cwd("../data").nothrow().quiet().then(handleShellErr);
|
await Bun.$`git reset --hard`.cwd("../data").nothrow().quiet().then(handleShellErr);
|
||||||
await Bun.write("../data/version.txt", version);
|
await Bun.write("../data/version.txt", version);
|
||||||
// canvas branch may not exist in test
|
// canvas branch may not exist in test
|
||||||
await Bun.$`git reset --hard`.cwd("../canvas").nothrow().quiet().then(() => {});
|
await Bun.$`git reset --hard`
|
||||||
|
.cwd("../canvas")
|
||||||
|
.nothrow()
|
||||||
|
.quiet()
|
||||||
|
.then(() => {});
|
||||||
await Bun.write("../canvas/version.txt", version).catch(() => {});
|
await Bun.write("../canvas/version.txt", version).catch(() => {});
|
||||||
|
|
||||||
await Bun.$`git restore --staged .`.cwd("../data").nothrow().quiet().then(handleShellErr);
|
await Bun.$`git restore --staged .`.cwd("../data").nothrow().quiet().then(handleShellErr);
|
||||||
await Bun.$`git restore --staged .`.cwd("../canvas").nothrow().quiet().then(() => {});
|
await Bun.$`git restore --staged .`
|
||||||
|
.cwd("../canvas")
|
||||||
|
.nothrow()
|
||||||
|
.quiet()
|
||||||
|
.then(() => {});
|
||||||
|
|
||||||
progress.update("preinit_discard", true);
|
progress.update("preinit_discard", true);
|
||||||
|
|
||||||
|
|
@ -76,11 +84,11 @@ export async function runTasks() {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await wrapPromise(
|
gzipDone = (await wrapPromise(
|
||||||
decompile(progress, join(apksFolder, "base", "assets", "index.android.bundle")),
|
decompile(progress, join(apksFolder, "base", "assets", "index.android.bundle")),
|
||||||
progress,
|
progress,
|
||||||
"decompile",
|
"decompile",
|
||||||
);
|
)) as Promise<void> | undefined;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(`Failed to decompile!\n${e}`);
|
throw new Error(`Failed to decompile!\n${e}`);
|
||||||
}
|
}
|
||||||
|
|
@ -88,8 +96,7 @@ export async function runTasks() {
|
||||||
const code = (await Bun.file(codePath).text()).replace(/\r/g, "").split("\n");
|
const code = (await Bun.file(codePath).text()).replace(/\r/g, "").split("\n");
|
||||||
|
|
||||||
await wrapPromise(codeTask(progress, code), progress, "code");
|
await wrapPromise(codeTask(progress, code), progress, "code");
|
||||||
if (progress.someFailed("code"))
|
if (progress.someFailed("code")) throw new Error(`Failed at parser tasks!\n${progress.prettyErrors("code")}`);
|
||||||
throw new Error(`Failed at parser tasks!\n${progress.prettyErrors("code")}`);
|
|
||||||
// colors is non-critical, run in background
|
// colors is non-critical, run in background
|
||||||
wrapPromise(colorsTask(code), progress, "colors").catch((e) => {
|
wrapPromise(colorsTask(code), progress, "colors").catch((e) => {
|
||||||
progress.update("colors", false, String(e));
|
progress.update("colors", false, String(e));
|
||||||
|
|
@ -103,22 +110,25 @@ export async function runTasks() {
|
||||||
progress.update("icons", null);
|
progress.update("icons", null);
|
||||||
progress.update("diff_icons", null);
|
progress.update("diff_icons", null);
|
||||||
|
|
||||||
let waits = 0;
|
if (gzipDone) {
|
||||||
while (!progress.isFinished("decompile_gzip") && waits < 120) {
|
try {
|
||||||
await Bun.sleep(1000);
|
await gzipDone;
|
||||||
waits++;
|
} catch (e) {
|
||||||
}
|
if (progress.someFailed("decompile_gzip"))
|
||||||
if (!progress.isFinished("decompile_gzip")) {
|
throw new Error(`Failed at the decompile gzip task!\n${progress.prettyErrors("decompile_gzip")}`);
|
||||||
console.warn("decompile_gzip still not finished after 120s, continuing anyway");
|
throw e;
|
||||||
progress.update("decompile_gzip", true);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (progress.someFailed("decompile_gzip"))
|
if (progress.someFailed("decompile_gzip"))
|
||||||
throw new Error(`Failed at the decompile gzip task!\n${progress.prettyErrors("decompile_gzip")}`);
|
throw new Error(`Failed at the decompile gzip task!\n${progress.prettyErrors("decompile_gzip")}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
differs = await diffs(progress);
|
const result = await diffs(progress);
|
||||||
progress.update("diff", true);
|
if (result) {
|
||||||
|
differs = result;
|
||||||
|
progress.update("diff", true);
|
||||||
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
progress.update("diff", false, formatError(e));
|
progress.update("diff", false, formatError(e));
|
||||||
throw new Error(`Failed to generate diffs!\n${formatError(e)}`);
|
throw new Error(`Failed to generate diffs!\n${formatError(e)}`);
|
||||||
|
|
@ -127,7 +137,7 @@ export async function runTasks() {
|
||||||
differs = mock;
|
differs = mock;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (differs && !isQuiet) {
|
if (!isQuiet) {
|
||||||
try {
|
try {
|
||||||
const { webhook } = await import("./tasks/webhook");
|
const { webhook } = await import("./tasks/webhook");
|
||||||
await wrapPromise(webhook(differs), progress, "webhook");
|
await wrapPromise(webhook(differs), progress, "webhook");
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { mkdir, rm, readdir, stat } from "node:fs/promises";
|
import { mkdir, readdir, rm, stat } from "node:fs/promises";
|
||||||
import { join as pathJoin } from "node:path";
|
import { join as pathJoin } from "node:path";
|
||||||
import { commit } from "../git";
|
import { commit } from "../git";
|
||||||
import type { Progress } from "../progress";
|
import type { Progress } from "../progress";
|
||||||
|
|
@ -13,7 +13,10 @@ function extractModuleId(filename: string): number | undefined {
|
||||||
}
|
}
|
||||||
|
|
||||||
function sanitizePath(p: string): string {
|
function sanitizePath(p: string): string {
|
||||||
return p.replace(/\\/g, "/").replace(/^\.\.\//, "").replace(/[<>:"|?*]/g, "_");
|
return p
|
||||||
|
.replace(/\\/g, "/")
|
||||||
|
.replace(/^\.\.\//, "")
|
||||||
|
.replace(/[<>:"|?*]/g, "_");
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function code(progress: Progress, _code: string[]) {
|
export default async function code(progress: Progress, _code: string[]) {
|
||||||
|
|
|
||||||
|
|
@ -28,19 +28,34 @@ function evalRawModule(snippet: string): any {
|
||||||
code = code.replace(/set\.fileFinishedImporting/g, "__importSet.fileFinishedImporting");
|
code = code.replace(/set\.fileFinishedImporting/g, "__importSet.fileFinishedImporting");
|
||||||
code = code.replace(/export\s+const\s+_private/, "const _private");
|
code = code.replace(/export\s+const\s+_private/, "const _private");
|
||||||
code = code + "\n; _private;";
|
code = code + "\n; _private;";
|
||||||
return runInNewContext(code, {});
|
const result = runInNewContext(code, {});
|
||||||
|
if (!result || typeof result !== "object" || !result.RawColors || typeof result.RawColors !== "object") {
|
||||||
|
throw new Error(
|
||||||
|
"VM evaluation failed: RawColors not found or invalid shape. The regex transforms may not match the current decompiler output.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function evalSemanticModule(snippet: string): any {
|
function evalSemanticModule(snippet: string): any {
|
||||||
let code = snippet;
|
let code = snippet;
|
||||||
code = code.replace(/^import set from "set".*$/m, "const __importSet = { fileFinishedImporting: () => {} };");
|
code = code.replace(/^import set from "set".*$/m, "const __importSet = { fileFinishedImporting: () => {} };");
|
||||||
code = code.replace(/^import items from "items".*$/m, "const items = { _private: { Themes: new Proxy({}, { get(_, k){ return k.toString().toLowerCase(); } }) } };");
|
code = code.replace(
|
||||||
|
/^import items from "items".*$/m,
|
||||||
|
"const items = { _private: { Themes: new Proxy({}, { get(_, k){ return k.toString().toLowerCase(); } }) } };",
|
||||||
|
);
|
||||||
code = code.replace(/^import.*$/gm, "");
|
code = code.replace(/^import.*$/gm, "");
|
||||||
code = code.replace(/set\.fileFinishedImporting/g, "__importSet.fileFinishedImporting");
|
code = code.replace(/set\.fileFinishedImporting/g, "__importSet.fileFinishedImporting");
|
||||||
code = code.replace(/export\s+const\s+_private/, "const _private");
|
code = code.replace(/export\s+const\s+_private/, "const _private");
|
||||||
code = code + "\n; _private;";
|
code = code + "\n; _private;";
|
||||||
code = fixHermesDecIndex(code);
|
code = fixHermesDecIndex(code);
|
||||||
return runInNewContext(code, {});
|
const result = runInNewContext(code, {});
|
||||||
|
if (!result || typeof result !== "object" || !result.SemanticColors || typeof result.SemanticColors !== "object") {
|
||||||
|
throw new Error(
|
||||||
|
"VM evaluation failed: SemanticColors not found or invalid shape. The regex transforms may not match the current decompiler output.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getInternalRawColors(code: string[]) {
|
export function getInternalRawColors(code: string[]) {
|
||||||
|
|
@ -59,7 +74,9 @@ export function getInternalSemanticColors(code: string[], raw: Record<string, st
|
||||||
if (!snippet) throw new Error("Cannot find semantic definitions module");
|
if (!snippet) throw new Error("Cannot find semantic definitions module");
|
||||||
const mod = evalSemanticModule(snippet);
|
const mod = evalSemanticModule(snippet);
|
||||||
const semantic: SemanticColors = {};
|
const semantic: SemanticColors = {};
|
||||||
for (const [key, colors] of Object.entries(mod.SemanticColors as Record<string, Record<string, { raw: string; opacity: number }>>)) {
|
for (const [key, colors] of Object.entries(
|
||||||
|
mod.SemanticColors as Record<string, Record<string, { raw: string; opacity: number }>>,
|
||||||
|
)) {
|
||||||
const color: SemanticColors[string] = {};
|
const color: SemanticColors[string] = {};
|
||||||
for (const [theme, val] of Object.entries(colors)) {
|
for (const [theme, val] of Object.entries(colors)) {
|
||||||
if (typeof val !== "object" || !("raw" in val)) continue;
|
if (typeof val !== "object" || !("raw" in val)) continue;
|
||||||
|
|
@ -88,5 +105,9 @@ export default async function colors(code: string[]) {
|
||||||
await Bun.write("../canvas/semantic.json", JSON.stringify(sortObj(semantic), null, 4));
|
await Bun.write("../canvas/semantic.json", JSON.stringify(sortObj(semantic), null, 4));
|
||||||
await Bun.write("../canvas/semantic_simple.json", JSON.stringify(sortObj(convertSimpleSemantic(semantic)), null, 4));
|
await Bun.write("../canvas/semantic_simple.json", JSON.stringify(sortObj(convertSimpleSemantic(semantic)), null, 4));
|
||||||
|
|
||||||
await commit(["raw.json", "semantic.json", "semantic_simple.json"], `chore: update colors for ${cuteVersion}`, "../canvas");
|
await commit(
|
||||||
|
["raw.json", "semantic.json", "semantic_simple.json"],
|
||||||
|
`chore: update colors for ${cuteVersion}`,
|
||||||
|
"../canvas",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,18 +26,11 @@ export default async function decompile(progress: Progress, pathToBundle: string
|
||||||
progress.start("decompile_decompiling");
|
progress.start("decompile_decompiling");
|
||||||
const decompilerBin = join(pathToDecompiler, "target/release/hermes-decomp");
|
const decompilerBin = join(pathToDecompiler, "target/release/hermes-decomp");
|
||||||
if (!(await Bun.file(decompilerBin).exists())) {
|
if (!(await Bun.file(decompilerBin).exists())) {
|
||||||
await Bun.$`cargo build --release -p hbc-decomp-cli`
|
await Bun.$`cargo build --release -p hbc-decomp-cli`.cwd(pathToDecompiler).quiet().nothrow().then(handleShellErr);
|
||||||
.cwd(pathToDecompiler)
|
|
||||||
.quiet()
|
|
||||||
.nothrow()
|
|
||||||
.then(handleShellErr);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(await Bun.file(codePath).exists())) {
|
if (!(await Bun.file(codePath).exists())) {
|
||||||
await Bun.$`${decompilerBin} decompile ${pathToBundle} --output ${codePath}`
|
await Bun.$`${decompilerBin} decompile ${pathToBundle} --output ${codePath}`.quiet().nothrow().then(handleShellErr);
|
||||||
.quiet()
|
|
||||||
.nothrow()
|
|
||||||
.then(handleShellErr);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(await exists(modulesPath))) {
|
if (!(await exists(modulesPath))) {
|
||||||
|
|
@ -70,15 +63,20 @@ export default async function decompile(progress: Progress, pathToBundle: string
|
||||||
|
|
||||||
const gzipper = new Worker(gzipWorkerURL);
|
const gzipper = new Worker(gzipWorkerURL);
|
||||||
progress.start("decompile_gzip");
|
progress.start("decompile_gzip");
|
||||||
|
const { promise: gzipDone, resolve: resolveGzip } = Promise.withResolvers<void>();
|
||||||
gzipper.addEventListener("message", async ({ data }) => {
|
gzipper.addEventListener("message", async ({ data }) => {
|
||||||
if (data === true) {
|
if (data === true) {
|
||||||
await commit([gzFile, "module-paths.json", "index.android.bundle"], `chore: update decompiled code for ${cuteVersion}`);
|
await commit(
|
||||||
|
[gzFile, "module-paths.json", "index.android.bundle"],
|
||||||
|
`chore: update decompiled code for ${cuteVersion}`,
|
||||||
|
);
|
||||||
progress.update("decompile_gzip", true);
|
progress.update("decompile_gzip", true);
|
||||||
}
|
}
|
||||||
gzipper.terminate();
|
gzipper.terminate();
|
||||||
|
resolveGzip();
|
||||||
});
|
});
|
||||||
gzipper.postMessage({ path: codePath, target: join("../data", gzFile) });
|
gzipper.postMessage({ path: codePath, target: join("../data", gzFile) });
|
||||||
} else {
|
return gzipDone;
|
||||||
progress.update("decompile_gzip", null);
|
|
||||||
}
|
}
|
||||||
|
progress.update("decompile_gzip", null);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,185 +12,183 @@ import { type Progress, wrapPromise } from "../progress";
|
||||||
import { diffAnyway, prevFiles } from "../shared";
|
import { diffAnyway, prevFiles } from "../shared";
|
||||||
import { join, parseJsonl } from "../utils";
|
import { join, parseJsonl } from "../utils";
|
||||||
|
|
||||||
async function diffRaw() {
|
interface DiffOpts<TOld, TNew> {
|
||||||
if (!prevFiles.has("raw.json")) {
|
getSource: (key: string, val: TNew) => string;
|
||||||
throw new Error("Missing prevFile: raw.json");
|
getLabel?: (key: string, val: TNew) => string;
|
||||||
}
|
getOldSource?: (key: string, val: TOld) => string;
|
||||||
|
getOldLabel?: (key: string, val: TOld) => string;
|
||||||
|
isRename: (newKey: string, oldVal: TOld, newVal: TNew) => boolean;
|
||||||
|
isChanged?: (oldVal: TOld, newVal: TNew) => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
const oldRaw = JSON.parse(new TextDecoder().decode(prevFiles.get("raw.json"))) as RawColors;
|
function diffEntries<TOld, TNew>(
|
||||||
const newRaw: RawColors = await Bun.file(join("../canvas", "raw.json")).json();
|
oldEntries: Record<string, TOld>,
|
||||||
|
newEntries: Record<string, TNew>,
|
||||||
const renamed = new Set<string>();
|
opts: DiffOpts<TOld, TNew>,
|
||||||
|
): Map<string, Diff> {
|
||||||
const changes = new Map<string, Diff>();
|
const changes = new Map<string, Diff>();
|
||||||
for (const [name, source] of Object.entries(newRaw))
|
const renamed = new Set<string>();
|
||||||
if (!oldRaw[name]) {
|
|
||||||
const oldName = Object.entries(oldRaw).find(([key, val]) => !newRaw[key] && val === source)?.[0];
|
|
||||||
|
|
||||||
if (!oldName)
|
for (const [name, newVal] of Object.entries(newEntries)) {
|
||||||
|
const oldVal = oldEntries[name];
|
||||||
|
if (oldVal === undefined) {
|
||||||
|
const oldName = Object.entries(oldEntries).find(
|
||||||
|
([key, val]) => !(key in newEntries) && opts.isRename(name, val, newVal),
|
||||||
|
)?.[0];
|
||||||
|
|
||||||
|
if (!oldName) {
|
||||||
changes.set(name, {
|
changes.set(name, {
|
||||||
type: DiffType.Added,
|
type: DiffType.Added,
|
||||||
source,
|
source: opts.getSource(name, newVal),
|
||||||
|
label: opts.getLabel?.(name, newVal),
|
||||||
});
|
});
|
||||||
else {
|
} else {
|
||||||
renamed.add(oldName);
|
renamed.add(oldName);
|
||||||
changes.set(name, {
|
changes.set(name, { type: DiffType.Renamed, oldName, source: opts.getSource(name, newVal) });
|
||||||
type: DiffType.Renamed,
|
|
||||||
oldName,
|
|
||||||
source,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} else if (oldRaw[name].toLowerCase() !== source.toLowerCase())
|
} else if (opts.isChanged?.(oldVal, newVal)) {
|
||||||
changes.set(name, {
|
changes.set(name, {
|
||||||
type: DiffType.Changed,
|
type: DiffType.Changed,
|
||||||
source,
|
source: opts.getSource(name, newVal),
|
||||||
oldSource: oldRaw[name],
|
label: opts.getLabel?.(name, newVal),
|
||||||
});
|
oldSource: opts.getOldSource?.(name, oldVal) ?? "",
|
||||||
for (const [name, source] of Object.entries(oldRaw))
|
oldLabel: opts.getOldLabel?.(name, oldVal),
|
||||||
if (!(newRaw[name] || renamed.has(name)))
|
|
||||||
changes.set(name, {
|
|
||||||
type: DiffType.Removed,
|
|
||||||
source,
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [name, oldVal] of Object.entries(oldEntries)) {
|
||||||
|
if (name in newEntries || renamed.has(name)) continue;
|
||||||
|
changes.set(name, {
|
||||||
|
type: DiffType.Removed,
|
||||||
|
source: opts.getOldSource?.(name, oldVal) ?? "",
|
||||||
|
label: opts.getOldLabel?.(name, oldVal),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return changes;
|
return changes;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSemanticLabel(semantic: any) {
|
function diffEntriesCode(
|
||||||
const light = semantic.light,
|
oldEntries: Record<string, { size: number; compSize: number }>,
|
||||||
dark = semantic.darker ?? semantic.dark;
|
newEntries: Record<string, { size: number; compSize: number }>,
|
||||||
if (dark && light) return `☀️ ${light[0]}, 🌙 ${dark[0]}`;
|
): Map<string, CodeDiff> {
|
||||||
|
const changes = new Map<string, CodeDiff>();
|
||||||
|
const renamed = new Set<string>();
|
||||||
|
|
||||||
|
for (const [name, newVal] of Object.entries(newEntries)) {
|
||||||
|
if (!(name in oldEntries)) {
|
||||||
|
const oldName = Object.entries(oldEntries).find(
|
||||||
|
([key, val]) => !(key in newEntries) && val.compSize === newVal.compSize,
|
||||||
|
)?.[0];
|
||||||
|
|
||||||
|
if (!oldName) {
|
||||||
|
changes.set(name, { type: DiffType.Added, size: newVal.size });
|
||||||
|
} else {
|
||||||
|
renamed.add(oldName);
|
||||||
|
changes.set(name, { type: DiffType.Renamed, oldName, size: newVal.size });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [name, oldVal] of Object.entries(oldEntries)) {
|
||||||
|
if (!(name in newEntries || renamed.has(name))) {
|
||||||
|
changes.set(name, { type: DiffType.Removed, size: oldVal.size });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
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("../canvas", "raw.json")).json();
|
||||||
|
|
||||||
|
return diffEntries(oldRaw, newRaw, {
|
||||||
|
getSource: (_, val) => val,
|
||||||
|
isRename: (newKey, oldVal, newVal) => !(newKey in newRaw) && oldVal === newVal,
|
||||||
|
isChanged: (oldVal, newVal) => oldVal.toLowerCase() !== newVal.toLowerCase(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSemanticLabel(semantic: Record<string, [string, unknown]>) {
|
||||||
|
const light = semantic.light;
|
||||||
|
const dark = semantic.darker ?? semantic.dark;
|
||||||
|
if (dark && light) return `${light[0]}, ${dark[0]}`;
|
||||||
return Object.entries(semantic)
|
return Object.entries(semantic)
|
||||||
.map(([name, [color]]) => `${name} ${color}`)
|
.map(([name, [color]]) => `${name} ${color}`)
|
||||||
.join(", ");
|
.join(", ");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function diffSemantic() {
|
async function diffSemantic() {
|
||||||
if (!prevFiles.has("semantic.json")) {
|
if (!prevFiles.has("semantic.json")) throw new Error("Missing prevFile: semantic.json");
|
||||||
throw new Error("Missing prevFile: semantic.json");
|
|
||||||
}
|
|
||||||
|
|
||||||
const oldSemantic = JSON.parse(new TextDecoder().decode(prevFiles.get("semantic.json"))) as SemanticColors;
|
const oldSemantic = JSON.parse(new TextDecoder().decode(prevFiles.get("semantic.json"))) as SemanticColors;
|
||||||
const newSemantic: SemanticColors = await Bun.file(join("../canvas", "semantic.json")).json();
|
const newSemantic: SemanticColors = await Bun.file(join("../canvas", "semantic.json")).json();
|
||||||
|
|
||||||
const renamed = new Set<string>();
|
const changes = diffEntries(oldSemantic, newSemantic, {
|
||||||
const changes = new Map<string, Diff>();
|
getSource: (_, val) =>
|
||||||
for (const [name, semantic] of Object.entries(newSemantic)) {
|
Object.values(val)
|
||||||
const source = Object.values(semantic)
|
.map(([color]) => color)
|
||||||
.map(([color]) => color)
|
.join(","),
|
||||||
.join(",");
|
getLabel: (_, val) => getSemanticLabel(val),
|
||||||
if (!oldSemantic[name]) {
|
isRename: (newKey, oldVal, newVal) => {
|
||||||
const themes = Object.keys(semantic).length;
|
const themes = Object.keys(newVal).length;
|
||||||
const oldName = Object.entries(oldSemantic).find(
|
return (
|
||||||
([key, val]) =>
|
!(newKey in newSemantic) &&
|
||||||
!newSemantic[key] &&
|
Object.keys(oldVal).length === themes &&
|
||||||
Object.keys(val).length === themes &&
|
Object.entries(oldVal).every(([skey, sval]) => sval[0] === newVal[skey]?.[0])
|
||||||
Object.entries(val).every(([skey, sval]) => sval[0] === semantic[skey][0]),
|
);
|
||||||
)?.[0];
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (!oldName)
|
for (const [name, semantic] of Object.entries(newSemantic)) {
|
||||||
changes.set(name, {
|
if (oldSemantic[name]) {
|
||||||
type: DiffType.Added,
|
for (const [theme, [color]] of Object.entries(semantic)) {
|
||||||
source,
|
if (!oldSemantic[name][theme]) {
|
||||||
label: getSemanticLabel(semantic),
|
changes.set(`${name}.${theme}`, { type: DiffType.Added, source: color });
|
||||||
});
|
} else if (oldSemantic[name][theme][0].toLowerCase() !== color.toLowerCase()) {
|
||||||
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}`, {
|
changes.set(`${name}.${theme}`, {
|
||||||
type: DiffType.Changed,
|
type: DiffType.Changed,
|
||||||
source: color,
|
source: color,
|
||||||
oldSource: oldSemantic[name][theme][0],
|
oldSource: oldSemantic[name][theme][0],
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (const [name, semantic] of Object.entries(oldSemantic)) {
|
|
||||||
if (renamed.has(name)) continue;
|
|
||||||
|
|
||||||
const source = Object.values(semantic)
|
for (const [name, semantic] of Object.entries(oldSemantic)) {
|
||||||
.map(([color]) => color)
|
if (name in newSemantic) {
|
||||||
.join(",");
|
for (const [theme, [source]] of Object.entries(semantic)) {
|
||||||
if (!newSemantic[name])
|
if (!newSemantic[name][theme]) {
|
||||||
changes.set(name, {
|
changes.set(`${name}.${theme}`, { type: DiffType.Removed, source });
|
||||||
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;
|
return changes;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function diffIcons() {
|
async function diffIcons() {
|
||||||
if (!prevFiles.has("icons.json")) {
|
if (!prevFiles.has("icons.json")) throw new Error("Missing prevFile: icons.json");
|
||||||
throw new Error("Missing prevFile: icons.json");
|
|
||||||
}
|
|
||||||
|
|
||||||
const oldIcons = JSON.parse(new TextDecoder().decode(prevFiles.get("icons.json"))) as Icons;
|
const oldIcons = JSON.parse(new TextDecoder().decode(prevFiles.get("icons.json"))) as Icons;
|
||||||
const newIcons: Icons = await Bun.file(join("../canvas", "icons.json")).json();
|
const newIcons: Icons = await Bun.file(join("../canvas", "icons.json")).json();
|
||||||
|
|
||||||
const iconDir = {
|
const iconDir = { old: join("../canvas", "oldicons"), new: join("../canvas", "icons") };
|
||||||
old: join("../canvas", "oldicons"),
|
|
||||||
new: join("../canvas", "icons"),
|
|
||||||
};
|
|
||||||
|
|
||||||
const renamed = new Set<string>();
|
return diffEntries(oldIcons, newIcons, {
|
||||||
const changes = new Map<string, Diff>();
|
getSource: (_, val) => join(iconDir.new, val.file),
|
||||||
for (const [name, icon] of Object.entries(newIcons)) {
|
getLabel: (_, val) => val.hash.slice(0, 8),
|
||||||
const label = icon.hash.slice(0, 8);
|
getOldSource: (_, val) => join(iconDir.old, val.file),
|
||||||
if (!oldIcons[name]) {
|
getOldLabel: (_, val) => val.hash.slice(0, 8),
|
||||||
const oldName = Object.entries(oldIcons).find(([key, val]) => !newIcons[key] && val.hash === icon.hash)?.[0];
|
isRename: (newKey, oldVal, newVal) => !(newKey in newIcons) && oldVal.hash === newVal.hash,
|
||||||
|
isChanged: (oldVal, newVal) => oldVal.hash !== newVal.hash,
|
||||||
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) {
|
function parseSource(text: string) {
|
||||||
|
|
@ -201,42 +199,12 @@ function parseSource(text: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function diffCode() {
|
async function diffCode() {
|
||||||
if (!prevFiles.has("source.jsonl")) {
|
if (!prevFiles.has("source.jsonl")) throw new Error("Missing prevFile: source.jsonl");
|
||||||
throw new Error("Missing prevFile: source.jsonl");
|
|
||||||
}
|
|
||||||
|
|
||||||
const oldCode = parseSource(new TextDecoder().decode(prevFiles.get("source.jsonl")));
|
const oldCode = parseSource(new TextDecoder().decode(prevFiles.get("source.jsonl")));
|
||||||
const newCode = parseSource(await Bun.file(join("../data", "source.jsonl")).text());
|
const newCode = parseSource(await Bun.file(join("../data", "source.jsonl")).text());
|
||||||
|
|
||||||
const renamed = new Set<string>();
|
return diffEntriesCode(oldCode, newCode);
|
||||||
const changes = new Map<string, CodeDiff>();
|
|
||||||
for (const [name, { size, compSize }] of Object.entries(newCode)) {
|
|
||||||
if (!oldCode[name]) {
|
|
||||||
const oldName = Object.entries(oldCode).find(([key, val]) => !newCode[key] && val.compSize === compSize)?.[0];
|
|
||||||
|
|
||||||
if (!oldName)
|
|
||||||
changes.set(name, {
|
|
||||||
type: DiffType.Added,
|
|
||||||
size,
|
|
||||||
});
|
|
||||||
else {
|
|
||||||
renamed.add(oldName);
|
|
||||||
changes.set(name, {
|
|
||||||
type: DiffType.Renamed,
|
|
||||||
oldName,
|
|
||||||
size,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const [name, { size }] of Object.entries(oldCode))
|
|
||||||
if (!(newCode[name] || renamed.has(name)))
|
|
||||||
changes.set(name, {
|
|
||||||
type: DiffType.Removed,
|
|
||||||
size,
|
|
||||||
});
|
|
||||||
|
|
||||||
return changes;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function diffs(progress: Progress) {
|
export default async function diffs(progress: Progress) {
|
||||||
|
|
@ -251,7 +219,7 @@ export default async function diffs(progress: Progress) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const differs = {} as Differs;
|
const differs: Differs = { raw: new Map(), semantic: new Map(), icons: new Map(), code: new Map() };
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
gitChanged.has("raw.json")
|
gitChanged.has("raw.json")
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import { drawSections } from "../../canvas";
|
||||||
import { makeSections } from "../../canvas/factory";
|
import { makeSections } from "../../canvas/factory";
|
||||||
import { type CodeDiff, type Diff, type Differs, DiffType } from "../../types";
|
import { type CodeDiff, type Diff, type Differs, DiffType } from "../../types";
|
||||||
import { cuteVersion, maxDiffChanges, version } from "../shared";
|
import { cuteVersion, maxDiffChanges, version } from "../shared";
|
||||||
import { assert, formatBytes, sortEntries } from "../utils";
|
import { formatBytes, sortEntries } from "../utils";
|
||||||
|
|
||||||
function fileBase(path: string, other?: string) {
|
function fileBase(path: string, other?: string) {
|
||||||
if (!other) return `... ${basename(path)}`;
|
if (!other) return `... ${basename(path)}`;
|
||||||
|
|
@ -49,43 +49,39 @@ interface FormattedDiff {
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDiff(diffs: Map<string, Diff | CodeDiff>): FormattedDiff {
|
function formatDiff(diffs: Map<string, Diff | CodeDiff>): FormattedDiff {
|
||||||
const entries = [...diffs.entries()];
|
const entries = sortEntries([...diffs.entries()]);
|
||||||
|
|
||||||
const sections = {
|
const sections = {
|
||||||
Added: sortEntries(entries)
|
Added: entries
|
||||||
.map(
|
.filter(([, diff]) => diff.type === DiffType.Added)
|
||||||
([name, diff]) =>
|
.map(([name, diff]) => {
|
||||||
diff.type === DiffType.Added &&
|
if ("size" in diff) return `+ ${fileBase(name)} (${formatBytes(diff.size)})`;
|
||||||
("size" in diff
|
const d = diff as Extract<Diff, { type: DiffType.Added | DiffType.Removed }>;
|
||||||
? `+ ${fileBase(name)} (${formatBytes(diff.size)})`
|
return `+ ${name}: ${d.label || d.source}`;
|
||||||
: `+ ${name}: ${diff.label || diff.source}`),
|
}),
|
||||||
)
|
Changed: entries
|
||||||
.filter((x) => typeof x === "string"),
|
.filter(([, diff]) => diff.type === DiffType.Changed)
|
||||||
Changed: sortEntries(entries)
|
.map(([name, diff]) => {
|
||||||
.map(
|
const d = diff as Extract<Diff, { type: DiffType.Changed }>;
|
||||||
([name, diff]) =>
|
return `- ${name}: ${d.oldLabel || d.oldSource}\n+ ${name}: ${d.label || d.source}`;
|
||||||
diff.type === DiffType.Changed &&
|
}),
|
||||||
`- ${name}: ${diff.oldLabel || diff.oldSource}\n+ ${name}: ${diff.label || diff.source}`,
|
Renamed: entries
|
||||||
)
|
.filter(([, diff]) => diff.type === DiffType.Renamed)
|
||||||
.filter((x) => typeof x === "string"),
|
.map(([name, diff]) => {
|
||||||
Renamed: sortEntries(entries)
|
if ("size" in diff) {
|
||||||
.map(
|
const cd = diff as Extract<CodeDiff, { type: DiffType.Renamed }>;
|
||||||
([name, diff]) =>
|
return `- ${fileBase(cd.oldName, name)}\n+ ${fileBase(name, cd.oldName)}`;
|
||||||
diff.type === DiffType.Renamed &&
|
}
|
||||||
("size" in diff
|
const d = diff as Extract<Diff, { type: DiffType.Renamed }>;
|
||||||
? `- ${fileBase(diff.oldName, name)}\n+ ${fileBase(name, diff.oldName)}`
|
return `- ${d.oldName}\n+ ${name}`;
|
||||||
: `- ${diff.oldName}\n+ ${name}`),
|
}),
|
||||||
)
|
Removed: entries
|
||||||
.filter((x) => typeof x === "string"),
|
.filter(([, diff]) => diff.type === DiffType.Removed)
|
||||||
Removed: sortEntries(entries)
|
.map(([name, diff]) => {
|
||||||
.map(
|
if ("size" in diff) return `- ${fileBase(name)} (${formatBytes(diff.size)})`;
|
||||||
([name, diff]) =>
|
const d = diff as Extract<Diff, { type: DiffType.Added | DiffType.Removed }>;
|
||||||
diff.type === DiffType.Removed &&
|
return `- ${name}: ${d.label || d.source}`;
|
||||||
("size" in diff
|
}),
|
||||||
? `- ${fileBase(name)} (${formatBytes(diff.size)})`
|
|
||||||
: `- ${name}: ${diff.label || diff.source}`),
|
|
||||||
)
|
|
||||||
.filter((x) => typeof x === "string"),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const text = Object.entries(sections).filter(([, lines]) => lines.length);
|
const text = Object.entries(sections).filter(([, lines]) => lines.length);
|
||||||
|
|
@ -214,7 +210,7 @@ async function sendWebhook(webhook: string, role: string, embeds: WebhookEmbed[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function webhook(diffs: Differs) {
|
export async function webhook(diffs: Differs) {
|
||||||
if (diffs.raw?.size || diffs.semantic?.size) {
|
if (diffs.raw.size || diffs.semantic.size) {
|
||||||
const wh = process.env.color_webhook;
|
const wh = process.env.color_webhook;
|
||||||
if (!wh) {
|
if (!wh) {
|
||||||
console.warn("Missing color_webhook, skipping color webhook");
|
console.warn("Missing color_webhook, skipping color webhook");
|
||||||
|
|
@ -223,14 +219,14 @@ export async function webhook(diffs: Differs) {
|
||||||
wh,
|
wh,
|
||||||
"1227327297795657850",
|
"1227327297795657850",
|
||||||
[
|
[
|
||||||
diffs.raw?.size && {
|
diffs.raw.size && {
|
||||||
title: "Raw colors",
|
title: "Raw colors",
|
||||||
body: formatDiff(diffs.raw),
|
body: formatDiff(diffs.raw),
|
||||||
image: drawSections(await makeSections(diffs.raw)),
|
image: drawSections(await makeSections(diffs.raw)),
|
||||||
footer: makeFooter(diffs.raw.size, "raw color"),
|
footer: makeFooter(diffs.raw.size, "raw color"),
|
||||||
key: "raw-colors",
|
key: "raw-colors",
|
||||||
},
|
},
|
||||||
diffs.semantic?.size && {
|
diffs.semantic.size && {
|
||||||
title: "Semantic colors",
|
title: "Semantic colors",
|
||||||
body: formatDiff(diffs.semantic),
|
body: formatDiff(diffs.semantic),
|
||||||
image: drawSections(await makeSections(diffs.semantic)),
|
image: drawSections(await makeSections(diffs.semantic)),
|
||||||
|
|
@ -243,7 +239,7 @@ export async function webhook(diffs: Differs) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (diffs.icons?.size) {
|
if (diffs.icons.size) {
|
||||||
const wh = process.env.icons_webhook;
|
const wh = process.env.icons_webhook;
|
||||||
if (!wh) {
|
if (!wh) {
|
||||||
console.warn("Missing icons_webhook, skipping");
|
console.warn("Missing icons_webhook, skipping");
|
||||||
|
|
@ -265,7 +261,7 @@ export async function webhook(diffs: Differs) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (diffs.code?.size) {
|
if (diffs.code.size) {
|
||||||
const wh = process.env.code_webhook;
|
const wh = process.env.code_webhook;
|
||||||
if (!wh) {
|
if (!wh) {
|
||||||
console.warn("Missing code_webhook, skipping");
|
console.warn("Missing code_webhook, skipping");
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue