Initial Commit
This commit is contained in:
143
packages/devtools/tooling/electrobun/src/icons/icons.ts
Normal file
143
packages/devtools/tooling/electrobun/src/icons/icons.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import { existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import {
|
||||
ELECTROBUN_CODES,
|
||||
ElectrobunError,
|
||||
} from "../error-codes/error-codes.js";
|
||||
import {
|
||||
GenerateIconsOptionsSchema,
|
||||
parseOrThrow,
|
||||
} from "../schemas/schemas.js";
|
||||
import { recordIcons } from "../telemetry/telemetry.js";
|
||||
import type {
|
||||
IconRendererAdapter,
|
||||
IconRendererInput,
|
||||
IconReport,
|
||||
} from "../types/electrobun.types.js";
|
||||
|
||||
const ICNS_SIZES = [16, 32, 64, 128, 256, 512, 1024] as const;
|
||||
const ICO_SIZES = [16, 32, 48, 64, 128, 256] as const;
|
||||
const PNG_SIZES = [16, 32, 48, 64, 128, 256, 512, 1024] as const;
|
||||
|
||||
interface IconzModule {
|
||||
createIconz(input: {
|
||||
input: string;
|
||||
output: string;
|
||||
icons: Record<
|
||||
string,
|
||||
{
|
||||
type: string;
|
||||
name: string;
|
||||
sizes: readonly number[];
|
||||
folder?: string;
|
||||
}
|
||||
>;
|
||||
}): { generate(): Promise<unknown> };
|
||||
}
|
||||
|
||||
/**
|
||||
* Default icon renderer backed by the `iconz` package. Loads the upstream
|
||||
* library lazily so test environments that swap in a recording adapter
|
||||
* never pay the import cost.
|
||||
*/
|
||||
export const iconzRendererAdapter: IconRendererAdapter = {
|
||||
async render(input: IconRendererInput) {
|
||||
const mod = (await import("iconz")) as unknown as IconzModule;
|
||||
const iconz = mod.createIconz({
|
||||
input: input.input,
|
||||
output: input.output,
|
||||
icons: {
|
||||
appIcns: {
|
||||
type: "icns",
|
||||
name: input.icons.icns.name,
|
||||
sizes: input.icons.icns.sizes,
|
||||
},
|
||||
appIco: {
|
||||
type: "ico",
|
||||
name: input.icons.ico.name,
|
||||
sizes: input.icons.ico.sizes,
|
||||
},
|
||||
pngIcons: {
|
||||
type: "png",
|
||||
name: input.icons.png.name,
|
||||
sizes: input.icons.png.sizes,
|
||||
folder: ".",
|
||||
},
|
||||
},
|
||||
});
|
||||
await iconz.generate();
|
||||
const files = [
|
||||
`${input.icons.icns.name}.icns`,
|
||||
`${input.icons.ico.name}.ico`,
|
||||
...input.icons.png.sizes.map(
|
||||
(size) =>
|
||||
`${input.icons.png.name.replace("{{dims}}", String(size))}.png`
|
||||
),
|
||||
];
|
||||
return { files };
|
||||
},
|
||||
};
|
||||
|
||||
export interface GenerateIconsOptions {
|
||||
readonly inputPath: string;
|
||||
readonly outputDir: string;
|
||||
}
|
||||
|
||||
export interface GenerateIconsDeps {
|
||||
readonly renderer?: IconRendererAdapter;
|
||||
readonly fileExists?: (path: string) => boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the icon pipeline against the user's input image. The default
|
||||
* renderer is `iconz`; tests may inject a recording fake to assert the
|
||||
* `IconRendererInput` shape. Returns an `IconReport`; raises
|
||||
* `ELECTROBUN.ICON_SOURCE_MISSING` when the input path does not exist,
|
||||
* and wraps renderer throws as `ELECTROBUN.ICON_RENDER_FAILED` (the
|
||||
* report's `error` field carries the underlying message).
|
||||
*/
|
||||
export async function generateIcons(
|
||||
options: GenerateIconsOptions,
|
||||
deps: GenerateIconsDeps = {}
|
||||
): Promise<IconReport> {
|
||||
const parsed = parseOrThrow(
|
||||
GenerateIconsOptionsSchema,
|
||||
options,
|
||||
"GenerateIconsOptions"
|
||||
);
|
||||
const fileExists = deps.fileExists ?? ((p: string) => existsSync(p));
|
||||
if (!fileExists(parsed.inputPath)) {
|
||||
recordIcons("failed");
|
||||
throw new ElectrobunError(
|
||||
ELECTROBUN_CODES.ICON_SOURCE_MISSING,
|
||||
`icon source not found: ${parsed.inputPath}`,
|
||||
{ details: { inputPath: parsed.inputPath } }
|
||||
);
|
||||
}
|
||||
const renderer = deps.renderer ?? iconzRendererAdapter;
|
||||
const iconsDir = join(parsed.outputDir, "icons");
|
||||
try {
|
||||
const { files } = await renderer.render({
|
||||
input: parsed.inputPath,
|
||||
output: iconsDir,
|
||||
icons: {
|
||||
icns: { name: "app", sizes: ICNS_SIZES },
|
||||
ico: { name: "app", sizes: ICO_SIZES },
|
||||
png: { name: "icon-{{dims}}", sizes: PNG_SIZES },
|
||||
},
|
||||
});
|
||||
recordIcons("generated");
|
||||
return {
|
||||
generated: true,
|
||||
files: files.map((f) => `icons/${f}`),
|
||||
};
|
||||
} catch (cause) {
|
||||
const message = cause instanceof Error ? cause.message : String(cause);
|
||||
recordIcons("failed");
|
||||
return {
|
||||
generated: false,
|
||||
files: [],
|
||||
error: `Failed to generate icons: ${message}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user