@olwiba/dx 0.0.22 → 0.0.23
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 +30 -0
- package/dist/cli.js +84 -17
- package/dist/generate-assets.d.ts +6 -0
- package/dist/generate-assets.js +79 -13
- package/dist/generate-assets.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -81,6 +81,36 @@ bunx @olwiba/dx ascii-gif \
|
|
|
81
81
|
--out ./public/olwibaDX.gif
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
+
### Asset Generator
|
|
85
|
+
|
|
86
|
+
Generate favicons, app icons, manifest files, robots.txt, and an `og-image.png` from a Lucide icon or SVG mark.
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
bunx @olwiba/dx generate-assets \
|
|
90
|
+
--name "myApp" \
|
|
91
|
+
--icon ./public/logo.svg \
|
|
92
|
+
--color "#0d9488" \
|
|
93
|
+
--og-component ./public/search-bar.svg \
|
|
94
|
+
--out ./public
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`--og-component` is optional and only affects the social image: it renders the given SVG/PNG/JPG/WebP
|
|
98
|
+
component bottom-middle on a solid `--color` background, with a small `--icon` + `--name` wordmark
|
|
99
|
+
grouped above it. Without it, `og-image.png` uses the default large-logo layout. Favicons and app
|
|
100
|
+
icons always use `--icon`.
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
import { generateAssets } from "@olwiba/dx/generate-assets";
|
|
104
|
+
|
|
105
|
+
await generateAssets({
|
|
106
|
+
name: "myApp",
|
|
107
|
+
icon: "./public/logo.svg",
|
|
108
|
+
color: "#0d9488",
|
|
109
|
+
outputDir: "./public",
|
|
110
|
+
ogComponent: "./public/search-bar.svg",
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
84
114
|
### Preview Generator
|
|
85
115
|
|
|
86
116
|
Puppeteer-based screenshot tool used to pre-render the isometric preview tiles on docs sites.
|
package/dist/cli.js
CHANGED
|
@@ -1740,6 +1740,14 @@ function getSvgInner(svgPath) {
|
|
|
1740
1740
|
const inner = svg.replace(/<svg[^>]*>/, "").replace(/<\/svg>/, "").trim();
|
|
1741
1741
|
return { inner, viewBox };
|
|
1742
1742
|
}
|
|
1743
|
+
function resolveIcon(icon) {
|
|
1744
|
+
const mode = detectIconMode(icon);
|
|
1745
|
+
if (mode === "svg") {
|
|
1746
|
+
const { inner, viewBox } = getSvgInner(icon);
|
|
1747
|
+
return { mode, inner, viewBox };
|
|
1748
|
+
}
|
|
1749
|
+
return { mode, inner: getLucideInner(icon), viewBox: 24 };
|
|
1750
|
+
}
|
|
1743
1751
|
function buildIconSvgLucide(inner, color, size) {
|
|
1744
1752
|
const pad = Math.round(size * 0.18);
|
|
1745
1753
|
const area = size - pad * 2;
|
|
@@ -1798,8 +1806,44 @@ function buildOgSvgCustom(inner, viewBox, color) {
|
|
|
1798
1806
|
</g>
|
|
1799
1807
|
</svg>`;
|
|
1800
1808
|
}
|
|
1809
|
+
function buildOgGlowSvg(box) {
|
|
1810
|
+
const spread = 40;
|
|
1811
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${OG_WIDTH}" height="${OG_HEIGHT}">
|
|
1812
|
+
<defs>
|
|
1813
|
+
<filter id="glow" x="-40%" y="-40%" width="180%" height="180%">
|
|
1814
|
+
<feGaussianBlur stdDeviation="46"/>
|
|
1815
|
+
</filter>
|
|
1816
|
+
</defs>
|
|
1817
|
+
<rect x="${box.left - spread}" y="${box.top - spread}" width="${box.width + spread * 2}" height="${box.height + spread * 2}" rx="48" fill="#ffffff" opacity="0.22" filter="url(#glow)"/>
|
|
1818
|
+
</svg>`;
|
|
1819
|
+
}
|
|
1820
|
+
function buildOgSvgWithSmallLogo(mode, inner, viewBox, color, name) {
|
|
1821
|
+
const markSize = OG_HEADER.markSize;
|
|
1822
|
+
const gap = 28;
|
|
1823
|
+
const fontSize = 56;
|
|
1824
|
+
const groupY = OG_HEADER.top;
|
|
1825
|
+
const scale = markSize / viewBox;
|
|
1826
|
+
const textWidth = Math.round(name.length * fontSize * 0.58);
|
|
1827
|
+
const totalWidth = markSize + gap + textWidth;
|
|
1828
|
+
const groupX = Math.round((1200 - totalWidth) / 2);
|
|
1829
|
+
const logoMarkup = mode === "lucide" ? (() => {
|
|
1830
|
+
const sw = (2.5 / scale).toFixed(4);
|
|
1831
|
+
return `<g transform="translate(${groupX} ${groupY}) scale(${scale})" stroke="white" fill="none" stroke-width="${sw}" stroke-linecap="round" stroke-linejoin="round">
|
|
1832
|
+
${inner}
|
|
1833
|
+
</g>`;
|
|
1834
|
+
})() : `<g transform="translate(${groupX} ${groupY}) scale(${scale})">
|
|
1835
|
+
${inner}
|
|
1836
|
+
</g>`;
|
|
1837
|
+
const textX = groupX + markSize + gap;
|
|
1838
|
+
const textY = groupY + markSize / 2;
|
|
1839
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${OG_WIDTH}" height="${OG_HEIGHT}">
|
|
1840
|
+
<rect width="${OG_WIDTH}" height="${OG_HEIGHT}" fill="${color}"/>
|
|
1841
|
+
${logoMarkup}
|
|
1842
|
+
<text x="${textX}" y="${textY}" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="${fontSize}" font-weight="700" fill="white" dominant-baseline="middle" letter-spacing="-1">${escapeXml(name)}</text>
|
|
1843
|
+
</svg>`;
|
|
1844
|
+
}
|
|
1801
1845
|
async function generateAssets(config) {
|
|
1802
|
-
const { name, icon, color, outputDir } = config;
|
|
1846
|
+
const { name, icon, color, outputDir, ogComponent } = config;
|
|
1803
1847
|
let sharpFn;
|
|
1804
1848
|
try {
|
|
1805
1849
|
sharpFn = (await import('sharp')).default;
|
|
@@ -1811,17 +1855,8 @@ async function generateAssets(config) {
|
|
|
1811
1855
|
mkdirSync(outputDir, { recursive: true });
|
|
1812
1856
|
const faviconDir = join(outputDir, "favicon");
|
|
1813
1857
|
mkdirSync(faviconDir, { recursive: true });
|
|
1814
|
-
const mode = detectIconMode(icon);
|
|
1815
1858
|
const written = [];
|
|
1816
|
-
|
|
1817
|
-
let viewBox = 24;
|
|
1818
|
-
if (mode === "svg") {
|
|
1819
|
-
const parsed = getSvgInner(icon);
|
|
1820
|
-
inner = parsed.inner;
|
|
1821
|
-
viewBox = parsed.viewBox;
|
|
1822
|
-
} else {
|
|
1823
|
-
inner = getLucideInner(icon);
|
|
1824
|
-
}
|
|
1859
|
+
const { mode, inner, viewBox } = resolveIcon(icon);
|
|
1825
1860
|
const buildIcon = (size) => mode === "svg" ? buildIconSvgCustom(inner, viewBox, color, size) : buildIconSvgLucide(inner, color, size);
|
|
1826
1861
|
for (const size of FAVICON_SIZES) {
|
|
1827
1862
|
const png = await sharpFn(Buffer.from(buildIcon(size))).png().toBuffer();
|
|
@@ -1835,8 +1870,31 @@ async function generateAssets(config) {
|
|
|
1835
1870
|
writeFileSync(dest, png);
|
|
1836
1871
|
written.push(dest);
|
|
1837
1872
|
}
|
|
1838
|
-
const ogSvg = mode === "svg" ? buildOgSvgCustom(inner, viewBox, color) : buildOgSvgLucide(inner, name, color);
|
|
1839
|
-
|
|
1873
|
+
const ogSvg = ogComponent ? buildOgSvgWithSmallLogo(mode, inner, viewBox, color, name) : mode === "svg" ? buildOgSvgCustom(inner, viewBox, color) : buildOgSvgLucide(inner, name, color);
|
|
1874
|
+
let ogPng = await sharpFn(Buffer.from(ogSvg)).png().toBuffer();
|
|
1875
|
+
if (ogComponent) {
|
|
1876
|
+
const sideMargin = 72;
|
|
1877
|
+
const maxWidth = OG_WIDTH - sideMargin * 2;
|
|
1878
|
+
const headerBottom = OG_HEADER.top + OG_HEADER.markSize + OG_HEADER.gapBelow;
|
|
1879
|
+
const componentBuf = readFileSync(resolve(process.cwd(), ogComponent));
|
|
1880
|
+
const resized = await sharpFn(componentBuf, { density: 300 }).resize({ width: maxWidth, withoutEnlargement: true }).png().toBuffer();
|
|
1881
|
+
const { width: compWidth = maxWidth, height: compHeight = 0 } = await sharpFn(resized).metadata();
|
|
1882
|
+
const left = Math.round((OG_WIDTH - compWidth) / 2);
|
|
1883
|
+
const fits = headerBottom + compHeight <= OG_HEIGHT;
|
|
1884
|
+
const top = fits ? Math.round(headerBottom + (OG_HEIGHT - headerBottom - compHeight) / 2) : headerBottom;
|
|
1885
|
+
ogPng = await sharpFn(ogPng).composite([
|
|
1886
|
+
// Glow first: it has to sit under the component, and sharp applies
|
|
1887
|
+
// composites in array order.
|
|
1888
|
+
{
|
|
1889
|
+
input: Buffer.from(
|
|
1890
|
+
buildOgGlowSvg({ left, top, width: compWidth, height: compHeight })
|
|
1891
|
+
),
|
|
1892
|
+
left: 0,
|
|
1893
|
+
top: 0
|
|
1894
|
+
},
|
|
1895
|
+
{ input: resized, left, top }
|
|
1896
|
+
]).png().toBuffer();
|
|
1897
|
+
}
|
|
1840
1898
|
const ogDest = join(outputDir, "og-image.png");
|
|
1841
1899
|
writeFileSync(ogDest, ogPng);
|
|
1842
1900
|
written.push(ogDest);
|
|
@@ -1859,9 +1917,17 @@ async function generateAssets(config) {
|
|
|
1859
1917
|
written.push(robotsDest);
|
|
1860
1918
|
return { files: written };
|
|
1861
1919
|
}
|
|
1862
|
-
var FAVICON_SIZES, NAMED_ICONS;
|
|
1920
|
+
var OG_WIDTH, OG_HEIGHT, OG_HEADER, FAVICON_SIZES, NAMED_ICONS;
|
|
1863
1921
|
var init_generate_assets = __esm({
|
|
1864
1922
|
"src/generate-assets.ts"() {
|
|
1923
|
+
OG_WIDTH = 1200;
|
|
1924
|
+
OG_HEIGHT = 630;
|
|
1925
|
+
OG_HEADER = {
|
|
1926
|
+
top: 54,
|
|
1927
|
+
markSize: 96,
|
|
1928
|
+
/** Clear space between the wordmark's baseline and the component. */
|
|
1929
|
+
gapBelow: 30
|
|
1930
|
+
};
|
|
1865
1931
|
FAVICON_SIZES = [16, 32, 48, 64, 192, 512];
|
|
1866
1932
|
NAMED_ICONS = [
|
|
1867
1933
|
["apple-touch-icon.png", 180],
|
|
@@ -1880,7 +1946,7 @@ if (command === "skills" && subcommand === "install") {
|
|
|
1880
1946
|
await runGenerateAssets();
|
|
1881
1947
|
} else {
|
|
1882
1948
|
process.stdout.write(
|
|
1883
|
-
"Usage:\n dx skills install [--source <url>] [--target claude|amp] [--all] [--name a,b,c]\n dx ascii-gif --text <text> --out <file.gif>\n dx generate-assets --name <app> --icon <lucide-icon> --color <#hex> [--out <dir>]\n"
|
|
1949
|
+
"Usage:\n dx skills install [--source <url>] [--target claude|amp] [--all] [--name a,b,c]\n dx ascii-gif --text <text> --out <file.gif>\n dx generate-assets --name <app> --icon <lucide-icon> --color <#hex> [--out <dir>] [--og-component <svg-or-image-path>]\n"
|
|
1884
1950
|
);
|
|
1885
1951
|
}
|
|
1886
1952
|
async function runSkillsInstall() {
|
|
@@ -2044,9 +2110,10 @@ async function runGenerateAssets() {
|
|
|
2044
2110
|
const icon = flags.icon;
|
|
2045
2111
|
const color = flags.color;
|
|
2046
2112
|
const outputDir = flags.out ?? flags.output ?? "public";
|
|
2113
|
+
const ogComponent = flags["og-component"];
|
|
2047
2114
|
if (!name || !icon || !color) {
|
|
2048
2115
|
process.stderr.write(
|
|
2049
|
-
"Usage: dx generate-assets --name <app> --icon <lucide-icon> --color <#hex> [--out <dir>]\n"
|
|
2116
|
+
"Usage: dx generate-assets --name <app> --icon <lucide-icon> --color <#hex> [--out <dir>] [--og-component <svg-or-image-path>]\n"
|
|
2050
2117
|
);
|
|
2051
2118
|
process.exitCode = 1;
|
|
2052
2119
|
return;
|
|
@@ -2054,7 +2121,7 @@ async function runGenerateAssets() {
|
|
|
2054
2121
|
process.stdout.write(`Generating assets for "${name}"\u2026
|
|
2055
2122
|
`);
|
|
2056
2123
|
try {
|
|
2057
|
-
const result = await generateAssets2({ name, icon, color, outputDir });
|
|
2124
|
+
const result = await generateAssets2({ name, icon, color, outputDir, ogComponent });
|
|
2058
2125
|
process.stdout.write(`Generated ${result.files.length} files in ${outputDir}/
|
|
2059
2126
|
`);
|
|
2060
2127
|
for (const f of result.files) process.stdout.write(` ${f}
|
|
@@ -4,6 +4,12 @@ interface GenerateAssetsConfig {
|
|
|
4
4
|
icon: string;
|
|
5
5
|
color: string;
|
|
6
6
|
outputDir: string;
|
|
7
|
+
/**
|
|
8
|
+
* Optional path to a rendered component (SVG, PNG, JPG, or WebP) placed bottom-middle
|
|
9
|
+
* in og-image.png. When set, the OG image uses a solid `color` background with a smaller
|
|
10
|
+
* `icon` above the component instead of the default large-logo layout.
|
|
11
|
+
*/
|
|
12
|
+
ogComponent?: string;
|
|
7
13
|
}
|
|
8
14
|
interface GenerateAssetsResult {
|
|
9
15
|
files: string[];
|
package/dist/generate-assets.js
CHANGED
|
@@ -34,6 +34,14 @@ function getSvgInner(svgPath) {
|
|
|
34
34
|
const inner = svg.replace(/<svg[^>]*>/, "").replace(/<\/svg>/, "").trim();
|
|
35
35
|
return { inner, viewBox };
|
|
36
36
|
}
|
|
37
|
+
function resolveIcon(icon) {
|
|
38
|
+
const mode = detectIconMode(icon);
|
|
39
|
+
if (mode === "svg") {
|
|
40
|
+
const { inner, viewBox } = getSvgInner(icon);
|
|
41
|
+
return { mode, inner, viewBox };
|
|
42
|
+
}
|
|
43
|
+
return { mode, inner: getLucideInner(icon), viewBox: 24 };
|
|
44
|
+
}
|
|
37
45
|
function buildIconSvgLucide(inner, color, size) {
|
|
38
46
|
const pad = Math.round(size * 0.18);
|
|
39
47
|
const area = size - pad * 2;
|
|
@@ -92,6 +100,50 @@ function buildOgSvgCustom(inner, viewBox, color) {
|
|
|
92
100
|
</g>
|
|
93
101
|
</svg>`;
|
|
94
102
|
}
|
|
103
|
+
var OG_WIDTH = 1200;
|
|
104
|
+
var OG_HEIGHT = 630;
|
|
105
|
+
var OG_HEADER = {
|
|
106
|
+
top: 54,
|
|
107
|
+
markSize: 96,
|
|
108
|
+
/** Clear space between the wordmark's baseline and the component. */
|
|
109
|
+
gapBelow: 30
|
|
110
|
+
};
|
|
111
|
+
function buildOgGlowSvg(box) {
|
|
112
|
+
const spread = 40;
|
|
113
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${OG_WIDTH}" height="${OG_HEIGHT}">
|
|
114
|
+
<defs>
|
|
115
|
+
<filter id="glow" x="-40%" y="-40%" width="180%" height="180%">
|
|
116
|
+
<feGaussianBlur stdDeviation="46"/>
|
|
117
|
+
</filter>
|
|
118
|
+
</defs>
|
|
119
|
+
<rect x="${box.left - spread}" y="${box.top - spread}" width="${box.width + spread * 2}" height="${box.height + spread * 2}" rx="48" fill="#ffffff" opacity="0.22" filter="url(#glow)"/>
|
|
120
|
+
</svg>`;
|
|
121
|
+
}
|
|
122
|
+
function buildOgSvgWithSmallLogo(mode, inner, viewBox, color, name) {
|
|
123
|
+
const markSize = OG_HEADER.markSize;
|
|
124
|
+
const gap = 28;
|
|
125
|
+
const fontSize = 56;
|
|
126
|
+
const groupY = OG_HEADER.top;
|
|
127
|
+
const scale = markSize / viewBox;
|
|
128
|
+
const textWidth = Math.round(name.length * fontSize * 0.58);
|
|
129
|
+
const totalWidth = markSize + gap + textWidth;
|
|
130
|
+
const groupX = Math.round((1200 - totalWidth) / 2);
|
|
131
|
+
const logoMarkup = mode === "lucide" ? (() => {
|
|
132
|
+
const sw = (2.5 / scale).toFixed(4);
|
|
133
|
+
return `<g transform="translate(${groupX} ${groupY}) scale(${scale})" stroke="white" fill="none" stroke-width="${sw}" stroke-linecap="round" stroke-linejoin="round">
|
|
134
|
+
${inner}
|
|
135
|
+
</g>`;
|
|
136
|
+
})() : `<g transform="translate(${groupX} ${groupY}) scale(${scale})">
|
|
137
|
+
${inner}
|
|
138
|
+
</g>`;
|
|
139
|
+
const textX = groupX + markSize + gap;
|
|
140
|
+
const textY = groupY + markSize / 2;
|
|
141
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${OG_WIDTH}" height="${OG_HEIGHT}">
|
|
142
|
+
<rect width="${OG_WIDTH}" height="${OG_HEIGHT}" fill="${color}"/>
|
|
143
|
+
${logoMarkup}
|
|
144
|
+
<text x="${textX}" y="${textY}" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="${fontSize}" font-weight="700" fill="white" dominant-baseline="middle" letter-spacing="-1">${escapeXml(name)}</text>
|
|
145
|
+
</svg>`;
|
|
146
|
+
}
|
|
95
147
|
var FAVICON_SIZES = [16, 32, 48, 64, 192, 512];
|
|
96
148
|
var NAMED_ICONS = [
|
|
97
149
|
["apple-touch-icon.png", 180],
|
|
@@ -99,7 +151,7 @@ var NAMED_ICONS = [
|
|
|
99
151
|
["android-chrome-512x512.png", 512]
|
|
100
152
|
];
|
|
101
153
|
async function generateAssets(config) {
|
|
102
|
-
const { name, icon, color, outputDir } = config;
|
|
154
|
+
const { name, icon, color, outputDir, ogComponent } = config;
|
|
103
155
|
let sharpFn;
|
|
104
156
|
try {
|
|
105
157
|
sharpFn = (await import('sharp')).default;
|
|
@@ -111,17 +163,8 @@ async function generateAssets(config) {
|
|
|
111
163
|
mkdirSync(outputDir, { recursive: true });
|
|
112
164
|
const faviconDir = join(outputDir, "favicon");
|
|
113
165
|
mkdirSync(faviconDir, { recursive: true });
|
|
114
|
-
const mode = detectIconMode(icon);
|
|
115
166
|
const written = [];
|
|
116
|
-
|
|
117
|
-
let viewBox = 24;
|
|
118
|
-
if (mode === "svg") {
|
|
119
|
-
const parsed = getSvgInner(icon);
|
|
120
|
-
inner = parsed.inner;
|
|
121
|
-
viewBox = parsed.viewBox;
|
|
122
|
-
} else {
|
|
123
|
-
inner = getLucideInner(icon);
|
|
124
|
-
}
|
|
167
|
+
const { mode, inner, viewBox } = resolveIcon(icon);
|
|
125
168
|
const buildIcon = (size) => mode === "svg" ? buildIconSvgCustom(inner, viewBox, color, size) : buildIconSvgLucide(inner, color, size);
|
|
126
169
|
for (const size of FAVICON_SIZES) {
|
|
127
170
|
const png = await sharpFn(Buffer.from(buildIcon(size))).png().toBuffer();
|
|
@@ -135,8 +178,31 @@ async function generateAssets(config) {
|
|
|
135
178
|
writeFileSync(dest, png);
|
|
136
179
|
written.push(dest);
|
|
137
180
|
}
|
|
138
|
-
const ogSvg = mode === "svg" ? buildOgSvgCustom(inner, viewBox, color) : buildOgSvgLucide(inner, name, color);
|
|
139
|
-
|
|
181
|
+
const ogSvg = ogComponent ? buildOgSvgWithSmallLogo(mode, inner, viewBox, color, name) : mode === "svg" ? buildOgSvgCustom(inner, viewBox, color) : buildOgSvgLucide(inner, name, color);
|
|
182
|
+
let ogPng = await sharpFn(Buffer.from(ogSvg)).png().toBuffer();
|
|
183
|
+
if (ogComponent) {
|
|
184
|
+
const sideMargin = 72;
|
|
185
|
+
const maxWidth = OG_WIDTH - sideMargin * 2;
|
|
186
|
+
const headerBottom = OG_HEADER.top + OG_HEADER.markSize + OG_HEADER.gapBelow;
|
|
187
|
+
const componentBuf = readFileSync(resolve(process.cwd(), ogComponent));
|
|
188
|
+
const resized = await sharpFn(componentBuf, { density: 300 }).resize({ width: maxWidth, withoutEnlargement: true }).png().toBuffer();
|
|
189
|
+
const { width: compWidth = maxWidth, height: compHeight = 0 } = await sharpFn(resized).metadata();
|
|
190
|
+
const left = Math.round((OG_WIDTH - compWidth) / 2);
|
|
191
|
+
const fits = headerBottom + compHeight <= OG_HEIGHT;
|
|
192
|
+
const top = fits ? Math.round(headerBottom + (OG_HEIGHT - headerBottom - compHeight) / 2) : headerBottom;
|
|
193
|
+
ogPng = await sharpFn(ogPng).composite([
|
|
194
|
+
// Glow first: it has to sit under the component, and sharp applies
|
|
195
|
+
// composites in array order.
|
|
196
|
+
{
|
|
197
|
+
input: Buffer.from(
|
|
198
|
+
buildOgGlowSvg({ left, top, width: compWidth, height: compHeight })
|
|
199
|
+
),
|
|
200
|
+
left: 0,
|
|
201
|
+
top: 0
|
|
202
|
+
},
|
|
203
|
+
{ input: resized, left, top }
|
|
204
|
+
]).png().toBuffer();
|
|
205
|
+
}
|
|
140
206
|
const ogDest = join(outputDir, "og-image.png");
|
|
141
207
|
writeFileSync(ogDest, ogPng);
|
|
142
208
|
written.push(ogDest);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/generate-assets.ts"],"names":[],"mappings":"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","file":"generate-assets.js","sourcesContent":["import { mkdirSync, readFileSync, writeFileSync } from \"node:fs\"\nimport { createRequire } from \"node:module\"\nimport { join, resolve } from \"node:path\"\n\nexport interface GenerateAssetsConfig {\n name: string\n /** Lucide icon name (e.g. \"Zap\") OR path to an SVG file (e.g. \"./logo.svg\") */\n icon: string\n color: string\n outputDir: string\n}\n\nexport interface GenerateAssetsResult {\n files: string[]\n}\n\ntype IconMode = \"lucide\" | \"svg\"\n\nfunction detectIconMode(icon: string): IconMode {\n return icon.endsWith(\".svg\") || icon.includes(\"/\") || icon.includes(\"\\\\\") ? \"svg\" : \"lucide\"\n}\n\nfunction toKebabCase(name: string): string {\n return name\n .replace(/([A-Z])/g, (_, ch: string) => `-${ch.toLowerCase()}`)\n .replace(/^-/, \"\")\n .toLowerCase()\n}\n\nfunction escapeXml(str: string): string {\n return str\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\")\n}\n\nfunction getLucideInner(iconName: string): string {\n const kebab = toKebabCase(iconName)\n const req = createRequire(import.meta.url)\n let iconPath: string\n try {\n iconPath = req.resolve(`lucide-static/icons/${kebab}.svg`)\n } catch {\n throw new Error(\n `Unknown Lucide icon: \"${iconName}\" (tried \"${kebab}.svg\"). ` +\n `Browse icons at https://lucide.dev/icons`,\n )\n }\n const svg = readFileSync(iconPath, \"utf-8\")\n return svg\n .replace(/<svg[^>]*>/, \"\")\n .replace(/<\\/svg>/, \"\")\n .replace(/\\sstroke=\"currentColor\"/g, \"\")\n .replace(/\\sfill=\"none\"/g, \"\")\n .trim()\n}\n\nfunction getSvgInner(svgPath: string): { inner: string; viewBox: number } {\n const abs = resolve(process.cwd(), svgPath)\n const svg = readFileSync(abs, \"utf-8\")\n const vbMatch = svg.match(/viewBox=\"0 0 (\\d+\\.?\\d*) \\d/)\n const viewBox = vbMatch ? parseFloat(vbMatch[1]) : 24\n const inner = svg\n .replace(/<svg[^>]*>/, \"\")\n .replace(/<\\/svg>/, \"\")\n .trim()\n return { inner, viewBox }\n}\n\nfunction buildIconSvgLucide(inner: string, color: string, size: number): string {\n const pad = Math.round(size * 0.18)\n const area = size - pad * 2\n const scale = area / 24\n const rx = Math.round(size * 0.16)\n const sw = (2 / scale).toFixed(4)\n return `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${size}\" height=\"${size}\">\n <rect width=\"${size}\" height=\"${size}\" rx=\"${rx}\" fill=\"${color}\"/>\n <g transform=\"translate(${pad} ${pad}) scale(${scale})\" stroke=\"white\" fill=\"none\" stroke-width=\"${sw}\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n ${inner}\n </g>\n</svg>`\n}\n\nfunction buildIconSvgCustom(inner: string, viewBox: number, color: string, size: number): string {\n const pad = Math.round(size * 0.08)\n const area = size - pad * 2\n const scale = area / viewBox\n const rx = Math.round(size * 0.16)\n return `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${size}\" height=\"${size}\">\n <rect width=\"${size}\" height=\"${size}\" rx=\"${rx}\" fill=\"${color}\"/>\n <g transform=\"translate(${pad} ${pad}) scale(${scale})\">\n ${inner}\n </g>\n</svg>`\n}\n\nfunction buildOgSvgLucide(inner: string, name: string, color: string): string {\n const iconPx = 180\n const iconX = Math.round((1200 - iconPx) / 2)\n const iconY = 160\n const scale = iconPx / 24\n const sw = (2.5 / scale).toFixed(4)\n return `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1200\" height=\"630\">\n <defs>\n <radialGradient id=\"rg\" cx=\"50%\" cy=\"40%\" r=\"65%\">\n <stop offset=\"0%\" stop-color=\"${color}\" stop-opacity=\"0.22\"/>\n <stop offset=\"100%\" stop-color=\"#000000\" stop-opacity=\"0\"/>\n </radialGradient>\n </defs>\n <rect width=\"1200\" height=\"630\" fill=\"#09090b\"/>\n <rect width=\"1200\" height=\"630\" fill=\"url(#rg)\"/>\n <g transform=\"translate(${iconX} ${iconY}) scale(${scale})\" stroke=\"${color}\" fill=\"none\" stroke-width=\"${sw}\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n ${inner}\n </g>\n <text x=\"600\" y=\"470\" font-family=\"-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif\" font-size=\"72\" font-weight=\"700\" fill=\"white\" text-anchor=\"middle\" letter-spacing=\"-1\">${escapeXml(name)}</text>\n</svg>`\n}\n\nfunction buildOgSvgCustom(inner: string, viewBox: number, color: string): string {\n const s = 470\n const x = Math.round((1200 - s) / 2)\n const y = Math.round((630 - s) / 2)\n const scale = s / viewBox\n return `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1200\" height=\"630\">\n <rect width=\"1200\" height=\"630\" fill=\"${color}\"/>\n <g transform=\"translate(${x} ${y}) scale(${scale})\">\n ${inner}\n </g>\n</svg>`\n}\n\nconst FAVICON_SIZES = [16, 32, 48, 64, 192, 512] as const\n\nconst NAMED_ICONS: Array<[string, number]> = [\n [\"apple-touch-icon.png\", 180],\n [\"android-chrome-192x192.png\", 192],\n [\"android-chrome-512x512.png\", 512],\n]\n\nexport async function generateAssets(\n config: GenerateAssetsConfig,\n): Promise<GenerateAssetsResult> {\n const { name, icon, color, outputDir } = config\n\n let sharpFn: typeof import(\"sharp\")\n try {\n sharpFn = (await import(\"sharp\")).default\n } catch {\n throw new Error(\n \"sharp is required for asset generation.\\n\" +\n \"Install it in your project: bun add sharp lucide-static\",\n )\n }\n\n mkdirSync(outputDir, { recursive: true })\n const faviconDir = join(outputDir, \"favicon\")\n mkdirSync(faviconDir, { recursive: true })\n\n const mode = detectIconMode(icon)\n const written: string[] = []\n\n let inner: string\n let viewBox = 24\n\n if (mode === \"svg\") {\n const parsed = getSvgInner(icon)\n inner = parsed.inner\n viewBox = parsed.viewBox\n } else {\n inner = getLucideInner(icon)\n }\n\n const buildIcon = (size: number) =>\n mode === \"svg\"\n ? buildIconSvgCustom(inner, viewBox, color, size)\n : buildIconSvgLucide(inner, color, size)\n\n for (const size of FAVICON_SIZES) {\n const png = await sharpFn(Buffer.from(buildIcon(size))).png().toBuffer()\n const dest = join(faviconDir, `favicon-${size}.png`)\n writeFileSync(dest, png)\n written.push(dest)\n }\n\n for (const [filename, size] of NAMED_ICONS) {\n const png = await sharpFn(Buffer.from(buildIcon(size))).png().toBuffer()\n const dest = join(outputDir, filename)\n writeFileSync(dest, png)\n written.push(dest)\n }\n\n const ogSvg =\n mode === \"svg\"\n ? buildOgSvgCustom(inner, viewBox, color)\n : buildOgSvgLucide(inner, name, color)\n\n const ogPng = await sharpFn(Buffer.from(ogSvg)).png().toBuffer()\n const ogDest = join(outputDir, \"og-image.png\")\n writeFileSync(ogDest, ogPng)\n written.push(ogDest)\n\n const manifest = {\n name,\n short_name: name,\n theme_color: color,\n background_color: mode === \"svg\" ? color : \"#09090b\",\n display: \"standalone\",\n icons: [\n { src: \"/android-chrome-192x192.png\", sizes: \"192x192\", type: \"image/png\" },\n { src: \"/android-chrome-512x512.png\", sizes: \"512x512\", type: \"image/png\", purpose: \"maskable\" },\n ],\n }\n const manifestDest = join(outputDir, \"manifest.json\")\n writeFileSync(manifestDest, JSON.stringify(manifest, null, 2) + \"\\n\")\n written.push(manifestDest)\n\n const robotsDest = join(outputDir, \"robots.txt\")\n writeFileSync(robotsDest, \"User-agent: *\\nAllow: /\\n\")\n written.push(robotsDest)\n\n return { files: written }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/generate-assets.ts"],"names":[],"mappings":"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","file":"generate-assets.js","sourcesContent":["import { mkdirSync, readFileSync, writeFileSync } from \"node:fs\"\nimport { createRequire } from \"node:module\"\nimport { join, resolve } from \"node:path\"\n\nexport interface GenerateAssetsConfig {\n name: string\n /** Lucide icon name (e.g. \"Zap\") OR path to an SVG file (e.g. \"./logo.svg\") */\n icon: string\n color: string\n outputDir: string\n /**\n * Optional path to a rendered component (SVG, PNG, JPG, or WebP) placed bottom-middle\n * in og-image.png. When set, the OG image uses a solid `color` background with a smaller\n * `icon` above the component instead of the default large-logo layout.\n */\n ogComponent?: string\n}\n\nexport interface GenerateAssetsResult {\n files: string[]\n}\n\ntype IconMode = \"lucide\" | \"svg\"\n\nfunction detectIconMode(icon: string): IconMode {\n return icon.endsWith(\".svg\") || icon.includes(\"/\") || icon.includes(\"\\\\\") ? \"svg\" : \"lucide\"\n}\n\nfunction toKebabCase(name: string): string {\n return name\n .replace(/([A-Z])/g, (_, ch: string) => `-${ch.toLowerCase()}`)\n .replace(/^-/, \"\")\n .toLowerCase()\n}\n\nfunction escapeXml(str: string): string {\n return str\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\")\n}\n\nfunction getLucideInner(iconName: string): string {\n const kebab = toKebabCase(iconName)\n const req = createRequire(import.meta.url)\n let iconPath: string\n try {\n iconPath = req.resolve(`lucide-static/icons/${kebab}.svg`)\n } catch {\n throw new Error(\n `Unknown Lucide icon: \"${iconName}\" (tried \"${kebab}.svg\"). ` +\n `Browse icons at https://lucide.dev/icons`,\n )\n }\n const svg = readFileSync(iconPath, \"utf-8\")\n return svg\n .replace(/<svg[^>]*>/, \"\")\n .replace(/<\\/svg>/, \"\")\n .replace(/\\sstroke=\"currentColor\"/g, \"\")\n .replace(/\\sfill=\"none\"/g, \"\")\n .trim()\n}\n\nfunction getSvgInner(svgPath: string): { inner: string; viewBox: number } {\n const abs = resolve(process.cwd(), svgPath)\n const svg = readFileSync(abs, \"utf-8\")\n const vbMatch = svg.match(/viewBox=\"0 0 (\\d+\\.?\\d*) \\d/)\n const viewBox = vbMatch ? parseFloat(vbMatch[1]) : 24\n const inner = svg\n .replace(/<svg[^>]*>/, \"\")\n .replace(/<\\/svg>/, \"\")\n .trim()\n return { inner, viewBox }\n}\n\nfunction resolveIcon(icon: string): { mode: IconMode; inner: string; viewBox: number } {\n const mode = detectIconMode(icon)\n if (mode === \"svg\") {\n const { inner, viewBox } = getSvgInner(icon)\n return { mode, inner, viewBox }\n }\n return { mode, inner: getLucideInner(icon), viewBox: 24 }\n}\n\nfunction buildIconSvgLucide(inner: string, color: string, size: number): string {\n const pad = Math.round(size * 0.18)\n const area = size - pad * 2\n const scale = area / 24\n const rx = Math.round(size * 0.16)\n const sw = (2 / scale).toFixed(4)\n return `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${size}\" height=\"${size}\">\n <rect width=\"${size}\" height=\"${size}\" rx=\"${rx}\" fill=\"${color}\"/>\n <g transform=\"translate(${pad} ${pad}) scale(${scale})\" stroke=\"white\" fill=\"none\" stroke-width=\"${sw}\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n ${inner}\n </g>\n</svg>`\n}\n\nfunction buildIconSvgCustom(inner: string, viewBox: number, color: string, size: number): string {\n const pad = Math.round(size * 0.08)\n const area = size - pad * 2\n const scale = area / viewBox\n const rx = Math.round(size * 0.16)\n return `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${size}\" height=\"${size}\">\n <rect width=\"${size}\" height=\"${size}\" rx=\"${rx}\" fill=\"${color}\"/>\n <g transform=\"translate(${pad} ${pad}) scale(${scale})\">\n ${inner}\n </g>\n</svg>`\n}\n\nfunction buildOgSvgLucide(inner: string, name: string, color: string): string {\n const iconPx = 180\n const iconX = Math.round((1200 - iconPx) / 2)\n const iconY = 160\n const scale = iconPx / 24\n const sw = (2.5 / scale).toFixed(4)\n return `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1200\" height=\"630\">\n <defs>\n <radialGradient id=\"rg\" cx=\"50%\" cy=\"40%\" r=\"65%\">\n <stop offset=\"0%\" stop-color=\"${color}\" stop-opacity=\"0.22\"/>\n <stop offset=\"100%\" stop-color=\"#000000\" stop-opacity=\"0\"/>\n </radialGradient>\n </defs>\n <rect width=\"1200\" height=\"630\" fill=\"#09090b\"/>\n <rect width=\"1200\" height=\"630\" fill=\"url(#rg)\"/>\n <g transform=\"translate(${iconX} ${iconY}) scale(${scale})\" stroke=\"${color}\" fill=\"none\" stroke-width=\"${sw}\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n ${inner}\n </g>\n <text x=\"600\" y=\"470\" font-family=\"-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif\" font-size=\"72\" font-weight=\"700\" fill=\"white\" text-anchor=\"middle\" letter-spacing=\"-1\">${escapeXml(name)}</text>\n</svg>`\n}\n\nfunction buildOgSvgCustom(inner: string, viewBox: number, color: string): string {\n const s = 470\n const x = Math.round((1200 - s) / 2)\n const y = Math.round((630 - s) / 2)\n const scale = s / viewBox\n return `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1200\" height=\"630\">\n <rect width=\"1200\" height=\"630\" fill=\"${color}\"/>\n <g transform=\"translate(${x} ${y}) scale(${scale})\">\n ${inner}\n </g>\n</svg>`\n}\n\nconst OG_WIDTH = 1200\nconst OG_HEIGHT = 630\n\n/**\n * The wordmark band at the top, and the space left under it for the component.\n *\n * Shared by the SVG that draws the header and the compositing that places the\n * component, so the two cannot disagree about where one ends and the other\n * begins — the failure mode being a component that creeps up under the logo.\n */\nconst OG_HEADER = {\n top: 54,\n markSize: 96,\n /** Clear space between the wordmark's baseline and the component. */\n gapBelow: 30,\n} as const\n\n/**\n * A soft bloom behind the component, so it sits on the brand field rather than\n * on top of it. White at low opacity rather than a tint, so it reads as light\n * on any brand colour instead of as a second colour competing with it.\n */\nfunction buildOgGlowSvg(box: { left: number; top: number; width: number; height: number }): string {\n const spread = 40\n return `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${OG_WIDTH}\" height=\"${OG_HEIGHT}\">\n <defs>\n <filter id=\"glow\" x=\"-40%\" y=\"-40%\" width=\"180%\" height=\"180%\">\n <feGaussianBlur stdDeviation=\"46\"/>\n </filter>\n </defs>\n <rect x=\"${box.left - spread}\" y=\"${box.top - spread}\" width=\"${box.width + spread * 2}\" height=\"${box.height + spread * 2}\" rx=\"48\" fill=\"#ffffff\" opacity=\"0.22\" filter=\"url(#glow)\"/>\n</svg>`\n}\n\n/**\n * OG base image used when `ogComponent` is set: solid brand-`color` background with a\n * small logo mark + name wordmark centered as a group near the top. The component itself\n * is composited on top afterward, filling the space beneath.\n */\nfunction buildOgSvgWithSmallLogo(\n mode: IconMode,\n inner: string,\n viewBox: number,\n color: string,\n name: string,\n): string {\n const markSize = OG_HEADER.markSize\n const gap = 28\n const fontSize = 56\n const groupY = OG_HEADER.top\n const scale = markSize / viewBox\n\n // No text-measurement API in raw SVG generation, so estimate wordmark width from\n // character count to center the mark+text group as a whole.\n const textWidth = Math.round(name.length * fontSize * 0.58)\n const totalWidth = markSize + gap + textWidth\n const groupX = Math.round((1200 - totalWidth) / 2)\n\n const logoMarkup =\n mode === \"lucide\"\n ? (() => {\n const sw = (2.5 / scale).toFixed(4)\n return `<g transform=\"translate(${groupX} ${groupY}) scale(${scale})\" stroke=\"white\" fill=\"none\" stroke-width=\"${sw}\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n ${inner}\n </g>`\n })()\n : `<g transform=\"translate(${groupX} ${groupY}) scale(${scale})\">\n ${inner}\n </g>`\n\n const textX = groupX + markSize + gap\n const textY = groupY + markSize / 2\n\n return `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${OG_WIDTH}\" height=\"${OG_HEIGHT}\">\n <rect width=\"${OG_WIDTH}\" height=\"${OG_HEIGHT}\" fill=\"${color}\"/>\n ${logoMarkup}\n <text x=\"${textX}\" y=\"${textY}\" font-family=\"-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif\" font-size=\"${fontSize}\" font-weight=\"700\" fill=\"white\" dominant-baseline=\"middle\" letter-spacing=\"-1\">${escapeXml(name)}</text>\n</svg>`\n}\n\nconst FAVICON_SIZES = [16, 32, 48, 64, 192, 512] as const\n\nconst NAMED_ICONS: Array<[string, number]> = [\n [\"apple-touch-icon.png\", 180],\n [\"android-chrome-192x192.png\", 192],\n [\"android-chrome-512x512.png\", 512],\n]\n\nexport async function generateAssets(\n config: GenerateAssetsConfig,\n): Promise<GenerateAssetsResult> {\n const { name, icon, color, outputDir, ogComponent } = config\n\n let sharpFn: typeof import(\"sharp\")\n try {\n sharpFn = (await import(\"sharp\")).default\n } catch {\n throw new Error(\n \"sharp is required for asset generation.\\n\" +\n \"Install it in your project: bun add sharp lucide-static\",\n )\n }\n\n mkdirSync(outputDir, { recursive: true })\n const faviconDir = join(outputDir, \"favicon\")\n mkdirSync(faviconDir, { recursive: true })\n\n const written: string[] = []\n\n const { mode, inner, viewBox } = resolveIcon(icon)\n\n const buildIcon = (size: number) =>\n mode === \"svg\"\n ? buildIconSvgCustom(inner, viewBox, color, size)\n : buildIconSvgLucide(inner, color, size)\n\n for (const size of FAVICON_SIZES) {\n const png = await sharpFn(Buffer.from(buildIcon(size))).png().toBuffer()\n const dest = join(faviconDir, `favicon-${size}.png`)\n writeFileSync(dest, png)\n written.push(dest)\n }\n\n for (const [filename, size] of NAMED_ICONS) {\n const png = await sharpFn(Buffer.from(buildIcon(size))).png().toBuffer()\n const dest = join(outputDir, filename)\n writeFileSync(dest, png)\n written.push(dest)\n }\n\n const ogSvg = ogComponent\n ? buildOgSvgWithSmallLogo(mode, inner, viewBox, color, name)\n : mode === \"svg\"\n ? buildOgSvgCustom(inner, viewBox, color)\n : buildOgSvgLucide(inner, name, color)\n\n let ogPng = await sharpFn(Buffer.from(ogSvg)).png().toBuffer()\n\n if (ogComponent) {\n // Scaled to width and allowed to run off the bottom edge, rather than\n // shrunk to fit a box. A screenshot that fits entirely inside the frame\n // reads as a picture of an app; one that continues past the edge reads as\n // the app itself, and the detail stays legible at the size a link preview\n // is actually seen.\n //\n // The previous fixed 880x300 box assumed a wide strip — a composer, a\n // toolbar — and shrank anything squarer to fit its height: a 1.87:1\n // dashboard landed at 561x300, adrift in the middle of a 1200px canvas.\n const sideMargin = 72\n const maxWidth = OG_WIDTH - sideMargin * 2\n const headerBottom = OG_HEADER.top + OG_HEADER.markSize + OG_HEADER.gapBelow\n\n const componentBuf = readFileSync(resolve(process.cwd(), ogComponent))\n const resized = await sharpFn(componentBuf, { density: 300 })\n .resize({ width: maxWidth, withoutEnlargement: true })\n .png()\n .toBuffer()\n const { width: compWidth = maxWidth, height: compHeight = 0 } =\n await sharpFn(resized).metadata()\n\n const left = Math.round((OG_WIDTH - compWidth) / 2)\n // A component short enough to fit sits centred in the space under the\n // header; a tall one starts there and bleeds off the bottom. Without this\n // branch a wide strip would cling to the header with a gulf beneath it.\n const fits = headerBottom + compHeight <= OG_HEIGHT\n const top = fits\n ? Math.round(headerBottom + (OG_HEIGHT - headerBottom - compHeight) / 2)\n : headerBottom\n\n ogPng = await sharpFn(ogPng)\n .composite([\n // Glow first: it has to sit under the component, and sharp applies\n // composites in array order.\n {\n input: Buffer.from(\n buildOgGlowSvg({ left, top, width: compWidth, height: compHeight }),\n ),\n left: 0,\n top: 0,\n },\n { input: resized, left, top },\n ])\n .png()\n .toBuffer()\n }\n\n const ogDest = join(outputDir, \"og-image.png\")\n writeFileSync(ogDest, ogPng)\n written.push(ogDest)\n\n const manifest = {\n name,\n short_name: name,\n theme_color: color,\n background_color: mode === \"svg\" ? color : \"#09090b\",\n display: \"standalone\",\n icons: [\n { src: \"/android-chrome-192x192.png\", sizes: \"192x192\", type: \"image/png\" },\n { src: \"/android-chrome-512x512.png\", sizes: \"512x512\", type: \"image/png\", purpose: \"maskable\" },\n ],\n }\n const manifestDest = join(outputDir, \"manifest.json\")\n writeFileSync(manifestDest, JSON.stringify(manifest, null, 2) + \"\\n\")\n written.push(manifestDest)\n\n const robotsDest = join(outputDir, \"robots.txt\")\n writeFileSync(robotsDest, \"User-agent: *\\nAllow: /\\n\")\n written.push(robotsDest)\n\n return { files: written }\n}\n"]}
|