UNPKG

favicons

Version:
1,132 lines (1,131 loc) 31 kB
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;