144 lines
4.5 KiB
TypeScript
144 lines
4.5 KiB
TypeScript
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}`,
|
|
};
|
|
}
|
|
}
|