favicons
Version:
Favicon generator for Node.js
1,132 lines (1,131 loc) • 31 kB
JavaScript
Object.defineProperties(exports, {
__esModule: { value: true },
[Symbol.toStringTag]: { value: "Module" }
});
//#region \0rolldown/runtime.js
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
key = keys[i];
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
get: ((k) => from[k]).bind(null, key),
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
});
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
value: mod,
enumerable: true
}) : target, mod));
//#endregion
let stream = require("stream");
let path = require("path");
let fs_promises = require("fs/promises");
let sharp = require("sharp");
sharp = __toESM(sharp, 1);
let escape_html = require("escape-html");
escape_html = __toESM(escape_html, 1);
let xml2js = require("xml2js");
xml2js = __toESM(xml2js, 1);
//#region src/config/defaults.ts
const defaultOptions = {
path: "/",
appName: void 0,
appShortName: void 0,
appDescription: void 0,
developerName: void 0,
developerURL: void 0,
cacheBustingQueryParam: null,
dir: "auto",
lang: "en-US",
background: "#fff",
theme_color: "#fff",
appleStatusBarStyle: "black-translucent",
display: "standalone",
orientation: "any",
start_url: "/?homescreen=1",
version: "1.0",
pixel_art: false,
loadManifestWithCredentials: false,
manifestRelativePaths: false,
manifestMaskable: false,
preferRelatedApplications: false,
icons: {
android: true,
appleIcon: true,
appleStartup: true,
favicons: true,
windows: true,
yandex: true
},
output: {
images: true,
files: true,
html: true
}
};
//#endregion
//#region src/ico.ts
const HEADER_SIZE = 6;
const DIRECTORY_SIZE = 16;
const COLOR_MODE = 0;
const BITMAP_SIZE = 40;
function createHeader(n) {
const buf = Buffer.alloc(HEADER_SIZE);
buf.writeUInt16LE(0, 0);
buf.writeUInt16LE(1, 2);
buf.writeUInt16LE(n, 4);
return buf;
}
function createDirectory(image, offset) {
const buf = Buffer.alloc(DIRECTORY_SIZE);
const { width, height } = image.info;
const size = width * height * 4 + BITMAP_SIZE;
const bpp = 32;
buf.writeUInt8(width === 256 ? 0 : width, 0);
buf.writeUInt8(height === 256 ? 0 : height, 1);
buf.writeUInt8(0, 2);
buf.writeUInt8(0, 3);
buf.writeUInt16LE(1, 4);
buf.writeUInt16LE(bpp, 6);
buf.writeUInt32LE(size, 8);
buf.writeUInt32LE(offset, 12);
return buf;
}
function createBitmap(image, compression) {
const buf = Buffer.alloc(BITMAP_SIZE);
const { width, height } = image.info;
buf.writeUInt32LE(BITMAP_SIZE, 0);
buf.writeInt32LE(width, 4);
buf.writeInt32LE(height * 2, 8);
buf.writeUInt16LE(1, 12);
buf.writeUInt16LE(32, 14);
buf.writeUInt32LE(compression, 16);
buf.writeUInt32LE(width * height, 20);
buf.writeInt32LE(0, 24);
buf.writeInt32LE(0, 28);
buf.writeUInt32LE(0, 32);
buf.writeUInt32LE(0, 36);
return buf;
}
function createDib(image) {
const { width, height } = image.info;
const imageData = image.data;
const buf = Buffer.alloc(width * height * 4);
for (let y = 0; y < height; ++y) for (let x = 0; x < height; ++x) {
const offset = (y * width + x) * 4;
const r = imageData.readUInt8(offset);
const g = imageData.readUInt8(offset + 1);
const b = imageData.readUInt8(offset + 2);
const a = imageData.readUInt8(offset + 3);
const pos = (height - y - 1) * width + x;
buf.writeUInt8(b, pos * 4);
buf.writeUInt8(g, pos * 4 + 1);
buf.writeUInt8(r, pos * 4 + 2);
buf.writeUInt8(a, pos * 4 + 3);
}
return buf;
}
function toIco(images) {
let arr = [createHeader(images.length)];
let offset = HEADER_SIZE + DIRECTORY_SIZE * images.length;
const bitmaps = images.map((image) => {
const bitmapHeader = createBitmap(image, COLOR_MODE);
const dib = createDib(image);
return Buffer.concat([bitmapHeader, dib]);
});
for (let i = 0; i < images.length; ++i) {
const image = images[i];
const bitmap = bitmaps[i];
const dir = createDirectory(image, offset);
arr.push(dir);
offset += bitmap.length;
}
arr = [...arr, ...bitmaps];
return Buffer.concat(arr);
}
//#endregion
//#region src/svgtool.ts
function svgDensity(metadata, width, height) {
if (!metadata.width || !metadata.height) return;
const currentDensity = metadata.density ?? 72;
return Math.min(Math.max(1, currentDensity, currentDensity * width / metadata.width, currentDensity * height / metadata.height), 1e5);
}
//#endregion
//#region src/helpers.ts
function arrayComparator(a, b) {
const aArr = [a].flat(Infinity);
const bArr = [b].flat(Infinity);
for (let i = 0; i < Math.max(aArr.length, bArr.length); ++i) {
if (i >= aArr.length) return -1;
if (i >= bArr.length) return 1;
if (aArr[i] !== bArr[i]) return aArr[i] < bArr[i] ? -1 : 1;
}
return 0;
}
function minBy(array, comparator) {
return array.reduce((acc, cur) => comparator(acc, cur) < 0 ? acc : cur);
}
function minByKey(array, keyFn) {
return minBy(array, (a, b) => arrayComparator(keyFn(a), keyFn(b)));
}
function asString(arg) {
return typeof arg === "string" || arg instanceof String ? arg.toString() : void 0;
}
async function sourceImages(src) {
if (Buffer.isBuffer(src)) try {
return [{
data: src,
metadata: await (0, sharp.default)(src).metadata()
}];
} catch (error) {
return Promise.reject(new Error("Invalid image buffer", { cause: error }));
}
else if (typeof src === "string") return await sourceImages(await (0, fs_promises.readFile)(src));
else if (Array.isArray(src) && !src.some(Array.isArray)) {
if (!src.length) throw new Error("No source provided");
return (await Promise.all(src.map(sourceImages))).flat();
} else throw new Error("Invalid source type provided");
}
function flattenIconOptions(iconOptions) {
return iconOptions.sizes.map((size) => ({
...size,
offset: iconOptions.offset ?? 0,
pixelArt: iconOptions.pixelArt ?? false,
background: asString(iconOptions.background),
transparent: iconOptions.transparent,
rotate: iconOptions.rotate
}));
}
function relativeTo(base, path$1) {
if (!base) return path$1;
const directory = base.endsWith("/") ? base : `${base}/`;
const url = new URL(path$1, new URL(directory, "resolve://"));
return url.protocol === "resolve:" ? url.pathname : url.toString();
}
function bestSource(sourceset, width, height) {
const sideSize = Math.max(width, height);
return minByKey(sourceset, (icon) => {
const iconSideSize = Math.max(icon.metadata.width, icon.metadata.height);
return [
icon.metadata.format === "svg" ? 0 : 1,
iconSideSize >= sideSize ? 0 : 1,
Math.abs(iconSideSize - sideSize)
];
});
}
async function resize(source, width, height, pixelArt) {
if (source.metadata.format === "svg") {
const options = { density: svgDensity(source.metadata, width, height) };
return await (0, sharp.default)(source.data, options).resize({
width,
height,
fit: sharp.default.fit.contain,
background: "#00000000"
}).toBuffer();
} else return await (0, sharp.default)(source.data).ensureAlpha().resize({
width,
height,
fit: sharp.default.fit.contain,
background: "#00000000",
kernel: pixelArt && width >= source.metadata.width && height >= source.metadata.height ? "nearest" : "lanczos3"
}).toBuffer();
}
function createBlankImage(width, height, background) {
const transparent = !background || background === "transparent";
let image = (0, sharp.default)({ create: {
width,
height,
channels: transparent ? 4 : 3,
background: transparent ? "#00000000" : background
} });
if (transparent) image = image.ensureAlpha();
return image;
}
async function createPlane(sourceset, options) {
const offset = Math.round(Math.max(options.width, options.height) * options.offset / 100) || 0;
const width = options.width - offset * 2;
const height = options.height - offset * 2;
const image = await resize(bestSource(sourceset, width, height), width, height, options.pixelArt);
let pipeline = createBlankImage(options.width, options.height, options.background).composite([{
input: image,
left: offset,
top: offset
}]);
if (options.rotate) pipeline = pipeline.rotate(90);
return pipeline;
}
function toRawImage(pipeline) {
return pipeline.toColorspace("srgb").raw({ depth: "uchar" }).toBuffer({ resolveWithObject: true });
}
function toPng(pipeline) {
return pipeline.png().toBuffer();
}
async function createSvg(sourceset, options) {
const { width, height } = options;
const source = bestSource(sourceset, width, height);
if (source.metadata.format === "svg") return source.data;
else {
const encodedPng = (await toPng(await createPlane(sourceset, options))).toString("base64");
return Buffer.from(`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}">
<image width="${width}" height="${height}" xlink:href="data:image/png;base64,${encodedPng}"/>
</svg>`);
}
}
async function createFavicon(sourceset, name, iconOptions) {
const properties = flattenIconOptions(iconOptions);
const ext = (0, path.extname)(name);
if (ext === ".ico" || properties.length !== 1) return {
name,
contents: toIco(await Promise.all(properties.map((props) => createPlane(sourceset, props).then(toRawImage))))
};
else if (ext === ".svg") return {
name,
contents: await createSvg(sourceset, properties[0])
};
else return {
name,
contents: await createPlane(sourceset, properties[0]).then(toPng)
};
}
//#endregion
//#region src/config/icons.ts
function transparentIcon(width, height) {
return {
sizes: [{
width,
height: height ?? width
}],
offset: 0,
background: false,
transparent: true,
rotate: false
};
}
function transparentIcons(...sizes) {
return {
sizes: sizes.map((size) => ({
width: size,
height: size
})),
offset: 0,
background: false,
transparent: true,
rotate: false
};
}
function opaqueIcon(width, height) {
return {
sizes: [{
width,
height: height ?? width
}],
offset: 0,
background: true,
transparent: false,
rotate: false
};
}
function maskable(options) {
return {
...options,
purpose: "maskable"
};
}
//#endregion
//#region src/platforms/base.ts
function uniformIconOptions(options, iconsChoice, platformConfig) {
let result;
if (Array.isArray(iconsChoice)) {
const iconsChoices = Object.fromEntries(iconsChoice.map((choice) => typeof choice === "object" ? [choice.name, choice] : [choice, { name: choice }]));
result = platformConfig.filter((iconOptions) => iconOptions.name in iconsChoices).map((iconOptions) => ({
...iconOptions,
...iconsChoices[iconOptions.name]
}));
} else if (typeof iconsChoice === "object") result = platformConfig.filter((iconOptions) => !iconOptions.optional).map((iconOptions) => ({
...iconOptions,
...iconsChoice
}));
else result = platformConfig.filter((iconOptions) => !iconOptions.optional);
return result.map((iconOptions) => ({
pixelArt: options.pixel_art,
...iconOptions,
background: iconOptions.background === true ? options.background : asString(iconOptions.background)
}));
}
function attrSorkKey(key) {
const index = [
"name",
"rel",
"type",
"media",
"sizes"
].indexOf(key);
return index >= 0 ? `${index}_${key}` : `z_${key}`;
}
function renderHtmlTag(tag) {
const attrs = Object.entries(tag.attrs).toSorted((a, b) => attrSorkKey(a[0]).localeCompare(attrSorkKey(b[0]))).map(([key, value]) => {
if (value === true) return key;
if (value === false) return "";
return `${key}="${(0, escape_html.default)(value)}"`;
}).filter(Boolean).join(" ");
return `<${tag.tag} ${attrs || ""}>`;
}
var Platform = class {
options;
iconOptions;
constructor(options, iconOptions) {
this.options = options;
this.iconOptions = iconOptions;
}
async create(sourceset) {
const { output } = this.options;
const images = output.images ? await this.createImages(sourceset) : [];
const files = output.files ? await this.createFiles() : [];
let htmlTags = [];
if (output.html) htmlTags = await this.createHtml();
return {
images,
files,
html: htmlTags.map(renderHtmlTag),
htmlTags
};
}
async createImages(sourceset) {
return await Promise.all(this.iconOptions.map((iconOption) => createFavicon(sourceset, iconOption.name, iconOption)));
}
async createFiles() {
return [];
}
async createHtml() {
return [];
}
relative(path) {
return relativeTo(this.options.path, path);
}
cacheBusting(path) {
if (typeof this.options.cacheBustingQueryParam !== "string") return path;
const paramParts = this.options.cacheBustingQueryParam.split("=");
if (paramParts.length === 1) return path;
const url = new URL(path, "https://cache.busting");
url.searchParams.set(paramParts[0], paramParts.slice(1).join("="));
return url.origin === "https://cache.busting" ? url.pathname + url.search : url.toString();
}
};
//#endregion
//#region src/platforms/android.ts
const ICONS_OPTIONS$5 = [
{
name: "android-chrome-36x36.png",
...transparentIcon(36)
},
{
name: "android-chrome-48x48.png",
...transparentIcon(48)
},
{
name: "android-chrome-72x72.png",
...transparentIcon(72)
},
{
name: "android-chrome-96x96.png",
...transparentIcon(96)
},
{
name: "android-chrome-144x144.png",
...transparentIcon(144)
},
{
name: "android-chrome-192x192.png",
...transparentIcon(192)
},
{
name: "android-chrome-256x256.png",
...transparentIcon(256)
},
{
name: "android-chrome-384x384.png",
...transparentIcon(384)
},
{
name: "android-chrome-512x512.png",
...transparentIcon(512)
}
];
const ICONS_OPTIONS_MASKABLE = [
{
name: "android-chrome-maskable-36x36.png",
...maskable(transparentIcon(36))
},
{
name: "android-chrome-maskable-48x48.png",
...maskable(transparentIcon(48))
},
{
name: "android-chrome-maskable-72x72.png",
...maskable(transparentIcon(72))
},
{
name: "android-chrome-maskable-96x96.png",
...maskable(transparentIcon(96))
},
{
name: "android-chrome-maskable-144x144.png",
...maskable(transparentIcon(144))
},
{
name: "android-chrome-maskable-192x192.png",
...maskable(transparentIcon(192))
},
{
name: "android-chrome-maskable-256x256.png",
...maskable(transparentIcon(256))
},
{
name: "android-chrome-maskable-384x384.png",
...maskable(transparentIcon(384))
},
{
name: "android-chrome-maskable-512x512.png",
...maskable(transparentIcon(512))
}
];
const SHORTCUT_ICONS_OPTIONS = {
"36x36.png": transparentIcon(36),
"48x48.png": transparentIcon(48),
"72x72.png": transparentIcon(72),
"96x96.png": transparentIcon(96),
"144x144.png": transparentIcon(144),
"192x192.png": transparentIcon(192)
};
var AndroidPlatform = class extends Platform {
constructor(options) {
super(options, uniformIconOptions(options, options.icons.android, ICONS_OPTIONS$5));
}
async createImages(sourceset) {
let icons = await Promise.all(this.iconOptions.map((iconOption) => createFavicon(sourceset, iconOption.name, iconOption)));
if (this.options.manifestMaskable && typeof this.options.manifestMaskable !== "boolean") {
const maskableSourceset = await sourceImages(this.options.manifestMaskable);
const maskableIcons = await Promise.all(ICONS_OPTIONS_MASKABLE.map((iconOption) => createFavicon(maskableSourceset, iconOption.name, iconOption)));
icons = [...icons, ...maskableIcons];
}
if (Array.isArray(this.options.shortcuts) && this.options.shortcuts.length > 0) icons = [...icons, ...await this.shortcutIcons()];
return icons;
}
async createFiles() {
return [this.manifest()];
}
async createHtml() {
return [
{
tag: "link",
attrs: {
rel: "manifest",
href: this.cacheBusting(this.relative(this.manifestFileName())),
crossOrigin: this.options.loadManifestWithCredentials ? "use-credentials" : false
}
},
{
tag: "meta",
attrs: {
name: "mobile-web-app-capable",
content: "yes"
}
},
{
tag: "meta",
attrs: {
name: "theme-color",
content: this.options.theme_color || this.options.background
}
},
{
tag: "meta",
attrs: {
name: "application-name",
content: this.options.appName || false
}
}
];
}
async shortcutIcons() {
return (await Promise.all(this.options.shortcuts?.map(async (shortcut, index) => {
if (!shortcut.name || !shortcut.url || !shortcut.icon) return [];
const shortcutSourceset = await sourceImages(shortcut.icon);
return Promise.all(Object.entries(SHORTCUT_ICONS_OPTIONS).map(([shortcutName, option]) => createFavicon(shortcutSourceset, `shortcut${index + 1}-${shortcutName}`, option)));
}) ?? [])).flat();
}
manifestFileName() {
return this.options.files?.android?.manifestFileName ?? "manifest.webmanifest";
}
manifest() {
const { options } = this;
const basePath = options.manifestRelativePaths ? null : options.path;
const properties = {
name: options.appName,
short_name: options.appShortName || options.appName,
description: options.appDescription,
dir: options.dir,
lang: options.lang,
display: options.display,
orientation: options.orientation,
scope: options.scope,
start_url: options.start_url,
background_color: options.background,
theme_color: options.theme_color
};
if (options.preferRelatedApplications) properties.prefer_related_applications = options.preferRelatedApplications;
if (Array.isArray(options.relatedApplications) && options.relatedApplications.length > 0) properties.related_applications = options.relatedApplications;
let icons = this.iconOptions;
if (options.manifestMaskable && typeof options.manifestMaskable !== "boolean") icons = [...icons, ...ICONS_OPTIONS_MASKABLE];
const defaultPurpose = options.manifestMaskable === true ? "any maskable" : "any";
properties.icons = icons.map((iconOptions) => {
const { width, height } = iconOptions.sizes[0];
return {
src: this.cacheBusting(relativeTo(basePath, iconOptions.name)),
sizes: `${width}x${height}`,
type: "image/png",
purpose: iconOptions.purpose ?? defaultPurpose
};
});
if (Array.isArray(options.shortcuts) && options.shortcuts.length > 0) properties.shortcuts = this.manifestShortcuts(basePath);
return {
name: this.manifestFileName(),
contents: JSON.stringify(properties, null, 2)
};
}
manifestShortcuts(basePath) {
return this.options.shortcuts?.filter((shortcut) => shortcut.name && shortcut.url)?.map((shortcut, index) => ({
name: shortcut.name,
short_name: shortcut.short_name || shortcut.name,
description: shortcut.description,
url: shortcut.url,
icons: shortcut.icon ? Object.entries(SHORTCUT_ICONS_OPTIONS).map(([shortcutName, option]) => {
const { width, height } = option.sizes[0];
return {
src: this.cacheBusting(relativeTo(basePath, `shortcut${index + 1}-${shortcutName}`)),
sizes: `${width}x${height}`,
type: "image/png"
};
}) : void 0
})) ?? [];
}
};
//#endregion
//#region src/platforms/appleIcon.ts
const ICONS_OPTIONS$4 = [
{
name: "apple-touch-icon-57x57.png",
...opaqueIcon(57)
},
{
name: "apple-touch-icon-60x60.png",
...opaqueIcon(60)
},
{
name: "apple-touch-icon-72x72.png",
...opaqueIcon(72)
},
{
name: "apple-touch-icon-76x76.png",
...opaqueIcon(76)
},
{
name: "apple-touch-icon-114x114.png",
...opaqueIcon(114)
},
{
name: "apple-touch-icon-120x120.png",
...opaqueIcon(120)
},
{
name: "apple-touch-icon-144x144.png",
...opaqueIcon(144)
},
{
name: "apple-touch-icon-152x152.png",
...opaqueIcon(152)
},
{
name: "apple-touch-icon-167x167.png",
...opaqueIcon(167)
},
{
name: "apple-touch-icon-180x180.png",
...opaqueIcon(180)
},
{
name: "apple-touch-icon-1024x1024.png",
...opaqueIcon(1024)
},
{
name: "apple-touch-icon.png",
...opaqueIcon(180)
},
{
name: "apple-touch-icon-precomposed.png",
...opaqueIcon(180)
}
];
var AppleIconPlatform = class extends Platform {
constructor(options) {
super(options, uniformIconOptions(options, options.icons.appleIcon, ICONS_OPTIONS$4));
}
async createHtml() {
const icons = this.iconOptions.filter(({ name }) => /\d/.test(name)).map((options) => {
const { width, height } = options.sizes[0];
return {
tag: "link",
attrs: {
rel: "apple-touch-icon",
sizes: `${width}x${height}`,
href: this.cacheBusting(this.relative(options.name))
}
};
});
const name = this.options.appShortName || this.options.appName;
return [
...icons,
{
tag: "meta",
attrs: {
name: "apple-mobile-web-app-capable",
content: "yes"
}
},
{
tag: "meta",
attrs: {
name: "apple-mobile-web-app-status-bar-style",
content: this.options.appleStatusBarStyle
}
},
{
tag: "meta",
attrs: {
name: "apple-mobile-web-app-title",
content: name || false
}
}
];
}
};
//#endregion
//#region src/platforms/appleStartup.ts
const SCREEN_SIZES = [
{
deviceWidth: 320,
deviceHeight: 568,
pixelRatio: 2
},
{
deviceWidth: 375,
deviceHeight: 667,
pixelRatio: 2
},
{
deviceWidth: 375,
deviceHeight: 812,
pixelRatio: 3
},
{
deviceWidth: 390,
deviceHeight: 844,
pixelRatio: 3
},
{
deviceWidth: 393,
deviceHeight: 852,
pixelRatio: 3
},
{
deviceWidth: 414,
deviceHeight: 896,
pixelRatio: 2
},
{
deviceWidth: 414,
deviceHeight: 896,
pixelRatio: 3
},
{
deviceWidth: 414,
deviceHeight: 736,
pixelRatio: 3
},
{
deviceWidth: 414,
deviceHeight: 736,
pixelRatio: 3
},
{
deviceWidth: 428,
deviceHeight: 926,
pixelRatio: 3
},
{
deviceWidth: 430,
deviceHeight: 932,
pixelRatio: 3
},
{
deviceWidth: 744,
deviceHeight: 1133,
pixelRatio: 2
},
{
deviceWidth: 768,
deviceHeight: 1024,
pixelRatio: 2
},
{
deviceWidth: 810,
deviceHeight: 1080,
pixelRatio: 2
},
{
deviceWidth: 820,
deviceHeight: 1080,
pixelRatio: 2
},
{
deviceWidth: 834,
deviceHeight: 1194,
pixelRatio: 2
},
{
deviceWidth: 834,
deviceHeight: 1112,
pixelRatio: 2
},
{
deviceWidth: 1024,
deviceHeight: 1366,
pixelRatio: 2
}
];
function iconOptions() {
const result = {};
for (const size of SCREEN_SIZES) {
const pixelWidth = size.deviceWidth * size.pixelRatio;
const pixelHeight = size.deviceHeight * size.pixelRatio;
const namePortrait = `apple-touch-startup-image-${pixelWidth}x${pixelHeight}.png`;
result[namePortrait] = {
name: namePortrait,
...opaqueIcon(pixelWidth, pixelHeight),
...size,
orientation: "portrait"
};
const nameLandscape = `apple-touch-startup-image-${pixelHeight}x${pixelWidth}.png`;
result[nameLandscape] = {
name: nameLandscape,
...opaqueIcon(pixelHeight, pixelWidth),
...size,
orientation: "landscape"
};
}
return Object.values(result);
}
const ICONS_OPTIONS$3 = iconOptions();
var AppleStartupPlatform = class extends Platform {
constructor(options) {
super(options, uniformIconOptions(options, options.icons.appleStartup, ICONS_OPTIONS$3));
}
async createHtml() {
return this.iconOptions.map((item) => ({
tag: "link",
attrs: {
rel: "apple-touch-startup-image",
media: `(device-width: ${item.deviceWidth}px) and (device-height: ${item.deviceHeight}px) and (-webkit-device-pixel-ratio: ${item.pixelRatio}) and (orientation: ${item.orientation})`,
href: this.cacheBusting(this.relative(item.name))
}
}));
}
};
//#endregion
//#region src/platforms/favicons.ts
const ICONS_OPTIONS$2 = [
{
name: "favicon.ico",
...transparentIcons(16, 24, 32, 48, 64)
},
{
name: "favicon-16x16.png",
...transparentIcon(16)
},
{
name: "favicon-32x32.png",
...transparentIcon(32)
},
{
name: "favicon-48x48.png",
...transparentIcon(48)
},
{
name: "favicon.svg",
...transparentIcon(1024),
optional: true
}
];
var FaviconsPlatform = class extends Platform {
constructor(options) {
super(options, uniformIconOptions(options, options.icons.favicons, ICONS_OPTIONS$2));
}
async createHtml() {
return this.iconOptions.map(({ name, ...options }) => {
const attrs = {
rel: "icon",
type: "image/png",
href: this.cacheBusting(this.relative(name))
};
if (name.endsWith(".ico")) attrs.type = "image/x-icon";
else if (name.endsWith(".svg")) attrs.type = "image/svg+xml";
else {
const { width, height } = options.sizes[0];
attrs.sizes = `${width}x${height}`;
}
return {
tag: "link",
attrs
};
});
}
};
//#endregion
//#region src/platforms/windows.ts
const ICONS_OPTIONS$1 = [
{
name: "mstile-70x70.png",
...transparentIcon(70)
},
{
name: "mstile-144x144.png",
...transparentIcon(144)
},
{
name: "mstile-150x150.png",
...transparentIcon(150)
},
{
name: "mstile-310x150.png",
...transparentIcon(310, 150)
},
{
name: "mstile-310x310.png",
...transparentIcon(310)
}
];
const SUPPORTED_TILES = [
{
name: "square70x70logo",
width: 70,
height: 70
},
{
name: "square150x150logo",
width: 150,
height: 150
},
{
name: "wide310x150logo",
width: 310,
height: 150
},
{
name: "square310x310logo",
width: 310,
height: 310
}
];
function hasSize(size, icon) {
return icon.sizes.length === 1 && icon.sizes[0].width === size.width && icon.sizes[0].height === size.height;
}
var WindowsPlatform = class extends Platform {
constructor(options) {
super(options, uniformIconOptions(options, options.icons.windows, ICONS_OPTIONS$1));
if (!this.options.background) throw new Error("`background` is required for Windows icons");
}
async createFiles() {
return [this.browserConfig()];
}
async createHtml() {
const tags = [{
tag: "meta",
attrs: {
name: "msapplication-TileColor",
content: this.options.background
}
}];
const tile = "mstile-144x144.png";
if (this.iconOptions.find((o) => o.name === tile)) tags.push({
tag: "meta",
attrs: {
name: "msapplication-TileImage",
content: this.cacheBusting(this.relative(tile))
}
});
tags.push({
tag: "meta",
attrs: {
name: "msapplication-config",
content: this.cacheBusting(this.relative(this.manifestFileName()))
}
});
return tags;
}
manifestFileName() {
return this.options.files?.windows?.manifestFileName ?? "browserconfig.xml";
}
browserConfig() {
const basePath = this.options.manifestRelativePaths ? null : this.options.path;
const tile = {};
for (const { name, ...size } of SUPPORTED_TILES) {
const icon = this.iconOptions.find((iconOption) => hasSize(size, iconOption));
if (icon) tile[name] = { $: { src: this.cacheBusting(relativeTo(basePath, icon.name)) } };
}
const browserconfig = { browserconfig: { msapplication: { tile: {
...tile,
TileColor: { _: this.options.background }
} } } };
const contents = new xml2js.default.Builder({ xmldec: {
version: "1.0",
encoding: "utf-8",
standalone: void 0
} }).buildObject(browserconfig);
return {
name: this.manifestFileName(),
contents
};
}
};
//#endregion
//#region src/platforms/yandex.ts
const ICONS_OPTIONS = [{
name: "yandex-browser-50x50.png",
...transparentIcon(50)
}];
var YandexPlatform = class extends Platform {
constructor(options) {
super(options, uniformIconOptions(options, options.icons.yandex, ICONS_OPTIONS));
}
async createFiles() {
return [this.manifest()];
}
async createHtml() {
return [{
tag: "link",
attrs: {
rel: "yandex-tableau-widget",
href: this.cacheBusting(this.relative(this.manifestFileName()))
}
}];
}
manifestFileName() {
return this.options.files?.yandex?.manifestFileName ?? "yandex-browser-manifest.json";
}
manifest() {
const basePath = this.options.manifestRelativePaths ? null : this.options.path;
const logo = this.iconOptions[0].name;
const properties = {
version: this.options.version,
api_version: 1,
layout: {
logo: this.cacheBusting(relativeTo(basePath, logo)),
color: this.options.background,
show_title: true
}
};
return {
name: this.manifestFileName(),
contents: JSON.stringify(properties, null, 2)
};
}
};
//#endregion
//#region src/platforms/index.ts
function getPlatform(name, options) {
switch (name) {
case "android": return new AndroidPlatform(options);
case "appleIcon": return new AppleIconPlatform(options);
case "appleStartup": return new AppleStartupPlatform(options);
case "favicons": return new FaviconsPlatform(options);
case "windows": return new WindowsPlatform(options);
case "yandex": return new YandexPlatform(options);
default: throw new Error(`Unsupported platform ${name}`);
}
}
//#endregion
//#region src/index.ts
const config = { defaults: defaultOptions };
async function favicons(source, options = {}) {
const fullOptions = {
...defaultOptions,
...options,
icons: {
...defaultOptions.icons,
...options.icons
},
output: {
...defaultOptions.output,
...options.output
}
};
const sourceset = await sourceImages(source);
const platforms = Object.keys(fullOptions.icons).filter((platform) => fullOptions.icons[platform]).sort((a, b) => {
if (a === "favicons") return -1;
if (b === "favicons") return 1;
return a.localeCompare(b);
});
const responses = [];
for (const platformName of platforms) {
const platform = getPlatform(platformName, fullOptions);
responses.push(await platform.create(sourceset));
}
return {
images: responses.flatMap((r) => r.images),
files: responses.flatMap((r) => r.files),
html: responses.flatMap((r) => r.html),
htmlTags: responses.flatMap((r) => r.htmlTags)
};
}
var FaviconStream = class extends stream.Transform {
#options;
#handleHTML;
constructor(options, handleHTML) {
super({ objectMode: true });
this.#options = options;
this.#handleHTML = handleHTML;
}
_transform(file, _encoding, callback) {
const { html: htmlPath, pipeHTML, ...options } = this.#options;
favicons(file, options).then(({ images, files, html, htmlTags }) => {
for (const { name, contents } of [...images, ...files]) this.push({
name,
contents: this.#convertContent(contents)
});
if (this.#handleHTML) this.#handleHTML(html, htmlTags);
if (pipeHTML) this.push({
name: htmlPath,
contents: this.#convertContent(html.join("\n"))
});
callback(null);
}).catch(callback);
}
#convertContent(contents) {
return (this.#options.emitBuffers ?? true) && !Buffer.isBuffer(contents) ? Buffer.from(contents) : contents;
}
};
function stream$1(options, handleHTML) {
return new FaviconStream(options, handleHTML);
}
//#endregion
exports.config = config;
exports.default = favicons;
exports.favicons = favicons;
exports.stream = stream$1;