favgen 0.2.4 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  This is a simple CLI tool to generate an optimized set of favicons from a single input file. Icons are optimized in terms of both size and quantity (nowadays you don't need that many of them). They are produced according to [this article](https://evilmartians.com/chronicles/how-to-favicon-in-2021-six-files-that-fit-most-needs) which served as an inspiration for the tool.
4
4
 
5
+ ## CLI usage
6
+
5
7
  Use it like this: `npx favgen /path/to/input -o /path/to/output`.
6
8
 
7
9
  You can tweak the following settings by giving additional commands:
@@ -23,6 +25,8 @@ Additionally, a sample `manifest.webmanifest` file is produced which includes re
23
25
  Besides that, PNG output is optimized by `sharp` (which uses `pngquant`) and SVG output is optimized by [SVGO](https://github.com/svg/svgo).
24
26
  Also, color palette is reduced to 64 colors by default in order to reduce assets’ size.
25
27
 
28
+ ## JS API usage
29
+
26
30
  The tool can be also used as API:
27
31
  ```js
28
32
  const { produceIcons } = require("favgen")
@@ -32,7 +36,14 @@ const paletteSize = 64 // default value
32
36
  produceIcons(inputFilePath, outputDirPath, paletteSize)
33
37
  ```
34
38
 
35
- Vite plugin usage:
39
+ ## Vite plugin usage
40
+
41
+ Install:
42
+ ```bash
43
+ npm i favgen
44
+ ```
45
+
46
+ Configure in `vite.config.js` or `vite.config.ts`:
36
47
  ```js
37
48
  import { defineConfig } from "vite"
38
49
  import { favgenVitePlugin } from "favgen"
@@ -47,3 +58,67 @@ export default defineConfig({
47
58
  ],
48
59
  })
49
60
  ```
61
+
62
+ Vite plugin options:
63
+ - `source` (required): path to source image (SVG, PNG, JPEG, WebP, GIF, AVIF, TIFF)
64
+ - `colors` (optional): PNG palette size between 2 and 256 (`64` by default)
65
+ - `assetsPath` (optional): subdirectory where generated assets are emitted (e.g. `favicons`)
66
+
67
+ The plugin runs on build and:
68
+ - generates favicon assets using the same logic as CLI/API
69
+ - emits them into the final build output
70
+ - injects links into built HTML:
71
+ - `favicon.ico`
72
+ - `icon.svg` (only when source is SVG)
73
+ - `apple-touch-icon.png`
74
+ - `manifest.webmanifest`
75
+
76
+ Generated files:
77
+ - `icon.svg` (only for SVG source)
78
+ - `favicon.ico`
79
+ - `icon-192.png`
80
+ - `icon-512.png`
81
+ - `icon-mask.png`
82
+ - `apple-touch-icon.png`
83
+ - `manifest.webmanifest`
84
+
85
+ Manifest icon URLs and injected HTML links are automatically adjusted for Vite `base` and `assetsPath` settings.
86
+
87
+ ## Webpack plugin usage
88
+
89
+ Install:
90
+ ```bash
91
+ npm i favgen
92
+ ```
93
+
94
+ Configure in your webpack config:
95
+ ```js
96
+ const FavgenWebpackPlugin = require("favgen").FavgenWebpackPlugin
97
+
98
+ module.exports = {
99
+ // ...rest of config
100
+ plugins: [
101
+ new FavgenWebpackPlugin({
102
+ source: "src/assets/logo.svg",
103
+ colors: 64,
104
+ assetsPath: "favicons",
105
+ }),
106
+ ],
107
+ }
108
+ ```
109
+
110
+ Webpack plugin options:
111
+ - `source` (required): path to source image (SVG, PNG, JPEG, WebP, GIF, AVIF, TIFF)
112
+ - `colors` (optional): PNG palette size between 2 and 256 (`64` by default)
113
+ - `assetsPath` (optional): subdirectory where generated assets are emitted (e.g. `favicons`)
114
+
115
+ The plugin runs during webpack compilation and:
116
+ - generates favicon assets using the same logic as CLI/API
117
+ - emits assets into compilation output
118
+ - injects links into generated `.html` assets:
119
+ - `favicon.ico`
120
+ - `icon.svg` (only when source is SVG)
121
+ - `apple-touch-icon.png`
122
+ - `manifest.webmanifest`
123
+
124
+ Manifest icon URLs and injected HTML links are automatically adjusted for webpack `output.publicPath` and `assetsPath`.
package/lib/index.d.mts CHANGED
@@ -1,5 +1,8 @@
1
1
  import produceIconsImport from "./generator.js";
2
2
  import favgenVitePluginImport from "./vite-plugin.js";
3
+ import FavgenWebpackPluginImport from "./webpack-plugin.js";
3
4
  export declare const produceIcons: typeof produceIconsImport;
4
5
  export declare const favgenVitePlugin: typeof favgenVitePluginImport;
6
+ export declare const FavgenWebpackPlugin: typeof FavgenWebpackPluginImport;
5
7
  export type { FavgenVitePluginOptions } from "./vite-plugin.js";
8
+ export type { FavgenWebpackPluginOptions } from "./webpack-plugin.js";
package/lib/index.d.ts CHANGED
@@ -1,3 +1,5 @@
1
1
  export { default as produceIcons } from "./generator";
2
2
  export { default as favgenVitePlugin } from "./vite-plugin";
3
+ export { default as FavgenWebpackPlugin } from "./webpack-plugin";
3
4
  export type { FavgenVitePluginOptions } from "./vite-plugin";
5
+ export type { FavgenWebpackPluginOptions } from "./webpack-plugin";
package/lib/index.js CHANGED
@@ -3,9 +3,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.favgenVitePlugin = exports.produceIcons = void 0;
6
+ exports.FavgenWebpackPlugin = exports.favgenVitePlugin = exports.produceIcons = void 0;
7
7
  // eslint-disable-next-line import/prefer-default-export
8
8
  var generator_1 = require("./generator");
9
9
  Object.defineProperty(exports, "produceIcons", { enumerable: true, get: function () { return __importDefault(generator_1).default; } });
10
10
  var vite_plugin_1 = require("./vite-plugin");
11
11
  Object.defineProperty(exports, "favgenVitePlugin", { enumerable: true, get: function () { return __importDefault(vite_plugin_1).default; } });
12
+ var webpack_plugin_1 = require("./webpack-plugin");
13
+ Object.defineProperty(exports, "FavgenWebpackPlugin", { enumerable: true, get: function () { return __importDefault(webpack_plugin_1).default; } });
package/lib/index.mjs CHANGED
@@ -1,11 +1,15 @@
1
1
  import produceIconsImport from "./generator.js";
2
2
  import favgenVitePluginImport from "./vite-plugin.js";
3
+ import FavgenWebpackPluginImport from "./webpack-plugin.js";
3
4
  function normalizeDefaultExport(mod) {
4
- return (typeof mod === "object" &&
5
- mod !== null &&
6
- "default" in mod)
7
- ? mod.default
8
- : mod;
5
+ let current = mod;
6
+ while (typeof current === "object" &&
7
+ current !== null &&
8
+ "default" in current) {
9
+ current = current.default;
10
+ }
11
+ return current;
9
12
  }
10
13
  export const produceIcons = normalizeDefaultExport(produceIconsImport);
11
14
  export const favgenVitePlugin = normalizeDefaultExport(favgenVitePluginImport);
15
+ export const FavgenWebpackPlugin = normalizeDefaultExport(FavgenWebpackPluginImport);
@@ -0,0 +1,53 @@
1
+ /// <reference types="node" />
2
+ /// <reference types="node" />
3
+ export declare type FavgenWebpackPluginOptions = {
4
+ source: string;
5
+ colors?: number;
6
+ assetsPath?: string;
7
+ };
8
+ declare type RawSource = {
9
+ source: () => string | Buffer;
10
+ };
11
+ declare type RawSourceConstructor = new (source: string | Buffer) => RawSource;
12
+ declare type Compilation = {
13
+ assets: Record<string, RawSource>;
14
+ hooks: {
15
+ processAssets: {
16
+ tapPromise: (options: {
17
+ name: string;
18
+ stage: number;
19
+ }, callback: () => Promise<void>) => void;
20
+ };
21
+ };
22
+ emitAsset: (name: string, asset: RawSource) => void;
23
+ updateAsset: (name: string, asset: RawSource) => void;
24
+ };
25
+ declare type Compiler = {
26
+ options: {
27
+ context?: string;
28
+ output?: {
29
+ publicPath?: string | "auto";
30
+ };
31
+ };
32
+ hooks: {
33
+ thisCompilation: {
34
+ tap: (name: string, callback: (compilation: Compilation) => void) => void;
35
+ };
36
+ };
37
+ webpack: {
38
+ Compilation: {
39
+ PROCESS_ASSETS_STAGE_ADDITIONS: number;
40
+ };
41
+ sources: {
42
+ RawSource: RawSourceConstructor;
43
+ };
44
+ };
45
+ };
46
+ export default class FavgenWebpackPlugin {
47
+ private readonly sourcePath;
48
+ private readonly paletteSize;
49
+ private readonly assetsPath;
50
+ constructor(options: FavgenWebpackPluginOptions);
51
+ apply(compiler: Compiler): void;
52
+ }
53
+ export {};
@@ -0,0 +1,141 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const promises_1 = __importDefault(require("fs/promises"));
7
+ const os_1 = __importDefault(require("os"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const generator_1 = __importDefault(require("./generator"));
10
+ function normalizeAssetsPath(rawPath) {
11
+ if (!rawPath) {
12
+ return "";
13
+ }
14
+ return rawPath.replace(/^\/+/, "").replace(/\/+$/, "");
15
+ }
16
+ function isValidPaletteSize(size) {
17
+ return Number.isInteger(size) && size >= 2 && size <= 256;
18
+ }
19
+ function normalizePublicPath(publicPath) {
20
+ if (!publicPath || publicPath === "auto") {
21
+ return "/";
22
+ }
23
+ return publicPath;
24
+ }
25
+ function getAssetUrl(publicPath, assetsPath, filename) {
26
+ const relativePath = assetsPath
27
+ ? path_1.default.posix.join(assetsPath, filename)
28
+ : filename;
29
+ const normalizedPublicPath = publicPath.trim();
30
+ if (normalizedPublicPath === "." || normalizedPublicPath === "./") {
31
+ return relativePath;
32
+ }
33
+ if (/^https?:\/\//.test(normalizedPublicPath)) {
34
+ return new URL(relativePath, normalizedPublicPath).toString();
35
+ }
36
+ const publicPathWithSlash = normalizedPublicPath.endsWith("/")
37
+ ? normalizedPublicPath
38
+ : `${normalizedPublicPath}/`;
39
+ return `${publicPathWithSlash}${relativePath}`;
40
+ }
41
+ function rewriteManifest(manifestBuffer, publicPath, assetsPath) {
42
+ const manifest = JSON.parse(manifestBuffer.toString("utf8"));
43
+ if (Array.isArray(manifest.icons)) {
44
+ manifest.icons = manifest.icons.map((icon) => {
45
+ const sourcePath = icon.src;
46
+ const iconFilename = typeof sourcePath === "string"
47
+ ? path_1.default.posix.basename(sourcePath)
48
+ : "";
49
+ return {
50
+ ...icon,
51
+ src: getAssetUrl(publicPath, assetsPath, iconFilename),
52
+ };
53
+ });
54
+ }
55
+ return Buffer.from(JSON.stringify(manifest, null, 2));
56
+ }
57
+ function getHtmlTagStrings(publicPath, assetsPath, hasSvgIcon) {
58
+ const tags = [
59
+ `<link rel="icon" href="${getAssetUrl(publicPath, assetsPath, "favicon.ico")}" sizes="any">`,
60
+ `<link rel="apple-touch-icon" href="${getAssetUrl(publicPath, assetsPath, "apple-touch-icon.png")}">`,
61
+ `<link rel="manifest" href="${getAssetUrl(publicPath, assetsPath, "manifest.webmanifest")}">`,
62
+ ];
63
+ if (hasSvgIcon) {
64
+ tags.splice(1, 0, `<link rel="icon" href="${getAssetUrl(publicPath, assetsPath, "icon.svg")}" type="image/svg+xml">`);
65
+ }
66
+ return tags;
67
+ }
68
+ function injectTagsIntoHtml(htmlText, tags) {
69
+ const missingTags = tags.filter((tag) => !htmlText.includes(tag));
70
+ if (missingTags.length === 0) {
71
+ return htmlText;
72
+ }
73
+ const tagsBlock = `${missingTags.join("\n")}\n`;
74
+ if (/<\/head>/i.test(htmlText)) {
75
+ return htmlText.replace(/<\/head>/i, `${tagsBlock}</head>`);
76
+ }
77
+ return `${tagsBlock}${htmlText}`;
78
+ }
79
+ class FavgenWebpackPlugin {
80
+ constructor(options) {
81
+ this.sourcePath = options.source;
82
+ this.paletteSize = options.colors ?? 64;
83
+ this.assetsPath = normalizeAssetsPath(options.assetsPath);
84
+ }
85
+ apply(compiler) {
86
+ compiler.hooks.thisCompilation.tap("favgen-webpack-plugin", (compilation) => {
87
+ const stage = compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS;
88
+ const { RawSource } = compiler.webpack.sources;
89
+ compilation.hooks.processAssets.tapPromise({ name: "favgen-webpack-plugin", stage }, async () => {
90
+ if (!this.sourcePath || this.sourcePath.trim().length === 0) {
91
+ throw new Error("favgen-webpack-plugin: `source` option is required.");
92
+ }
93
+ if (!isValidPaletteSize(this.paletteSize)) {
94
+ throw new Error("favgen-webpack-plugin: `colors` must be an integer between 2 and 256.");
95
+ }
96
+ const projectRoot = compiler.options.context ?? process.cwd();
97
+ const publicPath = normalizePublicPath(compiler.options.output?.publicPath);
98
+ const resolvedSourcePath = path_1.default.isAbsolute(this.sourcePath)
99
+ ? this.sourcePath
100
+ : path_1.default.resolve(projectRoot, this.sourcePath);
101
+ const tempDirPath = await promises_1.default.mkdtemp(path_1.default.join(os_1.default.tmpdir(), "favgen-webpack-"));
102
+ try {
103
+ await (0, generator_1.default)(resolvedSourcePath, tempDirPath, this.paletteSize);
104
+ const iconFilenames = (await promises_1.default.readdir(tempDirPath)).sort();
105
+ const hasSvgIcon = iconFilenames.includes("icon.svg");
106
+ const generatedAssets = await Promise.all(iconFilenames.map(async (filename) => ({
107
+ filename,
108
+ source: await promises_1.default.readFile(path_1.default.join(tempDirPath, filename)),
109
+ })));
110
+ generatedAssets.forEach((asset) => {
111
+ const outputFilename = this.assetsPath
112
+ ? path_1.default.posix.join(this.assetsPath, asset.filename)
113
+ : asset.filename;
114
+ const outputSource = asset.filename === "manifest.webmanifest"
115
+ ? rewriteManifest(asset.source, publicPath, this.assetsPath)
116
+ : asset.source;
117
+ compilation.emitAsset(outputFilename, new RawSource(outputSource));
118
+ });
119
+ const htmlTags = getHtmlTagStrings(publicPath, this.assetsPath, hasSvgIcon);
120
+ Object.entries(compilation.assets).forEach(([filename, asset]) => {
121
+ if (!filename.endsWith(".html")) {
122
+ return;
123
+ }
124
+ const currentSource = asset.source();
125
+ const htmlText = Buffer.isBuffer(currentSource)
126
+ ? currentSource.toString("utf8")
127
+ : String(currentSource);
128
+ const nextHtmlText = injectTagsIntoHtml(htmlText, htmlTags);
129
+ if (nextHtmlText !== htmlText) {
130
+ compilation.updateAsset(filename, new RawSource(nextHtmlText));
131
+ }
132
+ });
133
+ }
134
+ finally {
135
+ await promises_1.default.rm(tempDirPath, { recursive: true, force: true });
136
+ }
137
+ });
138
+ });
139
+ }
140
+ }
141
+ exports.default = FavgenWebpackPlugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "favgen",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "CLI tool to generate a set of favicons from a single input file.",
5
5
  "keywords": [
6
6
  "favicon"
@@ -25,11 +25,15 @@
25
25
  "bin/"
26
26
  ],
27
27
  "peerDependencies": {
28
- "vite": "^5.0.0 || ^6.0.0 || ^7.0.0"
28
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
29
+ "webpack": "^5.0.0"
29
30
  },
30
31
  "peerDependenciesMeta": {
31
32
  "vite": {
32
33
  "optional": true
34
+ },
35
+ "webpack": {
36
+ "optional": true
33
37
  }
34
38
  },
35
39
  "engines": {