@getvitops/utils 0.1.0
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 +26 -0
- package/dist/favicon.d.mts +20 -0
- package/dist/favicon.d.mts.map +1 -0
- package/dist/favicon.mjs +127 -0
- package/dist/favicon.mjs.map +1 -0
- package/dist/index.d.mts +2 -0
- package/dist/index.mjs +3 -0
- package/package.json +28 -0
package/README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# @getvitops/utils
|
|
2
|
+
|
|
3
|
+
Shared build-time utilities for the Vitops toolchain. Used under the hood by
|
|
4
|
+
[`@getvitops/cli`](https://www.npmjs.com/package/@getvitops/cli) and
|
|
5
|
+
[`@getvitops/vite`](https://www.npmjs.com/package/@getvitops/vite), and usable on its own.
|
|
6
|
+
|
|
7
|
+
## Favicon generation
|
|
8
|
+
|
|
9
|
+
Rasterize a source SVG/PNG into the standard favicon set (`favicon.ico`,
|
|
10
|
+
`icon-{16?,32,192,512}.png`, `apple-touch-icon.png`, `icon-mask.png`).
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { generateFavicons } from '@getvitops/utils';
|
|
14
|
+
|
|
15
|
+
await generateFavicons({
|
|
16
|
+
source: 'brand/logo.svg', // SVG or PNG
|
|
17
|
+
lowResSource: 'brand/logo-16.svg', // optional simplified 16px source
|
|
18
|
+
outputDir: 'public',
|
|
19
|
+
});
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`sharp` + `png-to-ico` are loaded lazily (only when `generateFavicons` runs).
|
|
23
|
+
`oxipng` lossless crush is applied when it's on `PATH` and skipped otherwise.
|
|
24
|
+
|
|
25
|
+
Also available via the CLI (`vitops favicon -i logo.svg -o public`) and the Vite plugin's
|
|
26
|
+
`favicon` option.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
//#region src/favicon.d.ts
|
|
2
|
+
interface FaviconOptions {
|
|
3
|
+
/** Source image — an SVG or PNG. */
|
|
4
|
+
source: string;
|
|
5
|
+
/** Optional simplified source for the 16px icon. */
|
|
6
|
+
lowResSource?: string;
|
|
7
|
+
/** Directory to write into (default: current directory). */
|
|
8
|
+
outputDir?: string;
|
|
9
|
+
/** Run `oxipng` on the larger PNGs when available (default: true). */
|
|
10
|
+
optimize?: boolean;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Generate the standard favicon set from an SVG/PNG source into `outputDir`:
|
|
14
|
+
* `favicon.ico`, `icon-{16?,32,192,512}.png`, `apple-touch-icon.png`, `icon-mask.png`.
|
|
15
|
+
* Returns the list of written file paths.
|
|
16
|
+
*/
|
|
17
|
+
declare function generateFavicons(opts: FaviconOptions): Promise<string[]>;
|
|
18
|
+
//#endregion
|
|
19
|
+
export { FaviconOptions, generateFavicons };
|
|
20
|
+
//# sourceMappingURL=favicon.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"favicon.d.mts","names":[],"sources":["../src/favicon.ts"],"mappings":";UAiBiB,cAAA;EAAA;EAEf,MAAA;;EAEA,YAAA;EAFA;EAIA,SAAA;EAAA;EAEA,QAAA;AAAA;AAAQ;AAsBV;;;;AAtBU,iBAsBY,gBAAA,CAAiB,IAAA,EAAM,cAAA,GAAiB,OAAO"}
|
package/dist/favicon.mjs
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
2
|
+
import { writeFile } from "node:fs/promises";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
import cp from "node:child_process";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
|
|
7
|
+
//#region src/favicon.ts
|
|
8
|
+
/**
|
|
9
|
+
* Favicon generation — rasterize a source SVG/PNG into the standard favicon set.
|
|
10
|
+
*
|
|
11
|
+
* Ported from the repo's `lib/favicon.ts` so the logic lives in one place instead
|
|
12
|
+
* of being copied per client. `sharp` + `png-to-ico` are loaded lazily (dynamic
|
|
13
|
+
* import) so merely importing this module is cheap; the native `sharp` binary is
|
|
14
|
+
* only touched when `generateFavicons` actually runs. `oxipng` (lossless PNG
|
|
15
|
+
* crush) is used when present on PATH and skipped otherwise — it is optional.
|
|
16
|
+
*/
|
|
17
|
+
const exec = promisify(cp.exec);
|
|
18
|
+
let warnedNoOxipng = false;
|
|
19
|
+
async function optimizePng(file, enabled) {
|
|
20
|
+
if (!enabled) return;
|
|
21
|
+
try {
|
|
22
|
+
await exec(`oxipng -o 3 "${file}"`);
|
|
23
|
+
} catch {
|
|
24
|
+
if (!warnedNoOxipng) {
|
|
25
|
+
warnedNoOxipng = true;
|
|
26
|
+
console.warn("favicon: oxipng not available — skipping lossless PNG optimization.");
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Generate the standard favicon set from an SVG/PNG source into `outputDir`:
|
|
32
|
+
* `favicon.ico`, `icon-{16?,32,192,512}.png`, `apple-touch-icon.png`, `icon-mask.png`.
|
|
33
|
+
* Returns the list of written file paths.
|
|
34
|
+
*/
|
|
35
|
+
async function generateFavicons(opts) {
|
|
36
|
+
const { source, lowResSource } = opts;
|
|
37
|
+
if (!source) throw new Error("favicon: no source path provided");
|
|
38
|
+
if (!source.endsWith(".svg") && !source.endsWith(".png")) throw new Error("favicon: source must be an SVG or PNG file");
|
|
39
|
+
const dir = (opts.outputDir ?? ".").replace(/\/+$/, "");
|
|
40
|
+
const optimize = opts.optimize ?? true;
|
|
41
|
+
mkdirSync(dir, { recursive: true });
|
|
42
|
+
const out = (name) => join(dir, name);
|
|
43
|
+
const [{ default: sharp }, { default: pngToIco }] = await Promise.all([import("sharp"), import("png-to-ico")]);
|
|
44
|
+
const written = [];
|
|
45
|
+
await Promise.all([
|
|
46
|
+
32,
|
|
47
|
+
192,
|
|
48
|
+
512
|
|
49
|
+
].map((size) => sharp(source, { density: 384 }).resize(size, size, {
|
|
50
|
+
fit: "contain",
|
|
51
|
+
background: {
|
|
52
|
+
r: 0,
|
|
53
|
+
g: 0,
|
|
54
|
+
b: 0,
|
|
55
|
+
alpha: 0
|
|
56
|
+
}
|
|
57
|
+
}).png().toFile(out(`icon-${size}.png`)).then(() => written.push(out(`icon-${size}.png`)))));
|
|
58
|
+
if (lowResSource) {
|
|
59
|
+
await sharp(lowResSource, { density: 384 }).resize(16, 16, {
|
|
60
|
+
fit: "contain",
|
|
61
|
+
background: {
|
|
62
|
+
r: 0,
|
|
63
|
+
g: 0,
|
|
64
|
+
b: 0,
|
|
65
|
+
alpha: 0
|
|
66
|
+
}
|
|
67
|
+
}).png().toFile(out("icon-16.png"));
|
|
68
|
+
written.push(out("icon-16.png"));
|
|
69
|
+
}
|
|
70
|
+
const icoSources = [out("icon-32.png")];
|
|
71
|
+
if (lowResSource) icoSources.unshift(out("icon-16.png"));
|
|
72
|
+
await writeFile(out("favicon.ico"), await pngToIco(icoSources));
|
|
73
|
+
written.push(out("favicon.ico"));
|
|
74
|
+
await sharp(out("icon-512.png")).resize(140, 140, {
|
|
75
|
+
fit: "contain",
|
|
76
|
+
background: {
|
|
77
|
+
r: 0,
|
|
78
|
+
g: 0,
|
|
79
|
+
b: 0,
|
|
80
|
+
alpha: 0
|
|
81
|
+
}
|
|
82
|
+
}).extend({
|
|
83
|
+
top: 20,
|
|
84
|
+
bottom: 20,
|
|
85
|
+
left: 20,
|
|
86
|
+
right: 20,
|
|
87
|
+
background: {
|
|
88
|
+
r: 0,
|
|
89
|
+
g: 0,
|
|
90
|
+
b: 0,
|
|
91
|
+
alpha: 0
|
|
92
|
+
}
|
|
93
|
+
}).png().toFile(out("apple-touch-icon.png"));
|
|
94
|
+
written.push(out("apple-touch-icon.png"));
|
|
95
|
+
await sharp(out("icon-512.png")).resize(409, 409, {
|
|
96
|
+
fit: "contain",
|
|
97
|
+
background: {
|
|
98
|
+
r: 0,
|
|
99
|
+
g: 0,
|
|
100
|
+
b: 0,
|
|
101
|
+
alpha: 0
|
|
102
|
+
}
|
|
103
|
+
}).extend({
|
|
104
|
+
top: 52,
|
|
105
|
+
bottom: 51,
|
|
106
|
+
left: 52,
|
|
107
|
+
right: 51,
|
|
108
|
+
background: {
|
|
109
|
+
r: 0,
|
|
110
|
+
g: 0,
|
|
111
|
+
b: 0,
|
|
112
|
+
alpha: 0
|
|
113
|
+
}
|
|
114
|
+
}).png().toFile(out("icon-mask.png"));
|
|
115
|
+
written.push(out("icon-mask.png"));
|
|
116
|
+
await Promise.all([
|
|
117
|
+
"icon-192.png",
|
|
118
|
+
"icon-512.png",
|
|
119
|
+
"apple-touch-icon.png",
|
|
120
|
+
"icon-mask.png"
|
|
121
|
+
].map((image) => optimizePng(out(image), optimize)));
|
|
122
|
+
return written;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
//#endregion
|
|
126
|
+
export { generateFavicons };
|
|
127
|
+
//# sourceMappingURL=favicon.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"favicon.mjs","names":[],"sources":["../src/favicon.ts"],"sourcesContent":["/**\n * Favicon generation — rasterize a source SVG/PNG into the standard favicon set.\n *\n * Ported from the repo's `lib/favicon.ts` so the logic lives in one place instead\n * of being copied per client. `sharp` + `png-to-ico` are loaded lazily (dynamic\n * import) so merely importing this module is cheap; the native `sharp` binary is\n * only touched when `generateFavicons` actually runs. `oxipng` (lossless PNG\n * crush) is used when present on PATH and skipped otherwise — it is optional.\n */\nimport { mkdirSync } from 'node:fs';\nimport { writeFile } from 'node:fs/promises';\nimport { promisify } from 'node:util';\nimport cp from 'node:child_process';\nimport { join } from 'node:path';\n\nconst exec = promisify(cp.exec);\n\nexport interface FaviconOptions {\n /** Source image — an SVG or PNG. */\n source: string;\n /** Optional simplified source for the 16px icon. */\n lowResSource?: string;\n /** Directory to write into (default: current directory). */\n outputDir?: string;\n /** Run `oxipng` on the larger PNGs when available (default: true). */\n optimize?: boolean;\n}\n\nlet warnedNoOxipng = false;\nasync function optimizePng(file: string, enabled: boolean): Promise<void> {\n if (!enabled) return;\n try {\n await exec(`oxipng -o 3 \"${file}\"`);\n } catch {\n // oxipng missing or failed — lossless crush is best-effort, so skip quietly.\n if (!warnedNoOxipng) {\n warnedNoOxipng = true;\n console.warn('favicon: oxipng not available — skipping lossless PNG optimization.');\n }\n }\n}\n\n/**\n * Generate the standard favicon set from an SVG/PNG source into `outputDir`:\n * `favicon.ico`, `icon-{16?,32,192,512}.png`, `apple-touch-icon.png`, `icon-mask.png`.\n * Returns the list of written file paths.\n */\nexport async function generateFavicons(opts: FaviconOptions): Promise<string[]> {\n const { source, lowResSource } = opts;\n if (!source) throw new Error('favicon: no source path provided');\n if (!source.endsWith('.svg') && !source.endsWith('.png'))\n throw new Error('favicon: source must be an SVG or PNG file');\n\n const dir = (opts.outputDir ?? '.').replace(/\\/+$/, '');\n const optimize = opts.optimize ?? true;\n mkdirSync(dir, { recursive: true });\n const out = (name: string) => join(dir, name);\n\n // Lazy-load the native/heavy deps only when actually generating.\n const [{ default: sharp }, { default: pngToIco }] = await Promise.all([\n import('sharp'),\n import('png-to-ico'),\n ]);\n\n const written: string[] = [];\n\n // Rasterize the main source at each size. sharp reads SVG natively via libvips;\n // rendering at high density first keeps small-size edges crisp (ignored for PNG).\n await Promise.all(\n [32, 192, 512].map((size) =>\n sharp(source, { density: 384 })\n .resize(size, size, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } })\n .png()\n .toFile(out(`icon-${size}.png`))\n .then(() => written.push(out(`icon-${size}.png`))),\n ),\n );\n\n // Optional low-res 16px variant.\n if (lowResSource) {\n await sharp(lowResSource, { density: 384 })\n .resize(16, 16, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } })\n .png()\n .toFile(out('icon-16.png'));\n written.push(out('icon-16.png'));\n }\n\n // favicon.ico — sharp can't encode .ico, so png-to-ico packs the PNGs into the\n // multi-resolution container. Browsers pick the best size inside the file.\n const icoSources = [out('icon-32.png')];\n if (lowResSource) icoSources.unshift(out('icon-16.png'));\n await writeFile(out('favicon.ico'), await pngToIco(icoSources));\n written.push(out('favicon.ico'));\n\n // apple-touch-icon: 140x140 logo centered on a 180x180 transparent canvas.\n await sharp(out('icon-512.png'))\n .resize(140, 140, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } })\n .extend({\n top: 20,\n bottom: 20,\n left: 20,\n right: 20,\n background: { r: 0, g: 0, b: 0, alpha: 0 },\n })\n .png()\n .toFile(out('apple-touch-icon.png'));\n written.push(out('apple-touch-icon.png'));\n\n // icon-mask (maskable): 409x409 logo centered on a 512x512 canvas.\n await sharp(out('icon-512.png'))\n .resize(409, 409, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } })\n .extend({\n top: 52,\n bottom: 51,\n left: 52,\n right: 51,\n background: { r: 0, g: 0, b: 0, alpha: 0 },\n })\n .png()\n .toFile(out('icon-mask.png'));\n written.push(out('icon-mask.png'));\n\n // Lossless crush of the larger PNGs (best-effort; needs oxipng on PATH).\n await Promise.all(\n ['icon-192.png', 'icon-512.png', 'apple-touch-icon.png', 'icon-mask.png'].map((image) =>\n optimizePng(out(image), optimize),\n ),\n );\n\n return written;\n}\n"],"mappings":"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"}
|
package/dist/index.d.mts
ADDED
package/dist/index.mjs
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@getvitops/utils",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shared build-time utilities for the Vitops toolchain — favicon generation and friends.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist"
|
|
8
|
+
],
|
|
9
|
+
"type": "module",
|
|
10
|
+
"types": "./dist/index.d.mts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.mts",
|
|
14
|
+
"import": "./dist/index.mjs"
|
|
15
|
+
},
|
|
16
|
+
"./favicon": {
|
|
17
|
+
"types": "./dist/favicon.d.mts",
|
|
18
|
+
"import": "./dist/favicon.mjs"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"png-to-ico": "^3.0.2",
|
|
26
|
+
"sharp": "^0.35.3"
|
|
27
|
+
}
|
|
28
|
+
}
|